forked from Mirrors/jsonapi
Compare commits
73 Commits
feature/em
...
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 | |
Aren Patel | 8127e1640e | |
Igor Zibarev | 103c21c224 | |
Sam Woodard | 6600c8fdc1 | |
Sam Woodard | 1ac83a4625 | |
Stratos Neiros | bf4e01db8d | |
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 | |
Sam Woodard | e0fc4eed33 | |
Jason McCallister | 081fb8a5c2 | |
Jason McCallister | c036316c9d | |
Aren Patel | a06052dd83 | |
Aren Patel | 8d89a9020f | |
Aren Patel | 2ce5c379b0 | |
Aren Patel | 5e0c586099 | |
Aren Patel | 3b01bb5fe6 | |
Markus Ritberger | fc6968dfe7 | |
Markus Ritberger | b391a84b75 | |
Slemgrim | af21dba1b2 | |
Slemgrim | 0c97f0cf8d | |
Slemgrim | ab915ccbc1 | |
Slemgrim | f0b268e046 | |
Slemgrim | ec077ed283 | |
Slemgrim | e13a19922d |
|
@ -1,4 +1 @@
|
|||
/bin/
|
||||
/pkg/
|
||||
/src/
|
||||
/.idea
|
||||
/examples/examples
|
||||
|
|
14
.travis.yml
14
.travis.yml
|
@ -1,7 +1,13 @@
|
|||
language: go
|
||||
arch:
|
||||
- amd64
|
||||
- ppc64le
|
||||
go:
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
- 1.13.x
|
||||
- 1.14.x
|
||||
- 1.15.x
|
||||
- 1.16.x
|
||||
- tip
|
||||
script: script/test -v
|
||||
script: go test ./... -v
|
||||
|
|
34
README.md
34
README.md
|
@ -1,10 +1,15 @@
|
|||
# jsonapi
|
||||
|
||||
[![Build Status](https://travis-ci.org/google/jsonapi.svg?branch=master)](https://travis-ci.org/google/jsonapi) [![GoDoc](https://godoc.org/github.com/google/jsonapi?status.svg)](http://godoc.org/github.com/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)
|
||||
[![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
|
||||
[JSON API - jsonapi.org](http://jsonapi.org) spec in go.
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
|
@ -74,7 +79,7 @@ all of your data easily.
|
|||
|
||||
[examples/app.go](https://github.com/google/jsonapi/blob/master/examples/app.go)
|
||||
|
||||
This runnable file demonstrates the implementation of a create, a show,
|
||||
This program demonstrates the implementation of a create, a show,
|
||||
and a list [http.Handler](http://golang.org/pkg/net/http#Handler). It
|
||||
outputs some example requests and responses as well as serialized
|
||||
examples of the source/target structs to json. That is to say, I show
|
||||
|
@ -83,13 +88,13 @@ turned it into your struct types.
|
|||
|
||||
To run,
|
||||
|
||||
* Make sure you have go installed
|
||||
* Make sure you have [Go installed](https://golang.org/doc/install)
|
||||
* Create the following directories or similar: `~/go`
|
||||
* Set `GOPATH` to `PWD` in your shell session, `export GOPATH=$PWD`
|
||||
* `go get github.com/google/jsonapi`. (Append `-u` after `get` if you
|
||||
are updating.)
|
||||
* `go run $GOPATH/src/github.com/google/jsonapi/examples/app.go` or `cd
|
||||
$GOPATH/src/github.com/google/jsonapi/examples && go run app.go`
|
||||
* `cd $GOPATH/src/github.com/google/jsonapi/examples`
|
||||
* `go build && ./examples`
|
||||
|
||||
## `jsonapi` Tag Reference
|
||||
|
||||
|
@ -179,7 +184,7 @@ to-many from being serialized.
|
|||
**All `Marshal` and `Unmarshal` methods expect pointers to struct
|
||||
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?
|
||||
|
||||
### Create Record Example
|
||||
|
@ -341,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
|
||||
This package also implements support for JSON API compatible `errors` payloads using the following types.
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ const (
|
|||
annotationRelation = "relation"
|
||||
annotationOmitEmpty = "omitempty"
|
||||
annotationISO8601 = "iso8601"
|
||||
annotationRFC3339 = "rfc3339"
|
||||
annotationSeperator = ","
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
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:
|
||||
|
||||
|
|
|
@ -12,10 +12,7 @@ import (
|
|||
// http://jsonapi.org/format/#document-top-level
|
||||
// and here: http://jsonapi.org/format/#error-objects.
|
||||
func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error {
|
||||
if err := json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects})
|
||||
}
|
||||
|
||||
// ErrorsPayload is a serializer struct for representing a valid JSON API errors payload.
|
||||
|
|
|
@ -8,31 +8,31 @@ func fixtureBlogCreate(i int) *Blog {
|
|||
Title: "Title 1",
|
||||
CreatedAt: time.Now(),
|
||||
Posts: []*Post{
|
||||
&Post{
|
||||
{
|
||||
ID: 1 * i,
|
||||
Title: "Foo",
|
||||
Body: "Bar",
|
||||
Comments: []*Comment{
|
||||
&Comment{
|
||||
{
|
||||
ID: 1 * i,
|
||||
Body: "foo",
|
||||
},
|
||||
&Comment{
|
||||
{
|
||||
ID: 2 * i,
|
||||
Body: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
&Post{
|
||||
{
|
||||
ID: 2 * i,
|
||||
Title: "Fuubar",
|
||||
Body: "Bas",
|
||||
Comments: []*Comment{
|
||||
&Comment{
|
||||
{
|
||||
ID: 1 * i,
|
||||
Body: "foo",
|
||||
},
|
||||
&Comment{
|
||||
{
|
||||
ID: 3 * i,
|
||||
Body: "bas",
|
||||
},
|
||||
|
@ -44,11 +44,11 @@ func fixtureBlogCreate(i int) *Blog {
|
|||
Title: "Foo",
|
||||
Body: "Bar",
|
||||
Comments: []*Comment{
|
||||
&Comment{
|
||||
{
|
||||
ID: 1 * i,
|
||||
Body: "foo",
|
||||
},
|
||||
&Comment{
|
||||
{
|
||||
ID: 2 * i,
|
||||
Body: "bar",
|
||||
},
|
||||
|
|
|
@ -84,3 +84,35 @@ func TestExampleHandler_get_list(t *testing.T) {
|
|||
t.Fatalf("Expected a status of %d, got %d", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHttpErrorWhenHeaderDoesNotMatch(t *testing.T) {
|
||||
r, err := http.NewRequest(http.MethodGet, "/blogs", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Header.Set(headerAccept, "application/xml")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := &ExampleHandler{}
|
||||
handler.ServeHTTP(rr, r)
|
||||
|
||||
if rr.Code != http.StatusUnsupportedMediaType {
|
||||
t.Fatal("expected Unsupported Media Type staus error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHttpErrorWhenMethodDoesNotMatch(t *testing.T) {
|
||||
r, err := http.NewRequest(http.MethodPatch, "/blogs", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Header.Set(headerAccept, jsonapi.MediaType)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := &ExampleHandler{}
|
||||
handler.ServeHTTP(rr, r)
|
||||
|
||||
if rr.Code != http.StatusNotFound {
|
||||
t.Fatal("expected HTTP Status Not Found status error")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/google/jsonapi"
|
||||
)
|
||||
|
||||
// Blog is a model representing a blog site
|
||||
type Blog struct {
|
||||
ID int `jsonapi:"primary,blogs"`
|
||||
Title string `jsonapi:"attr,title"`
|
||||
|
@ -17,6 +18,7 @@ type Blog struct {
|
|||
ViewCount int `jsonapi:"attr,view_count"`
|
||||
}
|
||||
|
||||
// Post is a model representing a post on a blog
|
||||
type Post struct {
|
||||
ID int `jsonapi:"primary,posts"`
|
||||
BlogID int `jsonapi:"attr,blog_id"`
|
||||
|
@ -25,19 +27,21 @@ type Post struct {
|
|||
Comments []*Comment `jsonapi:"relation,comments"`
|
||||
}
|
||||
|
||||
// Comment is a model representing a user submitted comment
|
||||
type Comment struct {
|
||||
ID int `jsonapi:"primary,comments"`
|
||||
PostID int `jsonapi:"attr,post_id"`
|
||||
Body string `jsonapi:"attr,body"`
|
||||
}
|
||||
|
||||
// Blog Links
|
||||
// JSONAPILinks implements the Linkable interface for a blog
|
||||
func (blog Blog) JSONAPILinks() *jsonapi.Links {
|
||||
return &jsonapi.Links{
|
||||
"self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID),
|
||||
}
|
||||
}
|
||||
|
||||
// JSONAPIRelationshipLinks implements the RelationshipLinkable interface for a blog
|
||||
func (blog Blog) JSONAPIRelationshipLinks(relation string) *jsonapi.Links {
|
||||
if relation == "posts" {
|
||||
return &jsonapi.Links{
|
||||
|
@ -52,13 +56,14 @@ func (blog Blog) JSONAPIRelationshipLinks(relation string) *jsonapi.Links {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Blog Meta
|
||||
// JSONAPIMeta implements the Metable interface for a blog
|
||||
func (blog Blog) JSONAPIMeta() *jsonapi.Meta {
|
||||
return &jsonapi.Meta{
|
||||
"detail": "extra details regarding the blog",
|
||||
}
|
||||
}
|
||||
|
||||
// JSONAPIRelationshipMeta implements the RelationshipMetable interface for a blog
|
||||
func (blog Blog) JSONAPIRelationshipMeta(relation string) *jsonapi.Meta {
|
||||
if relation == "posts" {
|
||||
return &jsonapi.Meta{
|
||||
|
|
|
@ -25,10 +25,14 @@ type WithPointer struct {
|
|||
FloatVal *float32 `jsonapi:"attr,float-val"`
|
||||
}
|
||||
|
||||
type Timestamp struct {
|
||||
ID int `jsonapi:"primary,timestamps"`
|
||||
Time time.Time `jsonapi:"attr,timestamp,iso8601"`
|
||||
Next *time.Time `jsonapi:"attr,next,iso8601"`
|
||||
type TimestampModel struct {
|
||||
ID int `jsonapi:"primary,timestamps"`
|
||||
DefaultV time.Time `jsonapi:"attr,defaultv"`
|
||||
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 {
|
||||
|
@ -155,3 +159,39 @@ func (bc *BadComment) JSONAPILinks() *Links {
|
|||
"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 (
|
||||
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
||||
unsupportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -23,17 +23,42 @@ var (
|
|||
// 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.
|
||||
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
|
||||
// (numeric) but the Struct field was a non numeric type (i.e. not int, uint,
|
||||
// float, etc)
|
||||
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 = 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
|
||||
// struct fields. This method supports single request payloads only, at the
|
||||
// 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
|
||||
// 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)
|
||||
|
||||
if err := json.NewDecoder(in).Decode(payload); err != nil {
|
||||
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"
|
||||
|
||||
if payload.Included != nil {
|
||||
|
@ -106,12 +131,12 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
|
|||
}
|
||||
|
||||
for _, data := range payload.Data {
|
||||
model := reflect.New(t.Elem())
|
||||
err := unmarshalNode(data, model, &includedMap)
|
||||
model := new(T)
|
||||
err := unmarshalNode(data, reflect.ValueOf(model), &includedMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
models = append(models, model.Interface())
|
||||
models = append(models, *model)
|
||||
}
|
||||
|
||||
return models, nil
|
||||
|
@ -125,7 +150,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
}()
|
||||
|
||||
modelValue := model.Elem()
|
||||
modelType := model.Type().Elem()
|
||||
modelType := modelValue.Type()
|
||||
|
||||
var er error
|
||||
|
||||
|
@ -139,7 +164,6 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
fieldValue := modelValue.Field(i)
|
||||
|
||||
args := strings.Split(tag, ",")
|
||||
|
||||
if len(args) < 1 {
|
||||
er = ErrBadJSONAPIStructTag
|
||||
break
|
||||
|
@ -154,10 +178,6 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
}
|
||||
|
||||
if annotation == annotationPrimary {
|
||||
if data.ID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check the JSON API Type
|
||||
if data.Type != args[1] {
|
||||
er = fmt.Errorf(
|
||||
|
@ -168,6 +188,10 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
break
|
||||
}
|
||||
|
||||
if data.ID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// ID will have to be transmitted as astring per the JSON API spec
|
||||
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
|
||||
// (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:
|
||||
idValue, err := handleNumeric(floatValue, fieldType.Type, fieldValue)
|
||||
if err != nil {
|
||||
// We had a JSON float (numeric), but our field was not one of the
|
||||
// allowed numeric types
|
||||
er = ErrBadJSONAPIID
|
||||
|
@ -244,213 +237,26 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
fieldValue.Set(reflect.ValueOf(data.ClientID))
|
||||
} else if annotation == annotationAttribute {
|
||||
attributes := data.Attributes
|
||||
|
||||
if attributes == nil || len(data.Attributes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var iso8601 bool
|
||||
|
||||
if len(args) > 2 {
|
||||
for _, arg := range args[2:] {
|
||||
if arg == annotationISO8601 {
|
||||
iso8601 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val := attributes[args[1]]
|
||||
attribute := attributes[args[1]]
|
||||
|
||||
// continue if the attribute was not included in the request
|
||||
if val == nil {
|
||||
if attribute == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(val)
|
||||
|
||||
// Handle field of type time.Time
|
||||
if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
|
||||
if iso8601 {
|
||||
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
|
||||
structField := fieldType
|
||||
value, err := unmarshalAttribute(attribute, args, structField, fieldValue)
|
||||
if err != nil {
|
||||
er = err
|
||||
break
|
||||
}
|
||||
|
||||
if fieldValue.Type() == reflect.TypeOf([]string{}) {
|
||||
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))
|
||||
|
||||
assign(fieldValue, value)
|
||||
} else if annotation == annotationRelation {
|
||||
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
||||
|
||||
|
@ -522,7 +328,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
}
|
||||
|
||||
} 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
|
||||
// field is expecting a ptr assign will assign a ptr.
|
||||
func assign(field, value reflect.Value) {
|
||||
value = reflect.Indirect(value)
|
||||
|
||||
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)
|
||||
} 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
|
||||
}
|
||||
|
|
650
request_test.go
650
request_test.go
|
@ -3,6 +3,7 @@ package jsonapi
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"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) {
|
||||
out := new(WithPointer)
|
||||
if err := UnmarshalPayload(
|
||||
strings.NewReader(`{"data": {}}`), out); err != nil {
|
||||
t.Fatalf("Error unmarshalling to Foo")
|
||||
strings.NewReader(`{"data":{"type":"with-pointers"}}`), out); err != nil {
|
||||
t.Fatalf("Error unmarshalling to Foo: %v", err)
|
||||
}
|
||||
|
||||
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)
|
||||
in := map[string]interface{}{
|
||||
"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)
|
||||
|
||||
|
@ -136,6 +146,71 @@ func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) {
|
|||
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_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) {
|
||||
|
@ -236,7 +311,10 @@ 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)
|
||||
payload, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in := bytes.NewReader(payload)
|
||||
out := new(Post)
|
||||
|
||||
|
@ -264,80 +342,183 @@ func TestUnmarshalSetsAttrs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalParsesISO8601(t *testing.T) {
|
||||
payload := &OnePayload{
|
||||
Data: &Node{
|
||||
Type: "timestamps",
|
||||
Attributes: map[string]interface{}{
|
||||
"timestamp": "2016-08-17T08:27:12Z",
|
||||
func TestUnmarshal_Times(t *testing.T) {
|
||||
aTime := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
|
||||
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
in := bytes.NewBuffer(nil)
|
||||
json.NewEncoder(in).Encode(payload)
|
||||
|
||||
out := new(Timestamp)
|
||||
|
||||
if err := UnmarshalPayload(in, out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
|
||||
|
||||
if !out.Time.Equal(expected) {
|
||||
t.Fatal("Parsing the ISO8601 timestamp failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalParsesISO8601TimePointer(t *testing.T) {
|
||||
payload := &OnePayload{
|
||||
Data: &Node{
|
||||
Type: "timestamps",
|
||||
Attributes: map[string]interface{}{
|
||||
"next": "2016-08-17T08:27:12Z",
|
||||
{
|
||||
desc: "default_byPointer",
|
||||
inputPayload: &OnePayload{
|
||||
Data: &Node{
|
||||
Type: "timestamps",
|
||||
Attributes: map[string]interface{}{
|
||||
"defaultp": aTime.Unix(),
|
||||
},
|
||||
},
|
||||
},
|
||||
verification: func(tm *TimestampModel) error {
|
||||
if !tm.DefaultP.Equal(aTime) {
|
||||
return errors.New("times not equal!")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
in := bytes.NewBuffer(nil)
|
||||
json.NewEncoder(in).Encode(payload)
|
||||
|
||||
out := new(Timestamp)
|
||||
|
||||
if err := UnmarshalPayload(in, out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
|
||||
|
||||
if !out.Next.Equal(expected) {
|
||||
t.Fatal("Parsing the ISO8601 timestamp failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalInvalidISO8601(t *testing.T) {
|
||||
payload := &OnePayload{
|
||||
Data: &Node{
|
||||
Type: "timestamps",
|
||||
Attributes: map[string]interface{}{
|
||||
"timestamp": "17 Aug 16 08:027 MST",
|
||||
{
|
||||
desc: "default_invalid",
|
||||
inputPayload: &OnePayload{
|
||||
Data: &Node{
|
||||
Type: "timestamps",
|
||||
Attributes: map[string]interface{}{
|
||||
"defaultv": "not a timestamp!",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
// ISO 8601:
|
||||
{
|
||||
desc: "iso8601_byValue",
|
||||
inputPayload: &OnePayload{
|
||||
Data: &Node{
|
||||
Type: "timestamps",
|
||||
Attributes: map[string]interface{}{
|
||||
"iso8601v": "2016-08-17T08:27:12Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
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)
|
||||
json.NewEncoder(in).Encode(payload)
|
||||
|
||||
out := new(Timestamp)
|
||||
|
||||
if err := UnmarshalPayload(in, out); err != ErrInvalidISO8601 {
|
||||
t.Fatalf("Expected ErrInvalidISO8601, got %v", err)
|
||||
out := &TimestampModel{}
|
||||
err := UnmarshalPayload(in, out)
|
||||
if got, want := (err != nil), tc.wantErr; got != want {
|
||||
t.Fatalf("UnmarshalPayload error: got %v, want %v", got, want)
|
||||
}
|
||||
if tc.verification != nil {
|
||||
if err := tc.verification(out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) {
|
||||
data, _ := payload(samplePayloadWithoutIncluded())
|
||||
data, err := json.Marshal(samplePayloadWithoutIncluded())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in := bytes.NewReader(data)
|
||||
out := new(Post)
|
||||
|
||||
|
@ -603,7 +784,7 @@ func TestUnmarshalManyPayload(t *testing.T) {
|
|||
}
|
||||
in := bytes.NewReader(data)
|
||||
|
||||
posts, err := UnmarshalManyPayload(in, reflect.TypeOf(new(Post)))
|
||||
posts, err := UnmarshalManyPayload[Post](in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -611,13 +792,6 @@ func TestUnmarshalManyPayload(t *testing.T) {
|
|||
if len(posts) != 2 {
|
||||
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) {
|
||||
|
@ -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{} {
|
||||
return 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 {
|
||||
payload := &OnePayload{
|
||||
Data: &Node{
|
||||
|
@ -753,7 +1002,7 @@ func samplePayload() io.Reader {
|
|||
Relationships: map[string]interface{}{
|
||||
"posts": &RelationshipManyNode{
|
||||
Data: []*Node{
|
||||
&Node{
|
||||
{
|
||||
Type: "posts",
|
||||
Attributes: map[string]interface{}{
|
||||
"title": "Foo",
|
||||
|
@ -761,7 +1010,7 @@ func samplePayload() io.Reader {
|
|||
},
|
||||
ClientID: "1",
|
||||
},
|
||||
&Node{
|
||||
{
|
||||
Type: "posts",
|
||||
Attributes: map[string]interface{}{
|
||||
"title": "X",
|
||||
|
@ -782,14 +1031,14 @@ func samplePayload() io.Reader {
|
|||
Relationships: map[string]interface{}{
|
||||
"comments": &RelationshipManyNode{
|
||||
Data: []*Node{
|
||||
&Node{
|
||||
{
|
||||
Type: "comments",
|
||||
Attributes: map[string]interface{}{
|
||||
"body": "Great post!",
|
||||
},
|
||||
ClientID: "4",
|
||||
},
|
||||
&Node{
|
||||
{
|
||||
Type: "comments",
|
||||
Attributes: map[string]interface{}{
|
||||
"body": "Needs some work!",
|
||||
|
@ -866,16 +1115,16 @@ func testModel() *Blog {
|
|||
Title: "Title 1",
|
||||
CreatedAt: time.Now(),
|
||||
Posts: []*Post{
|
||||
&Post{
|
||||
{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Body: "Bar",
|
||||
Comments: []*Comment{
|
||||
&Comment{
|
||||
{
|
||||
ID: 1,
|
||||
Body: "foo",
|
||||
},
|
||||
&Comment{
|
||||
{
|
||||
ID: 2,
|
||||
Body: "bar",
|
||||
},
|
||||
|
@ -885,16 +1134,16 @@ func testModel() *Blog {
|
|||
Body: "foo",
|
||||
},
|
||||
},
|
||||
&Post{
|
||||
{
|
||||
ID: 2,
|
||||
Title: "Fuubar",
|
||||
Body: "Bas",
|
||||
Comments: []*Comment{
|
||||
&Comment{
|
||||
{
|
||||
ID: 1,
|
||||
Body: "foo",
|
||||
},
|
||||
&Comment{
|
||||
{
|
||||
ID: 3,
|
||||
Body: "bas",
|
||||
},
|
||||
|
@ -910,11 +1159,11 @@ func testModel() *Blog {
|
|||
Title: "Foo",
|
||||
Body: "Bar",
|
||||
Comments: []*Comment{
|
||||
&Comment{
|
||||
{
|
||||
ID: 1,
|
||||
Body: "foo",
|
||||
},
|
||||
&Comment{
|
||||
{
|
||||
ID: 2,
|
||||
Body: "bar",
|
||||
},
|
||||
|
@ -945,3 +1194,218 @@ func sampleSerializedEmbeddedTestModel() *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)
|
||||
}
|
||||
}
|
||||
|
|
46
response.go
46
response.go
|
@ -19,7 +19,7 @@ var (
|
|||
// was not a valid numeric type.
|
||||
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 argument was expected to
|
||||
// be a slice of *Structs; MarshalMany will return this error when its
|
||||
// interface{} argument is invalid.
|
||||
ErrExpectedSlice = errors.New("models should be a slice of struct pointers")
|
||||
|
@ -68,10 +68,7 @@ func MarshalPayload(w io.Writer, models interface{}) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return json.NewEncoder(w).Encode(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()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
// 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}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
func visitModelNode(model interface{}, included *map[string]*Node,
|
||||
|
@ -207,9 +197,20 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
|||
node := new(Node)
|
||||
|
||||
var er error
|
||||
value := reflect.ValueOf(model)
|
||||
if value.Kind() == reflect.Ptr && value.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
modelValue := reflect.ValueOf(model).Elem()
|
||||
modelType := reflect.ValueOf(model).Type().Elem()
|
||||
var modelValue reflect.Value
|
||||
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++ {
|
||||
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
|
||||
// allowed numeric types
|
||||
er = ErrBadJSONAPIID
|
||||
}
|
||||
|
||||
if er != nil {
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -286,7 +290,7 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
|||
node.ClientID = clientID
|
||||
}
|
||||
} else if annotation == annotationAttribute {
|
||||
var omitEmpty, iso8601 bool
|
||||
var omitEmpty, iso8601, rfc3339 bool
|
||||
|
||||
if len(args) > 2 {
|
||||
for _, arg := range args[2:] {
|
||||
|
@ -295,6 +299,8 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
|||
omitEmpty = true
|
||||
case annotationISO8601:
|
||||
iso8601 = true
|
||||
case annotationRFC3339:
|
||||
rfc3339 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -312,6 +318,8 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
|||
|
||||
if iso8601 {
|
||||
node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat)
|
||||
} else if rfc3339 {
|
||||
node.Attributes[args[1]] = t.UTC().Format(time.RFC3339)
|
||||
} else {
|
||||
node.Attributes[args[1]] = t.Unix()
|
||||
}
|
||||
|
@ -332,6 +340,8 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
|||
|
||||
if iso8601 {
|
||||
node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat)
|
||||
} else if rfc3339 {
|
||||
node.Attributes[args[1]] = tm.UTC().Format(time.RFC3339)
|
||||
} else {
|
||||
node.Attributes[args[1]] = tm.Unix()
|
||||
}
|
||||
|
@ -341,7 +351,7 @@ func visitModelNode(model interface{}, included *map[string]*Node,
|
|||
emptyValue := reflect.Zero(fieldValue.Type())
|
||||
|
||||
// See if we need to omit this field
|
||||
if omitEmpty && fieldValue.Interface() == emptyValue.Interface() {
|
||||
if omitEmpty && reflect.DeepEqual(fieldValue.Interface(), emptyValue.Interface()) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
282
response_test.go
282
response_test.go
|
@ -3,6 +3,7 @@ package jsonapi
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
@ -11,7 +12,7 @@ import (
|
|||
|
||||
func TestMarshalPayload(t *testing.T) {
|
||||
book := &Book{ID: 1}
|
||||
books := []*Book{book, &Book{ID: 2}}
|
||||
books := []*Book{book, {ID: 2}}
|
||||
var jsonData map[string]interface{}
|
||||
|
||||
// One
|
||||
|
@ -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) {
|
||||
tags := []string{"fiction", "sale"}
|
||||
b := &Book{ID: 1, Tags: tags}
|
||||
|
@ -194,6 +224,61 @@ func TestWithOmitsEmptyAnnotationOnRelation_MixedData(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWithOmitsEmptyAnnotationOnAttribute(t *testing.T) {
|
||||
type Phone struct {
|
||||
Number string `json:"number"`
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
City string `json:"city"`
|
||||
Street string `json:"street"`
|
||||
}
|
||||
|
||||
type Tags map[string]int
|
||||
|
||||
type Author struct {
|
||||
ID int `jsonapi:"primary,authors"`
|
||||
Name string `jsonapi:"attr,title"`
|
||||
Phones []*Phone `jsonapi:"attr,phones,omitempty"`
|
||||
Address *Address `jsonapi:"attr,address,omitempty"`
|
||||
Tags Tags `jsonapi:"attr,tags,omitempty"`
|
||||
}
|
||||
|
||||
author := &Author{
|
||||
ID: 999,
|
||||
Name: "Igor",
|
||||
Phones: nil, // should be omitted
|
||||
Address: nil, // should be omitted
|
||||
Tags: Tags{"dogs": 1, "cats": 2}, // should not be omitted
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
if err := MarshalPayload(out, author); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var jsonData map[string]interface{}
|
||||
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that there is no field "phones" in attributes
|
||||
payload := jsonData["data"].(map[string]interface{})
|
||||
attributes := payload["attributes"].(map[string]interface{})
|
||||
if _, ok := attributes["title"]; !ok {
|
||||
t.Fatal("Was expecting the data.attributes.title to have NOT been omitted")
|
||||
}
|
||||
if _, ok := attributes["phones"]; ok {
|
||||
t.Fatal("Was expecting the data.attributes.phones to have been omitted")
|
||||
}
|
||||
if _, ok := attributes["address"]; ok {
|
||||
t.Fatal("Was expecting the data.attributes.phones to have been omitted")
|
||||
}
|
||||
if _, ok := attributes["tags"]; !ok {
|
||||
t.Fatal("Was expecting the data.attributes.tags to have NOT been omitted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalIDPtr(t *testing.T) {
|
||||
id, make, model := "123e4567-e89b-12d3-a456-426655440000", "Ford", "Mustang"
|
||||
car := &Car{
|
||||
|
@ -290,9 +375,9 @@ func TestOmitsEmptyAnnotation(t *testing.T) {
|
|||
t.Fatalf("Was expecting the data.attributes.pages key/value to have been omitted - it was not and had a value of %v", val)
|
||||
}
|
||||
|
||||
// Verify the implicity omitted fields were omitted
|
||||
// Verify the implicitly omitted fields were omitted
|
||||
if val, exists := attributes["PublishedAt"]; exists {
|
||||
t.Fatalf("Was expecting the data.attributes.PublishedAt key/value to have been implicity omitted - it was not and had a value of %v", val)
|
||||
t.Fatalf("Was expecting the data.attributes.PublishedAt key/value to have been implicitly omitted - it was not and had a value of %v", val)
|
||||
}
|
||||
|
||||
// Verify the unset fields were not omitted
|
||||
|
@ -326,7 +411,7 @@ func TestHasPrimaryAnnotation(t *testing.T) {
|
|||
}
|
||||
|
||||
if data.ID != "5" {
|
||||
t.Fatalf("ID not transfered")
|
||||
t.Fatalf("ID not transferred")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,58 +471,113 @@ func TestOmitsZeroTimes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMarshalISO8601Time(t *testing.T) {
|
||||
testModel := &Timestamp{
|
||||
ID: 5,
|
||||
Time: time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC),
|
||||
}
|
||||
func TestMarshal_Times(t *testing.T) {
|
||||
aTime := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC)
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
if err := MarshalPayload(out, testModel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp := new(OnePayload)
|
||||
if err := json.NewDecoder(out).Decode(resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data := resp.Data
|
||||
|
||||
if data.Attributes == nil {
|
||||
t.Fatalf("Expected attributes")
|
||||
}
|
||||
|
||||
if data.Attributes["timestamp"] != "2016-08-17T08:27:12Z" {
|
||||
t.Fatal("Timestamp was not serialised into ISO8601 correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalISO8601TimePointer(t *testing.T) {
|
||||
tm := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC)
|
||||
testModel := &Timestamp{
|
||||
ID: 5,
|
||||
Next: &tm,
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
if err := MarshalPayload(out, testModel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp := new(OnePayload)
|
||||
if err := json.NewDecoder(out).Decode(resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data := resp.Data
|
||||
|
||||
if data.Attributes == nil {
|
||||
t.Fatalf("Expected attributes")
|
||||
}
|
||||
|
||||
if data.Attributes["next"] != "2016-08-17T08:27:12Z" {
|
||||
t.Fatal("Next was not serialised into ISO8601 correctly")
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
input *TimestampModel
|
||||
verification func(data map[string]interface{}) error
|
||||
}{
|
||||
{
|
||||
desc: "default_byValue",
|
||||
input: &TimestampModel{
|
||||
ID: 5,
|
||||
DefaultV: aTime,
|
||||
},
|
||||
verification: func(root map[string]interface{}) error {
|
||||
v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["defaultv"].(float64)
|
||||
if got, want := int64(v), aTime.Unix(); got != want {
|
||||
return fmt.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "default_byPointer",
|
||||
input: &TimestampModel{
|
||||
ID: 5,
|
||||
DefaultP: &aTime,
|
||||
},
|
||||
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 {
|
||||
return fmt.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "iso8601_byValue",
|
||||
input: &TimestampModel{
|
||||
ID: 5,
|
||||
ISO8601V: aTime,
|
||||
},
|
||||
verification: func(root map[string]interface{}) error {
|
||||
v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["iso8601v"].(string)
|
||||
if got, want := v, aTime.UTC().Format(iso8601TimeFormat); got != want {
|
||||
return fmt.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -623,11 +763,11 @@ func TestMarshalPayloadWithoutIncluded(t *testing.T) {
|
|||
Title: "Foo",
|
||||
Body: "Bar",
|
||||
Comments: []*Comment{
|
||||
&Comment{
|
||||
{
|
||||
ID: 20,
|
||||
Body: "First",
|
||||
},
|
||||
&Comment{
|
||||
{
|
||||
ID: 21,
|
||||
Body: "Hello World",
|
||||
},
|
||||
|
@ -660,12 +800,12 @@ func TestMarshalPayload_many(t *testing.T) {
|
|||
Title: "Title 1",
|
||||
CreatedAt: time.Now(),
|
||||
Posts: []*Post{
|
||||
&Post{
|
||||
{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Body: "Bar",
|
||||
},
|
||||
&Post{
|
||||
{
|
||||
ID: 2,
|
||||
Title: "Fuubar",
|
||||
Body: "Bas",
|
||||
|
@ -682,12 +822,12 @@ func TestMarshalPayload_many(t *testing.T) {
|
|||
Title: "Title 2",
|
||||
CreatedAt: time.Now(),
|
||||
Posts: []*Post{
|
||||
&Post{
|
||||
{
|
||||
ID: 3,
|
||||
Title: "Foo",
|
||||
Body: "Bar",
|
||||
},
|
||||
&Post{
|
||||
{
|
||||
ID: 4,
|
||||
Title: "Fuubar",
|
||||
Body: "Bas",
|
||||
|
@ -770,8 +910,8 @@ func TestMarshalManyWithoutIncluded(t *testing.T) {
|
|||
|
||||
func TestMarshalMany_SliceOfInterfaceAndSliceOfStructsSameJSON(t *testing.T) {
|
||||
structs := []*Book{
|
||||
&Book{ID: 1, Author: "aren55555", ISBN: "abc"},
|
||||
&Book{ID: 2, Author: "shwoodard", ISBN: "xyz"},
|
||||
{ID: 1, Author: "aren55555", ISBN: "abc"},
|
||||
{ID: 2, Author: "shwoodard", ISBN: "xyz"},
|
||||
}
|
||||
interfaces := []interface{}{}
|
||||
for _, s := range structs {
|
||||
|
@ -823,16 +963,16 @@ func testBlog() *Blog {
|
|||
Title: "Title 1",
|
||||
CreatedAt: time.Now(),
|
||||
Posts: []*Post{
|
||||
&Post{
|
||||
{
|
||||
ID: 1,
|
||||
Title: "Foo",
|
||||
Body: "Bar",
|
||||
Comments: []*Comment{
|
||||
&Comment{
|
||||
{
|
||||
ID: 1,
|
||||
Body: "foo",
|
||||
},
|
||||
&Comment{
|
||||
{
|
||||
ID: 2,
|
||||
Body: "bar",
|
||||
},
|
||||
|
@ -842,16 +982,16 @@ func testBlog() *Blog {
|
|||
Body: "foo",
|
||||
},
|
||||
},
|
||||
&Post{
|
||||
{
|
||||
ID: 2,
|
||||
Title: "Fuubar",
|
||||
Body: "Bas",
|
||||
Comments: []*Comment{
|
||||
&Comment{
|
||||
{
|
||||
ID: 1,
|
||||
Body: "foo",
|
||||
},
|
||||
&Comment{
|
||||
{
|
||||
ID: 3,
|
||||
Body: "bas",
|
||||
},
|
||||
|
@ -867,11 +1007,11 @@ func testBlog() *Blog {
|
|||
Title: "Foo",
|
||||
Body: "Bar",
|
||||
Comments: []*Comment{
|
||||
&Comment{
|
||||
{
|
||||
ID: 1,
|
||||
Body: "foo",
|
||||
},
|
||||
&Comment{
|
||||
{
|
||||
ID: 2,
|
||||
Body: "bar",
|
||||
},
|
||||
|
|
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
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
go run examples/app.go examples/handler.go examples/fixtures.go examples/models.go
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
go test ./... "$@"
|
Loading…
Reference in New Issue