Added support for string, int(8,16,32,64), uint(8,16,32,64) and each … (#51)

* 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.
This commit is contained in:
Aren Patel 2016-09-22 15:02:30 -07:00 committed by GitHub
parent b6c6609ff2
commit 925ebf2136
4 changed files with 227 additions and 34 deletions

View File

@ -166,24 +166,84 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
continue continue
} }
// Check the JSON API Type
if data.Type != args[1] { 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 break
} }
if fieldValue.Kind() == reflect.String { // ID will have to be transmitted as astring per the JSON API spec
fieldValue.Set(reflect.ValueOf(data.ID)) v := reflect.ValueOf(data.ID)
} else if fieldValue.Kind() == reflect.Int {
id, err := strconv.Atoi(data.ID) // Deal with PTRS
if err != nil { var kind reflect.Kind
er = err if fieldValue.Kind() == reflect.Ptr {
break kind = fieldType.Type.Elem().Kind()
}
fieldValue.SetInt(int64(id))
} else { } 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 er = ErrBadJSONAPIID
break 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 { } else if annotation == clientIDAnnotation {
if data.ClientID == "" { if data.ClientID == "" {
continue continue
@ -367,12 +427,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
break break
} }
if fieldValue.Kind() == reflect.Ptr { assign(fieldValue, numericValue)
fieldValue.Set(numericValue)
} else {
fieldValue.Set(reflect.Indirect(numericValue))
}
continue continue
} }
@ -488,3 +543,13 @@ func fullNode(n *Node, included *map[string]*Node) *Node {
return n 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))
}
}

View File

@ -14,7 +14,7 @@ type BadModel struct {
} }
type WithPointer struct { type WithPointer struct {
ID string `jsonapi:"primary,with-pointers"` ID *uint64 `jsonapi:"primary,with-pointers"`
Name *string `jsonapi:"attr,name"` Name *string `jsonapi:"attr,name"`
IsActive *bool `jsonapi:"attr,is-active"` IsActive *bool `jsonapi:"attr,is-active"`
IntVal *int `jsonapi:"attr,int-val"` 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) out := new(WithPointer)
in := map[string]interface{}{ in := map[string]interface{}{
"name": "The name", "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) { func TestUnmarshalSetsAttrs(t *testing.T) {
out, err := unmarshalSamplePayload() out, err := unmarshalSamplePayload()
if err != nil { if err != nil {
@ -221,7 +266,7 @@ func TestUnmarshalInvalidISO8601(t *testing.T) {
} }
func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) { func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) {
data, _ := samplePayloadWithoutIncluded() data, _ := payload(samplePayloadWithoutIncluded())
in := bytes.NewReader(data) in := bytes.NewReader(data)
out := new(Post) out := new(Post)
@ -393,8 +438,8 @@ func unmarshalSamplePayload() (*Blog, error) {
return out, nil return out, nil
} }
func samplePayloadWithoutIncluded() (result []byte, err error) { func samplePayloadWithoutIncluded() map[string]interface{} {
data := map[string]interface{}{ return map[string]interface{}{
"data": map[string]interface{}{ "data": map[string]interface{}{
"type": "posts", "type": "posts",
"id": "1", "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) result, err = json.Marshal(data)
return return
} }

View File

@ -17,7 +17,8 @@ var (
ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format") ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format")
// ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field // ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field
// was not a valid numeric type. // 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 // ErrExpectedSlice is returned when a variable or arugment was expected to
// be a slice of *Structs; MarshalMany will return this error when its // be a slice of *Structs; MarshalMany will return this error when its
// interface{} argument is invalid. // interface{} argument is invalid.
@ -189,6 +190,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
var er error var er error
modelValue := reflect.ValueOf(model).Elem() modelValue := reflect.ValueOf(model).Elem()
modelType := reflect.ValueOf(model).Type().Elem()
for i := 0; i < modelValue.NumField(); i++ { for i := 0; i < modelValue.NumField(); i++ {
structField := modelValue.Type().Field(i) structField := modelValue.Type().Field(i)
@ -198,6 +200,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
} }
fieldValue := modelValue.Field(i) fieldValue := modelValue.Field(i)
fieldType := modelType.Field(i)
args := strings.Split(tag, ",") args := strings.Split(tag, ",")
@ -215,18 +218,44 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
} }
if annotation == "primary" { if annotation == "primary" {
id := fieldValue.Interface() v := fieldValue
switch nID := id.(type) { // Deal with PTRS
case string: var kind reflect.Kind
node.ID = nID if fieldValue.Kind() == reflect.Ptr {
case int: kind = fieldType.Type.Elem().Kind()
node.ID = strconv.Itoa(nID) v = reflect.Indirect(fieldValue)
case int64: } else {
node.ID = strconv.FormatInt(nID, 10) kind = fieldType.Type.Kind()
case uint64: }
node.ID = strconv.FormatUint(nID, 10)
// 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: default:
// We had a JSON float (numeric), but our field was not one of the
// allowed numeric types
er = ErrBadJSONAPIID er = ErrBadJSONAPIID
break break
} }

View File

@ -21,7 +21,7 @@ type Blog struct {
type Post struct { type Post struct {
Blog Blog
ID int `jsonapi:"primary,posts"` ID uint64 `jsonapi:"primary,posts"`
BlogID int `jsonapi:"attr,blog_id"` BlogID int `jsonapi:"attr,blog_id"`
ClientID string `jsonapi:"client-id"` ClientID string `jsonapi:"client-id"`
Title string `jsonapi:"attr,title"` Title string `jsonapi:"attr,title"`
@ -38,7 +38,7 @@ type Comment struct {
} }
type Book struct { type Book struct {
ID int `jsonapi:"primary,books"` ID uint64 `jsonapi:"primary,books"`
Author string `jsonapi:"attr,author"` Author string `jsonapi:"attr,author"`
ISBN string `jsonapi:"attr,isbn"` ISBN string `jsonapi:"attr,isbn"`
Title string `jsonapi:"attr,title,omitempty"` Title string `jsonapi:"attr,title,omitempty"`
@ -53,6 +53,58 @@ type Timestamp struct {
Next *time.Time `jsonapi:"attr,next,iso8601"` 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) { func TestOmitsEmptyAnnotation(t *testing.T) {
book := &Book{ book := &Book{
Author: "aren55555", Author: "aren55555",