Compare commits

...

73 Commits

Author SHA1 Message Date
Preston Baxter 403bd2e40b handle pointers being dumb 2023-11-21 19:14:36 -06:00
Preston Baxter b73a8a772f remove runtime. Update to go 1.21 and add type parameters to UnmarshallPayloadMany 2023-11-18 16:39:31 -06:00
Aren Patel 1e07b10d47
go mod init (#205) 2021-04-05 11:13:49 -07:00
Aren Patel f3b4acfd23
RFC 3339 support for both Marshal and Unmarshal. (#204)
* RFC 3339 support for both Marshal and Unmarshal.

* Post merge cleanup

* Update request_test.go

Co-authored-by: Quetzy Garcia <quetzy.garcia@integrate.com>

* Spelling

* Update request.go

Co-authored-by: Quetzy Garcia <quetzy.garcia@integrate.com>

* Simplify the ISO 8601 logic. No need for the const rfc3339TimeFormat use time.RFC3339 directly.

Co-authored-by: Quetzy Garcia <quetzy.garcia@integrate.com>
2021-04-05 11:13:02 -07:00
Aren Patel 58702143d4
Revert "Add RFC3339 timestamp (#201)" (#203)
This reverts commit c0ee6d2554.
2021-04-03 09:11:22 -07:00
Omar Ismail c0ee6d2554
Add RFC3339 timestamp (#201)
@omarismail LGTM.

I'm curious what you think about perhaps documenting these `iso8601` and `rfc3339` in the `Readme.md`? How did you find that this tag option/value existed? How can we make this better for others vs having to search the library implementation?
2021-04-02 17:10:43 -07:00
Quetzy Garcia b10ff4bf78
[FIX] Issue 176 (#199)
* fix(request): change the order of the resource data check to prioritise the type when unmarshalling
- fixes #176

* tests(request): update the TestUnmarshalPayload_ptrsAllNil test that broke

* Using %v instead of %s

* Added a test case to demonstrate that this library follows the spec: covered the case where the incoming request payload is missing the `type` field, an error is expected.

Co-authored-by: Aren Patel <git@arenpatel.com>
2021-03-06 12:33:18 -08:00
Aren Patel 471426f0d9
Update .travis.yml 2021-02-17 13:33:41 -08:00
Matti Andreas Nielsen 4fcdb6314a
doc.go: Fix gramma 'deserialzied' -> 'deserialized' (#167) 2021-02-17 13:33:12 -08:00
Aren Patel f822737867
Update .travis.yml 2020-10-22 15:56:00 -07:00
ujjwalsh b862aa6e32
added support for linux on power (#195) 2020-10-22 15:54:02 -07:00
Aren Patel 3e3da1210d
Update README.md 2020-08-25 11:36:04 -07:00
Aren Patel c8283f632f
Update .travis.yml 2020-02-25 16:29:10 -08:00
Sam Woodard d0428f63eb
Merge pull request #165 from shwoodard/fix-lint
Fix golint
2018-10-16 08:00:55 -07:00
Sam Woodard 4a0c98e9d4 add docs for runtime.go 2018-10-16 07:34:22 -07:00
Sam Woodard e9f117e24a fix logic error, break needs to be for loop not switch 2018-10-16 07:33:57 -07:00
Sam Woodard 6bf44faa3c return function return value when is only err 2018-10-16 07:33:23 -07:00
Sam Woodard 9246c912f5
Merge pull request #159 from shwoodard/shwoodard-defined-type-attributes
Add support for attributes of custom defined types
2018-10-15 16:24:20 -07:00
Sam Woodard e22856db88 add documentation for primative custom types only 2018-10-11 09:08:03 -07:00
Sam Woodard 1947fea11f wrote failing test for example so removing from docs 2018-10-11 09:04:55 -07:00
Sam Woodard 906357051e run gofmt on package 2018-10-11 04:47:50 -07:00
Sam Woodard d9a610774a
Merge pull request #160 from google/fix-readme-typos
Fix typos
2018-10-05 12:57:15 -07:00
Sam Woodard 941c167d93
Fix typo "the the" 2018-10-05 12:52:00 -07:00
Sam Woodard 43592a3ebe
Fix typos 2018-10-05 12:50:13 -07:00
Sam Woodard ed08d4f02a refactor, consistency, add test to ensure we don't need additional type check 2018-10-05 08:57:33 -07:00
Sam Woodard d05fcd97df one line method removed 2018-10-05 08:56:32 -07:00
Sam Woodard ab24913148 adjust test so values are like from a json payload 2018-10-05 07:29:18 -07:00
CrushedPixel 87c6b8e5b5 Fixed format types 2018-10-05 07:29:06 -07:00
CrushedPixel ccac636b4b Add support for attributes of custom defined types 2018-10-05 07:10:00 -07:00
Sam Woodard 5307399ec1 add new go version to travis and remove oldest (#158) 2018-10-04 13:56:12 -07:00
Sam Woodard bdc73a22a3
Merge pull request #99 from Slemgrim/master
Unmarshalling of nested structs
2018-10-04 12:04:27 -07:00
Markus Ritberger 417d4eb8fb
Merge pull request #6 from shwoodard/shwoodard-slemgrim-pull-99
Use jsonapi struct tags for nested attrs
2018-10-04 18:04:16 +02:00
Sam Woodard 3c8221b373 use jsonapi tags for nested attrs 2018-10-03 13:51:04 -07:00
Stuart Auld 3b9f84a311 Added README to explain how to support custom types (#149) (#150)
* Added README to explain how to support custom types (#149)

* Fix incorrect docstring
2018-08-21 14:31:52 -07:00
Ilya Baturin 5d047c6bc6 fixes issue with slice of nil pointers (#144)
fixes panic in case of marshaling of slice of nils like `[]*<SomeType>{nil, nil}`
2018-06-17 19:19:26 -07:00
Markus Ritberger 8b7e0bc2c0
Merge pull request #5 from Slemgrim/refactor-ptr-error
Refactor ptr error and cleanup
2018-03-14 21:47:34 +01:00
Markus Ritberger d490a0f637 remove whitespaces and stick to 80chars 2018-03-14 21:43:51 +01:00
Markus Ritberger 72f7bad5b3 check for ptr error type in tests 2018-03-14 21:43:24 +01:00
Markus Ritberger 9bc94d8c70 replace public function with custom error type 2018-03-14 20:52:27 +01:00
Sam Woodard 2dcc18f436
Merge pull request #133 from google/u/aren55555/remove-scripts-dir
Remove the script dir, the readme documents a way to run the example …
2018-03-12 18:38:58 -07:00
Aren Patel 8127e1640e
Remove the script dir, the readme documents a way to run the example without using script/example. 2018-03-12 18:24:05 -07:00
Igor Zibarev 103c21c224 Fix omitempty for attributes (#119)
* Fix omitempty for attributes

Fix panic on omitempty field for attribute that is not of
a primitive type.

Fixes #103

* Add more types for omitempty test
2018-03-12 18:15:16 -07:00
Sam Woodard 6600c8fdc1 Remove funky go structure paths and add examples bin (#132) 2018-03-12 18:01:40 -07:00
Sam Woodard 1ac83a4625 Add go 1.10 to .travis.yml (#131) 2018-03-12 17:55:56 -07:00
Stratos Neiros bf4e01db8d Use go build instead of go run for example app (#127)
Using go run $GOPATH/src/github.com/google/jsonapi/examples/app.go as
shown in the Readme, leads to errors (#126) since go run requires all
the .go files of a program.

This commit changes the Readme to show how to run the example app using
go build to avoid this issue.
2018-03-12 17:54:39 -07:00
Markus Ritberger e428b86c25
Merge pull request #4 from msabramo/fix-test-failures
Fix test failures
2018-01-25 16:51:16 +01:00
Marc Abramowitz 7c2ceac7c5 Fix test failures 2018-01-25 07:28:20 -08:00
Markus Ritberger 21b4945ad6
Merge pull request #3 from msabramo/pr-99-make-nested-struct-ptr-work
Make nested struct pointers work
2018-01-22 12:11:07 +01:00
Markus Ritberger bb266b4483
Merge pull request #2 from msabramo/pr-99-show-type-for-ErrUnsupportedPtrType
Show type for ErrUnsupportedPtrType
2018-01-22 08:33:37 +01:00
Markus Ritberger 339909da0d
Merge pull request #1 from msabramo/msabramo-pr-99
Test more things in TestUnmarshalNestedStruct
2018-01-22 08:10:08 +01:00
Marc Abramowitz a3b3bb2cb5 Show type for ErrUnsupportedPtrType
to aid in troubleshooting.

Before:

```
Pointer type in struct is not supported
```

After:

```
jsonapi: Can't unmarshal true (bool) to struct field `Name`, which is a pointer to `string`
```
2018-01-17 13:38:32 -08:00
Marc Abramowitz 16e19ab9f9 Make nested struct pointers work 2018-01-17 13:02:30 -08:00
Marc Abramowitz b28beab7f3 Add TestUnmarshalNestedStructPtr 2018-01-17 13:02:30 -08:00
Marc Abramowitz e3c0871f34 Test more things in TestUnmarshalNestedStruct 2018-01-17 08:12:59 -08:00
Slemgrim 0400041771 fix wrong type for formatting 2017-11-21 19:15:24 +08:00
Slemgrim a6577dfae8 Add more speaking tests
tests should have an expected outcome vs. an actual outcome to make them easier to debug
2017-11-21 19:05:50 +08:00
Slemgrim 7e5c9014d9 Merge remote-tracking branch 'upstream/master' 2017-11-21 18:42:48 +08:00
Sam Woodard e0fc4eed33
Merge pull request #118 from themccallister/patch-1
Add tests for mismatching accept headers
2017-11-08 09:49:09 -08:00
Jason McCallister 081fb8a5c2 Add additional test for method not found 2017-10-23 22:46:32 -04:00
Jason McCallister c036316c9d Add tests for mismatching accept headers 2017-10-23 22:41:44 -04:00
Aren Patel a06052dd83
Run gofmt -s to simplifiy. 2017-09-13 12:59:59 -07:00
Aren Patel 8d89a9020f
Fix comment. 2017-09-13 12:56:47 -07:00
Aren Patel 2ce5c379b0
Address go lint comments for the examples. 2017-09-13 12:56:01 -07:00
Aren Patel 5e0c586099
Corrected spelling mistakes. 2017-09-13 12:52:31 -07:00
Aren Patel 3b01bb5fe6
Added go report card. 2017-09-13 12:49:52 -07:00
Markus Ritberger fc6968dfe7 change nested structs to json annotation instead of json:api. It never made sense to use json:api annotation in nested structs 2017-08-14 20:41:26 +02:00
Markus Ritberger b391a84b75 remove duplicated code 2017-08-14 20:39:14 +02:00
Slemgrim af21dba1b2 handle nested struct slices 2017-07-15 15:22:03 +02:00
Slemgrim 0c97f0cf8d extract attribute handling into custom method for recursive usage later on 2017-07-11 21:43:12 +02:00
Slemgrim ab915ccbc1 unify type handler 2017-07-10 22:50:43 +02:00
Slemgrim f0b268e046 Merge remote-tracking branch 'upstream/master' 2017-07-10 22:16:45 +02:00
Slemgrim ec077ed283 Merge branch 'master' of https://github.com/Slemgrim/jsonapi 2017-07-10 22:12:10 +02:00
Slemgrim e13a19922d extract type handling 2017-07-10 22:09:01 +02:00
18 changed files with 1286 additions and 574 deletions

5
.gitignore vendored
View File

@ -1,4 +1 @@
/bin/
/pkg/
/src/
/.idea
/examples/examples

View File

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

View File

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

View File

@ -9,6 +9,7 @@ const (
annotationRelation = "relation"
annotationOmitEmpty = "omitempty"
annotationISO8601 = "iso8601"
annotationRFC3339 = "rfc3339"
annotationSeperator = ","
iso8601TimeFormat = "2006-01-02T15:04:05Z"

4
doc.go
View File

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

View File

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

View File

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

View File

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

View File

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

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/google/jsonapi
go 1.21

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
#!/bin/bash
set -e
go run examples/app.go examples/handler.go examples/fixtures.go examples/models.go

View File

@ -1,4 +0,0 @@
#!/bin/bash
set -e
go test ./... "$@"