Merge pull request #83 from colincross/embed
Support embedded anonymous property structs
This commit is contained in:
commit
6afb72ff1b
6 changed files with 394 additions and 21 deletions
|
@ -240,8 +240,15 @@ func newDocs(t *doc.Type) (*PropertyStructDocs, error) {
|
|||
|
||||
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 {
|
||||
|
|
|
@ -140,7 +140,7 @@ func ZeroProperties(structValue reflect.Value) {
|
|||
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 @@ func ZeroProperties(structValue reflect.Value) {
|
|||
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()))
|
||||
|
|
|
@ -130,6 +130,26 @@ var clonePropertiesTestCases = []struct {
|
|||
},
|
||||
},
|
||||
{
|
||||
// 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 @@ 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)
|
||||
|
@ -267,6 +348,25 @@ var cloneEmptyPropertiesTestCases = []struct {
|
|||
S: &struct{ S string }{},
|
||||
},
|
||||
},
|
||||
{
|
||||
// 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{}{},
|
||||
|
@ -295,11 +395,61 @@ var cloneEmptyPropertiesTestCases = []struct {
|
|||
},
|
||||
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 TestCloneEmptyProperties(t *testing.T) {
|
|||
|
||||
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) {
|
||||
|
|
|
@ -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