datastore: support for loading nested entity values
This change is an almost-copy of a change submitted to
cloud.google.com/go/datastore
Change-Id: Idbfc200b7a3a76603e02527d9de4e93f3ccb648c
diff --git a/datastore/load.go b/datastore/load.go
index b1cac60..f69dad4 100644
--- a/datastore/load.go
+++ b/datastore/load.go
@@ -10,6 +10,7 @@
"strings"
"time"
+ "github.com/golang/protobuf/proto"
"google.golang.org/appengine"
pb "google.golang.org/appengine/internal/datastore"
)
@@ -21,6 +22,7 @@
typeOfGeoPoint = reflect.TypeOf(appengine.GeoPoint{})
typeOfTime = reflect.TypeOf(time.Time{})
typeOfKeyPtr = reflect.TypeOf(&Key{})
+ typeOfEntityPtr = reflect.TypeOf(&Entity{})
)
// typeMismatchReason returns a string explaining why the property p could not
@@ -146,6 +148,8 @@
meaning = pb.Property_GEORSS_POINT
case typeOfTime:
meaning = pb.Property_GD_WHEN
+ case typeOfEntityPtr:
+ meaning = pb.Property_ENTITY_PROTO
}
var err error
pValue, err = propValue(iv.value, meaning)
@@ -154,7 +158,7 @@
}
}
- if errReason := setVal(pValue, v); errReason != "" {
+ if errReason := setVal(v, pValue); errReason != "" {
// Set the slice back to its zero value.
if slice.IsValid() {
slice.Set(reflect.Zero(slice.Type()))
@@ -169,8 +173,8 @@
return ""
}
-// setVal sets v to the value of Property p.
-func setVal(pValue interface{}, v reflect.Value) string {
+// setVal sets v to the value pValue.
+func setVal(v reflect.Value, pValue interface{}) string {
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
x, ok := pValue.(int64)
@@ -233,7 +237,28 @@
}
v.Set(reflect.ValueOf(x))
default:
- return typeMismatchReason(pValue, v)
+ ent, ok := pValue.(*Entity)
+ if !ok {
+ return typeMismatchReason(pValue, v)
+ }
+
+ // Recursively load nested struct
+ pls, err := newStructPLS(v.Addr().Interface())
+ if err != nil {
+ return err.Error()
+ }
+
+ // if ent has a Key value and our struct has a Key field,
+ // load the Entity's Key value into the Key field on the struct.
+ if ent.Key != nil && pls.codec.keyField != -1 {
+
+ pls.v.Field(pls.codec.keyField).Set(reflect.ValueOf(ent.Key))
+ }
+
+ err = pls.Load(ent.Properties)
+ if err != nil {
+ return err.Error()
+ }
}
case reflect.Slice:
x, ok := pValue.([]byte)
@@ -255,7 +280,8 @@
return ""
}
-// initField is similar to reflect's Value.FieldByIndex, but
+// initField is similar to reflect's Value.FieldByIndex, in that it
+// returns the nested struct field corresponding to index, but it
// initialises any nil pointers encountered when traversing the structure.
func initField(val reflect.Value, index []int) reflect.Value {
for _, i := range index[:len(index)-1] {
@@ -272,14 +298,14 @@
// loadEntity loads an EntityProto into PropertyLoadSaver or struct pointer.
func loadEntity(dst interface{}, src *pb.EntityProto) (err error) {
- props, err := protoToProperties(src)
+ ent, err := protoToEntity(src)
if err != nil {
return err
}
if e, ok := dst.(PropertyLoadSaver); ok {
- return e.Load(props)
+ return e.Load(ent.Properties)
}
- return LoadStruct(dst, props)
+ return LoadStruct(dst, ent.Properties)
}
func (s structPLS) Load(props []Property) error {
@@ -303,9 +329,9 @@
return nil
}
-func protoToProperties(src *pb.EntityProto) ([]Property, error) {
+func protoToEntity(src *pb.EntityProto) (*Entity, error) {
props, rawProps := src.Property, src.RawProperty
- out := make([]Property, 0, len(props)+len(rawProps))
+ outProps := make([]Property, 0, len(props)+len(rawProps))
for {
var (
x *pb.Property
@@ -330,14 +356,21 @@
return nil, err
}
}
- out = append(out, Property{
+ outProps = append(outProps, Property{
Name: x.GetName(),
Value: value,
NoIndex: noIndex,
Multiple: x.GetMultiple(),
})
}
- return out, nil
+
+ var key *Key
+ if src.Key != nil {
+ // Ignore any error, since nested entity values
+ // are allowed to have an invalid key.
+ key, _ = protoToKey(src.Key)
+ }
+ return &Entity{key, outProps}, nil
}
// propValue returns a Go value that combines the raw PropertyValue with a
@@ -359,6 +392,13 @@
return appengine.BlobKey(*v.StringValue), nil
} else if m == pb.Property_BYTESTRING {
return ByteString(*v.StringValue), nil
+ } else if m == pb.Property_ENTITY_PROTO {
+ var ent pb.EntityProto
+ err := proto.Unmarshal([]byte(*v.StringValue), &ent)
+ if err != nil {
+ return nil, err
+ }
+ return protoToEntity(&ent)
} else {
return *v.StringValue, nil
}
diff --git a/datastore/load_test.go b/datastore/load_test.go
new file mode 100644
index 0000000..46029bb
--- /dev/null
+++ b/datastore/load_test.go
@@ -0,0 +1,656 @@
+// Copyright 2016 Google Inc. All Rights Reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package datastore
+
+import (
+ "reflect"
+ "testing"
+
+ proto "github.com/golang/protobuf/proto"
+ pb "google.golang.org/appengine/internal/datastore"
+)
+
+type Simple struct {
+ I int64
+}
+
+type SimpleWithTag struct {
+ I int64 `datastore:"II"`
+}
+
+type NestedSimpleWithTag struct {
+ A SimpleWithTag `datastore:"AA"`
+}
+
+type NestedSliceOfSimple struct {
+ A []Simple
+}
+
+type SimpleTwoFields struct {
+ S string
+ SS string
+}
+
+type NestedSimpleAnonymous struct {
+ Simple
+ X string
+}
+
+type NestedSimple struct {
+ A Simple
+ I int64
+}
+
+type NestedSimple1 struct {
+ A Simple
+ X string
+}
+
+type NestedSimple2X struct {
+ AA NestedSimple
+ A SimpleTwoFields
+ S string
+}
+
+type BDotB struct {
+ B string `datastore:"B.B"`
+}
+
+type ABDotB struct {
+ A BDotB
+}
+
+type MultiAnonymous struct {
+ Simple
+ SimpleTwoFields
+ X string
+}
+
+var (
+ // these values need to be addressable
+ testString2 = "two"
+ testString3 = "three"
+ testInt64 = int64(2)
+
+ fieldNameI = "I"
+ fieldNameX = "X"
+ fieldNameS = "S"
+ fieldNameSS = "SS"
+ fieldNameADotI = "A.I"
+ fieldNameAADotII = "AA.II"
+ fieldNameADotBDotB = "A.B.B"
+)
+
+func TestLoadEntityNestedLegacy(t *testing.T) {
+ testCases := []struct {
+ desc string
+ src *pb.EntityProto
+ want interface{}
+ }{
+ {
+ "nested",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameX,
+ Value: &pb.PropertyValue{
+ StringValue: &testString2,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameADotI,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ },
+ },
+ },
+ &NestedSimple1{
+ A: Simple{I: testInt64},
+ X: testString2,
+ },
+ },
+ {
+ "nested with tag",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameAADotII,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ },
+ },
+ },
+ &NestedSimpleWithTag{
+ A: SimpleWithTag{I: testInt64},
+ },
+ },
+ {
+ "nested with anonymous struct field",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameX,
+ Value: &pb.PropertyValue{
+ StringValue: &testString2,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameI,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ },
+ },
+ },
+ &NestedSimpleAnonymous{
+ Simple: Simple{I: testInt64},
+ X: testString2,
+ },
+ },
+ {
+ "nested with dotted field tag",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameADotBDotB,
+ Value: &pb.PropertyValue{
+ StringValue: &testString2,
+ },
+ },
+ },
+ },
+ &ABDotB{
+ A: BDotB{
+ B: testString2,
+ },
+ },
+ },
+ {
+ "nested with dotted field tag",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameI,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameS,
+ Value: &pb.PropertyValue{
+ StringValue: &testString2,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameSS,
+ Value: &pb.PropertyValue{
+ StringValue: &testString3,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameX,
+ Value: &pb.PropertyValue{
+ StringValue: &testString3,
+ },
+ },
+ },
+ },
+ &MultiAnonymous{
+ Simple: Simple{I: testInt64},
+ SimpleTwoFields: SimpleTwoFields{S: "two", SS: "three"},
+ X: "three",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ dst := reflect.New(reflect.TypeOf(tc.want).Elem()).Interface()
+ err := loadEntity(dst, tc.src)
+ if err != nil {
+ t.Errorf("loadEntity: %s: %v", tc.desc, err)
+ continue
+ }
+
+ if !reflect.DeepEqual(tc.want, dst) {
+ t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, dst, tc.want)
+ }
+ }
+}
+
+type WithKey struct {
+ X string
+ I int64
+ K *Key `datastore:"__key__"`
+}
+
+type NestedWithKey struct {
+ N WithKey
+ Y string
+}
+
+var (
+ incompleteKey = newKey("", nil)
+ invalidKey = newKey("s", incompleteKey)
+
+ // these values need to be addressable
+ fieldNameA = "A"
+ fieldNameK = "K"
+ fieldNameN = "N"
+ fieldNameY = "Y"
+ fieldNameAA = "AA"
+ fieldNameII = "II"
+ fieldNameBDotB = "B.B"
+
+ entityProtoMeaning = pb.Property_ENTITY_PROTO
+
+ TRUE = true
+ FALSE = false
+)
+
+var (
+ simpleEntityProto, nestedSimpleEntityProto,
+ simpleTwoFieldsEntityProto, simpleWithTagEntityProto,
+ bDotBEntityProto, withKeyEntityProto string
+)
+
+func init() {
+ // simpleEntityProto corresponds to:
+ // Simple{I: testInt64}
+ simpleEntityProtob, err := proto.Marshal(&pb.EntityProto{
+ Key: keyToProto("", incompleteKey),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameI,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ Multiple: &FALSE,
+ },
+ },
+ EntityGroup: &pb.Path{},
+ })
+ if err != nil {
+ panic(err)
+ }
+ simpleEntityProto = string(simpleEntityProtob)
+
+ // nestedSimpleEntityProto corresponds to:
+ // NestedSimple{
+ // A: Simple{I: testInt64},
+ // I: testInt64,
+ // }
+ nestedSimpleEntityProtob, err := proto.Marshal(&pb.EntityProto{
+ Key: keyToProto("", incompleteKey),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameA,
+ Meaning: &entityProtoMeaning,
+ Value: &pb.PropertyValue{
+ StringValue: &simpleEntityProto,
+ },
+ Multiple: &FALSE,
+ },
+ &pb.Property{
+ Name: &fieldNameI,
+ Meaning: &entityProtoMeaning,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ Multiple: &FALSE,
+ },
+ },
+ EntityGroup: &pb.Path{},
+ })
+ if err != nil {
+ panic(err)
+ }
+ nestedSimpleEntityProto = string(nestedSimpleEntityProtob)
+
+ // simpleTwoFieldsEntityProto corresponds to:
+ // SimpleTwoFields{S: testString2, SS: testString3}
+ simpleTwoFieldsEntityProtob, err := proto.Marshal(&pb.EntityProto{
+ Key: keyToProto("", incompleteKey),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameS,
+ Value: &pb.PropertyValue{
+ StringValue: &testString2,
+ },
+ Multiple: &FALSE,
+ },
+ &pb.Property{
+ Name: &fieldNameSS,
+ Value: &pb.PropertyValue{
+ StringValue: &testString3,
+ },
+ Multiple: &FALSE,
+ },
+ },
+ EntityGroup: &pb.Path{},
+ })
+ if err != nil {
+ panic(err)
+ }
+ simpleTwoFieldsEntityProto = string(simpleTwoFieldsEntityProtob)
+
+ // simpleWithTagEntityProto corresponds to:
+ // SimpleWithTag{I: testInt64}
+ simpleWithTagEntityProtob, err := proto.Marshal(&pb.EntityProto{
+ Key: keyToProto("", incompleteKey),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameII,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ Multiple: &FALSE,
+ },
+ },
+ EntityGroup: &pb.Path{},
+ })
+ if err != nil {
+ panic(err)
+ }
+ simpleWithTagEntityProto = string(simpleWithTagEntityProtob)
+
+ // bDotBEntityProto corresponds to:
+ // BDotB{
+ // B: testString2,
+ // }
+ bDotBEntityProtob, err := proto.Marshal(&pb.EntityProto{
+ Key: keyToProto("", incompleteKey),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameBDotB,
+ Value: &pb.PropertyValue{
+ StringValue: &testString2,
+ },
+ Multiple: &FALSE,
+ },
+ },
+ EntityGroup: &pb.Path{},
+ })
+ if err != nil {
+ panic(err)
+ }
+ bDotBEntityProto = string(bDotBEntityProtob)
+
+ // withKeyEntityProto corresponds to:
+ // WithKey{
+ // X: testString3,
+ // I: testInt64,
+ // K: testKey1a,
+ // }
+ withKeyEntityProtob, err := proto.Marshal(&pb.EntityProto{
+ Key: keyToProto("", testKey1a),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameX,
+ Value: &pb.PropertyValue{
+ StringValue: &testString3,
+ },
+ Multiple: &FALSE,
+ },
+ &pb.Property{
+ Name: &fieldNameI,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ Multiple: &FALSE,
+ },
+ },
+ EntityGroup: &pb.Path{},
+ })
+ if err != nil {
+ panic(err)
+ }
+ withKeyEntityProto = string(withKeyEntityProtob)
+
+}
+
+func TestLoadEntityNested(t *testing.T) {
+ testCases := []struct {
+ desc string
+ src *pb.EntityProto
+ want interface{}
+ }{
+ {
+ "nested basic",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameA,
+ Meaning: &entityProtoMeaning,
+ Value: &pb.PropertyValue{
+ StringValue: &simpleEntityProto,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameI,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ },
+ },
+ },
+ &NestedSimple{
+ A: Simple{I: 2},
+ I: 2,
+ },
+ },
+ {
+ "nested with struct tags",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameAA,
+ Meaning: &entityProtoMeaning,
+ Value: &pb.PropertyValue{
+ StringValue: &simpleWithTagEntityProto,
+ },
+ },
+ },
+ },
+ &NestedSimpleWithTag{
+ A: SimpleWithTag{I: testInt64},
+ },
+ },
+ {
+ "nested 2x",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameAA,
+ Meaning: &entityProtoMeaning,
+ Value: &pb.PropertyValue{
+ StringValue: &nestedSimpleEntityProto,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameA,
+ Meaning: &entityProtoMeaning,
+ Value: &pb.PropertyValue{
+ StringValue: &simpleTwoFieldsEntityProto,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameS,
+ Value: &pb.PropertyValue{
+ StringValue: &testString3,
+ },
+ },
+ },
+ },
+ &NestedSimple2X{
+ AA: NestedSimple{
+ A: Simple{I: testInt64},
+ I: testInt64,
+ },
+ A: SimpleTwoFields{S: testString2, SS: testString3},
+ S: testString3,
+ },
+ },
+ {
+ "nested anonymous",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameI,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameX,
+ Value: &pb.PropertyValue{
+ StringValue: &testString2,
+ },
+ },
+ },
+ },
+ &NestedSimpleAnonymous{
+ Simple: Simple{I: testInt64},
+ X: testString2,
+ },
+ },
+ {
+ "nested simple with slice",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameA,
+ Meaning: &entityProtoMeaning,
+ Multiple: &TRUE,
+ Value: &pb.PropertyValue{
+ StringValue: &simpleEntityProto,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameA,
+ Meaning: &entityProtoMeaning,
+ Multiple: &TRUE,
+ Value: &pb.PropertyValue{
+ StringValue: &simpleEntityProto,
+ },
+ },
+ },
+ },
+ &NestedSliceOfSimple{
+ A: []Simple{Simple{I: testInt64}, Simple{I: testInt64}},
+ },
+ },
+ {
+ "nested with multiple anonymous fields",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameI,
+ Value: &pb.PropertyValue{
+ Int64Value: &testInt64,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameS,
+ Value: &pb.PropertyValue{
+ StringValue: &testString2,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameSS,
+ Value: &pb.PropertyValue{
+ StringValue: &testString3,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameX,
+ Value: &pb.PropertyValue{
+ StringValue: &testString2,
+ },
+ },
+ },
+ },
+ &MultiAnonymous{
+ Simple: Simple{I: testInt64},
+ SimpleTwoFields: SimpleTwoFields{S: testString2, SS: testString3},
+ X: testString2,
+ },
+ },
+ {
+ "nested with dotted field tag",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameA,
+ Meaning: &entityProtoMeaning,
+ Value: &pb.PropertyValue{
+ StringValue: &bDotBEntityProto,
+ },
+ },
+ },
+ },
+ &ABDotB{
+ A: BDotB{
+ B: testString2,
+ },
+ },
+ },
+ {
+ "nested entity with key",
+ &pb.EntityProto{
+ Key: keyToProto("some-app-id", testKey0),
+ Property: []*pb.Property{
+ &pb.Property{
+ Name: &fieldNameY,
+ Value: &pb.PropertyValue{
+ StringValue: &testString2,
+ },
+ },
+ &pb.Property{
+ Name: &fieldNameN,
+ Meaning: &entityProtoMeaning,
+ Value: &pb.PropertyValue{
+ StringValue: &withKeyEntityProto,
+ },
+ },
+ },
+ },
+ &NestedWithKey{
+ Y: testString2,
+ N: WithKey{
+ X: testString3,
+ I: testInt64,
+ K: testKey1a,
+ },
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ dst := reflect.New(reflect.TypeOf(tc.want).Elem()).Interface()
+ err := loadEntity(dst, tc.src)
+ if err != nil {
+ t.Errorf("loadEntity: %s: %v", tc.desc, err)
+ continue
+ }
+
+ if !reflect.DeepEqual(tc.want, dst) {
+ t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, dst, tc.want)
+ }
+ }
+}
diff --git a/datastore/prop.go b/datastore/prop.go
index 905bcf0..55ab9fb 100644
--- a/datastore/prop.go
+++ b/datastore/prop.go
@@ -36,6 +36,7 @@
// - appengine.BlobKey
// - appengine.GeoPoint
// - []byte (up to 1 megabyte in length)
+ // - *Entity (representing a nested struct)
// This set is smaller than the set of valid struct field types that the
// datastore can load and save. A Property Value cannot be a slice (apart
// from []byte); use multiple Properties instead. Also, a Value's type
@@ -63,6 +64,13 @@
Multiple bool
}
+// An Entity is the value type for a nested struct.
+// This type is only used for a Property's Value.
+type Entity struct {
+ Key *Key
+ Properties []Property
+}
+
// ByteString is a short byte slice (up to 1500 bytes) that can be indexed.
type ByteString []byte
@@ -127,6 +135,10 @@
// hasSlice is whether a struct or any of its nested or embedded structs
// has a slice-typed field (other than []byte).
hasSlice bool
+ // keyField is the index of a *Key field with structTag __key__.
+ // This field is not relevant for the top level struct, only for
+ // nested structs.
+ keyField int
// complete is whether the structCodec is complete. An incomplete
// structCodec may be encountered when walking a recursive struct.
complete bool
@@ -165,6 +177,9 @@
}
c = &structCodec{
fields: make(map[string]fieldCodec),
+ // We initialize keyField to -1 so that the zero-value is not
+ // misinterpreted as index 0.
+ keyField: -1,
}
// Add c to the structCodecs map before we are sure it is good. If t is
@@ -192,13 +207,19 @@
for _, t := range tags[1:] {
opts[t] = true
}
- if name == "" {
+ switch {
+ case name == "":
if !f.Anonymous {
name = f.Name
}
- } else if name == "-" {
+ case name == "-":
continue
- } else if !validPropertyName(name) {
+ case name == "__key__":
+ if f.Type != typeOfKeyPtr {
+ return nil, fmt.Errorf("datastore: __key__ field on struct %v is not a *datastore.Key", t)
+ }
+ c.keyField = i
+ case !validPropertyName(name):
return nil, fmt.Errorf("datastore: struct tag has invalid property name: %q", name)
}
@@ -266,8 +287,9 @@
codec *structCodec
}
-// newStructPLS returns a PropertyLoadSaver for the struct pointer p.
-func newStructPLS(p interface{}) (PropertyLoadSaver, error) {
+// newStructPLS returns a structPLS, which implements the
+// PropertyLoadSaver interface, for the struct pointer p.
+func newStructPLS(p interface{}) (*structPLS, error) {
v := reflect.ValueOf(p)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
return nil, ErrInvalidEntityType
@@ -277,7 +299,7 @@
if err != nil {
return nil, err
}
- return structPLS{v, codec}, nil
+ return &structPLS{v, codec}, nil
}
// LoadStruct loads the properties from p to dst.
diff --git a/datastore/save.go b/datastore/save.go
index d3ecba3..b09e8b3 100644
--- a/datastore/save.go
+++ b/datastore/save.go
@@ -166,7 +166,7 @@
if err != nil {
return fmt.Errorf("datastore: unsupported struct field: %v", err)
}
- return sub.(structPLS).save(props, name+".", noIndex, multiple)
+ return sub.save(props, name+".", noIndex, multiple)
}
}
if p.Value == nil {