datastore: load and save elements behind interfaces
Previously we didn't consider elements that
could be behind an interface field e.g.
type Foo struct {
Bar interface{}
}
&Foo{
Bar: "bar",
}
which would produce:
datastore: unsupported struct field type: interface {}
We now extract the underlying element, during both
Loads and Saves.
Fixes #1474
Change-Id: If675f3b9338c9f9f4d6d1c5a7032acce7250f651
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/42771
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Tyler Bui-Palsulich <tbp@google.com>
diff --git a/datastore/integration_test.go b/datastore/integration_test.go
index 2fd8e0d..2c20c2f 100644
--- a/datastore/integration_test.go
+++ b/datastore/integration_test.go
@@ -161,9 +161,10 @@
I int
S string
T time.Time
+ U interface{}
}
- x0 := X{66, "99", timeNow.Truncate(time.Millisecond)}
+ x0 := X{66, "99", timeNow.Truncate(time.Millisecond), "X"}
k, err := client.Put(ctx, IncompleteKey("BasicsX", nil), &x0)
if err != nil {
t.Fatalf("client.Put: %v", err)
diff --git a/datastore/load.go b/datastore/load.go
index e75ccef..ffed38b 100644
--- a/datastore/load.go
+++ b/datastore/load.go
@@ -292,6 +292,18 @@
return overflowReason(x, v)
}
v.SetFloat(x)
+
+ case reflect.Interface:
+ if !v.CanSet() {
+ return fmt.Sprintf("%v is unsettable", v.Type())
+ }
+
+ rpValue := reflect.ValueOf(pValue)
+ if !rpValue.Type().AssignableTo(v.Type()) {
+ return fmt.Sprintf("%q is not assignable to %q", rpValue.Type(), v.Type())
+ }
+ v.Set(rpValue)
+
case reflect.Ptr:
// v must be a pointer to either a Key, an Entity, or one of the supported basic types.
if v.Type() != typeOfKeyPtr && v.Type().Elem().Kind() != reflect.Struct && !isValidPointerType(v.Type().Elem()) {
diff --git a/datastore/load_test.go b/datastore/load_test.go
index f381cdf..24167ba 100644
--- a/datastore/load_test.go
+++ b/datastore/load_test.go
@@ -15,6 +15,7 @@
package datastore
import (
+ "fmt"
"reflect"
"testing"
"time"
@@ -425,6 +426,83 @@
type NestedSimple2 struct {
A *Simple
I int
+ U interface{}
+}
+
+type withTypedInterface struct {
+ Field fmt.Stringer
+}
+
+type withUntypedInterface struct {
+ Field interface{}
+}
+
+func TestLoadToInterface(t *testing.T) {
+ testCases := []struct {
+ name string
+ src *pb.Entity
+ dst interface{}
+ want interface{}
+ wantErr string
+ }{
+ {
+ name: "Typed interface",
+ src: &pb.Entity{
+ Key: keyToProto(testKey0),
+ Properties: map[string]*pb.Value{
+ "Field": {ValueType: &pb.Value_StringValue{
+ StringValue: "Foo",
+ }},
+ },
+ },
+ dst: &withTypedInterface{},
+ wantErr: `datastore: cannot load field "Field" into a "datastore.withTypedInterface": "string" is not assignable to "fmt.Stringer"`,
+ },
+ {
+ name: "Untyped interface, fresh struct",
+ src: &pb.Entity{
+ Key: keyToProto(testKey0),
+ Properties: map[string]*pb.Value{
+ "Field": {ValueType: &pb.Value_StringValue{
+ StringValue: "Foo",
+ }},
+ },
+ },
+ dst: &withUntypedInterface{},
+ want: &withUntypedInterface{Field: "Foo"},
+ },
+ {
+ name: "Untyped interface, already set",
+ src: &pb.Entity{
+ Key: keyToProto(testKey0),
+ Properties: map[string]*pb.Value{
+ "Field": {ValueType: &pb.Value_StringValue{
+ StringValue: "Newly set",
+ }},
+ },
+ },
+ dst: &withUntypedInterface{Field: 1e9},
+ want: &withUntypedInterface{Field: "Newly set"},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := loadEntityProto(tc.dst, tc.src)
+ if tc.wantErr != "" {
+ if err == nil || err.Error() != tc.wantErr {
+ t.Fatalf("Error mismatch\nGot: %s\nWant: %s", err, tc.wantErr)
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("loadEntityProto: %v", err)
+ }
+ if diff := testutil.Diff(tc.dst, tc.want); diff != "" {
+ t.Fatalf("Mismatch: got - want +\n%s", diff)
+ }
+ }
+ })
+ }
}
func TestAlreadyPopulatedDst(t *testing.T) {
@@ -471,6 +549,7 @@
Properties: map[string]*pb.Value{
"A": {ValueType: &pb.Value_NullValue{}},
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
+ "U": {ValueType: &pb.Value_StringValue{StringValue: "replaced"}},
},
},
}},
@@ -483,6 +562,7 @@
&NestedSimple2{
A: &Simple{I: 2},
/* I: 0 */
+ U: 1e9,
},
0,
},
@@ -492,6 +572,7 @@
&NestedSimple2{
/* A: nil, */
I: 2,
+ U: "replaced",
},
5,
},
diff --git a/datastore/save.go b/datastore/save.go
index 2a44469..d92aeff 100644
--- a/datastore/save.go
+++ b/datastore/save.go
@@ -47,27 +47,9 @@
return propertiesToProto(key, props)
}
-// TODO(djd): Convert this and below to return ([]Property, error).
-func saveStructProperty(props *[]Property, name string, opts saveOpts, v reflect.Value) error {
- p := Property{
- Name: name,
- NoIndex: opts.noIndex,
- }
-
- if opts.omitEmpty && isEmptyValue(v) {
- return nil
- }
-
- // First check if field type implements PLS. If so, use PLS to
- // save.
- ok, err := plsFieldSave(props, p, name, opts, v)
- if err != nil {
- return err
- }
- if ok {
- return nil
- }
-
+// reflectFieldSave extracts the underlying value of v by reflection,
+// and tries to extract a Property that'll be appended to props.
+func reflectFieldSave(props *[]Property, p Property, name string, opts saveOpts, v reflect.Value) error {
switch x := v.Interface().(type) {
case *Key, time.Time, GeoPoint:
p.Value = x
@@ -81,6 +63,13 @@
p.Value = v.String()
case reflect.Float32, reflect.Float64:
p.Value = v.Float()
+
+ case reflect.Interface:
+ // Extract the interface's underlying value and then retry the save.
+ // See issue https://github.com/googleapis/google-cloud-go/issues/1474.
+ v = v.Elem()
+ return reflectFieldSave(props, p, name, opts, v)
+
case reflect.Slice:
if v.Type().Elem().Kind() == reflect.Uint8 {
p.Value = v.Bytes()
@@ -141,6 +130,7 @@
}
}
}
+
if p.Value == nil {
return fmt.Errorf("datastore: unsupported struct field type: %v", v.Type())
}
@@ -148,6 +138,30 @@
return nil
}
+// TODO(djd): Convert this and below to return ([]Property, error).
+func saveStructProperty(props *[]Property, name string, opts saveOpts, v reflect.Value) error {
+ p := Property{
+ Name: name,
+ NoIndex: opts.noIndex,
+ }
+
+ if opts.omitEmpty && isEmptyValue(v) {
+ return nil
+ }
+
+ // First check if field type implements PLS. If so, use PLS to
+ // save.
+ ok, err := plsFieldSave(props, p, name, opts, v)
+ if err != nil {
+ return err
+ }
+ if ok {
+ return nil
+ }
+
+ return reflectFieldSave(props, p, name, opts, v)
+}
+
// plsFieldSave first tries to converts v's value to a PLS, then v's addressed
// value to a PLS. If neither succeeds, plsFieldSave returns false for first return
// value.
diff --git a/datastore/save_test.go b/datastore/save_test.go
index 36e9fb0..3ca1ca8 100644
--- a/datastore/save_test.go
+++ b/datastore/save_test.go
@@ -287,3 +287,88 @@
}
}
}
+
+func TestSaveFieldsWithInterface(t *testing.T) {
+ // We should be able to extract the underlying value behind an interface.
+ // See issue https://github.com/googleapis/google-cloud-go/issues/1474.
+
+ type n1 struct {
+ Inner interface{}
+ }
+
+ type n2 struct {
+ Inner2 *n1
+ }
+ type n3 struct {
+ N2 interface{}
+ }
+
+ cases := []struct {
+ name string
+ in interface{}
+ want []Property
+ }{
+ {
+ name: "Non-Nil value",
+ in: &struct {
+ Value interface{}
+ ID int
+ key interface{}
+ }{
+ Value: "this is a string",
+ ID: 17,
+ key: "key1",
+ },
+ want: []Property{
+ {Name: "Value", Value: "this is a string"},
+ {Name: "ID", Value: int64(17)},
+ },
+ },
+ {
+ name: "Nil value",
+ in: &struct {
+ foo interface{}
+ }{
+ foo: (*string)(nil),
+ },
+ want: nil,
+ },
+ {
+ name: "Nested",
+ in: &n3{
+ N2: &n2{
+ Inner2: &n1{
+ Inner: "Innest",
+ },
+ },
+ },
+ want: []Property{
+ {
+ Name: "N2",
+ Value: &Entity{
+ Properties: []Property{{
+ Name: "Inner2",
+ Value: &Entity{
+ Properties: []Property{{
+ Name: "Inner", Value: "Innest",
+ }},
+ },
+ }},
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := SaveStruct(tt.in)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff := testutil.Diff(got, tt.want); diff != "" {
+ t.Fatalf("got - want +\n%s", diff)
+ }
+ })
+ }
+}