forked from Mirrors/jsonapi
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:
parent
b6c6609ff2
commit
925ebf2136
97
request.go
97
request.go
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
51
response.go
51
response.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue