Support embedded anonymous property structs
Allow property structs to contain anonymous embedded structs and interfaces. Properties in an anonymous embedded struct or interface are treated as if they were properties in the embedding struct.
This commit is contained in:
parent
83cedbec85
commit
9d1469d559
4 changed files with 290 additions and 14 deletions
|
@ -181,8 +181,69 @@ var clonePropertiesTestCases = []struct {
|
|||
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)
|
||||
|
|
|
@ -409,6 +409,90 @@ var appendPropertiesTestCases = []struct {
|
|||
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
|
||||
|
||||
|
|
30
unpack.go
30
unpack.go
|
@ -134,8 +134,10 @@ func unpackStructValue(namePrefix string, structValue reflect.Value,
|
|||
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 @@ func unpackStructValue(namePrefix string, structValue reflect.Value,
|
|||
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.
|
||||
|
|
129
unpack_test.go
129
unpack_test.go
|
@ -228,8 +228,137 @@ var validUnpackTestCases = []struct {
|
|||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 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)
|
||||
|
|
Loading…
Reference in a new issue