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)