forked from Mirrors/jsonapi
Compare commits
60 Commits
u/aren5555
...
master
Author | SHA1 | Date |
---|---|---|
Preston Baxter | 403bd2e40b | |
Preston Baxter | b73a8a772f | |
Aren Patel | 1e07b10d47 | |
Aren Patel | f3b4acfd23 | |
Aren Patel | 58702143d4 | |
Omar Ismail | c0ee6d2554 | |
Quetzy Garcia | b10ff4bf78 | |
Aren Patel | 471426f0d9 | |
Matti Andreas Nielsen | 4fcdb6314a | |
Aren Patel | f822737867 | |
ujjwalsh | b862aa6e32 | |
Aren Patel | 3e3da1210d | |
Aren Patel | c8283f632f | |
Sam Woodard | d0428f63eb | |
Sam Woodard | 4a0c98e9d4 | |
Sam Woodard | e9f117e24a | |
Sam Woodard | 6bf44faa3c | |
Sam Woodard | 9246c912f5 | |
Sam Woodard | e22856db88 | |
Sam Woodard | 1947fea11f | |
Sam Woodard | 906357051e | |
Sam Woodard | d9a610774a | |
Sam Woodard | 941c167d93 | |
Sam Woodard | 43592a3ebe | |
Sam Woodard | ed08d4f02a | |
Sam Woodard | d05fcd97df | |
Sam Woodard | ab24913148 | |
CrushedPixel | 87c6b8e5b5 | |
CrushedPixel | ccac636b4b | |
Sam Woodard | 5307399ec1 | |
Sam Woodard | bdc73a22a3 | |
Markus Ritberger | 417d4eb8fb | |
Sam Woodard | 3c8221b373 | |
Stuart Auld | 3b9f84a311 | |
Ilya Baturin | 5d047c6bc6 | |
Markus Ritberger | 8b7e0bc2c0 | |
Markus Ritberger | d490a0f637 | |
Markus Ritberger | 72f7bad5b3 | |
Markus Ritberger | 9bc94d8c70 | |
Sam Woodard | 2dcc18f436 | |
Markus Ritberger | e428b86c25 | |
Marc Abramowitz | 7c2ceac7c5 | |
Markus Ritberger | 21b4945ad6 | |
Markus Ritberger | bb266b4483 | |
Markus Ritberger | 339909da0d | |
Marc Abramowitz | a3b3bb2cb5 | |
Marc Abramowitz | 16e19ab9f9 | |
Marc Abramowitz | b28beab7f3 | |
Marc Abramowitz | e3c0871f34 | |
Slemgrim | 0400041771 | |
Slemgrim | a6577dfae8 | |
Slemgrim | 7e5c9014d9 | |
Markus Ritberger | fc6968dfe7 | |
Markus Ritberger | b391a84b75 | |
Slemgrim | af21dba1b2 | |
Slemgrim | 0c97f0cf8d | |
Slemgrim | ab915ccbc1 | |
Slemgrim | f0b268e046 | |
Slemgrim | ec077ed283 | |
Slemgrim | e13a19922d |
12
.travis.yml
12
.travis.yml
|
@ -1,7 +1,13 @@
|
||||||
language: go
|
language: go
|
||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
- ppc64le
|
||||||
go:
|
go:
|
||||||
- 1.8.x
|
- 1.11.x
|
||||||
- 1.9.x
|
- 1.12.x
|
||||||
- 1.10.x
|
- 1.13.x
|
||||||
|
- 1.14.x
|
||||||
|
- 1.15.x
|
||||||
|
- 1.16.x
|
||||||
- tip
|
- tip
|
||||||
script: go test ./... -v
|
script: go test ./... -v
|
||||||
|
|
22
README.md
22
README.md
|
@ -3,10 +3,13 @@
|
||||||
[![Build Status](https://travis-ci.org/google/jsonapi.svg?branch=master)](https://travis-ci.org/google/jsonapi)
|
[![Build Status](https://travis-ci.org/google/jsonapi.svg?branch=master)](https://travis-ci.org/google/jsonapi)
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/google/jsonapi)](https://goreportcard.com/report/github.com/google/jsonapi)
|
[![Go Report Card](https://goreportcard.com/badge/github.com/google/jsonapi)](https://goreportcard.com/report/github.com/google/jsonapi)
|
||||||
[![GoDoc](https://godoc.org/github.com/google/jsonapi?status.svg)](http://godoc.org/github.com/google/jsonapi)
|
[![GoDoc](https://godoc.org/github.com/google/jsonapi?status.svg)](http://godoc.org/github.com/google/jsonapi)
|
||||||
|
[![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/)
|
||||||
|
|
||||||
A serializer/deserializer for JSON payloads that comply to the
|
A serializer/deserializer for JSON payloads that comply to the
|
||||||
[JSON API - jsonapi.org](http://jsonapi.org) spec in go.
|
[JSON API - jsonapi.org](http://jsonapi.org) spec in go.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -181,7 +184,7 @@ to-many from being serialized.
|
||||||
**All `Marshal` and `Unmarshal` methods expect pointers to struct
|
**All `Marshal` and `Unmarshal` methods expect pointers to struct
|
||||||
instance or slices of the same contained with the `interface{}`s**
|
instance or slices of the same contained with the `interface{}`s**
|
||||||
|
|
||||||
Now you have your structs prepared to be seralized or materialized, What
|
Now you have your structs prepared to be serialized or materialized, What
|
||||||
about the rest?
|
about the rest?
|
||||||
|
|
||||||
### Create Record Example
|
### Create Record Example
|
||||||
|
@ -343,6 +346,23 @@ func (post Post) JSONAPIRelationshipMeta(relation string) *Meta {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom types
|
||||||
|
|
||||||
|
Custom types are supported for primitive types, only, as attributes. Examples,
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CustomIntType int
|
||||||
|
type CustomFloatType float64
|
||||||
|
type CustomStringType string
|
||||||
|
```
|
||||||
|
|
||||||
|
Types like following are not supported, but may be in the future:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CustomMapType map[string]interface{}
|
||||||
|
type CustomSliceMapType []map[string]interface{}
|
||||||
|
```
|
||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
This package also implements support for JSON API compatible `errors` payloads using the following types.
|
This package also implements support for JSON API compatible `errors` payloads using the following types.
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ const (
|
||||||
annotationRelation = "relation"
|
annotationRelation = "relation"
|
||||||
annotationOmitEmpty = "omitempty"
|
annotationOmitEmpty = "omitempty"
|
||||||
annotationISO8601 = "iso8601"
|
annotationISO8601 = "iso8601"
|
||||||
|
annotationRFC3339 = "rfc3339"
|
||||||
annotationSeperator = ","
|
annotationSeperator = ","
|
||||||
|
|
||||||
iso8601TimeFormat = "2006-01-02T15:04:05Z"
|
iso8601TimeFormat = "2006-01-02T15:04:05Z"
|
||||||
|
|
4
doc.go
4
doc.go
|
@ -2,7 +2,7 @@
|
||||||
Package jsonapi provides a serializer and deserializer for jsonapi.org spec payloads.
|
Package jsonapi provides a serializer and deserializer for jsonapi.org spec payloads.
|
||||||
|
|
||||||
You can keep your model structs as is and use struct field tags to indicate to jsonapi
|
You can keep your model structs as is and use struct field tags to indicate to jsonapi
|
||||||
how you want your response built or your request deserialzied. What about my relationships?
|
how you want your response built or your request deserialized. What about my relationships?
|
||||||
jsonapi supports relationships out of the box and will even side load them in your response
|
jsonapi supports relationships out of the box and will even side load them in your response
|
||||||
into an "included" array--that contains associated objects.
|
into an "included" array--that contains associated objects.
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ Value, attr: "attr,<key name in attributes hash>[,<extra arguments>]"
|
||||||
|
|
||||||
These fields' values should end up in the "attribute" hash for a record. The first
|
These fields' values should end up in the "attribute" hash for a record. The first
|
||||||
argument must be, "attr', and the second should be the name for the key to display in
|
argument must be, "attr', and the second should be the name for the key to display in
|
||||||
the the "attributes" hash for that record.
|
the "attributes" hash for that record.
|
||||||
|
|
||||||
The following extra arguments are also supported:
|
The following extra arguments are also supported:
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,7 @@ import (
|
||||||
// http://jsonapi.org/format/#document-top-level
|
// http://jsonapi.org/format/#document-top-level
|
||||||
// and here: http://jsonapi.org/format/#error-objects.
|
// and here: http://jsonapi.org/format/#error-objects.
|
||||||
func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error {
|
func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error {
|
||||||
if err := json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects}); err != nil {
|
return json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects})
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorsPayload is a serializer struct for representing a valid JSON API errors payload.
|
// ErrorsPayload is a serializer struct for representing a valid JSON API errors payload.
|
||||||
|
|
|
@ -25,10 +25,14 @@ type WithPointer struct {
|
||||||
FloatVal *float32 `jsonapi:"attr,float-val"`
|
FloatVal *float32 `jsonapi:"attr,float-val"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Timestamp struct {
|
type TimestampModel struct {
|
||||||
ID int `jsonapi:"primary,timestamps"`
|
ID int `jsonapi:"primary,timestamps"`
|
||||||
Time time.Time `jsonapi:"attr,timestamp,iso8601"`
|
DefaultV time.Time `jsonapi:"attr,defaultv"`
|
||||||
Next *time.Time `jsonapi:"attr,next,iso8601"`
|
DefaultP *time.Time `jsonapi:"attr,defaultp"`
|
||||||
|
ISO8601V time.Time `jsonapi:"attr,iso8601v,iso8601"`
|
||||||
|
ISO8601P *time.Time `jsonapi:"attr,iso8601p,iso8601"`
|
||||||
|
RFC3339V time.Time `jsonapi:"attr,rfc3339v,rfc3339"`
|
||||||
|
RFC3339P *time.Time `jsonapi:"attr,rfc3339p,rfc3339"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Car struct {
|
type Car struct {
|
||||||
|
@ -155,3 +159,39 @@ func (bc *BadComment) JSONAPILinks() *Links {
|
||||||
"self": []string{"invalid", "should error"},
|
"self": []string{"invalid", "should error"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Company struct {
|
||||||
|
ID string `jsonapi:"primary,companies"`
|
||||||
|
Name string `jsonapi:"attr,name"`
|
||||||
|
Boss Employee `jsonapi:"attr,boss"`
|
||||||
|
Teams []Team `jsonapi:"attr,teams"`
|
||||||
|
FoundedAt time.Time `jsonapi:"attr,founded-at,iso8601"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Team struct {
|
||||||
|
Name string `jsonapi:"attr,name"`
|
||||||
|
Leader *Employee `jsonapi:"attr,leader"`
|
||||||
|
Members []Employee `jsonapi:"attr,members"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Employee struct {
|
||||||
|
Firstname string `jsonapi:"attr,firstname"`
|
||||||
|
Surname string `jsonapi:"attr,surname"`
|
||||||
|
Age int `jsonapi:"attr,age"`
|
||||||
|
HiredAt *time.Time `jsonapi:"attr,hired-at,iso8601"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomIntType int
|
||||||
|
type CustomFloatType float64
|
||||||
|
type CustomStringType string
|
||||||
|
|
||||||
|
type CustomAttributeTypes struct {
|
||||||
|
ID string `jsonapi:"primary,customtypes"`
|
||||||
|
|
||||||
|
Int CustomIntType `jsonapi:"attr,int"`
|
||||||
|
IntPtr *CustomIntType `jsonapi:"attr,intptr"`
|
||||||
|
IntPtrNull *CustomIntType `jsonapi:"attr,intptrnull"`
|
||||||
|
|
||||||
|
Float CustomFloatType `jsonapi:"attr,float"`
|
||||||
|
String CustomStringType `jsonapi:"attr,string"`
|
||||||
|
}
|
||||||
|
|
600
request.go
600
request.go
|
@ -13,7 +13,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
unsupportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -23,17 +23,42 @@ var (
|
||||||
// ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes
|
// ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes
|
||||||
// "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string.
|
// "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string.
|
||||||
ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps")
|
ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps")
|
||||||
|
// ErrInvalidRFC3339 is returned when a struct has a time.Time type field and includes
|
||||||
|
// "rfc3339" in the tag spec, but the JSON value was not an RFC3339 timestamp string.
|
||||||
|
ErrInvalidRFC3339 = errors.New("Only strings can be parsed as dates, RFC3339 timestamps")
|
||||||
// ErrUnknownFieldNumberType is returned when the JSON value was a float
|
// ErrUnknownFieldNumberType is returned when the JSON value was a float
|
||||||
// (numeric) but the Struct field was a non numeric type (i.e. not int, uint,
|
// (numeric) but the Struct field was a non numeric type (i.e. not int, uint,
|
||||||
// float, etc)
|
// float, etc)
|
||||||
ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type")
|
ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type")
|
||||||
// ErrUnsupportedPtrType is returned when the Struct field was a pointer but
|
|
||||||
// the JSON value was of a different type
|
|
||||||
ErrUnsupportedPtrType = errors.New("Pointer type in struct is not supported")
|
|
||||||
// ErrInvalidType is returned when the given type is incompatible with the expected type.
|
// ErrInvalidType is returned when the given type is incompatible with the expected type.
|
||||||
ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation.
|
ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation.
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrUnsupportedPtrType is returned when the Struct field was a pointer but
|
||||||
|
// the JSON value was of a different type
|
||||||
|
type ErrUnsupportedPtrType struct {
|
||||||
|
rf reflect.Value
|
||||||
|
t reflect.Type
|
||||||
|
structField reflect.StructField
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eupt ErrUnsupportedPtrType) Error() string {
|
||||||
|
typeName := eupt.t.Elem().Name()
|
||||||
|
kind := eupt.t.Elem().Kind()
|
||||||
|
if kind.String() != "" && kind.String() != typeName {
|
||||||
|
typeName = fmt.Sprintf("%s (%s)", typeName, kind.String())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"jsonapi: Can't unmarshal %+v (%s) to struct field `%s`, which is a pointer to `%s`",
|
||||||
|
eupt.rf, eupt.rf.Type().Kind(), eupt.structField.Name, typeName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) error {
|
||||||
|
return ErrUnsupportedPtrType{rf, t, structField}
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalPayload converts an io into a struct instance using jsonapi tags on
|
// UnmarshalPayload converts an io into a struct instance using jsonapi tags on
|
||||||
// struct fields. This method supports single request payloads only, at the
|
// struct fields. This method supports single request payloads only, at the
|
||||||
// moment. Bulk creates and updates are not supported yet.
|
// moment. Bulk creates and updates are not supported yet.
|
||||||
|
@ -88,14 +113,14 @@ func UnmarshalPayload(in io.Reader, model interface{}) error {
|
||||||
|
|
||||||
// UnmarshalManyPayload converts an io into a set of struct instances using
|
// UnmarshalManyPayload converts an io into a set of struct instances using
|
||||||
// jsonapi tags on the type's struct fields.
|
// jsonapi tags on the type's struct fields.
|
||||||
func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
|
func UnmarshalManyPayload[T any](in io.Reader) ([]T, error) {
|
||||||
payload := new(ManyPayload)
|
payload := new(ManyPayload)
|
||||||
|
|
||||||
if err := json.NewDecoder(in).Decode(payload); err != nil {
|
if err := json.NewDecoder(in).Decode(payload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
models := []interface{}{} // will be populated from the "data"
|
models := make([]T, 0, len(payload.Data)) // will be populated from the "data"
|
||||||
includedMap := map[string]*Node{} // will be populate from the "included"
|
includedMap := map[string]*Node{} // will be populate from the "included"
|
||||||
|
|
||||||
if payload.Included != nil {
|
if payload.Included != nil {
|
||||||
|
@ -106,12 +131,12 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, data := range payload.Data {
|
for _, data := range payload.Data {
|
||||||
model := reflect.New(t.Elem())
|
model := new(T)
|
||||||
err := unmarshalNode(data, model, &includedMap)
|
err := unmarshalNode(data, reflect.ValueOf(model), &includedMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
models = append(models, model.Interface())
|
models = append(models, *model)
|
||||||
}
|
}
|
||||||
|
|
||||||
return models, nil
|
return models, nil
|
||||||
|
@ -125,7 +150,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
modelValue := model.Elem()
|
modelValue := model.Elem()
|
||||||
modelType := model.Type().Elem()
|
modelType := modelValue.Type()
|
||||||
|
|
||||||
var er error
|
var er error
|
||||||
|
|
||||||
|
@ -139,7 +164,6 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
fieldValue := modelValue.Field(i)
|
fieldValue := modelValue.Field(i)
|
||||||
|
|
||||||
args := strings.Split(tag, ",")
|
args := strings.Split(tag, ",")
|
||||||
|
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
er = ErrBadJSONAPIStructTag
|
er = ErrBadJSONAPIStructTag
|
||||||
break
|
break
|
||||||
|
@ -154,10 +178,6 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
}
|
}
|
||||||
|
|
||||||
if annotation == annotationPrimary {
|
if annotation == annotationPrimary {
|
||||||
if data.ID == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the JSON API Type
|
// Check the JSON API Type
|
||||||
if data.Type != args[1] {
|
if data.Type != args[1] {
|
||||||
er = fmt.Errorf(
|
er = fmt.Errorf(
|
||||||
|
@ -168,6 +188,10 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if data.ID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// ID will have to be transmitted as astring per the JSON API spec
|
// ID will have to be transmitted as astring per the JSON API spec
|
||||||
v := reflect.ValueOf(data.ID)
|
v := reflect.ValueOf(data.ID)
|
||||||
|
|
||||||
|
@ -196,39 +220,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
|
|
||||||
// Convert the numeric float to one of the supported ID numeric types
|
// Convert the numeric float to one of the supported ID numeric types
|
||||||
// (int[8,16,32,64] or uint[8,16,32,64])
|
// (int[8,16,32,64] or uint[8,16,32,64])
|
||||||
var idValue reflect.Value
|
idValue, err := handleNumeric(floatValue, fieldType.Type, fieldValue)
|
||||||
switch kind {
|
if err != nil {
|
||||||
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
|
// We had a JSON float (numeric), but our field was not one of the
|
||||||
// allowed numeric types
|
// allowed numeric types
|
||||||
er = ErrBadJSONAPIID
|
er = ErrBadJSONAPIID
|
||||||
|
@ -244,213 +237,26 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
fieldValue.Set(reflect.ValueOf(data.ClientID))
|
fieldValue.Set(reflect.ValueOf(data.ClientID))
|
||||||
} else if annotation == annotationAttribute {
|
} else if annotation == annotationAttribute {
|
||||||
attributes := data.Attributes
|
attributes := data.Attributes
|
||||||
|
|
||||||
if attributes == nil || len(data.Attributes) == 0 {
|
if attributes == nil || len(data.Attributes) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var iso8601 bool
|
attribute := attributes[args[1]]
|
||||||
|
|
||||||
if len(args) > 2 {
|
|
||||||
for _, arg := range args[2:] {
|
|
||||||
if arg == annotationISO8601 {
|
|
||||||
iso8601 = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val := attributes[args[1]]
|
|
||||||
|
|
||||||
// continue if the attribute was not included in the request
|
// continue if the attribute was not included in the request
|
||||||
if val == nil {
|
if attribute == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
v := reflect.ValueOf(val)
|
structField := fieldType
|
||||||
|
value, err := unmarshalAttribute(attribute, args, structField, fieldValue)
|
||||||
// Handle field of type time.Time
|
if err != nil {
|
||||||
if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
|
er = err
|
||||||
if iso8601 {
|
break
|
||||||
var tm string
|
|
||||||
if v.Kind() == reflect.String {
|
|
||||||
tm = v.Interface().(string)
|
|
||||||
} else {
|
|
||||||
er = ErrInvalidISO8601
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := time.Parse(iso8601TimeFormat, tm)
|
|
||||||
if err != nil {
|
|
||||||
er = ErrInvalidISO8601
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(t))
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var at int64
|
|
||||||
|
|
||||||
if v.Kind() == reflect.Float64 {
|
|
||||||
at = int64(v.Interface().(float64))
|
|
||||||
} else if v.Kind() == reflect.Int {
|
|
||||||
at = v.Int()
|
|
||||||
} else {
|
|
||||||
return ErrInvalidTime
|
|
||||||
}
|
|
||||||
|
|
||||||
t := time.Unix(at, 0)
|
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(t))
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if fieldValue.Type() == reflect.TypeOf([]string{}) {
|
assign(fieldValue, value)
|
||||||
values := make([]string, v.Len())
|
|
||||||
for i := 0; i < v.Len(); i++ {
|
|
||||||
values[i] = v.Index(i).Interface().(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(values))
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
|
||||||
if iso8601 {
|
|
||||||
var tm string
|
|
||||||
if v.Kind() == reflect.String {
|
|
||||||
tm = v.Interface().(string)
|
|
||||||
} else {
|
|
||||||
er = ErrInvalidISO8601
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := time.Parse(iso8601TimeFormat, tm)
|
|
||||||
if err != nil {
|
|
||||||
er = ErrInvalidISO8601
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
t := &v
|
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(t))
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var at int64
|
|
||||||
|
|
||||||
if v.Kind() == reflect.Float64 {
|
|
||||||
at = int64(v.Interface().(float64))
|
|
||||||
} else if v.Kind() == reflect.Int {
|
|
||||||
at = v.Int()
|
|
||||||
} else {
|
|
||||||
return ErrInvalidTime
|
|
||||||
}
|
|
||||||
|
|
||||||
v := time.Unix(at, 0)
|
|
||||||
t := &v
|
|
||||||
|
|
||||||
fieldValue.Set(reflect.ValueOf(t))
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON value was a float (numeric)
|
|
||||||
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 {
|
|
||||||
case reflect.Int:
|
|
||||||
n := int(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Int8:
|
|
||||||
n := int8(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Int16:
|
|
||||||
n := int16(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Int32:
|
|
||||||
n := int32(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Int64:
|
|
||||||
n := int64(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Uint:
|
|
||||||
n := uint(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Uint8:
|
|
||||||
n := uint8(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Uint16:
|
|
||||||
n := uint16(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Uint32:
|
|
||||||
n := uint32(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Uint64:
|
|
||||||
n := uint64(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Float32:
|
|
||||||
n := float32(floatValue)
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
case reflect.Float64:
|
|
||||||
n := floatValue
|
|
||||||
numericValue = reflect.ValueOf(&n)
|
|
||||||
default:
|
|
||||||
return ErrUnknownFieldNumberType
|
|
||||||
}
|
|
||||||
|
|
||||||
assign(fieldValue, numericValue)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field was a Pointer type
|
|
||||||
if fieldValue.Kind() == reflect.Ptr {
|
|
||||||
var concreteVal reflect.Value
|
|
||||||
|
|
||||||
switch cVal := val.(type) {
|
|
||||||
case string:
|
|
||||||
concreteVal = reflect.ValueOf(&cVal)
|
|
||||||
case bool:
|
|
||||||
concreteVal = reflect.ValueOf(&cVal)
|
|
||||||
case complex64:
|
|
||||||
concreteVal = reflect.ValueOf(&cVal)
|
|
||||||
case complex128:
|
|
||||||
concreteVal = reflect.ValueOf(&cVal)
|
|
||||||
case uintptr:
|
|
||||||
concreteVal = reflect.ValueOf(&cVal)
|
|
||||||
default:
|
|
||||||
return ErrUnsupportedPtrType
|
|
||||||
}
|
|
||||||
|
|
||||||
if fieldValue.Type() != concreteVal.Type() {
|
|
||||||
return ErrUnsupportedPtrType
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldValue.Set(concreteVal)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// As a final catch-all, ensure types line up to avoid a runtime panic.
|
|
||||||
if fieldValue.Kind() != v.Kind() {
|
|
||||||
return ErrInvalidType
|
|
||||||
}
|
|
||||||
fieldValue.Set(reflect.ValueOf(val))
|
|
||||||
|
|
||||||
} else if annotation == annotationRelation {
|
} else if annotation == annotationRelation {
|
||||||
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
||||||
|
|
||||||
|
@ -522,7 +328,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
er = fmt.Errorf(unsuportedStructTagMsg, annotation)
|
er = fmt.Errorf(unsupportedStructTagMsg, annotation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -542,9 +348,309 @@ func fullNode(n *Node, included *map[string]*Node) *Node {
|
||||||
// assign will take the value specified and assign it to the field; if
|
// assign will take the value specified and assign it to the field; if
|
||||||
// field is expecting a ptr assign will assign a ptr.
|
// field is expecting a ptr assign will assign a ptr.
|
||||||
func assign(field, value reflect.Value) {
|
func assign(field, value reflect.Value) {
|
||||||
|
value = reflect.Indirect(value)
|
||||||
|
|
||||||
if field.Kind() == reflect.Ptr {
|
if field.Kind() == reflect.Ptr {
|
||||||
|
// initialize pointer so it's value
|
||||||
|
// can be set by assignValue
|
||||||
|
field.Set(reflect.New(field.Type().Elem()))
|
||||||
|
field = field.Elem()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
assignValue(field, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign assigns the specified value to the field,
|
||||||
|
// expecting both values not to be pointer types.
|
||||||
|
func assignValue(field, value reflect.Value) {
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16,
|
||||||
|
reflect.Int32, reflect.Int64:
|
||||||
|
field.SetInt(value.Int())
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16,
|
||||||
|
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
field.SetUint(value.Uint())
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
field.SetFloat(value.Float())
|
||||||
|
case reflect.String:
|
||||||
|
field.SetString(value.String())
|
||||||
|
case reflect.Bool:
|
||||||
|
field.SetBool(value.Bool())
|
||||||
|
default:
|
||||||
field.Set(value)
|
field.Set(value)
|
||||||
} else {
|
|
||||||
field.Set(reflect.Indirect(value))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unmarshalAttribute(
|
||||||
|
attribute interface{},
|
||||||
|
args []string,
|
||||||
|
structField reflect.StructField,
|
||||||
|
fieldValue reflect.Value) (value reflect.Value, err error) {
|
||||||
|
value = reflect.ValueOf(attribute)
|
||||||
|
fieldType := structField.Type
|
||||||
|
|
||||||
|
// Handle field of type []string
|
||||||
|
if fieldValue.Type() == reflect.TypeOf([]string{}) {
|
||||||
|
value, err = handleStringSlice(attribute)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle field of type time.Time
|
||||||
|
if fieldValue.Type() == reflect.TypeOf(time.Time{}) ||
|
||||||
|
fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
||||||
|
value, err = handleTime(attribute, args, fieldValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle field of type struct
|
||||||
|
if fieldValue.Type().Kind() == reflect.Struct {
|
||||||
|
value, err = handleStruct(attribute, fieldValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle field containing slice of structs
|
||||||
|
if fieldValue.Type().Kind() == reflect.Slice &&
|
||||||
|
reflect.TypeOf(fieldValue.Interface()).Elem().Kind() == reflect.Struct {
|
||||||
|
value, err = handleStructSlice(attribute, fieldValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON value was a float (numeric)
|
||||||
|
if value.Kind() == reflect.Float64 {
|
||||||
|
value, err = handleNumeric(attribute, fieldType, fieldValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field was a Pointer type
|
||||||
|
if fieldValue.Kind() == reflect.Ptr {
|
||||||
|
value, err = handlePointer(attribute, args, fieldType, fieldValue, structField)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// As a final catch-all, ensure types line up to avoid a runtime panic.
|
||||||
|
if fieldValue.Kind() != value.Kind() {
|
||||||
|
err = ErrInvalidType
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStringSlice(attribute interface{}) (reflect.Value, error) {
|
||||||
|
v := reflect.ValueOf(attribute)
|
||||||
|
values := make([]string, v.Len())
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
values[i] = v.Index(i).Interface().(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.ValueOf(values), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) {
|
||||||
|
var isISO8601, isRFC3339 bool
|
||||||
|
v := reflect.ValueOf(attribute)
|
||||||
|
|
||||||
|
if len(args) > 2 {
|
||||||
|
for _, arg := range args[2:] {
|
||||||
|
if arg == annotationISO8601 {
|
||||||
|
isISO8601 = true
|
||||||
|
} else if arg == annotationRFC3339 {
|
||||||
|
isRFC3339 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isISO8601 {
|
||||||
|
if v.Kind() != reflect.String {
|
||||||
|
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := time.Parse(iso8601TimeFormat, v.Interface().(string))
|
||||||
|
if err != nil {
|
||||||
|
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldValue.Kind() == reflect.Ptr {
|
||||||
|
return reflect.ValueOf(&t), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.ValueOf(t), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isRFC3339 {
|
||||||
|
if v.Kind() != reflect.String {
|
||||||
|
return reflect.ValueOf(time.Now()), ErrInvalidRFC3339
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := time.Parse(time.RFC3339, v.Interface().(string))
|
||||||
|
if err != nil {
|
||||||
|
return reflect.ValueOf(time.Now()), ErrInvalidRFC3339
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldValue.Kind() == reflect.Ptr {
|
||||||
|
return reflect.ValueOf(&t), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.ValueOf(t), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var at int64
|
||||||
|
|
||||||
|
if v.Kind() == reflect.Float64 {
|
||||||
|
at = int64(v.Interface().(float64))
|
||||||
|
} else if v.Kind() == reflect.Int {
|
||||||
|
at = v.Int()
|
||||||
|
} else {
|
||||||
|
return reflect.ValueOf(time.Now()), ErrInvalidTime
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.Unix(at, 0)
|
||||||
|
|
||||||
|
return reflect.ValueOf(t), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleNumeric(
|
||||||
|
attribute interface{},
|
||||||
|
fieldType reflect.Type,
|
||||||
|
fieldValue reflect.Value) (reflect.Value, error) {
|
||||||
|
v := reflect.ValueOf(attribute)
|
||||||
|
floatValue := v.Interface().(float64)
|
||||||
|
|
||||||
|
var kind reflect.Kind
|
||||||
|
if fieldValue.Kind() == reflect.Ptr {
|
||||||
|
kind = fieldType.Elem().Kind()
|
||||||
|
} else {
|
||||||
|
kind = fieldType.Kind()
|
||||||
|
}
|
||||||
|
|
||||||
|
var numericValue reflect.Value
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case reflect.Int:
|
||||||
|
n := int(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Int8:
|
||||||
|
n := int8(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Int16:
|
||||||
|
n := int16(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Int32:
|
||||||
|
n := int32(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Int64:
|
||||||
|
n := int64(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Uint:
|
||||||
|
n := uint(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Uint8:
|
||||||
|
n := uint8(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Uint16:
|
||||||
|
n := uint16(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Uint32:
|
||||||
|
n := uint32(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Uint64:
|
||||||
|
n := uint64(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Float32:
|
||||||
|
n := float32(floatValue)
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
case reflect.Float64:
|
||||||
|
n := floatValue
|
||||||
|
numericValue = reflect.ValueOf(&n)
|
||||||
|
default:
|
||||||
|
return reflect.Value{}, ErrUnknownFieldNumberType
|
||||||
|
}
|
||||||
|
|
||||||
|
return numericValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePointer(
|
||||||
|
attribute interface{},
|
||||||
|
args []string,
|
||||||
|
fieldType reflect.Type,
|
||||||
|
fieldValue reflect.Value,
|
||||||
|
structField reflect.StructField) (reflect.Value, error) {
|
||||||
|
t := fieldValue.Type()
|
||||||
|
var concreteVal reflect.Value
|
||||||
|
|
||||||
|
switch cVal := attribute.(type) {
|
||||||
|
case string:
|
||||||
|
concreteVal = reflect.ValueOf(&cVal)
|
||||||
|
case bool:
|
||||||
|
concreteVal = reflect.ValueOf(&cVal)
|
||||||
|
case complex64, complex128, uintptr:
|
||||||
|
concreteVal = reflect.ValueOf(&cVal)
|
||||||
|
case map[string]interface{}:
|
||||||
|
var err error
|
||||||
|
concreteVal, err = handleStruct(attribute, fieldValue)
|
||||||
|
if err != nil {
|
||||||
|
return reflect.Value{}, newErrUnsupportedPtrType(
|
||||||
|
reflect.ValueOf(attribute), fieldType, structField)
|
||||||
|
}
|
||||||
|
return concreteVal, err
|
||||||
|
default:
|
||||||
|
return reflect.Value{}, newErrUnsupportedPtrType(
|
||||||
|
reflect.ValueOf(attribute), fieldType, structField)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t != concreteVal.Type() {
|
||||||
|
return reflect.Value{}, newErrUnsupportedPtrType(
|
||||||
|
reflect.ValueOf(attribute), fieldType, structField)
|
||||||
|
}
|
||||||
|
|
||||||
|
return concreteVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStruct(
|
||||||
|
attribute interface{},
|
||||||
|
fieldValue reflect.Value) (reflect.Value, error) {
|
||||||
|
|
||||||
|
data, err := json.Marshal(attribute)
|
||||||
|
if err != nil {
|
||||||
|
return reflect.Value{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
node := new(Node)
|
||||||
|
if err := json.Unmarshal(data, &node.Attributes); err != nil {
|
||||||
|
return reflect.Value{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var model reflect.Value
|
||||||
|
if fieldValue.Kind() == reflect.Ptr {
|
||||||
|
model = reflect.New(fieldValue.Type().Elem())
|
||||||
|
} else {
|
||||||
|
model = reflect.New(fieldValue.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := unmarshalNode(node, model, nil); err != nil {
|
||||||
|
return reflect.Value{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return model, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleStructSlice(
|
||||||
|
attribute interface{},
|
||||||
|
fieldValue reflect.Value) (reflect.Value, error) {
|
||||||
|
models := reflect.New(fieldValue.Type()).Elem()
|
||||||
|
dataMap := reflect.ValueOf(attribute).Interface().([]interface{})
|
||||||
|
for _, data := range dataMap {
|
||||||
|
model := reflect.New(fieldValue.Type().Elem()).Elem()
|
||||||
|
|
||||||
|
value, err := handleStruct(data, model)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
models = reflect.Append(models, reflect.Indirect(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
626
request_test.go
626
request_test.go
|
@ -3,6 +3,7 @@ package jsonapi
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -70,11 +71,20 @@ func TestUnmarshalToStructWithPointerAttr(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalPayload_missingTypeFieldShouldError(t *testing.T) {
|
||||||
|
if err := UnmarshalPayload(
|
||||||
|
strings.NewReader(`{"data":{"body":"hello world"}}`),
|
||||||
|
&Post{},
|
||||||
|
); err == nil {
|
||||||
|
t.Fatalf("Expected an error but did not get one")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUnmarshalPayload_ptrsAllNil(t *testing.T) {
|
func TestUnmarshalPayload_ptrsAllNil(t *testing.T) {
|
||||||
out := new(WithPointer)
|
out := new(WithPointer)
|
||||||
if err := UnmarshalPayload(
|
if err := UnmarshalPayload(
|
||||||
strings.NewReader(`{"data": {}}`), out); err != nil {
|
strings.NewReader(`{"data":{"type":"with-pointers"}}`), out); err != nil {
|
||||||
t.Fatalf("Error unmarshalling to Foo")
|
t.Fatalf("Error unmarshalling to Foo: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if out.ID != nil {
|
if out.ID != nil {
|
||||||
|
@ -121,12 +131,12 @@ func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) {
|
func TestUnmarshalToStructWithPointerAttr_BadType_bool(t *testing.T) {
|
||||||
out := new(WithPointer)
|
out := new(WithPointer)
|
||||||
in := map[string]interface{}{
|
in := map[string]interface{}{
|
||||||
"name": true, // This is the wrong type.
|
"name": true, // This is the wrong type.
|
||||||
}
|
}
|
||||||
expectedErrorMessage := ErrUnsupportedPtrType.Error()
|
expectedErrorMessage := "jsonapi: Can't unmarshal true (bool) to struct field `Name`, which is a pointer to `string`"
|
||||||
|
|
||||||
err := UnmarshalPayload(sampleWithPointerPayload(in), out)
|
err := UnmarshalPayload(sampleWithPointerPayload(in), out)
|
||||||
|
|
||||||
|
@ -136,6 +146,71 @@ func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) {
|
||||||
if err.Error() != expectedErrorMessage {
|
if err.Error() != expectedErrorMessage {
|
||||||
t.Fatalf("Unexpected error message: %s", err.Error())
|
t.Fatalf("Unexpected error message: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
if _, ok := err.(ErrUnsupportedPtrType); !ok {
|
||||||
|
t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalToStructWithPointerAttr_BadType_MapPtr(t *testing.T) {
|
||||||
|
out := new(WithPointer)
|
||||||
|
in := map[string]interface{}{
|
||||||
|
"name": &map[string]interface{}{"a": 5}, // This is the wrong type.
|
||||||
|
}
|
||||||
|
expectedErrorMessage := "jsonapi: Can't unmarshal map[a:5] (map) to struct field `Name`, which is a pointer to `string`"
|
||||||
|
|
||||||
|
err := UnmarshalPayload(sampleWithPointerPayload(in), out)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error due to invalid type.")
|
||||||
|
}
|
||||||
|
if err.Error() != expectedErrorMessage {
|
||||||
|
t.Fatalf("Unexpected error message: %s", err.Error())
|
||||||
|
}
|
||||||
|
if _, ok := err.(ErrUnsupportedPtrType); !ok {
|
||||||
|
t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalToStructWithPointerAttr_BadType_Struct(t *testing.T) {
|
||||||
|
out := new(WithPointer)
|
||||||
|
type FooStruct struct{ A int }
|
||||||
|
in := map[string]interface{}{
|
||||||
|
"name": FooStruct{A: 5}, // This is the wrong type.
|
||||||
|
}
|
||||||
|
expectedErrorMessage := "jsonapi: Can't unmarshal map[A:5] (map) to struct field `Name`, which is a pointer to `string`"
|
||||||
|
|
||||||
|
err := UnmarshalPayload(sampleWithPointerPayload(in), out)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error due to invalid type.")
|
||||||
|
}
|
||||||
|
if err.Error() != expectedErrorMessage {
|
||||||
|
t.Fatalf("Unexpected error message: %s", err.Error())
|
||||||
|
}
|
||||||
|
if _, ok := err.(ErrUnsupportedPtrType); !ok {
|
||||||
|
t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalToStructWithPointerAttr_BadType_IntSlice(t *testing.T) {
|
||||||
|
out := new(WithPointer)
|
||||||
|
type FooStruct struct{ A, B int }
|
||||||
|
in := map[string]interface{}{
|
||||||
|
"name": []int{4, 5}, // This is the wrong type.
|
||||||
|
}
|
||||||
|
expectedErrorMessage := "jsonapi: Can't unmarshal [4 5] (slice) to struct field `Name`, which is a pointer to `string`"
|
||||||
|
|
||||||
|
err := UnmarshalPayload(sampleWithPointerPayload(in), out)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error due to invalid type.")
|
||||||
|
}
|
||||||
|
if err.Error() != expectedErrorMessage {
|
||||||
|
t.Fatalf("Unexpected error message: %s", err.Error())
|
||||||
|
}
|
||||||
|
if _, ok := err.(ErrUnsupportedPtrType); !ok {
|
||||||
|
t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStringPointerField(t *testing.T) {
|
func TestStringPointerField(t *testing.T) {
|
||||||
|
@ -236,7 +311,10 @@ func TestUnmarshalSetsID(t *testing.T) {
|
||||||
func TestUnmarshal_nonNumericID(t *testing.T) {
|
func TestUnmarshal_nonNumericID(t *testing.T) {
|
||||||
data := samplePayloadWithoutIncluded()
|
data := samplePayloadWithoutIncluded()
|
||||||
data["data"].(map[string]interface{})["id"] = "non-numeric-id"
|
data["data"].(map[string]interface{})["id"] = "non-numeric-id"
|
||||||
payload, _ := payload(data)
|
payload, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
in := bytes.NewReader(payload)
|
in := bytes.NewReader(payload)
|
||||||
out := new(Post)
|
out := new(Post)
|
||||||
|
|
||||||
|
@ -264,80 +342,183 @@ func TestUnmarshalSetsAttrs(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalParsesISO8601(t *testing.T) {
|
func TestUnmarshal_Times(t *testing.T) {
|
||||||
payload := &OnePayload{
|
aTime := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
|
||||||
Data: &Node{
|
|
||||||
Type: "timestamps",
|
for _, tc := range []struct {
|
||||||
Attributes: map[string]interface{}{
|
desc string
|
||||||
"timestamp": "2016-08-17T08:27:12Z",
|
inputPayload *OnePayload
|
||||||
|
wantErr bool
|
||||||
|
verification func(tm *TimestampModel) error
|
||||||
|
}{
|
||||||
|
// Default:
|
||||||
|
{
|
||||||
|
desc: "default_byValue",
|
||||||
|
inputPayload: &OnePayload{
|
||||||
|
Data: &Node{
|
||||||
|
Type: "timestamps",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"defaultv": aTime.Unix(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verification: func(tm *TimestampModel) error {
|
||||||
|
if !tm.DefaultV.Equal(aTime) {
|
||||||
|
return errors.New("times not equal!")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
|
desc: "default_byPointer",
|
||||||
in := bytes.NewBuffer(nil)
|
inputPayload: &OnePayload{
|
||||||
json.NewEncoder(in).Encode(payload)
|
Data: &Node{
|
||||||
|
Type: "timestamps",
|
||||||
out := new(Timestamp)
|
Attributes: map[string]interface{}{
|
||||||
|
"defaultp": aTime.Unix(),
|
||||||
if err := UnmarshalPayload(in, out); err != nil {
|
},
|
||||||
t.Fatal(err)
|
},
|
||||||
}
|
},
|
||||||
|
verification: func(tm *TimestampModel) error {
|
||||||
expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
|
if !tm.DefaultP.Equal(aTime) {
|
||||||
|
return errors.New("times not equal!")
|
||||||
if !out.Time.Equal(expected) {
|
}
|
||||||
t.Fatal("Parsing the ISO8601 timestamp failed")
|
return nil
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnmarshalParsesISO8601TimePointer(t *testing.T) {
|
|
||||||
payload := &OnePayload{
|
|
||||||
Data: &Node{
|
|
||||||
Type: "timestamps",
|
|
||||||
Attributes: map[string]interface{}{
|
|
||||||
"next": "2016-08-17T08:27:12Z",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
|
desc: "default_invalid",
|
||||||
in := bytes.NewBuffer(nil)
|
inputPayload: &OnePayload{
|
||||||
json.NewEncoder(in).Encode(payload)
|
Data: &Node{
|
||||||
|
Type: "timestamps",
|
||||||
out := new(Timestamp)
|
Attributes: map[string]interface{}{
|
||||||
|
"defaultv": "not a timestamp!",
|
||||||
if err := UnmarshalPayload(in, out); err != nil {
|
},
|
||||||
t.Fatal(err)
|
},
|
||||||
}
|
},
|
||||||
|
wantErr: true,
|
||||||
expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
|
},
|
||||||
|
// ISO 8601:
|
||||||
if !out.Next.Equal(expected) {
|
{
|
||||||
t.Fatal("Parsing the ISO8601 timestamp failed")
|
desc: "iso8601_byValue",
|
||||||
}
|
inputPayload: &OnePayload{
|
||||||
}
|
Data: &Node{
|
||||||
|
Type: "timestamps",
|
||||||
func TestUnmarshalInvalidISO8601(t *testing.T) {
|
Attributes: map[string]interface{}{
|
||||||
payload := &OnePayload{
|
"iso8601v": "2016-08-17T08:27:12Z",
|
||||||
Data: &Node{
|
},
|
||||||
Type: "timestamps",
|
},
|
||||||
Attributes: map[string]interface{}{
|
},
|
||||||
"timestamp": "17 Aug 16 08:027 MST",
|
verification: func(tm *TimestampModel) error {
|
||||||
|
if !tm.ISO8601V.Equal(aTime) {
|
||||||
|
return errors.New("times not equal!")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
|
desc: "iso8601_byPointer",
|
||||||
|
inputPayload: &OnePayload{
|
||||||
|
Data: &Node{
|
||||||
|
Type: "timestamps",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"iso8601p": "2016-08-17T08:27:12Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verification: func(tm *TimestampModel) error {
|
||||||
|
if !tm.ISO8601P.Equal(aTime) {
|
||||||
|
return errors.New("times not equal!")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "iso8601_invalid",
|
||||||
|
inputPayload: &OnePayload{
|
||||||
|
Data: &Node{
|
||||||
|
Type: "timestamps",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"iso8601v": "not a timestamp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
// RFC 3339
|
||||||
|
{
|
||||||
|
desc: "rfc3339_byValue",
|
||||||
|
inputPayload: &OnePayload{
|
||||||
|
Data: &Node{
|
||||||
|
Type: "timestamps",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"rfc3339v": "2016-08-17T08:27:12Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verification: func(tm *TimestampModel) error {
|
||||||
|
if got, want := tm.RFC3339V, aTime; got != want {
|
||||||
|
return fmt.Errorf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "rfc3339_byPointer",
|
||||||
|
inputPayload: &OnePayload{
|
||||||
|
Data: &Node{
|
||||||
|
Type: "timestamps",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"rfc3339p": "2016-08-17T08:27:12Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verification: func(tm *TimestampModel) error {
|
||||||
|
if got, want := *tm.RFC3339P, aTime; got != want {
|
||||||
|
return fmt.Errorf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "rfc3339_invalid",
|
||||||
|
inputPayload: &OnePayload{
|
||||||
|
Data: &Node{
|
||||||
|
Type: "timestamps",
|
||||||
|
Attributes: map[string]interface{}{
|
||||||
|
"rfc3339v": "not a timestamp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
// Serialize the OnePayload using the standard JSON library.
|
||||||
|
in := bytes.NewBuffer(nil)
|
||||||
|
if err := json.NewEncoder(in).Encode(tc.inputPayload); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
in := bytes.NewBuffer(nil)
|
out := &TimestampModel{}
|
||||||
json.NewEncoder(in).Encode(payload)
|
err := UnmarshalPayload(in, out)
|
||||||
|
if got, want := (err != nil), tc.wantErr; got != want {
|
||||||
out := new(Timestamp)
|
t.Fatalf("UnmarshalPayload error: got %v, want %v", got, want)
|
||||||
|
}
|
||||||
if err := UnmarshalPayload(in, out); err != ErrInvalidISO8601 {
|
if tc.verification != nil {
|
||||||
t.Fatalf("Expected ErrInvalidISO8601, got %v", err)
|
if err := tc.verification(out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) {
|
func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) {
|
||||||
data, _ := payload(samplePayloadWithoutIncluded())
|
data, err := json.Marshal(samplePayloadWithoutIncluded())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
in := bytes.NewReader(data)
|
in := bytes.NewReader(data)
|
||||||
out := new(Post)
|
out := new(Post)
|
||||||
|
|
||||||
|
@ -603,7 +784,7 @@ func TestUnmarshalManyPayload(t *testing.T) {
|
||||||
}
|
}
|
||||||
in := bytes.NewReader(data)
|
in := bytes.NewReader(data)
|
||||||
|
|
||||||
posts, err := UnmarshalManyPayload(in, reflect.TypeOf(new(Post)))
|
posts, err := UnmarshalManyPayload[Post](in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -611,13 +792,6 @@ func TestUnmarshalManyPayload(t *testing.T) {
|
||||||
if len(posts) != 2 {
|
if len(posts) != 2 {
|
||||||
t.Fatal("Wrong number of posts")
|
t.Fatal("Wrong number of posts")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range posts {
|
|
||||||
_, ok := p.(*Post)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Was expecting a Post")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManyPayload_withLinks(t *testing.T) {
|
func TestManyPayload_withLinks(t *testing.T) {
|
||||||
|
@ -703,6 +877,86 @@ func TestManyPayload_withLinks(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalCustomTypeAttributes(t *testing.T) {
|
||||||
|
customInt := CustomIntType(5)
|
||||||
|
customFloat := CustomFloatType(1.5)
|
||||||
|
customString := CustomStringType("Test")
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"type": "customtypes",
|
||||||
|
"id": "1",
|
||||||
|
"attributes": map[string]interface{}{
|
||||||
|
"int": 5,
|
||||||
|
"intptr": 5,
|
||||||
|
"intptrnull": nil,
|
||||||
|
|
||||||
|
"float": 1.5,
|
||||||
|
"string": "Test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON API payload
|
||||||
|
customAttributeTypes := new(CustomAttributeTypes)
|
||||||
|
if err := UnmarshalPayload(bytes.NewReader(payload), customAttributeTypes); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, actual := customInt, customAttributeTypes.Int; expected != actual {
|
||||||
|
t.Fatalf("Was expecting custom int to be `%d`, got `%d`", expected, actual)
|
||||||
|
}
|
||||||
|
if expected, actual := customInt, *customAttributeTypes.IntPtr; expected != actual {
|
||||||
|
t.Fatalf("Was expecting custom int pointer to be `%d`, got `%d`", expected, actual)
|
||||||
|
}
|
||||||
|
if customAttributeTypes.IntPtrNull != nil {
|
||||||
|
t.Fatalf("Was expecting custom int pointer to be <nil>, got `%d`", customAttributeTypes.IntPtrNull)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, actual := customFloat, customAttributeTypes.Float; expected != actual {
|
||||||
|
t.Fatalf("Was expecting custom float to be `%f`, got `%f`", expected, actual)
|
||||||
|
}
|
||||||
|
if expected, actual := customString, customAttributeTypes.String; expected != actual {
|
||||||
|
t.Fatalf("Was expecting custom string to be `%s`, got `%s`", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalCustomTypeAttributes_ErrInvalidType(t *testing.T) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"type": "customtypes",
|
||||||
|
"id": "1",
|
||||||
|
"attributes": map[string]interface{}{
|
||||||
|
"int": "bad",
|
||||||
|
"intptr": 5,
|
||||||
|
"intptrnull": nil,
|
||||||
|
|
||||||
|
"float": 1.5,
|
||||||
|
"string": "Test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON API payload
|
||||||
|
customAttributeTypes := new(CustomAttributeTypes)
|
||||||
|
err = UnmarshalPayload(bytes.NewReader(payload), customAttributeTypes)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected an error unmarshalling the payload due to type mismatch, got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != ErrInvalidType {
|
||||||
|
t.Fatalf("Expected error to be %v, was %v", ErrInvalidType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func samplePayloadWithoutIncluded() map[string]interface{} {
|
func samplePayloadWithoutIncluded() map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"data": map[string]interface{}{
|
"data": map[string]interface{}{
|
||||||
|
@ -736,11 +990,6 @@ func samplePayloadWithoutIncluded() map[string]interface{} {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func payload(data map[string]interface{}) (result []byte, err error) {
|
|
||||||
result, err = json.Marshal(data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func samplePayload() io.Reader {
|
func samplePayload() io.Reader {
|
||||||
payload := &OnePayload{
|
payload := &OnePayload{
|
||||||
Data: &Node{
|
Data: &Node{
|
||||||
|
@ -945,3 +1194,218 @@ func sampleSerializedEmbeddedTestModel() *Blog {
|
||||||
|
|
||||||
return blog
|
return blog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalNestedStructPtr(t *testing.T) {
|
||||||
|
type Director struct {
|
||||||
|
Firstname string `jsonapi:"attr,firstname"`
|
||||||
|
Surname string `jsonapi:"attr,surname"`
|
||||||
|
}
|
||||||
|
type Movie struct {
|
||||||
|
ID string `jsonapi:"primary,movies"`
|
||||||
|
Name string `jsonapi:"attr,name"`
|
||||||
|
Director *Director `jsonapi:"attr,director"`
|
||||||
|
}
|
||||||
|
sample := map[string]interface{}{
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"type": "movies",
|
||||||
|
"id": "123",
|
||||||
|
"attributes": map[string]interface{}{
|
||||||
|
"name": "The Shawshank Redemption",
|
||||||
|
"director": map[string]interface{}{
|
||||||
|
"firstname": "Frank",
|
||||||
|
"surname": "Darabont",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(sample)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
in := bytes.NewReader(data)
|
||||||
|
out := new(Movie)
|
||||||
|
|
||||||
|
if err := UnmarshalPayload(in, out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Name != "The Shawshank Redemption" {
|
||||||
|
t.Fatalf("expected out.Name to be `The Shawshank Redemption`, but got `%s`", out.Name)
|
||||||
|
}
|
||||||
|
if out.Director.Firstname != "Frank" {
|
||||||
|
t.Fatalf("expected out.Director.Firstname to be `Frank`, but got `%s`", out.Director.Firstname)
|
||||||
|
}
|
||||||
|
if out.Director.Surname != "Darabont" {
|
||||||
|
t.Fatalf("expected out.Director.Surname to be `Darabont`, but got `%s`", out.Director.Surname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalNestedStruct(t *testing.T) {
|
||||||
|
boss := map[string]interface{}{
|
||||||
|
"firstname": "Hubert",
|
||||||
|
"surname": "Farnsworth",
|
||||||
|
"age": 176,
|
||||||
|
"hired-at": "2016-08-17T08:27:12Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
sample := map[string]interface{}{
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"type": "companies",
|
||||||
|
"id": "123",
|
||||||
|
"attributes": map[string]interface{}{
|
||||||
|
"name": "Planet Express",
|
||||||
|
"boss": boss,
|
||||||
|
"founded-at": "2016-08-17T08:27:12Z",
|
||||||
|
"teams": []map[string]interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "Dev",
|
||||||
|
"members": []map[string]interface{}{
|
||||||
|
map[string]interface{}{"firstname": "Sean"},
|
||||||
|
map[string]interface{}{"firstname": "Iz"},
|
||||||
|
},
|
||||||
|
"leader": map[string]interface{}{"firstname": "Iz"},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "DxE",
|
||||||
|
"members": []map[string]interface{}{
|
||||||
|
map[string]interface{}{"firstname": "Akshay"},
|
||||||
|
map[string]interface{}{"firstname": "Peri"},
|
||||||
|
},
|
||||||
|
"leader": map[string]interface{}{"firstname": "Peri"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(sample)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
in := bytes.NewReader(data)
|
||||||
|
out := new(Company)
|
||||||
|
|
||||||
|
if err := UnmarshalPayload(in, out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Boss.Firstname != "Hubert" {
|
||||||
|
t.Fatalf("expected `Hubert` at out.Boss.Firstname, but got `%s`", out.Boss.Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Boss.Age != 176 {
|
||||||
|
t.Fatalf("expected `176` at out.Boss.Age, but got `%d`", out.Boss.Age)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Boss.HiredAt.IsZero() {
|
||||||
|
t.Fatalf("expected out.Boss.HiredAt to be zero, but got `%t`", out.Boss.HiredAt.IsZero())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(out.Teams) != 2 {
|
||||||
|
t.Fatalf("expected len(out.Teams) to be 2, but got `%d`", len(out.Teams))
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Teams[0].Name != "Dev" {
|
||||||
|
t.Fatalf("expected out.Teams[0].Name to be `Dev`, but got `%s`", out.Teams[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Teams[1].Name != "DxE" {
|
||||||
|
t.Fatalf("expected out.Teams[1].Name to be `DxE`, but got `%s`", out.Teams[1].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(out.Teams[0].Members) != 2 {
|
||||||
|
t.Fatalf("expected len(out.Teams[0].Members) to be 2, but got `%d`", len(out.Teams[0].Members))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(out.Teams[1].Members) != 2 {
|
||||||
|
t.Fatalf("expected len(out.Teams[1].Members) to be 2, but got `%d`", len(out.Teams[1].Members))
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Teams[0].Members[0].Firstname != "Sean" {
|
||||||
|
t.Fatalf("expected out.Teams[0].Members[0].Firstname to be `Sean`, but got `%s`", out.Teams[0].Members[0].Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Teams[0].Members[1].Firstname != "Iz" {
|
||||||
|
t.Fatalf("expected out.Teams[0].Members[1].Firstname to be `Iz`, but got `%s`", out.Teams[0].Members[1].Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Teams[1].Members[0].Firstname != "Akshay" {
|
||||||
|
t.Fatalf("expected out.Teams[1].Members[0].Firstname to be `Akshay`, but got `%s`", out.Teams[1].Members[0].Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Teams[1].Members[1].Firstname != "Peri" {
|
||||||
|
t.Fatalf("expected out.Teams[1].Members[1].Firstname to be `Peri`, but got `%s`", out.Teams[1].Members[1].Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Teams[0].Leader.Firstname != "Iz" {
|
||||||
|
t.Fatalf("expected out.Teams[0].Leader.Firstname to be `Iz`, but got `%s`", out.Teams[0].Leader.Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Teams[1].Leader.Firstname != "Peri" {
|
||||||
|
t.Fatalf("expected out.Teams[1].Leader.Firstname to be `Peri`, but got `%s`", out.Teams[1].Leader.Firstname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalNestedStructSlice(t *testing.T) {
|
||||||
|
|
||||||
|
fry := map[string]interface{}{
|
||||||
|
"firstname": "Philip J.",
|
||||||
|
"surname": "Fry",
|
||||||
|
"age": 25,
|
||||||
|
"hired-at": "2016-08-17T08:27:12Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
bender := map[string]interface{}{
|
||||||
|
"firstname": "Bender Bending",
|
||||||
|
"surname": "Rodriguez",
|
||||||
|
"age": 19,
|
||||||
|
"hired-at": "2016-08-17T08:27:12Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveryCrew := map[string]interface{}{
|
||||||
|
"name": "Delivery Crew",
|
||||||
|
"members": []interface{}{
|
||||||
|
fry,
|
||||||
|
bender,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sample := map[string]interface{}{
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"type": "companies",
|
||||||
|
"id": "123",
|
||||||
|
"attributes": map[string]interface{}{
|
||||||
|
"name": "Planet Express",
|
||||||
|
"teams": []interface{}{
|
||||||
|
deliveryCrew,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(sample)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
in := bytes.NewReader(data)
|
||||||
|
out := new(Company)
|
||||||
|
|
||||||
|
if err := UnmarshalPayload(in, out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Teams[0].Name != "Delivery Crew" {
|
||||||
|
t.Fatalf("Nested struct not unmarshalled: Expected `Delivery Crew` but got `%s`", out.Teams[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(out.Teams[0].Members) != 2 {
|
||||||
|
t.Fatalf("Nested struct not unmarshalled: Expected to have `2` Members but got `%d`",
|
||||||
|
len(out.Teams[0].Members))
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.Teams[0].Members[0].Firstname != "Philip J." {
|
||||||
|
t.Fatalf("Nested struct not unmarshalled: Expected `Philip J.` but got `%s`",
|
||||||
|
out.Teams[0].Members[0].Firstname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
42
response.go
42
response.go
|
@ -68,10 +68,7 @@ func MarshalPayload(w io.Writer, models interface{}) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
return json.NewEncoder(w).Encode(payload)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal does the same as MarshalPayload except it just returns the payload
|
// Marshal does the same as MarshalPayload except it just returns the payload
|
||||||
|
@ -128,10 +125,7 @@ func MarshalPayloadWithoutIncluded(w io.Writer, model interface{}) error {
|
||||||
}
|
}
|
||||||
payload.clearIncluded()
|
payload.clearIncluded()
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
return json.NewEncoder(w).Encode(payload)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// marshalOne does the same as MarshalOnePayload except it just returns the
|
// marshalOne does the same as MarshalOnePayload except it just returns the
|
||||||
|
@ -195,11 +189,7 @@ func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
|
||||||
|
|
||||||
payload := &OnePayload{Data: rootNode}
|
payload := &OnePayload{Data: rootNode}
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
return json.NewEncoder(w).Encode(payload)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func visitModelNode(model interface{}, included *map[string]*Node,
|
func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
|
@ -207,9 +197,20 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
node := new(Node)
|
node := new(Node)
|
||||||
|
|
||||||
var er error
|
var er error
|
||||||
|
value := reflect.ValueOf(model)
|
||||||
|
if value.Kind() == reflect.Ptr && value.IsNil() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
modelValue := reflect.ValueOf(model).Elem()
|
var modelValue reflect.Value
|
||||||
modelType := reflect.ValueOf(model).Type().Elem()
|
var modelType reflect.Type
|
||||||
|
if value.Kind() == reflect.Ptr {
|
||||||
|
modelValue = value.Elem()
|
||||||
|
modelType = value.Type().Elem()
|
||||||
|
} else {
|
||||||
|
modelValue = value
|
||||||
|
modelType = value.Type()
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < modelValue.NumField(); i++ {
|
for i := 0; i < modelValue.NumField(); i++ {
|
||||||
structField := modelValue.Type().Field(i)
|
structField := modelValue.Type().Field(i)
|
||||||
|
@ -276,6 +277,9 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
// We had a JSON float (numeric), but our field was not one of the
|
// We had a JSON float (numeric), but our field was not one of the
|
||||||
// allowed numeric types
|
// allowed numeric types
|
||||||
er = ErrBadJSONAPIID
|
er = ErrBadJSONAPIID
|
||||||
|
}
|
||||||
|
|
||||||
|
if er != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,7 +290,7 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
node.ClientID = clientID
|
node.ClientID = clientID
|
||||||
}
|
}
|
||||||
} else if annotation == annotationAttribute {
|
} else if annotation == annotationAttribute {
|
||||||
var omitEmpty, iso8601 bool
|
var omitEmpty, iso8601, rfc3339 bool
|
||||||
|
|
||||||
if len(args) > 2 {
|
if len(args) > 2 {
|
||||||
for _, arg := range args[2:] {
|
for _, arg := range args[2:] {
|
||||||
|
@ -295,6 +299,8 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
omitEmpty = true
|
omitEmpty = true
|
||||||
case annotationISO8601:
|
case annotationISO8601:
|
||||||
iso8601 = true
|
iso8601 = true
|
||||||
|
case annotationRFC3339:
|
||||||
|
rfc3339 = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -312,6 +318,8 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
|
|
||||||
if iso8601 {
|
if iso8601 {
|
||||||
node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat)
|
node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat)
|
||||||
|
} else if rfc3339 {
|
||||||
|
node.Attributes[args[1]] = t.UTC().Format(time.RFC3339)
|
||||||
} else {
|
} else {
|
||||||
node.Attributes[args[1]] = t.Unix()
|
node.Attributes[args[1]] = t.Unix()
|
||||||
}
|
}
|
||||||
|
@ -332,6 +340,8 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
||||||
|
|
||||||
if iso8601 {
|
if iso8601 {
|
||||||
node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat)
|
node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat)
|
||||||
|
} else if rfc3339 {
|
||||||
|
node.Attributes[args[1]] = tm.UTC().Format(time.RFC3339)
|
||||||
} else {
|
} else {
|
||||||
node.Attributes[args[1]] = tm.Unix()
|
node.Attributes[args[1]] = tm.Unix()
|
||||||
}
|
}
|
||||||
|
|
187
response_test.go
187
response_test.go
|
@ -3,6 +3,7 @@ package jsonapi
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -37,6 +38,35 @@ func TestMarshalPayload(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarshalPayloadWithNulls(t *testing.T) {
|
||||||
|
|
||||||
|
books := []*Book{nil, {ID: 101}, nil}
|
||||||
|
var jsonData map[string]interface{}
|
||||||
|
|
||||||
|
out := bytes.NewBuffer(nil)
|
||||||
|
if err := MarshalPayload(out, books); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
raw, ok := jsonData["data"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("data key does not exist")
|
||||||
|
}
|
||||||
|
arr, ok := raw.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("data is not an Array")
|
||||||
|
}
|
||||||
|
for i := 0; i < len(arr); i++ {
|
||||||
|
if books[i] == nil && arr[i] != nil ||
|
||||||
|
books[i] != nil && arr[i] == nil {
|
||||||
|
t.Fatalf("restored data is not equal to source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarshal_attrStringSlice(t *testing.T) {
|
func TestMarshal_attrStringSlice(t *testing.T) {
|
||||||
tags := []string{"fiction", "sale"}
|
tags := []string{"fiction", "sale"}
|
||||||
b := &Book{ID: 1, Tags: tags}
|
b := &Book{ID: 1, Tags: tags}
|
||||||
|
@ -441,58 +471,113 @@ func TestOmitsZeroTimes(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalISO8601Time(t *testing.T) {
|
func TestMarshal_Times(t *testing.T) {
|
||||||
testModel := &Timestamp{
|
aTime := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC)
|
||||||
ID: 5,
|
|
||||||
Time: time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC),
|
|
||||||
}
|
|
||||||
|
|
||||||
out := bytes.NewBuffer(nil)
|
for _, tc := range []struct {
|
||||||
if err := MarshalPayload(out, testModel); err != nil {
|
desc string
|
||||||
t.Fatal(err)
|
input *TimestampModel
|
||||||
}
|
verification func(data map[string]interface{}) error
|
||||||
|
}{
|
||||||
resp := new(OnePayload)
|
{
|
||||||
if err := json.NewDecoder(out).Decode(resp); err != nil {
|
desc: "default_byValue",
|
||||||
t.Fatal(err)
|
input: &TimestampModel{
|
||||||
}
|
ID: 5,
|
||||||
|
DefaultV: aTime,
|
||||||
data := resp.Data
|
},
|
||||||
|
verification: func(root map[string]interface{}) error {
|
||||||
if data.Attributes == nil {
|
v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["defaultv"].(float64)
|
||||||
t.Fatalf("Expected attributes")
|
if got, want := int64(v), aTime.Unix(); got != want {
|
||||||
}
|
return fmt.Errorf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
if data.Attributes["timestamp"] != "2016-08-17T08:27:12Z" {
|
return nil
|
||||||
t.Fatal("Timestamp was not serialised into ISO8601 correctly")
|
},
|
||||||
}
|
},
|
||||||
}
|
{
|
||||||
|
desc: "default_byPointer",
|
||||||
func TestMarshalISO8601TimePointer(t *testing.T) {
|
input: &TimestampModel{
|
||||||
tm := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC)
|
ID: 5,
|
||||||
testModel := &Timestamp{
|
DefaultP: &aTime,
|
||||||
ID: 5,
|
},
|
||||||
Next: &tm,
|
verification: func(root map[string]interface{}) error {
|
||||||
}
|
v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["defaultp"].(float64)
|
||||||
|
if got, want := int64(v), aTime.Unix(); got != want {
|
||||||
out := bytes.NewBuffer(nil)
|
return fmt.Errorf("got %v, want %v", got, want)
|
||||||
if err := MarshalPayload(out, testModel); err != nil {
|
}
|
||||||
t.Fatal(err)
|
return nil
|
||||||
}
|
},
|
||||||
|
},
|
||||||
resp := new(OnePayload)
|
{
|
||||||
if err := json.NewDecoder(out).Decode(resp); err != nil {
|
desc: "iso8601_byValue",
|
||||||
t.Fatal(err)
|
input: &TimestampModel{
|
||||||
}
|
ID: 5,
|
||||||
|
ISO8601V: aTime,
|
||||||
data := resp.Data
|
},
|
||||||
|
verification: func(root map[string]interface{}) error {
|
||||||
if data.Attributes == nil {
|
v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["iso8601v"].(string)
|
||||||
t.Fatalf("Expected attributes")
|
if got, want := v, aTime.UTC().Format(iso8601TimeFormat); got != want {
|
||||||
}
|
return fmt.Errorf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
if data.Attributes["next"] != "2016-08-17T08:27:12Z" {
|
return nil
|
||||||
t.Fatal("Next was not serialised into ISO8601 correctly")
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "iso8601_byPointer",
|
||||||
|
input: &TimestampModel{
|
||||||
|
ID: 5,
|
||||||
|
ISO8601P: &aTime,
|
||||||
|
},
|
||||||
|
verification: func(root map[string]interface{}) error {
|
||||||
|
v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["iso8601p"].(string)
|
||||||
|
if got, want := v, aTime.UTC().Format(iso8601TimeFormat); got != want {
|
||||||
|
return fmt.Errorf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "rfc3339_byValue",
|
||||||
|
input: &TimestampModel{
|
||||||
|
ID: 5,
|
||||||
|
RFC3339V: aTime,
|
||||||
|
},
|
||||||
|
verification: func(root map[string]interface{}) error {
|
||||||
|
v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["rfc3339v"].(string)
|
||||||
|
if got, want := v, aTime.UTC().Format(time.RFC3339); got != want {
|
||||||
|
return fmt.Errorf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "rfc3339_byPointer",
|
||||||
|
input: &TimestampModel{
|
||||||
|
ID: 5,
|
||||||
|
RFC3339P: &aTime,
|
||||||
|
},
|
||||||
|
verification: func(root map[string]interface{}) error {
|
||||||
|
v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["rfc3339p"].(string)
|
||||||
|
if got, want := v, aTime.UTC().Format(time.RFC3339); got != want {
|
||||||
|
return fmt.Errorf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
out := bytes.NewBuffer(nil)
|
||||||
|
if err := MarshalPayload(out, tc.input); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Use the standard JSON library to traverse the genereated JSON payload.
|
||||||
|
data := map[string]interface{}{}
|
||||||
|
json.Unmarshal(out.Bytes(), &data)
|
||||||
|
if tc.verification != nil {
|
||||||
|
if err := tc.verification(data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
103
runtime.go
103
runtime.go
|
@ -1,103 +0,0 @@
|
||||||
package jsonapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"reflect"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Event int
|
|
||||||
|
|
||||||
const (
|
|
||||||
UnmarshalStart Event = iota
|
|
||||||
UnmarshalStop
|
|
||||||
MarshalStart
|
|
||||||
MarshalStop
|
|
||||||
)
|
|
||||||
|
|
||||||
type Runtime struct {
|
|
||||||
ctx map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Events func(*Runtime, Event, string, time.Duration)
|
|
||||||
|
|
||||||
var Instrumentation Events
|
|
||||||
|
|
||||||
func NewRuntime() *Runtime { return &Runtime{make(map[string]interface{})} }
|
|
||||||
|
|
||||||
func (r *Runtime) WithValue(key string, value interface{}) *Runtime {
|
|
||||||
r.ctx[key] = value
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Runtime) Value(key string) interface{} {
|
|
||||||
return r.ctx[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Runtime) Instrument(key string) *Runtime {
|
|
||||||
return r.WithValue("instrument", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Runtime) shouldInstrument() bool {
|
|
||||||
return Instrumentation != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Runtime) UnmarshalPayload(reader io.Reader, model interface{}) error {
|
|
||||||
return r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error {
|
|
||||||
return UnmarshalPayload(reader, model)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Runtime) UnmarshalManyPayload(reader io.Reader, kind reflect.Type) (elems []interface{}, err error) {
|
|
||||||
r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error {
|
|
||||||
elems, err = UnmarshalManyPayload(reader, kind)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Runtime) MarshalPayload(w io.Writer, model interface{}) error {
|
|
||||||
return r.instrumentCall(MarshalStart, MarshalStop, func() error {
|
|
||||||
return MarshalPayload(w, model)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Runtime) instrumentCall(start Event, stop Event, c func() error) error {
|
|
||||||
if !r.shouldInstrument() {
|
|
||||||
return c()
|
|
||||||
}
|
|
||||||
|
|
||||||
instrumentationGUID, err := newUUID()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
begin := time.Now()
|
|
||||||
Instrumentation(r, start, instrumentationGUID, time.Duration(0))
|
|
||||||
|
|
||||||
if err := c(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
diff := time.Duration(time.Now().UnixNano() - begin.UnixNano())
|
|
||||||
Instrumentation(r, stop, instrumentationGUID, diff)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// citation: http://play.golang.org/p/4FkNSiUDMg
|
|
||||||
func newUUID() (string, error) {
|
|
||||||
uuid := make([]byte, 16)
|
|
||||||
if _, err := io.ReadFull(rand.Reader, uuid); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// variant bits; see section 4.1.1
|
|
||||||
uuid[8] = uuid[8]&^0xc0 | 0x80
|
|
||||||
// version 4 (pseudo-random); see section 4.1.3
|
|
||||||
uuid[6] = uuid[6]&^0xf0 | 0x40
|
|
||||||
return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue