Merge pull request #83 from colincross/embed
Support embedded anonymous property structs
diff --git a/bootstrap/bpdoc/bpdoc.go b/bootstrap/bpdoc/bpdoc.go
index c9af9c8..d4b52a5 100644
--- a/bootstrap/bpdoc/bpdoc.go
+++ b/bootstrap/bpdoc/bpdoc.go
@@ -240,8 +240,15 @@
func structProperties(structType *ast.StructType) (props []PropertyDocs, err error) {
for _, f := range structType.Fields.List {
- //fmt.Printf("%T %#v\n", f, f)
- for _, n := range f.Names {
+ names := f.Names
+ if names == nil {
+ // Anonymous fields have no name, use the type as the name
+ // TODO: hide the name and make the properties show up in the embedding struct
+ if t, ok := f.Type.(*ast.Ident); ok {
+ names = append(names, t)
+ }
+ }
+ for _, n := range names {
var name, typ, tag, text string
var innerProps []PropertyDocs
if n != nil {
diff --git a/proptools/clone.go b/proptools/clone.go
index 7496584..3b23a57 100644
--- a/proptools/clone.go
+++ b/proptools/clone.go
@@ -140,7 +140,7 @@
fieldValue := structValue.Field(i)
switch fieldValue.Kind() {
- case reflect.Bool, reflect.String, reflect.Struct, reflect.Slice, reflect.Int, reflect.Uint:
+ case reflect.Bool, reflect.String, reflect.Slice, reflect.Int, reflect.Uint:
fieldValue.Set(reflect.Zero(fieldValue.Type()))
case reflect.Interface:
if fieldValue.IsNil() {
@@ -172,7 +172,8 @@
panic(fmt.Errorf("can't zero field %q: points to a %s",
field.Name, fieldValue.Elem().Kind()))
}
-
+ case reflect.Struct:
+ ZeroProperties(fieldValue)
default:
panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
field.Name, fieldValue.Kind()))
diff --git a/proptools/clone_test.go b/proptools/clone_test.go
index f7460cf..0679c12 100644
--- a/proptools/clone_test.go
+++ b/proptools/clone_test.go
@@ -130,6 +130,26 @@
},
},
{
+ // Clone nested interface
+ in: &struct {
+ Nested struct{ S interface{} }
+ }{
+ Nested: struct{ S interface{} }{
+ S: &struct{ S string }{
+ S: "string1",
+ },
+ },
+ },
+ out: &struct {
+ Nested struct{ S interface{} }
+ }{
+ Nested: struct{ S interface{} }{
+ S: &struct{ S string }{
+ S: "string1",
+ },
+ },
+ },
+ }, {
// Empty struct
in: &struct{}{},
out: &struct{}{},
@@ -161,8 +181,69 @@
S: nil,
},
},
+ {
+ // Anonymous struct
+ in: &struct {
+ EmbeddedStruct
+ Nested struct{ EmbeddedStruct }
+ }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string1",
+ },
+ Nested: struct{ EmbeddedStruct }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string2",
+ },
+ },
+ },
+ out: &struct {
+ EmbeddedStruct
+ Nested struct{ EmbeddedStruct }
+ }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string1",
+ },
+ Nested: struct{ EmbeddedStruct }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string2",
+ },
+ },
+ },
+ },
+ {
+ // Anonymous interface
+ in: &struct {
+ EmbeddedInterface
+ Nested struct{ EmbeddedInterface }
+ }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string1",
+ },
+ Nested: struct{ EmbeddedInterface }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string2",
+ },
+ },
+ },
+ out: &struct {
+ EmbeddedInterface
+ Nested struct{ EmbeddedInterface }
+ }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string1",
+ },
+ Nested: struct{ EmbeddedInterface }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string2",
+ },
+ },
+ },
+ },
}
+type EmbeddedStruct struct{ S string }
+type EmbeddedInterface interface{}
+
func TestCloneProperties(t *testing.T) {
for _, testCase := range clonePropertiesTestCases {
testString := fmt.Sprintf("%s", testCase.in)
@@ -268,6 +349,25 @@
},
},
{
+ // Clone nested interface
+ in: &struct {
+ Nested struct{ S interface{} }
+ }{
+ Nested: struct{ S interface{} }{
+ S: &struct{ S string }{
+ S: "string1",
+ },
+ },
+ },
+ out: &struct {
+ Nested struct{ S interface{} }
+ }{
+ Nested: struct{ S interface{} }{
+ S: &struct{ S string }{},
+ },
+ },
+ },
+ {
// Empty struct
in: &struct{}{},
out: &struct{}{},
@@ -295,11 +395,61 @@
},
out: &struct{ S *struct{} }{},
},
+ {
+ // Anonymous struct
+ in: &struct {
+ EmbeddedStruct
+ Nested struct{ EmbeddedStruct }
+ }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string1",
+ },
+ Nested: struct{ EmbeddedStruct }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string2",
+ },
+ },
+ },
+ out: &struct {
+ EmbeddedStruct
+ Nested struct{ EmbeddedStruct }
+ }{
+ EmbeddedStruct: EmbeddedStruct{},
+ Nested: struct{ EmbeddedStruct }{
+ EmbeddedStruct: EmbeddedStruct{},
+ },
+ },
+ },
+ {
+ // Anonymous interface
+ in: &struct {
+ EmbeddedInterface
+ Nested struct{ EmbeddedInterface }
+ }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string1",
+ },
+ Nested: struct{ EmbeddedInterface }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string2",
+ },
+ },
+ },
+ out: &struct {
+ EmbeddedInterface
+ Nested struct{ EmbeddedInterface }
+ }{
+ EmbeddedInterface: &struct{ S string }{},
+ Nested: struct{ EmbeddedInterface }{
+ EmbeddedInterface: &struct{ S string }{},
+ },
+ },
+ },
}
func TestCloneEmptyProperties(t *testing.T) {
for _, testCase := range cloneEmptyPropertiesTestCases {
- testString := fmt.Sprintf("%s", testCase.in)
+ testString := fmt.Sprintf("%#v", testCase.in)
got := CloneEmptyProperties(reflect.ValueOf(testCase.in).Elem()).Interface()
@@ -314,9 +464,9 @@
func TestZeroProperties(t *testing.T) {
for _, testCase := range cloneEmptyPropertiesTestCases {
- testString := fmt.Sprintf("%s", testCase.in)
+ testString := fmt.Sprintf("%#v", testCase.in)
- got := CloneEmptyProperties(reflect.ValueOf(testCase.in).Elem()).Interface()
+ got := CloneProperties(reflect.ValueOf(testCase.in).Elem()).Interface()
ZeroProperties(reflect.ValueOf(got).Elem())
if !reflect.DeepEqual(testCase.out, got) {
diff --git a/proptools/extend_test.go b/proptools/extend_test.go
index 35a1816..64b7040 100644
--- a/proptools/extend_test.go
+++ b/proptools/extend_test.go
@@ -409,6 +409,90 @@
S: nil,
},
},
+ {
+ // Anonymous struct
+ in1: &struct {
+ EmbeddedStruct
+ Nested struct{ EmbeddedStruct }
+ }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string1",
+ },
+ Nested: struct{ EmbeddedStruct }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string2",
+ },
+ },
+ },
+ in2: &struct {
+ EmbeddedStruct
+ Nested struct{ EmbeddedStruct }
+ }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string3",
+ },
+ Nested: struct{ EmbeddedStruct }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string4",
+ },
+ },
+ },
+ out: &struct {
+ EmbeddedStruct
+ Nested struct{ EmbeddedStruct }
+ }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string1string3",
+ },
+ Nested: struct{ EmbeddedStruct }{
+ EmbeddedStruct: EmbeddedStruct{
+ S: "string2string4",
+ },
+ },
+ },
+ },
+ {
+ // Anonymous interface
+ in1: &struct {
+ EmbeddedInterface
+ Nested struct{ EmbeddedInterface }
+ }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string1",
+ },
+ Nested: struct{ EmbeddedInterface }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string2",
+ },
+ },
+ },
+ in2: &struct {
+ EmbeddedInterface
+ Nested struct{ EmbeddedInterface }
+ }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string3",
+ },
+ Nested: struct{ EmbeddedInterface }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string4",
+ },
+ },
+ },
+ out: &struct {
+ EmbeddedInterface
+ Nested struct{ EmbeddedInterface }
+ }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string1string3",
+ },
+ Nested: struct{ EmbeddedInterface }{
+ EmbeddedInterface: &struct{ S string }{
+ S: "string2string4",
+ },
+ },
+ },
+ },
// Errors
diff --git a/unpack.go b/unpack.go
index 64c65e4..b023956 100644
--- a/unpack.go
+++ b/unpack.go
@@ -134,8 +134,10 @@
continue
}
+ propertyName := namePrefix + proptools.PropertyNameForField(field.Name)
+
if !fieldValue.CanSet() {
- panic(fmt.Errorf("field %s is not settable", field.Name))
+ panic(fmt.Errorf("field %s is not settable", propertyName))
}
// To make testing easier we validate the struct field's type regardless
@@ -146,47 +148,47 @@
case reflect.Slice:
elemType := field.Type.Elem()
if elemType.Kind() != reflect.String {
- panic(fmt.Errorf("field %s is a non-string slice", field.Name))
+ panic(fmt.Errorf("field %s is a non-string slice", propertyName))
}
case reflect.Interface:
if fieldValue.IsNil() {
- panic(fmt.Errorf("field %s contains a nil interface",
- field.Name))
+ panic(fmt.Errorf("field %s contains a nil interface", propertyName))
}
fieldValue = fieldValue.Elem()
elemType := fieldValue.Type()
if elemType.Kind() != reflect.Ptr {
- panic(fmt.Errorf("field %s contains a non-pointer interface",
- field.Name))
+ panic(fmt.Errorf("field %s contains a non-pointer interface", propertyName))
}
fallthrough
case reflect.Ptr:
switch ptrKind := fieldValue.Type().Elem().Kind(); ptrKind {
case reflect.Struct:
if fieldValue.IsNil() {
- panic(fmt.Errorf("field %s contains a nil pointer",
- field.Name))
+ panic(fmt.Errorf("field %s contains a nil pointer", propertyName))
}
fieldValue = fieldValue.Elem()
case reflect.Bool, reflect.String:
// Nothing
default:
- panic(fmt.Errorf("field %s contains a pointer to %s",
- field.Name, ptrKind))
+ panic(fmt.Errorf("field %s contains a pointer to %s", propertyName, ptrKind))
}
case reflect.Int, reflect.Uint:
if !proptools.HasTag(field, "blueprint", "mutated") {
- panic(fmt.Errorf(`int field %s must be tagged blueprint:"mutated"`, field.Name))
+ panic(fmt.Errorf(`int field %s must be tagged blueprint:"mutated"`, propertyName))
}
default:
- panic(fmt.Errorf("unsupported kind for field %s: %s",
- field.Name, kind))
+ panic(fmt.Errorf("unsupported kind for field %s: %s", propertyName, kind))
+ }
+
+ if field.Anonymous && fieldValue.Kind() == reflect.Struct {
+ newErrs := unpackStructValue(namePrefix, fieldValue, propertyMap, filterKey, filterValue)
+ errs = append(errs, newErrs...)
+ continue
}
// Get the property value if it was specified.
- propertyName := namePrefix + proptools.PropertyNameForField(field.Name)
packedProperty, ok := propertyMap[propertyName]
if !ok {
// This property wasn't specified.
diff --git a/unpack_test.go b/unpack_test.go
index 77a57ec..b33ae79 100644
--- a/unpack_test.go
+++ b/unpack_test.go
@@ -228,8 +228,137 @@
},
},
},
+
+ // Anonymous struct
+ {`
+ m {
+ name: "abc",
+ nested: {
+ name: "def",
+ },
+ }
+ `,
+ struct {
+ EmbeddedStruct
+ Nested struct {
+ EmbeddedStruct
+ }
+ }{
+ EmbeddedStruct: EmbeddedStruct{
+ Name: "abc",
+ },
+ Nested: struct {
+ EmbeddedStruct
+ }{
+ EmbeddedStruct: EmbeddedStruct{
+ Name: "def",
+ },
+ },
+ },
+ nil,
+ },
+
+ // Anonymous interface
+ {`
+ m {
+ name: "abc",
+ nested: {
+ name: "def",
+ },
+ }
+ `,
+ struct {
+ EmbeddedInterface
+ Nested struct {
+ EmbeddedInterface
+ }
+ }{
+ EmbeddedInterface: &struct{ Name string }{
+ Name: "abc",
+ },
+ Nested: struct {
+ EmbeddedInterface
+ }{
+ EmbeddedInterface: &struct{ Name string }{
+ Name: "def",
+ },
+ },
+ },
+ nil,
+ },
+
+ // Anonymous struct with name collision
+ {`
+ m {
+ name: "abc",
+ nested: {
+ name: "def",
+ },
+ }
+ `,
+ struct {
+ Name string
+ EmbeddedStruct
+ Nested struct {
+ Name string
+ EmbeddedStruct
+ }
+ }{
+ Name: "abc",
+ EmbeddedStruct: EmbeddedStruct{
+ Name: "abc",
+ },
+ Nested: struct {
+ Name string
+ EmbeddedStruct
+ }{
+ Name: "def",
+ EmbeddedStruct: EmbeddedStruct{
+ Name: "def",
+ },
+ },
+ },
+ nil,
+ },
+
+ // Anonymous interface with name collision
+ {`
+ m {
+ name: "abc",
+ nested: {
+ name: "def",
+ },
+ }
+ `,
+ struct {
+ Name string
+ EmbeddedInterface
+ Nested struct {
+ Name string
+ EmbeddedInterface
+ }
+ }{
+ Name: "abc",
+ EmbeddedInterface: &struct{ Name string }{
+ Name: "abc",
+ },
+ Nested: struct {
+ Name string
+ EmbeddedInterface
+ }{
+ Name: "def",
+ EmbeddedInterface: &struct{ Name string }{
+ Name: "def",
+ },
+ },
+ },
+ nil,
+ },
}
+type EmbeddedStruct struct{ Name string }
+type EmbeddedInterface interface{}
+
func TestUnpackProperties(t *testing.T) {
for _, testCase := range validUnpackTestCases {
r := bytes.NewBufferString(testCase.input)