From ea9a61104825d1f3b86984d244e296292fff11f6 Mon Sep 17 00:00:00 2001 From: David Fournier Date: Mon, 1 May 2017 20:04:15 +0200 Subject: [PATCH] Support extending model with anonymousField --- .helper_test.go.swp | Bin 0 -> 16384 bytes .models_test.go.swp | Bin 0 -> 16384 bytes .request_test.go.swp | Bin 0 -> 16384 bytes constants.go | 1 + helper.go | 65 +++++++++++++++ helper_test.go | 187 +++++++++++++++++++++++++++++++++++++++++++ models_test.go | 18 +++++ request.go | 64 ++++----------- request_test.go | 26 ++++++ response.go | 78 ++++++------------ response_test.go | 27 +++++++ 11 files changed, 362 insertions(+), 104 deletions(-) create mode 100644 .helper_test.go.swp create mode 100644 .models_test.go.swp create mode 100644 .request_test.go.swp create mode 100644 helper.go create mode 100644 helper_test.go diff --git a/.helper_test.go.swp b/.helper_test.go.swp new file mode 100644 index 0000000000000000000000000000000000000000..5048f30894f067fe5bb525fd43ca2e010fd126f7 GIT binary patch literal 16384 zcmeI2-ES0C7{(7pii@H~6B8rqbP?Po%k)bWOT1_)ABzDte1;&Q)19;1p|dl~%$c&a z6gBZHdPO2eqX~b2S7?+AglLF{Xo3(c2zV)KVxoe<8;$Wf^U>XIyW43yjbP3uzs&5+ z?3rhudFPz>Ju|&6JJK7am2wNg=N>{1ADF+(dHAP>UnN3RWk6GNrehfzwfO!TANM#} zH?xXf;|Oo_p)n0M=gnr@$~NaU*5~xfS+jsGW?rYw`)t!tidu6Y)r-{H%c#xdyjizt zu~Rr89H{4j!*a`4E+p-3t?sS9INl`PcklLkzYzO`1Hu8}fN(%KARG`52nU1%!h!#p z1FYCUp2vS`@c)60{n6ZA@6{< zz!C5qNP&?Bgq#QGzzZM&mVigWr5GVEfvqNO6uGR??KIgRS7 zy}NafyhG6)Dlt`NqLjGYw?tCJ5^JXl4XvfdwzNsqr6q>fNP3w(^Ozy(C#X! z$)Y(Omrm}cw76zJ+DKIYJhZEAv!pg?H(Irb&{^>|Xg6B52(DRC8?+l*Z8@4h-ars`AmqU}dostgJ`S&n1vf-t(%)Bf|SQi$2R8@en*i+ zOa3bV&C=CgwmLLSa*mOew&6|T_1;Uv7G-#E+0IxdULkf{Nib;{f0Yi6+ zfy7R{iVdx6N{kvRRXYvS7%Z5M?Ov25%&&I2=E1`3y5Y>thIboHmFhKo&3JuExt|(u zDQtgf<`bGG=R@h!`q+;3{U`VU;!9GEq@xk23`g)fpy>pYWVBm z8aN8Jf(+OUczu5wHT|dH6gUYEgA(Wht>918_kV!v;3JR)Jzy`0fsf(chu{OC0bcJf z2HaB#h>vhUIB<(PFiDc&jk>#$@VQ9_&&tNlrA<0pRp8@*9CEIze$rCb6VY4*5|;9$ z6#k~$Z0PDNjM(Th*%kjqCL8lC51Giimzt-s?uwhl@~q3BP@N!=t=Kz00|*zKsGFh= z&Y;QaiPSLHyZ+6>Q+m^IGtNoop1r%K;`mTgy~~MC$3o3SiwJWzAxSFZ|6H(u!Wz+?Q6QQGBmD?)AELCQTLwX-Hqw6dS%CO=n3U# zG(+WA(|s;7?E*TO3qb9R+I@632or3){b|+E{wRsbuV^Kv4K}LJZ+s^D(91@vugXHd z{h3HaMi8yUN(=qQYv?Oapt%YLru59N_JG*MpIYFK4Dr;HA3A2xK}@<0Pit62z|>Zi Y!9$5j^GJVc43B5|!DEyKjaMuC7X&}fPyhe` literal 0 HcmV?d00001 diff --git a/.models_test.go.swp b/.models_test.go.swp new file mode 100644 index 0000000000000000000000000000000000000000..de5f4e77a549816cb841f341cbdbc2d7dcc38026 GIT binary patch literal 16384 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 XQc 2 { - for _, arg := range args[2:] { + if len(args) > 1 { + for _, arg := range args[1:] { if arg == annotationISO8601 { iso8601 = true } } } - val := attributes[args[1]] + val := attributes[args[0]] // continue if the attribute was not included in the request if val == nil { @@ -362,15 +335,6 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) if v.Kind() == reflect.Float64 { floatValue := v.Interface().(float64) - // The field may or may not be a pointer to a numeric; the kind var - // will not contain a pointer type - var kind reflect.Kind - if fieldValue.Kind() == reflect.Ptr { - kind = fieldType.Type.Elem().Kind() - } else { - kind = fieldType.Type.Kind() - } - var numericValue reflect.Value switch kind { @@ -454,7 +418,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } else if annotation == annotationRelation { isSlice := fieldValue.Type().Kind() == reflect.Slice - if data.Relationships == nil || data.Relationships[args[1]] == nil { + if data.Relationships == nil || data.Relationships[args[0]] == nil { continue } @@ -464,7 +428,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) buf := bytes.NewBuffer(nil) - json.NewEncoder(buf).Encode(data.Relationships[args[1]]) + json.NewEncoder(buf).Encode(data.Relationships[args[0]]) json.NewDecoder(buf).Decode(relationship) data := relationship.Data @@ -493,7 +457,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) buf := bytes.NewBuffer(nil) json.NewEncoder(buf).Encode( - data.Relationships[args[1]], + data.Relationships[args[0]], ) json.NewDecoder(buf).Decode(relationship) diff --git a/request_test.go b/request_test.go index 1066733..bb31eca 100644 --- a/request_test.go +++ b/request_test.go @@ -70,6 +70,32 @@ func TestUnmarshalToStructWithPointerAttr(t *testing.T) { } } +func TestUnmarshall_attrFromExtendedAnonymousField(t *testing.T) { + out := new(WithExtendedAnonymousField) + commonField := "Common value" + data := map[string]interface{}{ + "data": map[string]interface{}{ + "type": "with-extended-anonymous-fields", + "id": "1", + "attributes": map[string]interface{}{ + "common_field": commonField, + }, + }, + } + b, err := json.Marshal(data) + if err != nil { + t.Fatal(err) + } + + if err := UnmarshalPayload(bytes.NewReader(b), out); err != nil { + t.Fatal(err) + } + + if expected, actual := commonField, out.CommonField; expected != actual { + t.Fatalf("Was expecting CommonField to be `%s`, got `%s`", expected, actual) + } +} + func TestUnmarshalPayload_ptrsAllNil(t *testing.T) { out := new(WithPointer) if err := UnmarshalPayload( diff --git a/response.go b/response.go index a8ac71f..9f2bad3 100644 --- a/response.go +++ b/response.go @@ -7,7 +7,6 @@ import ( "io" "reflect" "strconv" - "strings" "time" ) @@ -206,44 +205,15 @@ func visitModelNode(model interface{}, included *map[string]*Node, var er error - modelValue := reflect.ValueOf(model).Elem() - modelType := reflect.ValueOf(model).Type().Elem() + fields, er := extractFields(reflect.ValueOf(model)) - for i := 0; i < modelValue.NumField(); i++ { - structField := modelValue.Type().Field(i) - tag := structField.Tag.Get(annotationJSONAPI) - if tag == "" { - continue - } - - fieldValue := modelValue.Field(i) - fieldType := modelType.Field(i) - - args := strings.Split(tag, annotationSeperator) - - if len(args) < 1 { - er = ErrBadJSONAPIStructTag - break - } - - annotation := args[0] - - if (annotation == annotationClientID && len(args) != 1) || - (annotation != annotationClientID && len(args) < 2) { - er = ErrBadJSONAPIStructTag - break - } + for _, field := range fields { + fieldValue, annotation, kind, args := field.Value, field.Annotation, field.Kind, field.Args if annotation == annotationPrimary { v := fieldValue - - // Deal with PTRS - var kind reflect.Kind - if fieldValue.Kind() == reflect.Ptr { - kind = fieldType.Type.Elem().Kind() - v = reflect.Indirect(fieldValue) - } else { - kind = fieldType.Type.Kind() + if field.IsPtr { + v = reflect.Indirect(v) } // Handle allowed types @@ -277,7 +247,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, break } - node.Type = args[1] + node.Type = args[0] } else if annotation == annotationClientID { clientID := fieldValue.String() if clientID != "" { @@ -286,8 +256,8 @@ func visitModelNode(model interface{}, included *map[string]*Node, } else if annotation == annotationAttribute { var omitEmpty, iso8601 bool - if len(args) > 2 { - for _, arg := range args[2:] { + if len(args) > 1 { + for _, arg := range args[1:] { switch arg { case annotationOmitEmpty: omitEmpty = true @@ -309,9 +279,9 @@ func visitModelNode(model interface{}, included *map[string]*Node, } if iso8601 { - node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat) + node.Attributes[args[0]] = t.UTC().Format(iso8601TimeFormat) } else { - node.Attributes[args[1]] = t.Unix() + node.Attributes[args[0]] = t.Unix() } } else if fieldValue.Type() == reflect.TypeOf(new(time.Time)) { // A time pointer may be nil @@ -320,7 +290,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, continue } - node.Attributes[args[1]] = nil + node.Attributes[args[0]] = nil } else { tm := fieldValue.Interface().(*time.Time) @@ -329,9 +299,9 @@ func visitModelNode(model interface{}, included *map[string]*Node, } if iso8601 { - node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat) + node.Attributes[args[0]] = tm.UTC().Format(iso8601TimeFormat) } else { - node.Attributes[args[1]] = tm.Unix() + node.Attributes[args[0]] = tm.Unix() } } } else { @@ -345,17 +315,17 @@ func visitModelNode(model interface{}, included *map[string]*Node, strAttr, ok := fieldValue.Interface().(string) if ok { - node.Attributes[args[1]] = strAttr + node.Attributes[args[0]] = strAttr } else { - node.Attributes[args[1]] = fieldValue.Interface() + node.Attributes[args[0]] = fieldValue.Interface() } } } else if annotation == annotationRelation { var omitEmpty bool //add support for 'omitempty' struct tag for marshaling as absent - if len(args) > 2 { - omitEmpty = args[2] == annotationOmitEmpty + if len(args) > 1 { + omitEmpty = args[1] == annotationOmitEmpty } isSlice := fieldValue.Type().Kind() == reflect.Slice @@ -371,12 +341,12 @@ func visitModelNode(model interface{}, included *map[string]*Node, var relLinks *Links if linkableModel, ok := model.(RelationshipLinkable); ok { - relLinks = linkableModel.JSONAPIRelationshipLinks(args[1]) + relLinks = linkableModel.JSONAPIRelationshipLinks(args[0]) } var relMeta *Meta if metableModel, ok := model.(RelationshipMetable); ok { - relMeta = metableModel.JSONAPIRelationshipMeta(args[1]) + relMeta = metableModel.JSONAPIRelationshipMeta(args[0]) } if isSlice { @@ -400,20 +370,20 @@ func visitModelNode(model interface{}, included *map[string]*Node, shallowNodes = append(shallowNodes, toShallowNode(n)) } - node.Relationships[args[1]] = &RelationshipManyNode{ + node.Relationships[args[0]] = &RelationshipManyNode{ Data: shallowNodes, Links: relationship.Links, Meta: relationship.Meta, } } else { - node.Relationships[args[1]] = relationship + node.Relationships[args[0]] = relationship } } else { // to-one relationships // Handle null relationship case if fieldValue.IsNil() { - node.Relationships[args[1]] = &RelationshipOneNode{Data: nil} + node.Relationships[args[0]] = &RelationshipOneNode{Data: nil} continue } @@ -429,13 +399,13 @@ func visitModelNode(model interface{}, included *map[string]*Node, if sideload { appendIncluded(included, relationship) - node.Relationships[args[1]] = &RelationshipOneNode{ + node.Relationships[args[0]] = &RelationshipOneNode{ Data: toShallowNode(relationship), Links: relLinks, Meta: relMeta, } } else { - node.Relationships[args[1]] = &RelationshipOneNode{ + node.Relationships[args[0]] = &RelationshipOneNode{ Data: relationship, Links: relLinks, Meta: relMeta, diff --git a/response_test.go b/response_test.go index 331023e..99a135f 100644 --- a/response_test.go +++ b/response_test.go @@ -211,6 +211,33 @@ func TestMarshall_invalidIDType(t *testing.T) { } } +func TestMarshall_attrFromExtendedAnonymousField(t *testing.T) { + id, commonField := 1, "Common value" + model := &WithExtendedAnonymousField{} + model.ID = id + model.CommonField = commonField + + out := bytes.NewBuffer(nil) + + if err := MarshalOnePayload(out, model); err != nil { + t.Fatal(err) + } + + var jsonData map[string]interface{} + if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { + t.Fatal(err) + } + + attributes := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{}) + val, exists := attributes["common_field"] + if !exists { + t.Fatal("Was expecting the data.attributes.common_field member to exist") + } + if val != commonField { + t.Fatalf("Was expecting the data.attributes.common_field member to be `%s`, got `%s`", commonField, val) + } +} + func TestOmitsEmptyAnnotation(t *testing.T) { book := &Book{ Author: "aren55555",