-
Notifications
You must be signed in to change notification settings - Fork 218
Embedded structs v2 #78
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,14 +131,28 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) | |
|
|
||
| for i := 0; i < modelValue.NumField(); i++ { | ||
| fieldType := modelType.Field(i) | ||
| tag := fieldType.Tag.Get("jsonapi") | ||
| tag := fieldType.Tag.Get(annotationJSONAPI) | ||
|
|
||
| // handles embedded structs | ||
| if isEmbeddedStruct(fieldType) { | ||
| if shouldIgnoreField(tag) { | ||
| continue | ||
| } | ||
| model := reflect.ValueOf(modelValue.Field(i).Addr().Interface()) | ||
| err := unmarshalNode(data, model, included) | ||
| if err != nil { | ||
| er = err | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if tag == "" { | ||
| continue | ||
| } | ||
|
|
||
| fieldValue := modelValue.Field(i) | ||
|
|
||
| args := strings.Split(tag, ",") | ||
| args := strings.Split(tag, annotationSeperator) | ||
|
|
||
| if len(args) < 1 { | ||
| er = ErrBadJSONAPIStructTag | ||
|
|
@@ -446,7 +460,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) | |
| } | ||
|
|
||
| // As a final catch-all, ensure types line up to avoid a runtime panic. | ||
| if fieldValue.Kind() != v.Kind() { | ||
| // Ignore interfaces since interfaces are poly | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What did you mean by "poly"?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. polymorphic. But if you feel that isn't the correct term, a more accurate comment is welcome. Why does this matter? We were doing something like this: |
||
| if fieldValue.Kind() != reflect.Interface && fieldValue.Kind() != v.Kind() { | ||
| return ErrInvalidType | ||
| } | ||
| fieldValue.Set(reflect.ValueOf(val)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -211,6 +211,21 @@ func visitModelNode(model interface{}, included *map[string]*Node, | |
| for i := 0; i < modelValue.NumField(); i++ { | ||
| structField := modelValue.Type().Field(i) | ||
| tag := structField.Tag.Get(annotationJSONAPI) | ||
|
|
||
| // handles embedded structs | ||
| if isEmbeddedStruct(structField) { | ||
| if shouldIgnoreField(tag) { | ||
| continue | ||
| } | ||
| model := modelValue.Field(i).Addr().Interface() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see similar comment, I'll pass directly to |
||
| embNode, err := visitModelNode(model, included, sideload) | ||
| if err != nil { | ||
| er = err | ||
| break | ||
| } | ||
| node.merge(embNode) | ||
| } | ||
|
|
||
| if tag == "" { | ||
| continue | ||
| } | ||
|
|
@@ -517,3 +532,17 @@ func convertToSliceInterface(i *interface{}) ([]interface{}, error) { | |
| } | ||
| return response, nil | ||
| } | ||
|
|
||
| func isEmbeddedStruct(sField reflect.StructField) bool { | ||
| if sField.Anonymous && sField.Type.Kind() == reflect.Struct { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can be simplified by returning the comparison; don't need if
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func shouldIgnoreField(japiTag string) bool { | ||
| if strings.HasPrefix(japiTag, annotationIgnore) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can be simplified by returning the comparison; don't need if
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| return true | ||
| } | ||
| return false | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import ( | |
| "fmt" | ||
| "reflect" | ||
| "sort" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
| ) | ||
|
|
@@ -59,7 +60,7 @@ func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links { | |
| } | ||
|
|
||
| type Post struct { | ||
| Blog | ||
| Blog `jsonapi:"-"` | ||
| ID uint64 `jsonapi:"primary,posts"` | ||
| BlogID int `jsonapi:"attr,blog_id"` | ||
| ClientID string `jsonapi:"client-id"` | ||
|
|
@@ -829,6 +830,205 @@ func TestMarshalMany_InvalidIntefaceArgument(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| func TestMergeNode(t *testing.T) { | ||
| parent := &Node{ | ||
| Type: "Good", | ||
| ID: "99", | ||
| Attributes: map[string]interface{}{"fizz": "buzz"}, | ||
| } | ||
|
|
||
| child := &Node{ | ||
| Type: "Better", | ||
| ClientID: "1111", | ||
| Attributes: map[string]interface{}{"timbuk": 2}, | ||
| } | ||
|
|
||
| expected := &Node{ | ||
| Type: "Better", | ||
| ID: "99", | ||
| ClientID: "1111", | ||
| Attributes: map[string]interface{}{"fizz": "buzz", "timbuk": 2}, | ||
| } | ||
|
|
||
| parent.merge(child) | ||
|
|
||
| if !reflect.DeepEqual(expected, parent) { | ||
| t.Errorf("Got %+v Expected %+v", parent, expected) | ||
| } | ||
| } | ||
|
|
||
| func TestIsEmbeddedStruct(t *testing.T) { | ||
| type foo struct{} | ||
|
|
||
| structType := reflect.TypeOf(foo{}) | ||
| stringType := reflect.TypeOf("") | ||
| if structType.Kind() != reflect.Struct { | ||
| t.Fatal("structType.Kind() is not a struct.") | ||
| } | ||
| if stringType.Kind() != reflect.String { | ||
| t.Fatal("stringType.Kind() is not a string.") | ||
| } | ||
|
|
||
| type test struct { | ||
| scenario string | ||
| input reflect.StructField | ||
| expectedRes bool | ||
| } | ||
|
|
||
| tests := []test{ | ||
| test{ | ||
| scenario: "success", | ||
| input: reflect.StructField{Anonymous: true, Type: structType}, | ||
| expectedRes: true, | ||
| }, | ||
| test{ | ||
| scenario: "wrong type", | ||
| input: reflect.StructField{Anonymous: true, Type: stringType}, | ||
| expectedRes: false, | ||
| }, | ||
| test{ | ||
| scenario: "not embedded", | ||
| input: reflect.StructField{Type: structType}, | ||
| expectedRes: false, | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| res := isEmbeddedStruct(test.input) | ||
| if res != test.expectedRes { | ||
| t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestShouldIgnoreField(t *testing.T) { | ||
| type test struct { | ||
| scenario string | ||
| input string | ||
| expectedRes bool | ||
| } | ||
|
|
||
| tests := []test{ | ||
| test{ | ||
| scenario: "opt-out", | ||
| input: annotationIgnore, | ||
| expectedRes: true, | ||
| }, | ||
| test{ | ||
| scenario: "no tag", | ||
| input: "", | ||
| expectedRes: false, | ||
| }, | ||
| test{ | ||
| scenario: "wrong tag", | ||
| input: "wrong,tag", | ||
| expectedRes: false, | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| res := shouldIgnoreField(test.input) | ||
| if res != test.expectedRes { | ||
| t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestIsValidEmbeddedStruct(t *testing.T) { | ||
| type foo struct{} | ||
|
|
||
| structType := reflect.TypeOf(foo{}) | ||
| stringType := reflect.TypeOf("") | ||
| if structType.Kind() != reflect.Struct { | ||
| t.Fatal("structType.Kind() is not a struct.") | ||
| } | ||
| if stringType.Kind() != reflect.String { | ||
| t.Fatal("stringType.Kind() is not a string.") | ||
| } | ||
|
|
||
| type test struct { | ||
| scenario string | ||
| input reflect.StructField | ||
| expectedRes bool | ||
| } | ||
|
|
||
| tests := []test{ | ||
| test{ | ||
| scenario: "success", | ||
| input: reflect.StructField{Anonymous: true, Type: structType}, | ||
| expectedRes: true, | ||
| }, | ||
| test{ | ||
| scenario: "opt-out", | ||
| input: reflect.StructField{Anonymous: true, Tag: "jsonapi:\"-\"", Type: structType}, | ||
| expectedRes: false, | ||
| }, | ||
| test{ | ||
| scenario: "wrong type", | ||
| input: reflect.StructField{Anonymous: true, Type: stringType}, | ||
| expectedRes: false, | ||
| }, | ||
| test{ | ||
| scenario: "not embedded", | ||
| input: reflect.StructField{Type: structType}, | ||
| expectedRes: false, | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| res := (isEmbeddedStruct(test.input) && !shouldIgnoreField(test.input.Tag.Get(annotationJSONAPI))) | ||
| if res != test.expectedRes { | ||
| t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestMarshalUnmarshalCompositeStruct(t *testing.T) { | ||
| type Thing struct { | ||
| ID int `jsonapi:"primary,things"` | ||
| Fizz string `jsonapi:"attr,fizz"` | ||
| Buzz int `jsonapi:"attr,buzz"` | ||
| } | ||
|
|
||
| type Model struct { | ||
| Thing | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think some additional tests should be added to demonstrate what should happen when fields are overlapping. Ie both have a field w/ That behaviour will become expected by users of this lib; and such tests would ensure we don't break that expectation. |
||
| Foo string `jsonapi:"attr,foo"` | ||
| Bar string `jsonapi:"attr,bar"` | ||
| Bat string `jsonapi:"attr,bat"` | ||
| } | ||
|
|
||
| model := &Model{} | ||
| model.ID = 1 | ||
| model.Fizz = "fizzy" | ||
| model.Buzz = 99 | ||
| model.Foo = "fooey" | ||
| model.Bar = "barry" | ||
| model.Bat = "batty" | ||
|
|
||
| buf := bytes.NewBuffer(nil) | ||
| if err := MarshalOnePayload(buf, model); err != nil { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs to be updated to |
||
| t.Fatal(err) | ||
| } | ||
|
|
||
| // assert encoding from model to jsonapi output | ||
| expected := `{"data":{"type":"things","id":"1","attributes":{"bar":"barry","bat":"batty","buzz":99,"fizz":"fizzy","foo":"fooey"}}}` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should avoid adding string comparisons. Semantic JSON comparison is preferred... could add a test helper method like: func isJSONEqual(b1, b2 []byte) (result bool, err error) {
var i1, i2 interface{}
if err = json.Unmarshal(b1, &i1); err != nil {
return
}
if err = json.Unmarshal(b2, &i2); err != nil {
return
}
result = true
return
} |
||
| actual := strings.TrimSpace(string(buf.Bytes())) | ||
|
|
||
| if expected != actual { | ||
| t.Errorf("Got %+v Expected %+v", actual, expected) | ||
| } | ||
|
|
||
| dst := &Model{} | ||
| if err := UnmarshalPayload(buf, dst); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| // assert decoding from jsonapi output to model | ||
| if !reflect.DeepEqual(model, dst) { | ||
| t.Errorf("Got %#v Expected %#v", dst, model) | ||
| } | ||
| } | ||
|
|
||
| func testBlog() *Blog { | ||
| return &Blog{ | ||
| ID: 5, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be
model =There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think whether I assign to the original argument, or shadow it here, it shouldn't make a difference since the main loop of this function works off of
modelValue.Between assign/overwriting to the original argument vs initializing a new var, I feel like the latter is preferred. But to avoid confusion, I'll pass it directly to
unmarshalNode()vs using a shadowed var.Also, note, w/ handling
annotationRelation, we don't overwrite the originalmodelargument. https://github.com/google/jsonapi/blob/master/request.go#L474There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My gometalinter actually caught this not me :)