From 1dde0aee6ad1367be30b76f6ae5616729c11eeb2 Mon Sep 17 00:00:00 2001 From: bonny Date: Mon, 2 Sep 2019 23:58:26 -0700 Subject: [PATCH 1/5] added DecodeInto version to support local/nested oneOf --- decode/decode_test.go | 434 ++++++++++++++++++++++++++++++++++-------- decode/decoder.go | 235 ++++++++++++++++++++++- 2 files changed, 582 insertions(+), 87 deletions(-) diff --git a/decode/decode_test.go b/decode/decode_test.go index 5b4daa0..b747000 100644 --- a/decode/decode_test.go +++ b/decode/decode_test.go @@ -5,17 +5,16 @@ package decode_test import ( - "testing" - "fmt" "encoding/json" - + "fmt" . "github.com/smartystreets/goconvey/convey" "github.com/weberr13/go-decode/decode" + "testing" ) type SubRecord struct { - kind string - Name *string + kind string + Name *string } type MyString string @@ -29,10 +28,10 @@ func NewSubRecord() interface{} { } type SubRecord2 struct { - kind string - Name MyString + kind string + Name MyString PtrName *MyString - Subs []SubRecord + Subs []SubRecord } func (r SubRecord2) Discriminator() string { @@ -46,12 +45,12 @@ func NewSubRecord2() interface{} { } type Record struct { - kind string - Name string + kind string + Name string Optional *string - Num *int - Slice []string - Sub interface{} + Num *int + Slice []string + Sub interface{} } func (r Record) Discriminator() string { @@ -64,10 +63,209 @@ func NewRecord() interface{} { } } +type Envelope struct { + Pets []*BasePet +} + +type decoderDesc struct { +} + +func (dd decoderDesc) Make(pp, dk, dv string) (interface{}, error) { + return TypeFactory(fmt.Sprintf("%s(%s=%s)", pp, dk, dv)) +} + +func (dd decoderDesc) DiscriminatorFor(path string) string { + disc, ok := DiscriminatedOneOfSchemaMap[path] + if !ok { + return "" + } + return disc +} + +// Bark defines model for Bark. +type Bark struct { + Type *string `json:"type,omitempty"` + Volume *int `json:"volume,omitempty"` +} + +// BasePet defines model for BasePet. +type BasePet struct { + Age *int `json:"age,omitempty"` + Classes *[]PetHeritage `json:"classes,omitempty"` + Heritage *PetHeritage `json:"heritage,omitempty"` + Name *string `json:"name,omitempty"` + NestedHeritage interface{} `json:"nestedHeritage,omitempty"` + Species interface{} `json:"species,omitempty"` +} + +// Cat defines model for Cat. +type Cat struct { + Action interface{} `json:"action,omitempty"` + Mood *string `json:"mood,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Dog defines model for Dog. +type Dog struct { + Action interface{} `json:"action,omitempty"` + Kind *string `json:"kind,omitempty"` + Type *string `json:"type,omitempty"` +} + +// House defines model for House. +type House struct { + Name *string `json:"name,omitempty"` + Rooms *int `json:"rooms,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Meow defines model for Meow. +type Meow struct { + Squeel *string `json:"squeel,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Palace defines model for Palace. +type Palace struct { + Halls *int `json:"Halls,omitempty"` + Name *string `json:"name,omitempty"` + Towers *int `json:"towers,omitempty"` + Type *string `json:"type,omitempty"` +} + +// PetHeritage defines model for PetHeritage. +type PetHeritage struct { + Class interface{} `json:"class,omitempty"` + Name *string `json:"name,omitempty"` +} + +// Purr defines model for Purr. +type Purr struct { + Heritage *PetHeritage `json:"heritage,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Shack defines model for Shack. +type Shack struct { + Material *string `json:"material,omitempty"` + Name *string `json:"name,omitempty"` + Type *string `json:"type,omitempty"` +} + +func TypeFactory(kind string) (interface{}, error) { + fm := map[string]func() interface{}{ + "BasePet.heritage.class(type=House)": NewHouse, + "BasePet.heritage.class(type=Palace)": NewPalace, + "BasePet.heritage.class(type=Shack)": NewShack, + "BasePet.nestedHeritage(type=House)": NewHouse, + "BasePet.nestedHeritage(type=Palace)": NewPalace, + "BasePet.species(type=Cat)": NewCat, + "BasePet.species(type=Dog)": NewDog, + "Cat.action(type=Meow)": NewMeow, + "Cat.action(type=Purr)": NewPurr, + "Dog.action(type=Bark)": NewBark, + "PetHeritage.class(type=House)": NewHouse, + "PetHeritage.class(type=Palace)": NewPalace, + "PetHeritage.class(type=Shack)": NewShack, + "Purr.heritage.class(type=House)": NewHouse, + "Purr.heritage.class(type=Palace)": NewPalace, + "Purr.heritage.class(type=Shack)": NewShack, + } + f, ok := fm[kind] + if !ok { + return nil, fmt.Errorf("cannot find type %s", kind) + } + return f(), nil +} + +// Map +var DiscriminatedOneOfSchemaMap = map[string]string{ + + "BasePet.heritage.class": "type", + "BasePet.nestedHeritage": "type", + "BasePet.species": "type", + "Cat.action": "type", + "Dog.action": "type", + "PetHeritage.class": "type", + "Purr.heritage.class": "type", +} + +func NewBark() interface{} { + _d := "Type" + return &Bark{Type: &_d} +} + +func (r Bark) Discriminator() string { + return "type" +} + +func NewCat() interface{} { + _d := "Type" + return &Cat{Type: &_d} +} + +func (r Cat) Discriminator() string { + return "type" +} + +func NewDog() interface{} { + _d := "Type" + return &Dog{Type: &_d} +} + +func (r Dog) Discriminator() string { + return "type" +} + +func NewHouse() interface{} { + _d := "Type" + return &House{Type: &_d} +} + +func (r House) Discriminator() string { + return "type" +} + +func NewMeow() interface{} { + _d := "Type" + return &Meow{Type: &_d} +} + +func (r Meow) Discriminator() string { + return "type" +} + +func NewPalace() interface{} { + _d := "Type" + return &Palace{Type: &_d} +} + +func (r Palace) Discriminator() string { + return "type" +} + +func NewPurr() interface{} { + _d := "Type" + return &Purr{Type: &_d} +} + +func (r Purr) Discriminator() string { + return "type" +} + +func NewShack() interface{} { + _d := "Type" + return &Shack{Type: &_d} +} + +func (r Shack) Discriminator() string { + return "type" +} + func MyTestFactory(kind string) (interface{}, error) { - fm := map[string]func() interface{} { - "record": NewRecord, - "sub_record": NewSubRecord, + fm := map[string]func() interface{}{ + "record": NewRecord, + "sub_record": NewSubRecord, "sub_record2": NewSubRecord2, } f, ok := fm[kind] @@ -77,11 +275,87 @@ func MyTestFactory(kind string) (interface{}, error) { return f(), nil } +var oneOfTestPayload = ` +{ + "pets" : [ + { + "name": "Felix", + "age": 1, + "nestedHeritage": { + "type": "House", + "rooms": 8 + }, + "species": { + "type": "Cat", + "kind": "ALOOF", + "action": { + "type": "Purr", + "heritage": { + "name": "Buckingham", + "class": { + "type": "Palace", + "halls": 7, + "rooms": 20 + } + } + } + }, + "classes": [ + { + "name": "Taj Mahal", + "class": { + "type": "Palace", + "halls": 27, + "rooms": 10 + } + }, + { + "name": "White", + "class": { + "type": "House", + "halls": 12, + "rooms": 100 + } + } + ], + "heritage": { + "name": "Little house in the prairie", + "class": { + "type": "House", + "rooms": 20 + } + } + }, + + { + "name": "Jerry", + "age": 1, + "subtype": { + "type": "Dog", + "kind": "SHEPHERD", + "action": { + "type": "Bark", + "volume": 7 + } + }, + "species": { + "type": "Dog", + "kind": "SHEPHERD", + "action": { + "type": "Bark", + "volume": 7 + } + } + } + ] +} +` + func TestDecodeNestedObject(t *testing.T) { - + m := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "name": "bar", @@ -98,8 +372,8 @@ func TestDecodeNestedObject(t *testing.T) { }) Convey("unrully child object", t, func() { mp := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "name": "bar", @@ -111,8 +385,8 @@ func TestDecodeNestedObject(t *testing.T) { }) Convey("decode unruly child object in slice", t, func() { mp := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -129,8 +403,8 @@ func TestDecodeNestedObject(t *testing.T) { }) Convey("unmarshal unruly child object in slice", t, func() { mp := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -147,23 +421,23 @@ func TestDecodeNestedObject(t *testing.T) { _, err = decode.UnmarshalJSON(b, "kind", MyTestFactory) So(err, ShouldNotBeNil) }) - Convey("Decode a nested object", t, func(){ + Convey("Decode a nested object", t, func() { dec, err := decode.Decode(m, "kind", MyTestFactory) So(err, ShouldBeNil) rec, ok := dec.(*Record) So(ok, ShouldBeTrue) name := "bar" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, - Sub: &SubRecord{kind: "sub_record", Name: &name}, - }) - }) - Convey("Unmarshal a nested object, different subtype", t, func(){ + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, + Sub: &SubRecord{kind: "sub_record", Name: &name}, + }) + }) + Convey("Unmarshal a nested object, different subtype", t, func() { m := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -172,9 +446,9 @@ func TestDecodeNestedObject(t *testing.T) { "name": "1", }, }, - "kind": "sub_record2", + "kind": "sub_record2", "ptr_name": "sub_record2", - "name": "sub_record2", + "name": "sub_record2", }, } b, err := json.Marshal(m) @@ -186,13 +460,13 @@ func TestDecodeNestedObject(t *testing.T) { encapsulated := MyString("sub_record2") strName := "1" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, Sub: &SubRecord2{ - kind: "sub_record2", + kind: "sub_record2", PtrName: &encapsulated, - Name: encapsulated, + Name: encapsulated, Subs: []SubRecord{ SubRecord{ kind: "sub_record", @@ -202,10 +476,10 @@ func TestDecodeNestedObject(t *testing.T) { }, }) }) - Convey("Decode a nested object, different subtype", t, func(){ + Convey("Decode a nested object, different subtype", t, func() { m := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -223,13 +497,13 @@ func TestDecodeNestedObject(t *testing.T) { So(ok, ShouldBeTrue) strName := "1" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, Sub: &SubRecord2{ - kind: "sub_record2", + kind: "sub_record2", PtrName: nil, - Name: "", + Name: "", Subs: []SubRecord{ SubRecord{ kind: "sub_record", @@ -239,10 +513,10 @@ func TestDecodeNestedObject(t *testing.T) { }, }) }) - Convey("Decode a nested object, different subtype, pointer and aliased type values", t, func(){ + Convey("Decode a nested object, different subtype, pointer and aliased type values", t, func() { m := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -251,9 +525,9 @@ func TestDecodeNestedObject(t *testing.T) { "name": "1", }, }, - "kind": "sub_record2", + "kind": "sub_record2", "ptr_name": "sub_record2", - "name": "sub_record2", + "name": "sub_record2", }, } dec, err := decode.Decode(m, "kind", MyTestFactory) @@ -263,13 +537,13 @@ func TestDecodeNestedObject(t *testing.T) { encapsulated := MyString("sub_record2") strName := "1" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, Sub: &SubRecord2{ - kind: "sub_record2", + kind: "sub_record2", PtrName: &encapsulated, - Name: encapsulated, + Name: encapsulated, Subs: []SubRecord{ SubRecord{ kind: "sub_record", @@ -279,10 +553,10 @@ func TestDecodeNestedObject(t *testing.T) { }, }) }) - Convey("Decode a nested object, unexpected/misspelled fields", t, func(){ + Convey("Decode a nested object, unexpected/misspelled fields", t, func() { m := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -291,9 +565,9 @@ func TestDecodeNestedObject(t *testing.T) { "name": "1", }, }, - "kind": "sub_record2", + "kind": "sub_record2", "ptrname": "sub_record2", - "namer": "sub_record2", + "namer": "sub_record2", }, } dec, err := decode.Decode(m, "kind", MyTestFactory) @@ -302,11 +576,11 @@ func TestDecodeNestedObject(t *testing.T) { So(ok, ShouldBeTrue) strName := "1" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, Sub: &SubRecord2{ - kind: "sub_record2", + kind: "sub_record2", Subs: []SubRecord{ SubRecord{ kind: "sub_record", @@ -316,7 +590,7 @@ func TestDecodeNestedObject(t *testing.T) { }, }) }) - Convey("Unmarshal JSON of a nested object", t, func(){ + Convey("Unmarshal JSON of a nested object", t, func() { b, err := json.Marshal(m) So(err, ShouldBeNil) dec, err := decode.UnmarshalJSON(b, "kind", MyTestFactory) @@ -325,16 +599,22 @@ func TestDecodeNestedObject(t *testing.T) { So(ok, ShouldBeTrue) name := "bar" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, - Sub: &SubRecord{kind: "sub_record", Name: &name}, - }) + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, + Sub: &SubRecord{kind: "sub_record", Name: &name}, + }) }) - Convey("Unmarshal bad JSON", t, func(){ + Convey("Unmarshal bad JSON", t, func() { b, err := json.Marshal(m) So(err, ShouldBeNil) _, err = decode.UnmarshalJSON(b[1:], "kind", MyTestFactory) So(err, ShouldNotBeNil) }) -} \ No newline at end of file + Convey("Test OneOf decoding", t, func() { + v, err := decode.UnmarshalJSONInto([]byte(oneOfTestPayload), &Envelope{}, &decoderDesc{}) + So(err, ShouldBeNil) + _, err = json.MarshalIndent(v, "", " ") + So(err, ShouldBeNil) + }) +} diff --git a/decode/decoder.go b/decode/decoder.go index 2620b25..71944d9 100644 --- a/decode/decoder.go +++ b/decode/decoder.go @@ -5,16 +5,23 @@ package decode import ( - "fmt" - "reflect" "encoding/json" - + "fmt" "github.com/iancoleman/strcase" + "reflect" ) - // Factory makes Decodeable things described by their kind -type Factory func (kind string) (interface{}, error) +type Factory func(kind string) (interface{}, error) + +type DecoderDesc interface { + + // Create new instance of a type based on its path and discriminator value + Make(path, dk, dv string) (interface{}, error) + + // return discriminator property for path + DiscriminatorFor(path string) string +} // Decode a map into a Decodeable thing given the discriminator and the factory for all possible // types and embedded types @@ -52,12 +59,12 @@ func Decode(m map[string]interface{}, discriminator string, f Factory) (interfac s.Index(i).Set(reflect.Indirect(reflect.ValueOf(child2))) continue } - s.Index(i).Set(reflect.ValueOf(obj[i])) + s.Index(i).Set(reflect.ValueOf(obj[i])) } reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Set(s) continue } - if obj, ok := v.([]map[string]interface{}) ; ok { + if obj, ok := v.([]map[string]interface{}); ok { elemType := reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Type() s := reflect.MakeSlice(elemType, len(obj), len(obj)) for i := range obj { @@ -65,7 +72,7 @@ func Decode(m map[string]interface{}, discriminator string, f Factory) (interfac if err != nil { return nil, err } - s.Index(i).Set(reflect.Indirect(reflect.ValueOf(child2))) + s.Index(i).Set(reflect.Indirect(reflect.ValueOf(child2))) } reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Set(s) continue @@ -78,7 +85,7 @@ func Decode(m map[string]interface{}, discriminator string, f Factory) (interfac reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Set(pV.Elem().Addr()) continue } - if reflect.DeepEqual(reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)),reflect.Value{}) { + if reflect.DeepEqual(reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)), reflect.Value{}) { fmt.Printf("field by name %v not found", strcase.ToCamel(k)) continue } @@ -90,11 +97,209 @@ func Decode(m map[string]interface{}, discriminator string, f Factory) (interfac } } reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Set(reflect.ValueOf(v)) - + } return r, nil } +// Decode an object's attributes using DecoderDesc +func DecodeInto(m map[string]interface{}, o interface{}, dd DecoderDesc) (interface{}, error) { + + // bail if the passed in object is not a struct + if reflect.TypeOf(o).Kind() != reflect.Ptr || reflect.TypeOf(o).Elem().Kind() != reflect.Struct { + return nil, fmt.Errorf("Target object is not a struct pointer. Unsupprted") + } + + objSchemaName := reflect.TypeOf(o).Elem().Name() + + // for each field in the map, if the field is a OneOf (as described in dd), use the associated factory + for k, v := range m { + + fldName := strcase.ToCamel(k) + field := reflect.ValueOf(o).Elem().FieldByName(fldName) + + // ignore unknown fields + if !field.IsValid() { + continue + } + + // Decode a OneOf field and continue if it is + ok, e := decodeIntoOneOfField(field, fldName, objSchemaName, k, v, dd) + if e != nil { + return nil, e + } + if ok { + continue + } + + // decode regular fields, recursing into DecodeInto in case of object or array types + switch v.(type) { + case map[string]interface{}: + if e := decodeIntoObjectField(field, fldName, v.(map[string]interface{}), dd); e != nil { + return nil, e + } + continue + + case []interface{}: + if e := decodeIntoArrayField(field, fldName, v.([]interface{}), dd); e != nil { + return nil, e + } + continue + + case []map[string]interface{}: + if e := decodeIntoArrayOfObjectsField(field, fldName, v.([]map[string]interface{}), dd); e != nil { + return nil, e + } + continue + } + + // use reflection to set the field + if field.Kind() == reflect.Ptr { + newVal := reflect.TypeOf(field.Interface()).Elem() + pV := reflect.New(newVal) + pV.Elem().Set(reflect.ValueOf(v).Convert(newVal)) + field.Set(pV.Elem().Addr()) + continue + } + if reflect.DeepEqual(field, reflect.Value{}) { + fmt.Printf("field by name %v not found", fldName) + continue + } + if field.CanInterface() { + newVal := reflect.TypeOf(field.Interface()) + if newVal != reflect.TypeOf(v) { + field.Set(reflect.ValueOf(v).Convert(newVal)) + continue + } + } + field.Set(reflect.ValueOf(v)) + } + return o, nil +} + +func decodeIntoArrayOfObjectsField(field reflect.Value, fldName string, obj []map[string]interface{}, dd DecoderDesc) error { + elemType := field.Type() + s := reflect.MakeSlice(elemType, len(obj), len(obj)) + for i := range obj { + child, err := DecodeInto(obj[i], reflect.New(elemType), dd) + if err != nil { + return err + } + s.Index(i).Set(reflect.ValueOf(child)) + } + field.Set(s) + return nil +} + +func decodeIntoArrayField(field reflect.Value, name string, obj []interface{}, dd DecoderDesc) error { + var s reflect.Value + var ps reflect.Value + + // two options: + // - []*Type - this can be manually created + // - *[]Type - this is the codegen option + if field.Kind() == reflect.Ptr && field.Type().Elem().Kind() == reflect.Slice { + s = reflect.MakeSlice(field.Type().Elem(), len(obj), len(obj)) + ps = ptr(s) + + } else if field.Kind() == reflect.Slice && field.Type().Elem().Kind() == reflect.Ptr { + s = reflect.MakeSlice(field.Type(), len(obj), len(obj)) + + } + + // get the underlying element type + et := field.Type().Elem().Elem() + + for i := range obj { + objm, ok := obj[i].(map[string]interface{}) + if !ok { + s.Index(i).Set(reflect.ValueOf(obj[i])) + continue + } + + pV := reflect.New(et) + _, err := DecodeInto(objm, pV.Interface(), dd) + if err != nil { + return err + } + + // if field is *[]T, we need to deref this pointer + if ps.IsValid() { + pV = pV.Elem() + } + s.Index(i).Set(pV) + } + + // if we had a *[] type, ps will be initialized, use that as the value to set + if ps.IsValid() { + s = ps + } + + field.Set(s) + return nil +} + +func decodeIntoObjectField(field reflect.Value, fldName string, v map[string]interface{}, dd DecoderDesc) error { + if field.Kind() != reflect.Ptr { + return fmt.Errorf("expecting target field %s to be of type object pointer", fldName) + } + pV := reflect.New(field.Type().Elem()).Interface() + child, err := DecodeInto(v, pV, dd) + if err != nil { + return err + } + field.Set(reflect.ValueOf(child)) + return nil +} + +func decodeIntoOneOfField(field reflect.Value, fldName string, objSchemaName string, k string, v interface{}, dd DecoderDesc) (bool, error) { + + var pp string + var dk string + var dp interface{} + var dv string + var child interface{} + var err error + + pp = fmt.Sprintf("%s.%s", objSchemaName, k) + + // is this a OneOf property? + if dk = dd.DiscriminatorFor(pp); dk == "" { + return false, nil + } + + obj, ok := v.(map[string]interface{}) + if !ok { + return false, fmt.Errorf("expecting field %s to be of type object", fldName) + } + + if dp, ok = obj[dk]; !ok { + return false, fmt.Errorf("expecting OneOf field %s to to have a discriminator property %s", fldName, dk) + } + + if dv, ok = dp.(string); !ok { + return false, fmt.Errorf("expecting OneOf field %s's discriminator property `%s` value to be a string", fldName, dk) + } + + if child, err = dd.Make(pp, dk, dv); err != nil { + return false, err + } + + child, err = DecodeInto(obj, child, dd) + if err != nil { + return false, err + } + field.Set(reflect.ValueOf(child)) + return true, nil +} + +func ptr(v reflect.Value) reflect.Value { + pt := reflect.PtrTo(v.Type()) // create a *T type. + pv := reflect.New(pt.Elem()) // create a reflect.Value of type *T. + pv.Elem().Set(v) // sets pv to point to underlying value of v. + return pv +} + // UnmarshalJSON byte description of a Decodeable thing func UnmarshalJSON(b []byte, discriminator string, f Factory) (interface{}, error) { m := make(map[string]interface{}) @@ -104,3 +309,13 @@ func UnmarshalJSON(b []byte, discriminator string, f Factory) (interface{}, erro } return Decode(m, discriminator, f) } + +// UnmarshalJSON byte into an instance of object +func UnmarshalJSONInto(b []byte, o interface{}, dd DecoderDesc) (interface{}, error) { + m := make(map[string]interface{}) + err := json.Unmarshal(b, &m) + if err != nil { + return nil, err + } + return DecodeInto(m, o, dd) +} From 9b59b882a5a6ea8c60692497caf486e7fb65d933 Mon Sep 17 00:00:00 2001 From: bonny Date: Mon, 2 Sep 2019 23:58:26 -0700 Subject: [PATCH 2/5] added DecodeInto version to support local/nested oneOf --- decode/decode_test.go | 434 ++++++++++++++++++++++++++++++++++-------- decode/decoder.go | 247 +++++++++++++++++++++++- 2 files changed, 595 insertions(+), 86 deletions(-) diff --git a/decode/decode_test.go b/decode/decode_test.go index 5b4daa0..b747000 100644 --- a/decode/decode_test.go +++ b/decode/decode_test.go @@ -5,17 +5,16 @@ package decode_test import ( - "testing" - "fmt" "encoding/json" - + "fmt" . "github.com/smartystreets/goconvey/convey" "github.com/weberr13/go-decode/decode" + "testing" ) type SubRecord struct { - kind string - Name *string + kind string + Name *string } type MyString string @@ -29,10 +28,10 @@ func NewSubRecord() interface{} { } type SubRecord2 struct { - kind string - Name MyString + kind string + Name MyString PtrName *MyString - Subs []SubRecord + Subs []SubRecord } func (r SubRecord2) Discriminator() string { @@ -46,12 +45,12 @@ func NewSubRecord2() interface{} { } type Record struct { - kind string - Name string + kind string + Name string Optional *string - Num *int - Slice []string - Sub interface{} + Num *int + Slice []string + Sub interface{} } func (r Record) Discriminator() string { @@ -64,10 +63,209 @@ func NewRecord() interface{} { } } +type Envelope struct { + Pets []*BasePet +} + +type decoderDesc struct { +} + +func (dd decoderDesc) Make(pp, dk, dv string) (interface{}, error) { + return TypeFactory(fmt.Sprintf("%s(%s=%s)", pp, dk, dv)) +} + +func (dd decoderDesc) DiscriminatorFor(path string) string { + disc, ok := DiscriminatedOneOfSchemaMap[path] + if !ok { + return "" + } + return disc +} + +// Bark defines model for Bark. +type Bark struct { + Type *string `json:"type,omitempty"` + Volume *int `json:"volume,omitempty"` +} + +// BasePet defines model for BasePet. +type BasePet struct { + Age *int `json:"age,omitempty"` + Classes *[]PetHeritage `json:"classes,omitempty"` + Heritage *PetHeritage `json:"heritage,omitempty"` + Name *string `json:"name,omitempty"` + NestedHeritage interface{} `json:"nestedHeritage,omitempty"` + Species interface{} `json:"species,omitempty"` +} + +// Cat defines model for Cat. +type Cat struct { + Action interface{} `json:"action,omitempty"` + Mood *string `json:"mood,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Dog defines model for Dog. +type Dog struct { + Action interface{} `json:"action,omitempty"` + Kind *string `json:"kind,omitempty"` + Type *string `json:"type,omitempty"` +} + +// House defines model for House. +type House struct { + Name *string `json:"name,omitempty"` + Rooms *int `json:"rooms,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Meow defines model for Meow. +type Meow struct { + Squeel *string `json:"squeel,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Palace defines model for Palace. +type Palace struct { + Halls *int `json:"Halls,omitempty"` + Name *string `json:"name,omitempty"` + Towers *int `json:"towers,omitempty"` + Type *string `json:"type,omitempty"` +} + +// PetHeritage defines model for PetHeritage. +type PetHeritage struct { + Class interface{} `json:"class,omitempty"` + Name *string `json:"name,omitempty"` +} + +// Purr defines model for Purr. +type Purr struct { + Heritage *PetHeritage `json:"heritage,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Shack defines model for Shack. +type Shack struct { + Material *string `json:"material,omitempty"` + Name *string `json:"name,omitempty"` + Type *string `json:"type,omitempty"` +} + +func TypeFactory(kind string) (interface{}, error) { + fm := map[string]func() interface{}{ + "BasePet.heritage.class(type=House)": NewHouse, + "BasePet.heritage.class(type=Palace)": NewPalace, + "BasePet.heritage.class(type=Shack)": NewShack, + "BasePet.nestedHeritage(type=House)": NewHouse, + "BasePet.nestedHeritage(type=Palace)": NewPalace, + "BasePet.species(type=Cat)": NewCat, + "BasePet.species(type=Dog)": NewDog, + "Cat.action(type=Meow)": NewMeow, + "Cat.action(type=Purr)": NewPurr, + "Dog.action(type=Bark)": NewBark, + "PetHeritage.class(type=House)": NewHouse, + "PetHeritage.class(type=Palace)": NewPalace, + "PetHeritage.class(type=Shack)": NewShack, + "Purr.heritage.class(type=House)": NewHouse, + "Purr.heritage.class(type=Palace)": NewPalace, + "Purr.heritage.class(type=Shack)": NewShack, + } + f, ok := fm[kind] + if !ok { + return nil, fmt.Errorf("cannot find type %s", kind) + } + return f(), nil +} + +// Map +var DiscriminatedOneOfSchemaMap = map[string]string{ + + "BasePet.heritage.class": "type", + "BasePet.nestedHeritage": "type", + "BasePet.species": "type", + "Cat.action": "type", + "Dog.action": "type", + "PetHeritage.class": "type", + "Purr.heritage.class": "type", +} + +func NewBark() interface{} { + _d := "Type" + return &Bark{Type: &_d} +} + +func (r Bark) Discriminator() string { + return "type" +} + +func NewCat() interface{} { + _d := "Type" + return &Cat{Type: &_d} +} + +func (r Cat) Discriminator() string { + return "type" +} + +func NewDog() interface{} { + _d := "Type" + return &Dog{Type: &_d} +} + +func (r Dog) Discriminator() string { + return "type" +} + +func NewHouse() interface{} { + _d := "Type" + return &House{Type: &_d} +} + +func (r House) Discriminator() string { + return "type" +} + +func NewMeow() interface{} { + _d := "Type" + return &Meow{Type: &_d} +} + +func (r Meow) Discriminator() string { + return "type" +} + +func NewPalace() interface{} { + _d := "Type" + return &Palace{Type: &_d} +} + +func (r Palace) Discriminator() string { + return "type" +} + +func NewPurr() interface{} { + _d := "Type" + return &Purr{Type: &_d} +} + +func (r Purr) Discriminator() string { + return "type" +} + +func NewShack() interface{} { + _d := "Type" + return &Shack{Type: &_d} +} + +func (r Shack) Discriminator() string { + return "type" +} + func MyTestFactory(kind string) (interface{}, error) { - fm := map[string]func() interface{} { - "record": NewRecord, - "sub_record": NewSubRecord, + fm := map[string]func() interface{}{ + "record": NewRecord, + "sub_record": NewSubRecord, "sub_record2": NewSubRecord2, } f, ok := fm[kind] @@ -77,11 +275,87 @@ func MyTestFactory(kind string) (interface{}, error) { return f(), nil } +var oneOfTestPayload = ` +{ + "pets" : [ + { + "name": "Felix", + "age": 1, + "nestedHeritage": { + "type": "House", + "rooms": 8 + }, + "species": { + "type": "Cat", + "kind": "ALOOF", + "action": { + "type": "Purr", + "heritage": { + "name": "Buckingham", + "class": { + "type": "Palace", + "halls": 7, + "rooms": 20 + } + } + } + }, + "classes": [ + { + "name": "Taj Mahal", + "class": { + "type": "Palace", + "halls": 27, + "rooms": 10 + } + }, + { + "name": "White", + "class": { + "type": "House", + "halls": 12, + "rooms": 100 + } + } + ], + "heritage": { + "name": "Little house in the prairie", + "class": { + "type": "House", + "rooms": 20 + } + } + }, + + { + "name": "Jerry", + "age": 1, + "subtype": { + "type": "Dog", + "kind": "SHEPHERD", + "action": { + "type": "Bark", + "volume": 7 + } + }, + "species": { + "type": "Dog", + "kind": "SHEPHERD", + "action": { + "type": "Bark", + "volume": 7 + } + } + } + ] +} +` + func TestDecodeNestedObject(t *testing.T) { - + m := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "name": "bar", @@ -98,8 +372,8 @@ func TestDecodeNestedObject(t *testing.T) { }) Convey("unrully child object", t, func() { mp := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "name": "bar", @@ -111,8 +385,8 @@ func TestDecodeNestedObject(t *testing.T) { }) Convey("decode unruly child object in slice", t, func() { mp := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -129,8 +403,8 @@ func TestDecodeNestedObject(t *testing.T) { }) Convey("unmarshal unruly child object in slice", t, func() { mp := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -147,23 +421,23 @@ func TestDecodeNestedObject(t *testing.T) { _, err = decode.UnmarshalJSON(b, "kind", MyTestFactory) So(err, ShouldNotBeNil) }) - Convey("Decode a nested object", t, func(){ + Convey("Decode a nested object", t, func() { dec, err := decode.Decode(m, "kind", MyTestFactory) So(err, ShouldBeNil) rec, ok := dec.(*Record) So(ok, ShouldBeTrue) name := "bar" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, - Sub: &SubRecord{kind: "sub_record", Name: &name}, - }) - }) - Convey("Unmarshal a nested object, different subtype", t, func(){ + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, + Sub: &SubRecord{kind: "sub_record", Name: &name}, + }) + }) + Convey("Unmarshal a nested object, different subtype", t, func() { m := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -172,9 +446,9 @@ func TestDecodeNestedObject(t *testing.T) { "name": "1", }, }, - "kind": "sub_record2", + "kind": "sub_record2", "ptr_name": "sub_record2", - "name": "sub_record2", + "name": "sub_record2", }, } b, err := json.Marshal(m) @@ -186,13 +460,13 @@ func TestDecodeNestedObject(t *testing.T) { encapsulated := MyString("sub_record2") strName := "1" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, Sub: &SubRecord2{ - kind: "sub_record2", + kind: "sub_record2", PtrName: &encapsulated, - Name: encapsulated, + Name: encapsulated, Subs: []SubRecord{ SubRecord{ kind: "sub_record", @@ -202,10 +476,10 @@ func TestDecodeNestedObject(t *testing.T) { }, }) }) - Convey("Decode a nested object, different subtype", t, func(){ + Convey("Decode a nested object, different subtype", t, func() { m := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -223,13 +497,13 @@ func TestDecodeNestedObject(t *testing.T) { So(ok, ShouldBeTrue) strName := "1" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, Sub: &SubRecord2{ - kind: "sub_record2", + kind: "sub_record2", PtrName: nil, - Name: "", + Name: "", Subs: []SubRecord{ SubRecord{ kind: "sub_record", @@ -239,10 +513,10 @@ func TestDecodeNestedObject(t *testing.T) { }, }) }) - Convey("Decode a nested object, different subtype, pointer and aliased type values", t, func(){ + Convey("Decode a nested object, different subtype, pointer and aliased type values", t, func() { m := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -251,9 +525,9 @@ func TestDecodeNestedObject(t *testing.T) { "name": "1", }, }, - "kind": "sub_record2", + "kind": "sub_record2", "ptr_name": "sub_record2", - "name": "sub_record2", + "name": "sub_record2", }, } dec, err := decode.Decode(m, "kind", MyTestFactory) @@ -263,13 +537,13 @@ func TestDecodeNestedObject(t *testing.T) { encapsulated := MyString("sub_record2") strName := "1" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, Sub: &SubRecord2{ - kind: "sub_record2", + kind: "sub_record2", PtrName: &encapsulated, - Name: encapsulated, + Name: encapsulated, Subs: []SubRecord{ SubRecord{ kind: "sub_record", @@ -279,10 +553,10 @@ func TestDecodeNestedObject(t *testing.T) { }, }) }) - Convey("Decode a nested object, unexpected/misspelled fields", t, func(){ + Convey("Decode a nested object, unexpected/misspelled fields", t, func() { m := map[string]interface{}{ - "name": "foo", - "kind": "record", + "name": "foo", + "kind": "record", "slice": []string{"foo", "bar"}, "sub": map[string]interface{}{ "subs": []map[string]interface{}{ @@ -291,9 +565,9 @@ func TestDecodeNestedObject(t *testing.T) { "name": "1", }, }, - "kind": "sub_record2", + "kind": "sub_record2", "ptrname": "sub_record2", - "namer": "sub_record2", + "namer": "sub_record2", }, } dec, err := decode.Decode(m, "kind", MyTestFactory) @@ -302,11 +576,11 @@ func TestDecodeNestedObject(t *testing.T) { So(ok, ShouldBeTrue) strName := "1" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, Sub: &SubRecord2{ - kind: "sub_record2", + kind: "sub_record2", Subs: []SubRecord{ SubRecord{ kind: "sub_record", @@ -316,7 +590,7 @@ func TestDecodeNestedObject(t *testing.T) { }, }) }) - Convey("Unmarshal JSON of a nested object", t, func(){ + Convey("Unmarshal JSON of a nested object", t, func() { b, err := json.Marshal(m) So(err, ShouldBeNil) dec, err := decode.UnmarshalJSON(b, "kind", MyTestFactory) @@ -325,16 +599,22 @@ func TestDecodeNestedObject(t *testing.T) { So(ok, ShouldBeTrue) name := "bar" So(rec, ShouldResemble, &Record{ - kind: "record", - Name: "foo", - Slice: []string{"foo", "bar"}, - Sub: &SubRecord{kind: "sub_record", Name: &name}, - }) + kind: "record", + Name: "foo", + Slice: []string{"foo", "bar"}, + Sub: &SubRecord{kind: "sub_record", Name: &name}, + }) }) - Convey("Unmarshal bad JSON", t, func(){ + Convey("Unmarshal bad JSON", t, func() { b, err := json.Marshal(m) So(err, ShouldBeNil) _, err = decode.UnmarshalJSON(b[1:], "kind", MyTestFactory) So(err, ShouldNotBeNil) }) -} \ No newline at end of file + Convey("Test OneOf decoding", t, func() { + v, err := decode.UnmarshalJSONInto([]byte(oneOfTestPayload), &Envelope{}, &decoderDesc{}) + So(err, ShouldBeNil) + _, err = json.MarshalIndent(v, "", " ") + So(err, ShouldBeNil) + }) +} diff --git a/decode/decoder.go b/decode/decoder.go index 3869d0f..cf4f298 100644 --- a/decode/decoder.go +++ b/decode/decoder.go @@ -5,16 +5,21 @@ package decode import ( + "encoding/json" "fmt" "reflect" - "encoding/json" - + "github.com/iancoleman/strcase" ) +// Factory makes Decodeable things described by their kind +type Factory func(kind string) (interface{}, error) // Factory makes Decodeable things described by their kind -type Factory func (kind string) (interface{}, error) +type OneOfFactory func(map[string]interface{}) (interface{}, error) + +// PathFactory returns a Factory +type PathFactory func(path string) (func(map[string]interface{}) (interface{}, error), error) // Decode a map into a Decodeable thing given the discriminator and the factory for all possible // types and embedded types @@ -52,12 +57,12 @@ func Decode(m map[string]interface{}, discriminator string, f Factory) (interfac s.Index(i).Set(reflect.Indirect(reflect.ValueOf(child2))) continue } - s.Index(i).Set(reflect.ValueOf(obj[i])) + s.Index(i).Set(reflect.ValueOf(obj[i])) } reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Set(s) continue } - if obj, ok := v.([]map[string]interface{}) ; ok { + if obj, ok := v.([]map[string]interface{}); ok { elemType := reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Type() s := reflect.MakeSlice(elemType, len(obj), len(obj)) for i := range obj { @@ -65,7 +70,7 @@ func Decode(m map[string]interface{}, discriminator string, f Factory) (interfac if err != nil { return nil, err } - s.Index(i).Set(reflect.Indirect(reflect.ValueOf(child2))) + s.Index(i).Set(reflect.Indirect(reflect.ValueOf(child2))) } reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Set(s) continue @@ -78,8 +83,8 @@ func Decode(m map[string]interface{}, discriminator string, f Factory) (interfac reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Set(pV.Elem().Addr()) continue } - if reflect.DeepEqual(reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)),reflect.Value{}) { - //fmt.Printf("field by name %v not found", strcase.ToCamel(k)) + if reflect.DeepEqual(reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)), reflect.Value{}) { + fmt.Printf("field by name %v not found", strcase.ToCamel(k)) continue } if reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).CanInterface() { @@ -90,11 +95,225 @@ func Decode(m map[string]interface{}, discriminator string, f Factory) (interfac } } reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Set(reflect.ValueOf(v)) - + } return r, nil } +// Decode an object's attributes using DecoderDesc +func DecodeInto(m map[string]interface{}, o interface{}, pf PathFactory) (interface{}, error) { + + // bail if the passed in object is not a struct + if reflect.TypeOf(o).Kind() != reflect.Ptr || + (reflect.TypeOf(o).Elem().Kind() != reflect.Struct && reflect.TypeOf(o).Elem().Kind() != reflect.Slice) { + return nil, fmt.Errorf("Target object is not a struct/slice pointer. Unsupprted") + } + + objSchemaName := reflect.TypeOf(o).Elem().Name() + + // for each field in the map, if the field is a OneOf (as described in dd), use the associated factory + for k, v := range m { + + fldName := strcase.ToCamel(k) + field := reflect.ValueOf(o).Elem().FieldByName(fldName) + + // ignore unknown fields + if !field.IsValid() { + continue + } + + // decode regular fields, recursing into DecodeInto in case of object or array types + switch v.(type) { + case map[string]interface{}: + // Decode a OneOf field and continue if it is + ok, e := decodeIntoOneOfField(field, fldName, objSchemaName, k, v.(map[string]interface{}), pf) + if e != nil { + return nil, e + } + if ok { + continue + } + + if e := decodeIntoObjectField(field, fldName, v.(map[string]interface{}), pf); e != nil { + return nil, e + } + continue + + case []interface{}: + if e := decodeIntoArrayField(field, fldName, v.([]interface{}), pf); e != nil { + return nil, e + } + continue + + case []map[string]interface{}: + if e := decodeIntoArrayOfObjectsField(field, fldName, v.([]map[string]interface{}), pf); e != nil { + return nil, e + } + continue + } + + // use reflection to set the field + if field.Kind() == reflect.Ptr { + vV := reflect.ValueOf(v) + ft := reflect.TypeOf(field.Interface()).Elem() + nV := reflect.New(ft) + + if !vV.Type().ConvertibleTo(ft) { + return nil, fmt.Errorf("cannot convert value (%v) to field '%s' type\n", v, fldName) + } + nV.Elem().Set(vV.Convert(ft)) + field.Set(nV.Elem().Addr()) + continue + } + + // todo: this is not required because of field validity check above. Check this out + //if reflect.DeepEqual(field, reflect.Value{}) { + // fmt.Printf("field by name %v not found\n", fldName) + // continue + //} + + // special case for empty interfaces - they must represent objects hence we should not be here + if field.Type().NumMethod() == 0 { + return nil, fmt.Errorf("Invalid value found for field name %v (expected object, not basic type)\n", fldName) + } + + // todo: is this required anymore? + if field.CanInterface() { + newVal := reflect.TypeOf(field.Interface()) + if newVal != reflect.TypeOf(v) { + if newVal != nil && reflect.TypeOf(v).ConvertibleTo(newVal) { + field.Set(reflect.ValueOf(v).Convert(newVal)) + continue + } else { + return nil, fmt.Errorf("cannot convert value (%v) to field '%s's' type\n", v, fldName) + } + } + } + field.Set(reflect.ValueOf(v)) + } + return o, nil +} + +type iterable func() (next iterable, obj interface{}) + +func decodeIntoArray(field reflect.Value, fldName string, iter iterable, len int, pf PathFactory) error { + var s reflect.Value + var ps reflect.Value + + // two options: + // - []*Type - this can be manually created + // - *[]Type - this is the codegen option + if field.Kind() == reflect.Ptr && field.Type().Elem().Kind() == reflect.Slice { + s = reflect.MakeSlice(field.Type().Elem(), len, len) + ps = ptr(s) + } else if field.Kind() == reflect.Slice && field.Type().Elem().Kind() == reflect.Ptr { + s = reflect.MakeSlice(field.Type(), len, len) + } + + // get the underlying element type + et := field.Type().Elem().Elem() + + i := 0 + for next, o := iter(); next != nil; next, o = iter() { + pV := reflect.ValueOf(o) + + objm, ok := o.(map[string]interface{}) + if ok { + pV = reflect.New(et) + _, err := DecodeInto(objm, pV.Interface(), pf) + if err != nil { + return err + } + + // if field is *[]T, we need to deref this pointer + if ps.IsValid() { + pV = pV.Elem() + } + } + s.Index(i).Set(pV) + i++ + } + + if ps.IsValid() { + s = ps + } + + field.Set(s) + return nil + +} + +func decodeIntoArrayOfObjectsField(field reflect.Value, fldName string, obj []map[string]interface{}, pf PathFactory) error { + n := 0 + var i iterable + i = func() (iterable, interface{}) { + if n < (len(obj)) { + n++ + return i, obj[n-1] + } + return nil, nil + } + + return decodeIntoArray(field, fldName, i, len(obj), pf) +} + +func decodeIntoArrayField(field reflect.Value, fldName string, obj []interface{}, pf PathFactory) error { + n := 0 + var i iterable + i = func() (iterable, interface{}) { + if n < (len(obj)) { + n++ + return i, obj[n-1] + } + return nil, nil + } + + return decodeIntoArray(field, fldName, i, len(obj), pf) +} + +func decodeIntoObjectField(field reflect.Value, fldName string, v map[string]interface{}, pf PathFactory) error { + if field.Kind() != reflect.Ptr { + return fmt.Errorf("expecting target field %s to be of type object pointer", fldName) + } + pV := reflect.New(field.Type().Elem()).Interface() + child, err := DecodeInto(v, pV, pf) + if err != nil { + return err + } + field.Set(reflect.ValueOf(child)) + return nil +} + +func decodeIntoOneOfField(field reflect.Value, fldName string, objSchemaName string, k string, v map[string]interface{}, pf PathFactory) (bool, error) { + var pp string + var f OneOfFactory + var child interface{} + var err error + + pp = fmt.Sprintf("%s.%s", objSchemaName, k) + + // get a factory. If factory is nil, but no error, factory was not found for this field + if f, err = pf(pp); err != nil || f == nil { + return f != nil, err + } + + if child, err = f(v); err != nil { + return false, err + } + + if child, err = DecodeInto(v, child, pf); err == nil { + field.Set(reflect.ValueOf(child)) + } + return err == nil, err +} + +func ptr(v reflect.Value) reflect.Value { + pt := reflect.PtrTo(v.Type()) // create a *T type. + pv := reflect.New(pt.Elem()) // create a reflect.Value of type *T. + pv.Elem().Set(v) // sets pv to point to underlying value of v. + return pv +} + // UnmarshalJSON byte description of a Decodeable thing func UnmarshalJSON(b []byte, discriminator string, f Factory) (interface{}, error) { m := make(map[string]interface{}) @@ -104,3 +323,13 @@ func UnmarshalJSON(b []byte, discriminator string, f Factory) (interface{}, erro } return Decode(m, discriminator, f) } + +// UnmarshalJSON byte into an instance of object +func UnmarshalJSONInto(b []byte, o interface{}, pf PathFactory) (interface{}, error) { + m := make(map[string]interface{}) + err := json.Unmarshal(b, &m) + if err != nil { + return nil, err + } + return DecodeInto(m, o, pf) +} From 3d3de3e224b625040fcc0bd36a07854faf274abf Mon Sep 17 00:00:00 2001 From: bonny Date: Thu, 5 Sep 2019 00:07:38 -0700 Subject: [PATCH 3/5] changed to use FP as a paradigm for oneOf resolution and handling added tests to improve coverage --- decode/decode_pets_test.go | 304 +++++++++++++++++++++++++++++++ decode/decode_test.go | 363 +++++++++---------------------------- decode/testdata/pets1.json | 88 +++++++++ 3 files changed, 480 insertions(+), 275 deletions(-) create mode 100644 decode/decode_pets_test.go create mode 100644 decode/testdata/pets1.json diff --git a/decode/decode_pets_test.go b/decode/decode_pets_test.go new file mode 100644 index 0000000..194cb5f --- /dev/null +++ b/decode/decode_pets_test.go @@ -0,0 +1,304 @@ +// Package Pets provides primitives to interact the openapi HTTP API. +// +// Code generated by gitswarm.f5net.com/indigo/product/controller-thirdparty/oapi-codegen DO NOT EDIT. +package decode_test + +import ( + "fmt" +) + +// Accommodation defines model for Accommodation. +type Accommodation struct { + Class interface{} `json:"class,omitempty"` + Title *string `json:"title,omitempty"` +} + +// Bark defines model for Bark. +type Bark struct { + Type *string `json:"type,omitempty"` + Volume *int `json:"volume,omitempty"` +} + +// Cat defines model for Cat. +type Cat struct { + FavSound interface{} `json:"favSound,omitempty"` + Mood *string `json:"mood,omitempty"` + Sound interface{} `json:"sound,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Dog defines model for Dog. +type Dog struct { + Kind *string `json:"kind,omitempty"` + Sound interface{} `json:"sound,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Error defines model for Error. +type Error struct { + Code *string `json:"code,omitempty"` + Message *string `json:"message,omitempty"` +} + +// Growl defines model for Growl. +type Growl struct { + Severity *string `json:"severity,omitempty"` + Type *string `json:"type,omitempty"` +} + +// House defines model for House. +type House struct { + Name *string `json:"name,omitempty"` + Rooms *int `json:"rooms,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Kennel defines model for Kennel. +type Kennel struct { + Name *string `json:"name,omitempty"` + Rooms *int `json:"rooms,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Meow defines model for Meow. +type Meow struct { + Squeel *string `json:"squeel,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Palace defines model for Palace. +type Palace struct { + Halls *int `json:"Halls,omitempty"` + Name *string `json:"name,omitempty"` + Towers *int `json:"towers,omitempty"` + Type *string `json:"type,omitempty"` +} + +// PetOwner defines model for PetOwner. +type PetOwner struct { + Age *int `json:"age,omitempty"` + Dogs *[]Dog `json:"dogs,omitempty"` + Favorite interface{} `json:"favorite,omitempty"` + LivesIn interface{} `json:"livesIn,omitempty"` + Name *string `json:"name,omitempty"` + Owns *[]Accommodation `json:"owns,omitempty"` + PetLivesIn *Accommodation `json:"petLivesIn,omitempty"` +} + +// Purr defines model for Purr. +type Purr struct { + Heritage *Accommodation `json:"heritage,omitempty"` + Type *string `json:"type,omitempty"` +} + +// Shack defines model for Shack. +type Shack struct { + Material *string `json:"material,omitempty"` + Name *string `json:"name,omitempty"` + Type *string `json:"type,omitempty"` +} + +func factory(fm map[string]func() interface{}, path, dk string, o map[string]interface{}) (interface{}, error) { + var dp interface{} + var dv string + var ok bool + + if dp, ok = o[dk]; !ok { + return nil, fmt.Errorf("expecting OneOf object at path '%s' to to have a discriminator property '%s'", path, dk) + } + + if dv, ok = dp.(string); !ok { + return nil, fmt.Errorf("expecting OneOf field '%s's' discriminator property '%s' value to be a string", path, dk) + } + + f, ok := fm[dv] + if !ok { + return nil, fmt.Errorf("Unknown discriminator value '%s' when handling OneOf field '%s'", path, dv) + } + return f(), nil +} + +// Factory method for objects at path Accommodation.class +func Accommodation_class_Factory(o map[string]interface{}) (interface{}, error) { + fm := map[string]func() interface{}{ + "Shack": NewShack, + "BARK": NewKennel, + "Palace": NewPalace, + "House": NewHouse, + } + return factory(fm, "Accommodation.class", "type", o) +} + +// Factory method for objects at path Cat.favSound +func Cat_favSound_Factory(o map[string]interface{}) (interface{}, error) { + fm := map[string]func() interface{}{ + "MEOW": NewMeow, + "PURR": NewPurr, + } + return factory(fm, "Cat.favSound", "type", o) +} + +// Factory method for objects at path Cat.sound +func Cat_sound_Factory(o map[string]interface{}) (interface{}, error) { + fm := map[string]func() interface{}{ + "PURR": NewPurr, + "LOUD": NewMeow, + "WARN": NewGrowl, + } + return factory(fm, "Cat.sound", "type", o) +} + +// Factory method for objects at path Dog.sound +func Dog_sound_Factory(o map[string]interface{}) (interface{}, error) { + fm := map[string]func() interface{}{ + "BARK": NewBark, + } + return factory(fm, "Dog.sound", "type", o) +} + +// Factory method for objects at path PetOwner.favorite +func PetOwner_favorite_Factory(o map[string]interface{}) (interface{}, error) { + fm := map[string]func() interface{}{ + "Cat": NewCat, + "Dog": NewDog, + } + return factory(fm, "PetOwner.favorite", "type", o) +} + +// Factory method for objects at path PetOwner.livesIn +func PetOwner_livesIn_Factory(o map[string]interface{}) (interface{}, error) { + fm := map[string]func() interface{}{ + "Palace": NewPalace, + "House": NewHouse, + } + return factory(fm, "PetOwner.livesIn", "type", o) +} + +func SchemaPathFactory(path string) (func(map[string]interface{}) (interface{}, error), error) { + // Map + pathFactoryMap := map[string]func(map[string]interface{}) (interface{}, error){ + "Accommodation.class": Accommodation_class_Factory, + "Cat.favSound": Cat_favSound_Factory, + "Cat.sound": Cat_sound_Factory, + "Dog.sound": Dog_sound_Factory, + "PetOwner.favorite": PetOwner_favorite_Factory, + "PetOwner.livesIn": PetOwner_livesIn_Factory, + } + + return pathFactoryMap[path], nil +} + +func TypeFactory(kind string) (interface{}, error) { + // Map + var factoryMap = map[string]func() interface{}{ + "Accommodation.class(type=BARK)": NewKennel, + "Accommodation.class(type=House)": NewHouse, + "Accommodation.class(type=Palace)": NewPalace, + "Accommodation.class(type=Shack)": NewShack, + "Cat.favSound(type=MEOW)": NewMeow, + "Cat.favSound(type=PURR)": NewPurr, + "Cat.sound(type=LOUD)": NewMeow, + "Cat.sound(type=PURR)": NewPurr, + "Cat.sound(type=WARN)": NewGrowl, + "Dog.sound(type=BARK)": NewBark, + "PetOwner.favorite(type=Cat)": NewCat, + "PetOwner.favorite(type=Dog)": NewDog, + "PetOwner.livesIn(type=House)": NewHouse, + "PetOwner.livesIn(type=Palace)": NewPalace, + } + f, ok := factoryMap[kind] + if !ok { + return nil, fmt.Errorf("cannot find type %s", kind) + } + return f(), nil +} + +func NewBark() interface{} { + _d := "BARK" + return &Bark{Type: &_d} +} + +func (r Bark) Discriminator() string { + return "type" +} + +func NewCat() interface{} { + _d := "Cat" + return &Cat{Type: &_d} +} + +func (r Cat) Discriminator() string { + return "type" +} + +func NewDog() interface{} { + _d := "Dog" + return &Dog{Type: &_d} +} + +func (r Dog) Discriminator() string { + return "type" +} + +func NewGrowl() interface{} { + _d := "WARN" + return &Growl{Type: &_d} +} + +func (r Growl) Discriminator() string { + return "type" +} + +func NewHouse() interface{} { + _d := "House" + return &House{Type: &_d} +} + +func (r House) Discriminator() string { + return "type" +} + +func NewKennel() interface{} { + _d := "BARK" + return &Kennel{Type: &_d} +} + +func (r Kennel) Discriminator() string { + return "type" +} + +func NewMeow() interface{} { + _d := "MEOW" + return &Meow{Type: &_d} +} + +func (r Meow) Discriminator() string { + return "type" +} + +func NewPalace() interface{} { + _d := "Palace" + return &Palace{Type: &_d} +} + +func (r Palace) Discriminator() string { + return "type" +} + +func NewPurr() interface{} { + _d := "PURR" + return &Purr{Type: &_d} +} + +func (r Purr) Discriminator() string { + return "type" +} + +func NewShack() interface{} { + _d := "Shack" + return &Shack{Type: &_d} +} + +func (r Shack) Discriminator() string { + return "type" +} diff --git a/decode/decode_test.go b/decode/decode_test.go index b747000..dd493cb 100644 --- a/decode/decode_test.go +++ b/decode/decode_test.go @@ -7,9 +7,11 @@ package decode_test import ( "encoding/json" "fmt" + "io/ioutil" + "testing" + . "github.com/smartystreets/goconvey/convey" "github.com/weberr13/go-decode/decode" - "testing" ) type SubRecord struct { @@ -64,202 +66,7 @@ func NewRecord() interface{} { } type Envelope struct { - Pets []*BasePet -} - -type decoderDesc struct { -} - -func (dd decoderDesc) Make(pp, dk, dv string) (interface{}, error) { - return TypeFactory(fmt.Sprintf("%s(%s=%s)", pp, dk, dv)) -} - -func (dd decoderDesc) DiscriminatorFor(path string) string { - disc, ok := DiscriminatedOneOfSchemaMap[path] - if !ok { - return "" - } - return disc -} - -// Bark defines model for Bark. -type Bark struct { - Type *string `json:"type,omitempty"` - Volume *int `json:"volume,omitempty"` -} - -// BasePet defines model for BasePet. -type BasePet struct { - Age *int `json:"age,omitempty"` - Classes *[]PetHeritage `json:"classes,omitempty"` - Heritage *PetHeritage `json:"heritage,omitempty"` - Name *string `json:"name,omitempty"` - NestedHeritage interface{} `json:"nestedHeritage,omitempty"` - Species interface{} `json:"species,omitempty"` -} - -// Cat defines model for Cat. -type Cat struct { - Action interface{} `json:"action,omitempty"` - Mood *string `json:"mood,omitempty"` - Type *string `json:"type,omitempty"` -} - -// Dog defines model for Dog. -type Dog struct { - Action interface{} `json:"action,omitempty"` - Kind *string `json:"kind,omitempty"` - Type *string `json:"type,omitempty"` -} - -// House defines model for House. -type House struct { - Name *string `json:"name,omitempty"` - Rooms *int `json:"rooms,omitempty"` - Type *string `json:"type,omitempty"` -} - -// Meow defines model for Meow. -type Meow struct { - Squeel *string `json:"squeel,omitempty"` - Type *string `json:"type,omitempty"` -} - -// Palace defines model for Palace. -type Palace struct { - Halls *int `json:"Halls,omitempty"` - Name *string `json:"name,omitempty"` - Towers *int `json:"towers,omitempty"` - Type *string `json:"type,omitempty"` -} - -// PetHeritage defines model for PetHeritage. -type PetHeritage struct { - Class interface{} `json:"class,omitempty"` - Name *string `json:"name,omitempty"` -} - -// Purr defines model for Purr. -type Purr struct { - Heritage *PetHeritage `json:"heritage,omitempty"` - Type *string `json:"type,omitempty"` -} - -// Shack defines model for Shack. -type Shack struct { - Material *string `json:"material,omitempty"` - Name *string `json:"name,omitempty"` - Type *string `json:"type,omitempty"` -} - -func TypeFactory(kind string) (interface{}, error) { - fm := map[string]func() interface{}{ - "BasePet.heritage.class(type=House)": NewHouse, - "BasePet.heritage.class(type=Palace)": NewPalace, - "BasePet.heritage.class(type=Shack)": NewShack, - "BasePet.nestedHeritage(type=House)": NewHouse, - "BasePet.nestedHeritage(type=Palace)": NewPalace, - "BasePet.species(type=Cat)": NewCat, - "BasePet.species(type=Dog)": NewDog, - "Cat.action(type=Meow)": NewMeow, - "Cat.action(type=Purr)": NewPurr, - "Dog.action(type=Bark)": NewBark, - "PetHeritage.class(type=House)": NewHouse, - "PetHeritage.class(type=Palace)": NewPalace, - "PetHeritage.class(type=Shack)": NewShack, - "Purr.heritage.class(type=House)": NewHouse, - "Purr.heritage.class(type=Palace)": NewPalace, - "Purr.heritage.class(type=Shack)": NewShack, - } - f, ok := fm[kind] - if !ok { - return nil, fmt.Errorf("cannot find type %s", kind) - } - return f(), nil -} - -// Map -var DiscriminatedOneOfSchemaMap = map[string]string{ - - "BasePet.heritage.class": "type", - "BasePet.nestedHeritage": "type", - "BasePet.species": "type", - "Cat.action": "type", - "Dog.action": "type", - "PetHeritage.class": "type", - "Purr.heritage.class": "type", -} - -func NewBark() interface{} { - _d := "Type" - return &Bark{Type: &_d} -} - -func (r Bark) Discriminator() string { - return "type" -} - -func NewCat() interface{} { - _d := "Type" - return &Cat{Type: &_d} -} - -func (r Cat) Discriminator() string { - return "type" -} - -func NewDog() interface{} { - _d := "Type" - return &Dog{Type: &_d} -} - -func (r Dog) Discriminator() string { - return "type" -} - -func NewHouse() interface{} { - _d := "Type" - return &House{Type: &_d} -} - -func (r House) Discriminator() string { - return "type" -} - -func NewMeow() interface{} { - _d := "Type" - return &Meow{Type: &_d} -} - -func (r Meow) Discriminator() string { - return "type" -} - -func NewPalace() interface{} { - _d := "Type" - return &Palace{Type: &_d} -} - -func (r Palace) Discriminator() string { - return "type" -} - -func NewPurr() interface{} { - _d := "Type" - return &Purr{Type: &_d} -} - -func (r Purr) Discriminator() string { - return "type" -} - -func NewShack() interface{} { - _d := "Type" - return &Shack{Type: &_d} -} - -func (r Shack) Discriminator() string { - return "type" + Owners []*PetOwner } func MyTestFactory(kind string) (interface{}, error) { @@ -275,82 +82,6 @@ func MyTestFactory(kind string) (interface{}, error) { return f(), nil } -var oneOfTestPayload = ` -{ - "pets" : [ - { - "name": "Felix", - "age": 1, - "nestedHeritage": { - "type": "House", - "rooms": 8 - }, - "species": { - "type": "Cat", - "kind": "ALOOF", - "action": { - "type": "Purr", - "heritage": { - "name": "Buckingham", - "class": { - "type": "Palace", - "halls": 7, - "rooms": 20 - } - } - } - }, - "classes": [ - { - "name": "Taj Mahal", - "class": { - "type": "Palace", - "halls": 27, - "rooms": 10 - } - }, - { - "name": "White", - "class": { - "type": "House", - "halls": 12, - "rooms": 100 - } - } - ], - "heritage": { - "name": "Little house in the prairie", - "class": { - "type": "House", - "rooms": 20 - } - } - }, - - { - "name": "Jerry", - "age": 1, - "subtype": { - "type": "Dog", - "kind": "SHEPHERD", - "action": { - "type": "Bark", - "volume": 7 - } - }, - "species": { - "type": "Dog", - "kind": "SHEPHERD", - "action": { - "type": "Bark", - "volume": 7 - } - } - } - ] -} -` - func TestDecodeNestedObject(t *testing.T) { m := map[string]interface{}{ @@ -370,6 +101,18 @@ func TestDecodeNestedObject(t *testing.T) { _, err := decode.Decode(m, "name", MyTestFactory) So(err, ShouldNotBeNil) }) + + // todo: this panics because the expectation is that sub is an object, not base type, but should defend + //Convey("unrully child object - assigned wrong type", t, func() { + // mp := map[string]interface{}{ + // "name": "foo", + // "kind": "record", + // "slice": []string{"foo", "bar"}, + // "sub": "12", + // } + // _, err := decode.Decode(mp, "kind", MyTestFactory) + // So(err, ShouldNotBeNil) + //}) Convey("unrully child object", t, func() { mp := map[string]interface{}{ "name": "foo", @@ -611,10 +354,80 @@ func TestDecodeNestedObject(t *testing.T) { _, err = decode.UnmarshalJSON(b[1:], "kind", MyTestFactory) So(err, ShouldNotBeNil) }) - Convey("Test OneOf decoding", t, func() { - v, err := decode.UnmarshalJSONInto([]byte(oneOfTestPayload), &Envelope{}, &decoderDesc{}) + Convey("Test OneOf decoding - pets1.json", t, func() { + // load spec from testdata identified by file + bytes, err := ioutil.ReadFile("testdata/pets1.json") + if err != nil { + t.Fatal(err) + } + + v, err := decode.UnmarshalJSONInto(bytes, &Envelope{}, SchemaPathFactory) So(err, ShouldBeNil) _, err = json.MarshalIndent(v, "", " ") So(err, ShouldBeNil) }) + Convey("Test OneOf decoding - array of objects", t, func() { + b := `{ "name": "john", "owns": [{ "type": "Palace"}, {"type": "House"}]}` + _, err := decode.UnmarshalJSONInto([]byte(b), &PetOwner{}, SchemaPathFactory) + So(err, ShouldBeNil) + }) + Convey("Test OneOf decoding - array of user crafted objects ", t, func() { + var y = struct{ LivesIn *[]struct{ Age *int } }{} + var x = struct{ LivesIn []*struct{ Age *int } }{} + var z = struct{ LivesIn []*struct{ Age int } }{} + m := map[string]interface{}{ + "livesIn": []map[string]interface{}{ + {"age": 7}, + }, + } + + _, err := decode.DecodeInto(m, &y, SchemaPathFactory) + So(err, ShouldBeNil) + _, err = decode.DecodeInto(m, &x, SchemaPathFactory) + So(err, ShouldBeNil) + _, err = decode.DecodeInto(m, &z, SchemaPathFactory) + So(err, ShouldNotBeNil) + }) + Convey("Test OneOf decoding - array of objects - bad oneOf", t, func() { + + b := `{ "name": "john", "owns": [{ "class": "Palace"}, {"class":12}]}` + _, err := decode.UnmarshalJSONInto([]byte(b), &PetOwner{}, SchemaPathFactory) + So(err, ShouldNotBeNil) + }) + Convey("Test OneOf decoding - array of objects - bad property type", t, func() { + b := `{ "name": "john", "owns": [{ "class": { "type": "House", "rooms": "string"}}]}` + _, err := decode.UnmarshalJSONInto([]byte(b), &PetOwner{}, SchemaPathFactory) + So(err, ShouldNotBeNil) + }) + Convey("Test OneOf decoding - wrong oneOf discriminator", t, func() { + b := `{ "name": "john", "livesIn": { "class": "Palace"}}` + _, err := decode.UnmarshalJSONInto([]byte(b), &PetOwner{}, SchemaPathFactory) + So(err, ShouldNotBeNil) + }) + Convey("Test OneOf decoding - bad json", t, func() { + b := `{ "name": ` + _, err := decode.UnmarshalJSONInto([]byte(b), &PetOwner{}, SchemaPathFactory) + So(err, ShouldNotBeNil) + }) + Convey("Test OneOf decoding - cannot decode into object that has non pointer fields", t, func() { + var x = struct { + Name string + LivesIn []string + }{} + b := `{ "livesIn": { "class": "Palace"}}` + _, err := decode.UnmarshalJSONInto([]byte(b), &x, SchemaPathFactory) + So(err, ShouldNotBeNil) + var y = struct{ LivesIn *struct{ Age int } }{} + b = `{ "livesIn": { "age": 7}}` + _, err = decode.UnmarshalJSONInto([]byte(b), &y, SchemaPathFactory) + So(err, ShouldNotBeNil) + }) + Convey("Test OneOf decoding - cannot decode into object is not struct pointer", t, func() { + b := `{ "name": "john"}` + i := 1 + _, err := decode.UnmarshalJSONInto([]byte(b), i, SchemaPathFactory) + So(err, ShouldNotBeNil) + _, err = decode.UnmarshalJSONInto([]byte(b), &i, SchemaPathFactory) + So(err, ShouldNotBeNil) + }) } diff --git a/decode/testdata/pets1.json b/decode/testdata/pets1.json new file mode 100644 index 0000000..6c979ae --- /dev/null +++ b/decode/testdata/pets1.json @@ -0,0 +1,88 @@ +{ + "owners": [ + { + "name": "John", + "age": 27, + "favorite": { + "type": "Cat", + "mood": "ALOOF", + "sound": { + "type": "PURR", + "heritage": { + "name": "Buckingham", + "class": { + "type": "Palace", + "halls": 7, + "rooms": 20 + } + } + } + }, + "dogs": [ + { + "type": "Dog", + "kind": "SHEPHERD", + "sound": { + "type": "BARK", + "volume": 7 + } + }, + { + "type": "Dog", + "kind": "TOY", + "sound": { + "type": "BARK", + "volume": 1 + } + } + ], + "owns": [ + { + "name": "Buckingham", + "class": { + "type": "Palace", + "halls": 7, + "rooms": 20 + } + }, + { + "name": "Mysore", + "class": { + "type": "Palace", + "halls": 12, + "rooms": 10 + } + }, + { + "name": "White", + "class": { + "type": "House", + "halls": 12, + "rooms": 100 + } + }, + { + "name": "Little house in the prairie", + "class": { + "type": "House", + "rooms": 20 + } + } + ], + "petLivesIn": { + "name": "Buckingham", + "class": { + "type": "Palace", + "halls": 7, + "rooms": 20 + } + }, + "livesIn": { + "name": "Mysore", + "type": "Palace", + "halls": 12, + "rooms": 10 + } + } + ] +} From dabec328101254c83d3fc27a514684c18b2e2862 Mon Sep 17 00:00:00 2001 From: bonny Date: Fri, 6 Sep 2019 23:11:38 -0700 Subject: [PATCH 4/5] renamed iterable -> iterator --- decode/decoder.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/decode/decoder.go b/decode/decoder.go index f6f057e..a2cc589 100644 --- a/decode/decoder.go +++ b/decode/decoder.go @@ -57,7 +57,7 @@ func Decode(m map[string]interface{}, discriminator string, f Factory) (interfac s.Index(i).Set(reflect.Indirect(reflect.ValueOf(child2))) continue } - s.Index(i).Set(reflect.ValueOf(obj[i])) + s.Index(i).Set(reflect.ValueOf(obj[i])) } reflect.ValueOf(r).Elem().FieldByName(strcase.ToCamel(k)).Set(s) continue @@ -194,9 +194,9 @@ func DecodeInto(m map[string]interface{}, o interface{}, pf PathFactory) (interf return o, nil } -type iterable func() (next iterable, obj interface{}) +type iterator func() (next iterator, obj interface{}) -func decodeIntoArray(field reflect.Value, fldName string, iter iterable, len int, pf PathFactory) error { +func decodeIntoArray(field reflect.Value, fldName string, iter iterator, len int, pf PathFactory) error { var s reflect.Value var ps reflect.Value @@ -214,7 +214,7 @@ func decodeIntoArray(field reflect.Value, fldName string, iter iterable, len int et := field.Type().Elem().Elem() i := 0 - for next, o := iter(); next != nil; next, o = iter() { + for next, o := iter(); next != nil; next, o = next() { pV := reflect.ValueOf(o) objm, ok := o.(map[string]interface{}) @@ -245,8 +245,8 @@ func decodeIntoArray(field reflect.Value, fldName string, iter iterable, len int func decodeIntoArrayOfObjectsField(field reflect.Value, fldName string, obj []map[string]interface{}, pf PathFactory) error { n := 0 - var i iterable - i = func() (iterable, interface{}) { + var i iterator + i = func() (iterator, interface{}) { if n < (len(obj)) { n++ return i, obj[n-1] @@ -259,8 +259,8 @@ func decodeIntoArrayOfObjectsField(field reflect.Value, fldName string, obj []ma func decodeIntoArrayField(field reflect.Value, fldName string, obj []interface{}, pf PathFactory) error { n := 0 - var i iterable - i = func() (iterable, interface{}) { + var i iterator + i = func() (iterator, interface{}) { if n < (len(obj)) { n++ return i, obj[n-1] From c86f05315de67a1a46835754a049f986b94e07cc Mon Sep 17 00:00:00 2001 From: bonny Date: Tue, 10 Sep 2019 14:03:30 -0700 Subject: [PATCH 5/5] small changes to address weber's comments --- decode/{decode_pets_test.go => decode_pets_gen_test.go} | 0 decode/decode_test.go | 4 +--- decode/decoder.go | 8 +------- go.sum | 1 + 4 files changed, 3 insertions(+), 10 deletions(-) rename decode/{decode_pets_test.go => decode_pets_gen_test.go} (100%) diff --git a/decode/decode_pets_test.go b/decode/decode_pets_gen_test.go similarity index 100% rename from decode/decode_pets_test.go rename to decode/decode_pets_gen_test.go diff --git a/decode/decode_test.go b/decode/decode_test.go index dd493cb..e330769 100644 --- a/decode/decode_test.go +++ b/decode/decode_test.go @@ -357,9 +357,7 @@ func TestDecodeNestedObject(t *testing.T) { Convey("Test OneOf decoding - pets1.json", t, func() { // load spec from testdata identified by file bytes, err := ioutil.ReadFile("testdata/pets1.json") - if err != nil { - t.Fatal(err) - } + So(err, ShouldBeNil) v, err := decode.UnmarshalJSONInto(bytes, &Envelope{}, SchemaPathFactory) So(err, ShouldBeNil) diff --git a/decode/decoder.go b/decode/decoder.go index a2cc589..5f90d56 100644 --- a/decode/decoder.go +++ b/decode/decoder.go @@ -166,18 +166,11 @@ func DecodeInto(m map[string]interface{}, o interface{}, pf PathFactory) (interf continue } - // todo: this is not required because of field validity check above. Check this out - //if reflect.DeepEqual(field, reflect.Value{}) { - // fmt.Printf("field by name %v not found\n", fldName) - // continue - //} - // special case for empty interfaces - they must represent objects hence we should not be here if field.Type().NumMethod() == 0 { return nil, fmt.Errorf("Invalid value found for field name %v (expected object, not basic type)\n", fldName) } - // todo: is this required anymore? if field.CanInterface() { newVal := reflect.TypeOf(field.Interface()) if newVal != reflect.TypeOf(v) { @@ -272,6 +265,7 @@ func decodeIntoArrayField(field reflect.Value, fldName string, obj []interface{} } func decodeIntoObjectField(field reflect.Value, fldName string, v map[string]interface{}, pf PathFactory) error { + // todo: loosen this restriction in future if field.Kind() != reflect.Ptr { return fmt.Errorf("expecting target field %s to be of type object pointer", fldName) } diff --git a/go.sum b/go.sum index 423071d..9f90553 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 h1:ECW73yc9MY7935nNYXUkK7Dz17YuSUI9yqRqYS8aBww= github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=