Add ability to override just the metadata in a struct.
This is useful when the '$id' is actually multiple fields packed into a string,
but it's generally available for any metadata field.
R=dnj@chromium.org, estaab@chromium.org, maruel@chromium.org, tandrii@chromium.org, vadimsh@chromium.org
BUG=https://github.com/luci/gae/issues/15
Review URL: https://codereview.chromium.org/1401533007
diff --git a/service/datastore/pls.go b/service/datastore/pls.go
index 8661372..e3bb319 100644
--- a/service/datastore/pls.go
+++ b/service/datastore/pls.go
@@ -86,6 +86,13 @@
// // 'Lines' will serialized to the datastore in the field 'Lines'
// Lines []string
// }
+//
+// A pointer-to-struct may also implement MetaGetterSetter to provide more
+// sophistocated metadata values. Explicitly defined fields (as shown above)
+// always take precedence over fields manipulated by the MetaGetterSetter
+// methods. So if your GetMeta handles "kind", but you explicitly have a
+// $kind field, the $kind field will take precedence and your GetMeta
+// implementation will not be called for "kind".
func GetPLS(obj interface{}) PropertyLoadSaver {
v := reflect.ValueOf(obj)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
diff --git a/service/datastore/pls_impl.go b/service/datastore/pls_impl.go
index 067ed06..535b8d5 100644
--- a/service/datastore/pls_impl.go
+++ b/service/datastore/pls_impl.go
@@ -35,6 +35,7 @@
byName map[string]int
byIndex []structTag
hasSlice bool
+ mgs bool
problem error
}
@@ -43,6 +44,13 @@
c *structCodec
}
+func (p *structPLS) getMGS() MetaGetterSetter {
+ if !p.c.mgs {
+ return nil
+ }
+ return p.o.Addr().Interface().(MetaGetterSetter)
+}
+
var _ PropertyLoadSaver = (*structPLS)(nil)
// typeMismatchReason returns a string explaining why the property p could not
@@ -289,14 +297,23 @@
if err := p.Problem(); err != nil {
return nil, err
}
- idx, ok := p.c.byMeta[key]
- if !ok {
- if key == "kind" {
- return p.getDefaultKind(), nil
- }
- return nil, ErrMetaFieldUnset
+
+ if idx, ok := p.c.byMeta[key]; ok {
+ return p.getMetaFor(idx), nil
}
- return p.getMetaFor(idx), nil
+
+ if p.c.mgs {
+ ret, err := p.getMGS().GetMeta(key)
+ if err == nil {
+ return ret, err
+ } else if err != ErrMetaFieldUnset {
+ return nil, err
+ }
+ }
+ if key == "kind" {
+ return p.getDefaultKind(), nil
+ }
+ return nil, ErrMetaFieldUnset
}
func (p *structPLS) getMetaFor(idx int) interface{} {
@@ -315,7 +332,15 @@
}
func (p *structPLS) GetAllMeta() PropertyMap {
- ret := make(PropertyMap, len(p.c.byMeta)+1)
+ ret := PropertyMap(nil)
+ needKind := true
+ if p.c.mgs {
+ ret = p.getMGS().GetAllMeta()
+ _, haveKind := ret["$kind"]
+ needKind = !haveKind
+ } else {
+ ret = make(PropertyMap, len(p.c.byMeta)+1)
+ }
for k, idx := range p.c.byMeta {
val := p.getMetaFor(idx)
p := Property{}
@@ -324,8 +349,10 @@
}
ret["$"+k] = []Property{p}
}
- if _, ok := p.c.byMeta["kind"]; !ok {
- ret["$kind"] = []Property{MkPropertyNI(p.getDefaultKind())}
+ if needKind {
+ if _, ok := p.c.byMeta["kind"]; !ok {
+ ret["$kind"] = []Property{MkPropertyNI(p.getDefaultKind())}
+ }
}
return ret
}
@@ -340,6 +367,9 @@
}
idx, ok := p.c.byMeta[key]
if !ok {
+ if p.c.mgs {
+ return p.getMGS().SetMeta(key, val)
+ }
return ErrMetaFieldUnset
}
if !p.c.byIndex[idx].canSet {
@@ -418,6 +448,7 @@
byName: make(map[string]int, t.NumField()),
byMeta: make(map[string]int, t.NumField()),
problem: errRecursiveStruct, // we'll clear this later if it's not recursive
+ mgs: reflect.PtrTo(t).Implements(typeOfMGS),
}
defer func() {
// If the codec has a problem, free up the indexes
diff --git a/service/datastore/pls_test.go b/service/datastore/pls_test.go
index 3ec9867..7cb5a2b 100644
--- a/service/datastore/pls_test.go
+++ b/service/datastore/pls_test.go
@@ -537,6 +537,88 @@
K *Key
}
+type IDParser struct {
+ _kind string `gae:"$kind,CoolKind"`
+
+ // real $id is myParentName|myID
+ parent string `gae:"-"`
+ id int64 `gae:"-"`
+}
+
+var _ MetaGetterSetter = (*IDParser)(nil)
+
+func (i *IDParser) getFullID() string {
+ return fmt.Sprintf("%s|%d", i.parent, i.id)
+}
+
+func (i *IDParser) GetAllMeta() PropertyMap {
+ pm := PropertyMap{}
+ pm.SetMeta("id", i.getFullID())
+ return pm
+}
+
+func (i *IDParser) GetMeta(key string) (interface{}, error) {
+ if key == "id" {
+ return i.getFullID(), nil
+ }
+ return nil, ErrMetaFieldUnset
+}
+
+func (i *IDParser) GetMetaDefault(key string, dflt interface{}) interface{} {
+ return GetMetaDefaultImpl(i.GetMeta, key, dflt)
+}
+
+func (i *IDParser) SetMeta(key string, value interface{}) (err error) {
+ if key == "id" {
+ // let the panics flooowwww
+ vS := strings.SplitN(value.(string), "|", 2)
+ i.parent = vS[0]
+ i.id, err = strconv.ParseInt(vS[1], 10, 64)
+ return
+ }
+ return ErrMetaFieldUnset
+}
+
+type KindOverride struct {
+ ID int64 `gae:"$id"`
+
+ customKind string `gae:"-"`
+}
+
+var _ MetaGetterSetter = (*KindOverride)(nil)
+
+func (i *KindOverride) GetAllMeta() PropertyMap {
+ pm := PropertyMap{}
+ if i.customKind != "" {
+ pm.SetMeta("kind", i.customKind)
+ }
+ return pm
+}
+
+func (i *KindOverride) GetMeta(key string) (interface{}, error) {
+ if key == "kind" && i.customKind != "" {
+ return i.customKind, nil
+ }
+ return nil, ErrMetaFieldUnset
+}
+
+func (i *KindOverride) GetMetaDefault(key string, dflt interface{}) interface{} {
+ return GetMetaDefaultImpl(i.GetMeta, key, dflt)
+}
+
+func (i *KindOverride) SetMeta(key string, value interface{}) error {
+ if key == "kind" {
+ kind := value.(string)
+ if kind != "KindOverride" {
+ i.customKind = kind
+ } else {
+ i.customKind = ""
+ }
+ return nil
+ }
+ return ErrMetaFieldUnset
+}
+
type testCase struct {
desc string
src interface{}
@@ -1718,5 +1800,48 @@
pls := GetPLS(&BadDefault{})
So(pls.Problem().Error(), ShouldContainSubstring, "bad type")
})
+
+ Convey("MetaGetterSetter implementation (IDParser)", func() {
+ idp := &IDParser{parent: "moo", id: 100}
+ pls := GetPLS(idp)
+ So(pls.GetMetaDefault("id", ""), ShouldEqual, "moo|100")
+ So(pls.GetMetaDefault("kind", ""), ShouldEqual, "CoolKind")
+
+ So(pls.SetMeta("kind", "Something"), ShouldErrLike, "unexported field")
+ So(pls.SetMeta("id", "happy|27"), ShouldBeNil)
+
+ So(idp.parent, ShouldEqual, "happy")
+ So(idp.id, ShouldEqual, 27)
+
+ So(pls.GetAllMeta(), ShouldResemble, PropertyMap{
+ "$id": {MkPropertyNI("happy|27")},
+ "$kind": {MkPropertyNI("CoolKind")},
+ })
+ })
+
+ Convey("MetaGetterSetter implementation (KindOverride)", func() {
+ ko := &KindOverride{ID: 20}
+ pls := GetPLS(ko)
+ So(pls.GetMetaDefault("kind", ""), ShouldEqual, "KindOverride")
+
+ ko.customKind = "something"
+ So(pls.GetMetaDefault("kind", ""), ShouldEqual, "something")
+
+ So(pls.SetMeta("kind", "Nerp"), ShouldBeNil)
+ So(ko.customKind, ShouldEqual, "Nerp")
+
+ So(pls.SetMeta("kind", "KindOverride"), ShouldBeNil)
+ So(ko.customKind, ShouldEqual, "")
+
+ So(pls.GetAllMeta(), ShouldResemble, PropertyMap{
+ "$id": {MkPropertyNI(20)},
+ "$kind": {MkPropertyNI("KindOverride")},
+ })
+ ko.customKind = "wut"
+ So(pls.GetAllMeta(), ShouldResemble, PropertyMap{
+ "$id": {MkPropertyNI(20)},
+ "$kind": {MkPropertyNI("wut")},
+ })
+ })
})
}
diff --git a/service/datastore/properties.go b/service/datastore/properties.go
index 60d8b42..9746c67 100644
--- a/service/datastore/properties.go
+++ b/service/datastore/properties.go
@@ -680,15 +680,7 @@
// which was held by this PropertyLoadSaver.
Save(withMeta bool) (PropertyMap, error)
- MetaGetter
-
- // GetAllMeta returns a PropertyMap with all of the metadata in this
- // PropertyLoadSaver. If a metadata field has an error during serialization,
- // it is skipped.
- GetAllMeta() PropertyMap
-
- // SetMeta allows you to set the current value of the meta-keyed field.
- SetMeta(key string, val interface{}) error
+ MetaGetterSetter
// Problem indicates that this PLS has a fatal problem. Usually this is
// set when the underlying struct has recursion, invalid field types, nested
@@ -696,6 +688,30 @@
Problem() error
}
+// MetaGetterSetter is the subset of PropertyLoadSaver which pertains to
+// getting and saving metadata.
+//
+// A *struct may implement this interface to provide metadata which is
+// supplimental to the variety described by GetPLS. For example, this could be
+// used to implement a parsed-out $kind or $id.
+type MetaGetterSetter interface {
+ MetaGetter
+
+ // GetAllMeta returns a PropertyMap with all of the metadata in this
+ // MetaGetterSetter. If a metadata field has an error during serialization,
+ // it is skipped.
+ //
+ // If a *struct is implementing this, then it only needs to return the
+ // metadata fields which would be returned by its GetMeta implementation, and
+ // the `GetPLS` implementation will add any statically-defined metadata
+ // fields. So if GetMeta provides $id, but there's a simple tagged field for
+ // $kind, this method is only expected to return a PropertyMap with "$id".
+ GetAllMeta() PropertyMap
+
+ // SetMeta allows you to set the current value of the meta-keyed field.
+ SetMeta(key string, val interface{}) error
+}
+
// PropertyMap represents the contents of a datastore entity in a generic way.
// It maps from property name to a list of property values which correspond to
// that property name. It is the spiritual successor to PropertyList from the
diff --git a/service/datastore/reflect.go b/service/datastore/reflect.go
index d4b5b78..0ff5e83 100644
--- a/service/datastore/reflect.go
+++ b/service/datastore/reflect.go
@@ -23,4 +23,5 @@
typeOfString = reflect.TypeOf("")
typeOfTime = reflect.TypeOf(time.Time{})
typeOfToggle = reflect.TypeOf(Auto)
+ typeOfMGS = reflect.TypeOf((*MetaGetterSetter)(nil)).Elem()
)