From 925ebf2136461bb9d121340ec95a719bf9073d1c Mon Sep 17 00:00:00 2001 From: Aren Patel Date: Thu, 22 Sep 2016 15:02:30 -0700 Subject: [PATCH] =?UTF-8?q?Added=20support=20for=20string,=20int(8,16,32,6?= =?UTF-8?q?4),=20uint(8,16,32,64)=20and=20each=20=E2=80=A6=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added support for string, int(8,16,32,64), uint(8,16,32,64) and each of their ptr types as acceptable to use for the ID field. * No longer declaring a new idErr var; also eliminate the switch within a switch - if the ID field was a string or *string it will continue. Added a couple extra tests. --- request.go | 97 ++++++++++++++++++++++++++++++++++++++++-------- request_test.go | 57 +++++++++++++++++++++++++--- response.go | 51 +++++++++++++++++++------ response_test.go | 56 +++++++++++++++++++++++++++- 4 files changed, 227 insertions(+), 34 deletions(-) diff --git a/request.go b/request.go index 7e6d3eb..0cdc855 100644 --- a/request.go +++ b/request.go @@ -166,24 +166,84 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) continue } + // Check the JSON API Type if data.Type != args[1] { - er = fmt.Errorf("Trying to Unmarshal an object of type %#v, but %#v does not match", data.Type, args[1]) + er = fmt.Errorf( + "Trying to Unmarshal an object of type %#v, but %#v does not match", + data.Type, + args[1], + ) break } - if fieldValue.Kind() == reflect.String { - fieldValue.Set(reflect.ValueOf(data.ID)) - } else if fieldValue.Kind() == reflect.Int { - id, err := strconv.Atoi(data.ID) - if err != nil { - er = err - break - } - fieldValue.SetInt(int64(id)) + // ID will have to be transmitted as astring per the JSON API spec + v := reflect.ValueOf(data.ID) + + // Deal with PTRS + var kind reflect.Kind + if fieldValue.Kind() == reflect.Ptr { + kind = fieldType.Type.Elem().Kind() } else { + kind = fieldType.Type.Kind() + } + + // Handle String case + if kind == reflect.String { + assign(fieldValue, v) + continue + } + + // Value was not a string... only other supported type was a numeric, + // which would have been sent as a float value. + floatValue, err := strconv.ParseFloat(data.ID, 64) + if err != nil { + // Could not convert the value in the "id" attr to a float er = ErrBadJSONAPIID break } + + // Convert the numeric float to one of the supported ID numeric types + // (int[8,16,32,64] or uint[8,16,32,64]) + var idValue reflect.Value + switch kind { + case reflect.Int: + n := int(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Int8: + n := int8(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Int16: + n := int16(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Int32: + n := int32(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Int64: + n := int64(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Uint: + n := uint(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Uint8: + n := uint8(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Uint16: + n := uint16(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Uint32: + n := uint32(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Uint64: + n := uint64(floatValue) + idValue = reflect.ValueOf(&n) + default: + // We had a JSON float (numeric), but our field was not one of the + // allowed numeric types + er = ErrBadJSONAPIID + break + } + + assign(fieldValue, idValue) } else if annotation == clientIDAnnotation { if data.ClientID == "" { continue @@ -367,12 +427,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) break } - if fieldValue.Kind() == reflect.Ptr { - fieldValue.Set(numericValue) - } else { - fieldValue.Set(reflect.Indirect(numericValue)) - } - + assign(fieldValue, numericValue) continue } @@ -488,3 +543,13 @@ func fullNode(n *Node, included *map[string]*Node) *Node { return n } + +// assign will take the value specified and assign it to the field; if +// field is expecting a ptr assign will assign a ptr. +func assign(field, value reflect.Value) { + if field.Kind() == reflect.Ptr { + field.Set(value) + } else { + field.Set(reflect.Indirect(value)) + } +} diff --git a/request_test.go b/request_test.go index 9caeeba..45430a5 100644 --- a/request_test.go +++ b/request_test.go @@ -14,7 +14,7 @@ type BadModel struct { } type WithPointer struct { - ID string `jsonapi:"primary,with-pointers"` + ID *uint64 `jsonapi:"primary,with-pointers"` Name *string `jsonapi:"attr,name"` IsActive *bool `jsonapi:"attr,is-active"` IntVal *int `jsonapi:"attr,int-val"` @@ -46,7 +46,36 @@ func TestUnmarshalToStructWithPointerAttr(t *testing.T) { } } -func TestUnmarshalToStructWithPointerAttr_AbsentVal(t *testing.T) { +func TestUnmarshalPayload_ptrsAllNil(t *testing.T) { + out := new(WithPointer) + if err := UnmarshalPayload( + strings.NewReader(`{"data": {}}`), out); err != nil { + t.Fatalf("Error unmarshalling to Foo") + } + + if out.ID != nil { + t.Fatalf("Error unmarshalling; expected ID ptr to be nil") + } +} + +func TestUnmarshalPayloadWithPointerID(t *testing.T) { + out := new(WithPointer) + attrs := map[string]interface{}{} + + if err := UnmarshalPayload(sampleWithPointerPayload(attrs), out); err != nil { + t.Fatalf("Error unmarshalling to Foo") + } + + // these were present in the payload -- expect val to be not nil + if out.ID == nil { + t.Fatalf("Error unmarshalling; expected ID ptr to be not nil") + } + if e, a := uint64(2), *out.ID; e != a { + t.Fatalf("Was expecting the ID to have a value of %d, got %d", e, a) + } +} + +func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) { out := new(WithPointer) in := map[string]interface{}{ "name": "The name", @@ -133,6 +162,22 @@ func TestUnmarshalSetsID(t *testing.T) { } } +func TestUnmarshal_nonNumericID(t *testing.T) { + data := samplePayloadWithoutIncluded() + data["data"].(map[string]interface{})["id"] = "non-numeric-id" + payload, _ := payload(data) + in := bytes.NewReader(payload) + out := new(Post) + + if err := UnmarshalPayload(in, out); err != ErrBadJSONAPIID { + t.Fatalf( + "Was expecting a `%s` error, got `%s`", + ErrBadJSONAPIID, + err, + ) + } +} + func TestUnmarshalSetsAttrs(t *testing.T) { out, err := unmarshalSamplePayload() if err != nil { @@ -221,7 +266,7 @@ func TestUnmarshalInvalidISO8601(t *testing.T) { } func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) { - data, _ := samplePayloadWithoutIncluded() + data, _ := payload(samplePayloadWithoutIncluded()) in := bytes.NewReader(data) out := new(Post) @@ -393,8 +438,8 @@ func unmarshalSamplePayload() (*Blog, error) { return out, nil } -func samplePayloadWithoutIncluded() (result []byte, err error) { - data := map[string]interface{}{ +func samplePayloadWithoutIncluded() map[string]interface{} { + return map[string]interface{}{ "data": map[string]interface{}{ "type": "posts", "id": "1", @@ -424,7 +469,9 @@ func samplePayloadWithoutIncluded() (result []byte, err error) { }, }, } +} +func payload(data map[string]interface{}) (result []byte, err error) { result, err = json.Marshal(data) return } diff --git a/response.go b/response.go index d29736a..eb5d927 100644 --- a/response.go +++ b/response.go @@ -17,7 +17,8 @@ var ( ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format") // ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field // was not a valid numeric type. - ErrBadJSONAPIID = errors.New("id should be either string, int or uint") + ErrBadJSONAPIID = errors.New( + "id should be either string, int(8,16,32,64) or uint(8,16,32,64)") // ErrExpectedSlice is returned when a variable or arugment was expected to // be a slice of *Structs; MarshalMany will return this error when its // interface{} argument is invalid. @@ -189,6 +190,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool var er error modelValue := reflect.ValueOf(model).Elem() + modelType := reflect.ValueOf(model).Type().Elem() for i := 0; i < modelValue.NumField(); i++ { structField := modelValue.Type().Field(i) @@ -198,6 +200,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool } fieldValue := modelValue.Field(i) + fieldType := modelType.Field(i) args := strings.Split(tag, ",") @@ -215,18 +218,44 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool } if annotation == "primary" { - id := fieldValue.Interface() + v := fieldValue - switch nID := id.(type) { - case string: - node.ID = nID - case int: - node.ID = strconv.Itoa(nID) - case int64: - node.ID = strconv.FormatInt(nID, 10) - case uint64: - node.ID = strconv.FormatUint(nID, 10) + // 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() + } + + // Handle allowed types + switch kind { + case reflect.String: + node.ID = v.Interface().(string) + case reflect.Int: + node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10) + case reflect.Int8: + node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10) + case reflect.Int16: + node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10) + case reflect.Int32: + node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10) + case reflect.Int64: + node.ID = strconv.FormatInt(v.Interface().(int64), 10) + case reflect.Uint: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10) + case reflect.Uint8: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10) + case reflect.Uint16: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10) + case reflect.Uint32: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10) + case reflect.Uint64: + node.ID = strconv.FormatUint(v.Interface().(uint64), 10) default: + // We had a JSON float (numeric), but our field was not one of the + // allowed numeric types er = ErrBadJSONAPIID break } diff --git a/response_test.go b/response_test.go index 093aad1..cc0f982 100644 --- a/response_test.go +++ b/response_test.go @@ -21,7 +21,7 @@ type Blog struct { type Post struct { Blog - ID int `jsonapi:"primary,posts"` + ID uint64 `jsonapi:"primary,posts"` BlogID int `jsonapi:"attr,blog_id"` ClientID string `jsonapi:"client-id"` Title string `jsonapi:"attr,title"` @@ -38,7 +38,7 @@ type Comment struct { } type Book struct { - ID int `jsonapi:"primary,books"` + ID uint64 `jsonapi:"primary,books"` Author string `jsonapi:"attr,author"` ISBN string `jsonapi:"attr,isbn"` Title string `jsonapi:"attr,title,omitempty"` @@ -53,6 +53,58 @@ type Timestamp struct { Next *time.Time `jsonapi:"attr,next,iso8601"` } +type Car struct { + ID *string `jsonapi:"primary,cars"` + Make *string `jsonapi:"attr,make,omitempty"` + Model *string `jsonapi:"attr,model,omitempty"` + Year *uint `jsonapi:"attr,year,omitempty"` +} + +func TestMarshalIDPtr(t *testing.T) { + id, make, model := "123e4567-e89b-12d3-a456-426655440000", "Ford", "Mustang" + car := &Car{ + ID: &id, + Make: &make, + Model: &model, + } + + out := bytes.NewBuffer(nil) + if err := MarshalOnePayload(out, car); err != nil { + t.Fatal(err) + } + + var jsonData map[string]interface{} + if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { + t.Fatal(err) + } + data := jsonData["data"].(map[string]interface{}) + // attributes := data["attributes"].(map[string]interface{}) + + // Verify that the ID was sent + val, exists := data["id"] + if !exists { + t.Fatal("Was expecting the data.id member to exist") + } + if val != id { + t.Fatalf("Was expecting the data.id member to be `%s`, got `%s`", id, val) + } +} + +func TestMarshall_invalidIDType(t *testing.T) { + type badIDStruct struct { + ID *bool `jsonapi:"primary,cars"` + } + id := true + o := &badIDStruct{ID: &id} + + out := bytes.NewBuffer(nil) + if err := MarshalOnePayload(out, o); err != ErrBadJSONAPIID { + t.Fatalf( + "Was expecting a `%s` error, got `%s`", ErrBadJSONAPIID, err, + ) + } +} + func TestOmitsEmptyAnnotation(t *testing.T) { book := &Book{ Author: "aren55555",