Index: service/datastore/key.go |
diff --git a/service/datastore/key.go b/service/datastore/key.go |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e8a0879473dcfc02b2138d0a6f7e4ef48e0d8d28 |
--- /dev/null |
+++ b/service/datastore/key.go |
@@ -0,0 +1,403 @@ |
+// 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 |
+} |
+ |
+// Incomplete returns true iff this token doesn't define either a StringID or |
+// an IntID. |
+func (k KeyTok) Incomplete() bool { |
+ return k.StringID == "" && k.IntID == 0 |
+} |
+ |
+// Special returns true iff this token begins and ends with "__" |
+func (k KeyTok) Special() bool { |
+ return len(k.Kind) >= 2 && k.Kind[:2] == "__" && k.Kind[len(k.Kind)-2:] == "__" |
+} |
+ |
+// ID returns the 'active' id as a Property (either the StringID or the IntID). |
+func (k KeyTok) ID() Property { |
+ if k.StringID != "" { |
+ return Property{value: k.StringID, propType: PTString} |
+ } |
+ return Property{value: k.IntID, propType: PTInt} |
+} |
+ |
+// Less returns true iff k would sort before other. |
+func (k KeyTok) Less(other KeyTok) bool { |
+ if k.Kind < other.Kind { |
+ 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 |
+} |
+ |
+var _ interface { |
+ json.Marshaler |
+ json.Unmarshaler |
+} = (*Key)(nil) |
+ |
+// NewKeyToks 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) |
+ 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; len(elems) > 0; i, elems = i+1, elems[2:] { |
+ knd, ok := elems[0].(string) |
+ if !ok { |
+ panic(fmt.Errorf("datastore.MakeKey: bad kind: %v", elems[i])) |
+ } |
+ t := &toks[i] |
+ t.Kind = knd |
+ switch x := elems[1].(type) { |
+ case string: |
+ t.StringID = x |
+ case int: |
+ t.IntID = int64(x) |
+ case int32: |
+ t.IntID = int64(x) |
+ case int64: |
+ t.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 |
+} |
+ |
+// Last returns the last KeyTok in this Key. Non-nil Keys are always guaranteed |
+// to have at least one token. |
+func (k *Key) Last() KeyTok { |
+ return k.toks[len(k.toks)-1] |
+} |
+ |
+// AppID returns the application ID that this Key is for. |
+func (k *Key) AppID() string { return k.appID } |
+ |
+// 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 { |
+ b := bytes.NewBuffer(make([]byte, 0, 512)) |
+ 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.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 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 without padding. |
+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 len(k.toks) > 1 { |
+ ret := *k |
+ ret.toks = ret.toks[:1] |
+ return &ret |
+ } |
+ return k |
+} |
+ |
+// Less returns true iff k would sort before other. |
+func (k *Key) Less(other *Key) bool { |
+ if k.appID < other.appID { |
+ 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) |
+} |
+ |
+// GQL returns a correctly formatted Cloud Datastore GQL key literal. |
+// |
+// The flavor of GQL that this emits is defined here: |
+// https://cloud.google.com/datastore/docs/apis/gql/gql_reference |
+func (k *Key) GQL() string { |
+ ret := &bytes.Buffer{} |
+ fmt.Fprintf(ret, "KEY(DATASET(%s)", gqlQuoteString(k.appID)) |
+ if k.namespace != "" { |
+ fmt.Fprintf(ret, ", NAMESPACE(%s)", gqlQuoteString(k.namespace)) |
+ } |
+ for _, t := range k.toks { |
+ if t.IntID != 0 { |
+ fmt.Fprintf(ret, ", %s, %d", gqlQuoteString(t.Kind), t.IntID) |
+ } else { |
+ fmt.Fprintf(ret, ", %s, %s", gqlQuoteString(t.Kind), gqlQuoteString(t.StringID)) |
+ } |
+ } |
+ if _, err := ret.WriteString(")"); err != nil { |
+ panic(err) |
+ } |
+ return ret.String() |
+} |
+ |
+// Equal returns true iff the two keys represent identical key values. |
+func (k *Key) Equal(other *Key) (ret bool) { |
+ 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 |
+} |
+ |
+// Split componentizes the key into pieces (AppID, Namespace and tokens) |
+// |
+// Each token represents one piece of they key's 'path'. |
+// |
+// toks is guaranteed to be empty if and only if k is nil. If k is non-nil then |
+// it contains at least one token. |
+func (k *Key) Split() (appID, namespace string, toks []KeyTok) { |
+ appID = k.appID |
+ namespace = k.namespace |
+ toks = make([]KeyTok, len(k.toks)) |
+ copy(toks, k.toks) |
+ return |
+} |