Chromium Code Reviews| Index: service/datastore/key.go |
| diff --git a/service/datastore/key.go b/service/datastore/key.go |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..e4e3927d5db02484e9a66e26d3633fa56c4bf316 |
| --- /dev/null |
| +++ b/service/datastore/key.go |
| @@ -0,0 +1,384 @@ |
| +// Copyright 2015 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +package datastore |
| + |
| +import ( |
| + "bytes" |
| + "encoding/base64" |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| + "strings" |
| + |
| + "github.com/golang/protobuf/proto" |
| + pb "github.com/luci/gae/service/datastore/internal/protos/datastore" |
| +) |
| + |
| +// KeyTok is a single token from a multi-part Key. |
| +type KeyTok struct { |
| + Kind string |
| + IntID int64 |
| + StringID string |
| +} |
| + |
| +func (k KeyTok) Incomplete() bool { |
| + return k.StringID == "" && k.IntID == 0 |
| +} |
| + |
| +func (k KeyTok) Special() bool { |
| + return len(k.Kind) >= 2 && k.Kind[:2] == "__" |
| +} |
| + |
| +func (k KeyTok) ID() Property { |
| + if k.StringID != "" { |
| + return Property{value: k.StringID, propType: PTString} |
| + } |
| + return Property{value: k.IntID, propType: PTInt} |
| +} |
| + |
| +func (k KeyTok) Less(other KeyTok) bool { |
| + if k.Kind < other.Kind { |
|
dnj
2015/09/18 16:47:58
(see note about double-compare below)
|
| + return true |
| + } else if k.Kind > other.Kind { |
| + return false |
| + } |
| + a, b := k.ID(), other.ID() |
| + return a.Less(&b) |
| +} |
| + |
| +// Key is the type used for all datastore operations. |
| +type Key struct { |
| + appID string |
| + namespace string |
| + toks []KeyTok |
|
dnj
2015/09/18 16:47:58
Note that by construction, this array is expected
iannucci
2015/09/18 22:25:48
done
|
| +} |
| + |
| +var _ interface { |
| + json.Marshaler |
| + json.Unmarshaler |
| +} = (*Key)(nil) |
| + |
| +// NewToks creates a new Key. It is the Key implementation |
| +// returned from the various PropertyMap serialization routines, as well as |
| +// the native key implementation for the in-memory implementation of gae. |
| +// |
| +// See Interface.NewKeyToks for a version of this function which automatically |
| +// provides aid and ns. |
| +func NewKeyToks(aid, ns string, toks []KeyTok) *Key { |
| + if len(toks) == 0 { |
| + return nil |
| + } |
| + newToks := make([]KeyTok, len(toks)) |
| + copy(newToks, toks) |
|
dnj
2015/09/18 16:47:58
Since a KeyTok's fields are mutable, should this b
iannucci
2015/09/18 22:25:48
It is a deep copy, since there are no pointers inv
|
| + return &Key{aid, ns, newToks} |
| +} |
| + |
| +// NewKey is a wrapper around NewToks which has an interface similar |
| +// to NewKey in the SDK. |
| +// |
| +// See Interface.NewKey for a version of this function which automatically |
| +// provides aid and ns. |
| +func NewKey(aid, ns, kind, stringID string, intID int64, parent *Key) *Key { |
| + if parent == nil { |
| + return &Key{aid, ns, []KeyTok{{kind, intID, stringID}}} |
| + } |
| + |
| + toks := parent.toks |
| + newToks := make([]KeyTok, len(toks), len(toks)+1) |
| + copy(newToks, toks) |
| + newToks = append(newToks, KeyTok{kind, intID, stringID}) |
| + return &Key{aid, ns, newToks} |
| +} |
| + |
| +// MakeKey is a convenience function for manufacturing a *Key. It should only |
| +// be used when elems... is known statically (e.g. in the code) to be correct. |
| +// |
| +// elems is pairs of (string, string|int|int32|int64) pairs, which correspond to |
| +// Kind/id pairs. Example: |
| +// MakeKey("aid", "namespace", "Parent", 1, "Child", "id") |
| +// |
| +// Would create the key: |
| +// aid:namespace:/Parent,1/Child,id |
| +// |
| +// If elems is not parsable (e.g. wrong length, wrong types, etc.) this method |
| +// will panic. |
| +// |
| +// See Interface.MakeKey for a version of this function which automatically |
| +// provides aid and ns. |
| +func MakeKey(aid, ns string, elems ...interface{}) *Key { |
| + if len(elems) == 0 { |
| + return nil |
| + } |
| + |
| + if len(elems)%2 != 0 { |
| + panic(fmt.Errorf("datastore.MakeKey: odd number of tokens: %v", elems)) |
| + } |
| + toks := make([]KeyTok, len(elems)/2) |
| + for i := 0; i < len(elems); i += 2 { |
|
dnj
2015/09/18 16:47:58
Consider rewriting using slice-based traversal. I'
iannucci
2015/09/18 22:25:48
Done
|
| + knd, ok := elems[i].(string) |
| + if !ok { |
| + panic(fmt.Errorf("datastore.MakeKey: bad kind: %v", elems[i])) |
| + } |
| + toks[i/2].Kind = knd |
| + switch x := elems[i+1].(type) { |
| + case string: |
| + toks[i/2].StringID = x |
| + case int: |
| + toks[i/2].IntID = int64(x) |
| + case int32: |
| + toks[i/2].IntID = int64(x) |
| + case int64: |
| + toks[i/2].IntID = int64(x) |
| + default: |
| + panic(fmt.Errorf("datastore.MakeKey: bad id: %v", x)) |
| + } |
| + } |
| + |
| + return NewKeyToks(aid, ns, toks) |
| +} |
| + |
| +// NewKeyEncoded decodes and returns a *Key |
| +func NewKeyEncoded(encoded string) (ret *Key, err error) { |
| + ret = &Key{} |
| + // Re-add padding |
| + if m := len(encoded) % 4; m != 0 { |
| + encoded += strings.Repeat("=", 4-m) |
| + } |
| + b, err := base64.URLEncoding.DecodeString(encoded) |
| + if err != nil { |
| + return |
| + } |
| + |
| + r := &pb.Reference{} |
| + if err = proto.Unmarshal(b, r); err != nil { |
| + return |
| + } |
| + |
| + ret.appID = r.GetApp() |
| + ret.namespace = r.GetNameSpace() |
| + ret.toks = make([]KeyTok, len(r.Path.Element)) |
| + for i, e := range r.Path.Element { |
| + ret.toks[i] = KeyTok{ |
| + Kind: e.GetType(), |
| + IntID: e.GetId(), |
| + StringID: e.GetName(), |
| + } |
| + } |
| + return |
| +} |
| + |
| +func (k *Key) Last() KeyTok { |
| + if len(k.toks) > 0 { |
| + return k.toks[len(k.toks)-1] |
| + } |
| + return KeyTok{} |
| +} |
| + |
| +// AppID returns the application ID that this Key is for. |
| +func (k *Key) AppID() string { return k.appID } |
|
dnj
2015/09/18 16:47:58
(*Key)(nil) is treated inconsistently. For some me
iannucci
2015/09/18 22:25:48
In the SDK it's impossible to have a non-nil key w
|
| + |
| +// Namespace returns the namespace that this Key is for. |
| +func (k *Key) Namespace() string { return k.namespace } |
| + |
| +// String returns a human-readable representation of the key in the form of |
| +// AID:NS:/Kind,id/Kind,id/... |
| +func (k *Key) String() string { |
| + if k == nil { |
| + return "" |
| + } |
| + b := bytes.NewBuffer(make([]byte, 0, 512)) |
|
dnj
2015/09/18 16:47:57
I prefer:
bytes.Buffer{}
b.Grow(512)
But really w
iannucci
2015/09/18 22:25:48
This is a carry over from https://github.com/golan
|
| + fmt.Fprintf(b, "%s:%s:", k.appID, k.namespace) |
| + for _, t := range k.toks { |
| + if t.StringID != "" { |
| + fmt.Fprintf(b, "/%s,%q", t.Kind, t.StringID) |
| + } else { |
| + fmt.Fprintf(b, "/%s,%d", t.Kind, t.IntID) |
| + } |
| + } |
| + return b.String() |
| +} |
| + |
| +// Incomplete returns true iff k doesn't have an id yet. |
| +func (k *Key) Incomplete() bool { |
| + return k != nil && k.Last().Incomplete() |
| +} |
| + |
| +// Valid determines if a key is valid, according to a couple rules: |
| +// - k is not nil |
| +// - every token of k: |
| +// - (if !allowSpecial) token's kind doesn't start with '__' |
| +// - token's kind and appid are non-blank |
| +// - token is not incomplete |
| +// - all tokens have the same namespace and appid |
| +func (k *Key) Valid(allowSpecial bool, aid, ns string) bool { |
| + if k == nil { |
| + return false |
| + } |
| + if aid != k.AppID() || ns != k.Namespace() { |
| + return false |
| + } |
| + for _, t := range k.toks { |
| + if t.Incomplete() { |
| + return false |
| + } |
| + if !allowSpecial && t.Special() { |
| + return false |
| + } |
| + if t.Kind == "" { |
| + return false |
| + } |
| + if t.StringID != "" && t.IntID != 0 { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
| + |
| +// PartialValid returns true iff this key is suitable for use in a Put |
| +// operation. This is the same as Valid(k, false, ...), but also allowing k to |
| +// be Incomplete(). |
| +func (k *Key) PartialValid(aid, ns string) bool { |
| + if k.Incomplete() { |
| + k = NewKey(k.AppID(), k.Namespace(), k.Last().Kind, "", 1, k.Parent()) |
| + } |
| + return k.Valid(false, aid, ns) |
| +} |
| + |
| +// Parent returns the parent Key of this *Key, or nil. The parent |
| +// will always have the concrete type of *Key. |
| +func (k *Key) Parent() *Key { |
| + if len(k.toks) <= 1 { |
| + return nil |
| + } |
| + return &Key{k.appID, k.namespace, k.toks[:len(k.toks)-1]} |
| +} |
| + |
| +// MarshalJSON allows this key to be automatically marshaled by encoding/json. |
| +func (k *Key) MarshalJSON() ([]byte, error) { |
| + return []byte(`"` + k.Encode() + `"`), nil |
| +} |
| + |
| +// Encode encodes the provided key as a base64-encoded protobuf. |
| +// |
| +// This encoding is compatible with the SDK-provided encoding and is agnostic |
| +// to the underlying implementation of the Key. |
| +// |
| +// It's encoded with the urlsafe base64 table. |
|
dnj
2015/09/18 16:47:58
... with padding stripped off.
iannucci
2015/09/18 22:25:48
Done
|
| +func (k *Key) Encode() string { |
| + e := make([]*pb.Path_Element, len(k.toks)) |
| + for i, t := range k.toks { |
| + t := t |
| + e[i] = &pb.Path_Element{ |
| + Type: &t.Kind, |
| + } |
| + if t.StringID != "" { |
| + e[i].Name = &t.StringID |
| + } else { |
| + e[i].Id = &t.IntID |
| + } |
| + } |
| + var namespace *string |
| + if k.namespace != "" { |
| + namespace = &k.namespace |
| + } |
| + r, err := proto.Marshal(&pb.Reference{ |
| + App: &k.appID, |
| + NameSpace: namespace, |
| + Path: &pb.Path{ |
| + Element: e, |
| + }, |
| + }) |
| + if err != nil { |
| + panic(err) |
| + } |
| + |
| + // trim padding |
| + return strings.TrimRight(base64.URLEncoding.EncodeToString(r), "=") |
| +} |
| + |
| +// UnmarshalJSON allows this key to be automatically unmarshaled by encoding/json. |
| +func (k *Key) UnmarshalJSON(buf []byte) error { |
| + if len(buf) < 2 || buf[0] != '"' || buf[len(buf)-1] != '"' { |
| + return errors.New("datastore: bad JSON key") |
| + } |
| + nk, err := NewKeyEncoded(string(buf[1 : len(buf)-1])) |
| + if err != nil { |
| + return err |
| + } |
| + *k = *nk |
| + return nil |
| +} |
| + |
| +// Root returns the entity root for the given key. |
| +func (k *Key) Root() *Key { |
| + if k != nil && len(k.toks) > 1 { |
| + ret := *k |
| + ret.toks = ret.toks[:1] |
| + return &ret |
| + } |
| + return k |
| +} |
| + |
| +func (k *Key) Less(other *Key) bool { |
| + if k == nil && other != nil { |
| + return true |
| + } else if k != nil && other == nil { |
| + return false |
| + } |
|
dnj
2015/09/18 16:47:58
They could still both be nil. Do:
if k == nil {
iannucci
2015/09/18 22:25:48
none of this exists anymore. You have nil you get
|
| + |
| + if k.appID < other.appID { |
|
dnj
2015/09/18 16:47:58
Slightly better to avoid double string compare and
iannucci
2015/09/18 22:25:48
Which doesn't exist in 1.4 and whose 1.5 implement
dnj
2015/09/18 22:45:12
On 2015/09/18 22:25:48, iannucci wrote:
> On 2015/
|
| + return true |
| + } else if k.appID > other.appID { |
| + return false |
| + } |
| + |
| + if k.namespace < other.namespace { |
| + return true |
| + } else if k.namespace > other.namespace { |
| + return false |
| + } |
| + |
| + lim := len(k.toks) |
| + if len(other.toks) < lim { |
| + lim = len(other.toks) |
| + } |
| + for i := 0; i < lim; i++ { |
| + a, b := k.toks[i], other.toks[i] |
| + if a.Less(b) { |
| + return true |
| + } else if b.Less(a) { |
| + return false |
| + } |
| + } |
| + return len(k.toks) < len(other.toks) |
| +} |
| + |
| +// Equal returns true iff the two keys represent identical key values. |
| +func (k *Key) Equal(other *Key) (ret bool) { |
| + ret = k == nil && other == nil |
| + if !ret && k != nil && other != nil { |
| + ret = (k.appID == other.appID && |
| + k.namespace == other.namespace && |
| + len(k.toks) == len(other.toks)) |
| + if ret { |
| + for i, t := range k.toks { |
| + if ret = t == other.toks[i]; !ret { |
| + return |
| + } |
| + } |
| + } |
| + } |
| + return |
| +} |
| + |
| +func (k *Key) Split() (appID, namespace string, toks []KeyTok) { |
| + if k != nil { |
| + appID = k.appID |
| + namespace = k.namespace |
| + toks = make([]KeyTok, len(k.toks)) |
| + copy(toks, k.toks) |
| + } |
| + return |
| +} |