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
}
// 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))
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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",