pax_global_header00006660000000000000000000000064140326515350014516gustar00rootroot0000000000000052 comment=1e07b10d47d02b07ccaa2dfc5ceec143bdd81c14 jsonapi-1.0.0/000077500000000000000000000000001403265153500131575ustar00rootroot00000000000000jsonapi-1.0.0/.gitignore000066400000000000000000000000231403265153500151420ustar00rootroot00000000000000/examples/examples jsonapi-1.0.0/.travis.yml000066400000000000000000000002201403265153500152620ustar00rootroot00000000000000language: go arch: - amd64 - ppc64le go: - 1.11.x - 1.12.x - 1.13.x - 1.14.x - 1.15.x - 1.16.x - tip script: go test ./... -v jsonapi-1.0.0/LICENSE000066400000000000000000000020661403265153500141700ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Google Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. jsonapi-1.0.0/README.md000066400000000000000000000350471403265153500144470ustar00rootroot00000000000000# 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 ``` go get -u github.com/google/jsonapi ``` Or, see [Alternative Installation](#alternative-installation). ## Background You are working in your Go web application and you have a struct that is organized similarly to your database schema. You need to send and receive json payloads that adhere to the JSON API spec. Once you realize that your json needed to take on this special form, you go down the path of creating more structs to be able to serialize and deserialize JSON API payloads. Then there are more models required with this additional structure. Ugh! With JSON API, you can keep your model structs as is and use [StructTags](http://golang.org/pkg/reflect/#StructTag) to indicate to JSON API how you want your response built or your request deserialized. What about your relationships? JSON API supports relationships out of the box and will even put them in your response into an `included` side-loaded slice--that contains associated records. ## Introduction JSON API uses [StructField](http://golang.org/pkg/reflect/#StructField) tags to annotate the structs fields that you already have and use in your app and then reads and writes [JSON API](http://jsonapi.org) output based on the instructions you give the library in your JSON API tags. Let's take an example. In your app, you most likely have structs that look similar to these: ```go type Blog struct { ID int `json:"id"` Title string `json:"title"` Posts []*Post `json:"posts"` CurrentPost *Post `json:"current_post"` CurrentPostId int `json:"current_post_id"` CreatedAt time.Time `json:"created_at"` ViewCount int `json:"view_count"` } type Post struct { ID int `json:"id"` BlogID int `json:"blog_id"` Title string `json:"title"` Body string `json:"body"` Comments []*Comment `json:"comments"` } type Comment struct { Id int `json:"id"` PostID int `json:"post_id"` Body string `json:"body"` Likes uint `json:"likes_count,omitempty"` } ``` These structs may or may not resemble the layout of your database. But these are the ones that you want to use right? You wouldn't want to use structs like those that JSON API sends because it is difficult to get at all of your data easily. ## Example App [examples/app.go](https://github.com/google/jsonapi/blob/master/examples/app.go) 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 you that the library has successfully taken your JSON API request and turned it into your struct types. To run, * 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.) * `cd $GOPATH/src/github.com/google/jsonapi/examples` * `go build && ./examples` ## `jsonapi` Tag Reference ### Example The `jsonapi` [StructTags](http://golang.org/pkg/reflect/#StructTag) tells this library how to marshal and unmarshal your structs into JSON API payloads and your JSON API payloads to structs, respectively. Then Use JSON API's Marshal and Unmarshal methods to construct and read your responses and replies. Here's an example of the structs above using JSON API tags: ```go type Blog struct { ID int `jsonapi:"primary,blogs"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts"` CurrentPost *Post `jsonapi:"relation,current_post"` CurrentPostID int `jsonapi:"attr,current_post_id"` CreatedAt time.Time `jsonapi:"attr,created_at"` ViewCount int `jsonapi:"attr,view_count"` } type Post struct { ID int `jsonapi:"primary,posts"` BlogID int `jsonapi:"attr,blog_id"` Title string `jsonapi:"attr,title"` Body string `jsonapi:"attr,body"` Comments []*Comment `jsonapi:"relation,comments"` } type Comment struct { ID int `jsonapi:"primary,comments"` PostID int `jsonapi:"attr,post_id"` Body string `jsonapi:"attr,body"` Likes uint `jsonapi:"attr,likes-count,omitempty"` } ``` ### Permitted Tag Values #### `primary` ``` `jsonapi:"primary,"` ``` This indicates this is the primary key field for this struct type. Tag value arguments are comma separated. The first argument must be, `primary`, and the second must be the name that should appear in the `type`\* field for all data objects that represent this type of model. \* According the [JSON API](http://jsonapi.org) spec, the plural record types are shown in the examples, but not required. #### `attr` ``` `jsonapi:"attr,,"` ``` These fields' values will end up in the `attributes`hash for a record. The first argument must be, `attr`, and the second should be the name for the key to display in the `attributes` hash for that record. The optional third argument is `omitempty` - if it is present the field will not be present in the `"attributes"` if the field's value is equivalent to the field types empty value (ie if the `count` field is of type `int`, `omitempty` will omit the field when `count` has a value of `0`). Lastly, the spec indicates that `attributes` key names should be dasherized for multiple word field names. #### `relation` ``` `jsonapi:"relation,,"` ``` Relations are struct fields that represent a one-to-one or one-to-many relationship with other structs. JSON API will traverse the graph of relationships and marshal or unmarshal records. The first argument must be, `relation`, and the second should be the name of the relationship, used as the key in the `relationships` hash for the record. The optional third argument is `omitempty` - if present will prevent non existent to-one and to-many from being serialized. ## Methods Reference **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 serialized or materialized, What about the rest? ### Create Record Example You can Unmarshal a JSON API payload using [jsonapi.UnmarshalPayload](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload). It reads from an [io.Reader](https://golang.org/pkg/io/#Reader) containing a JSON API payload for one record (but can have related records). Then, it materializes a struct that you created and passed in (using new or &). Again, the method supports single records only, at the top level, in request payloads at the moment. Bulk creates and updates are not supported yet. After saving your record, you can use, [MarshalOnePayload](http://godoc.org/github.com/google/jsonapi#MarshalOnePayload), to write the JSON API response to an [io.Writer](https://golang.org/pkg/io/#Writer). #### `UnmarshalPayload` ```go UnmarshalPayload(in io.Reader, model interface{}) ``` Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload) #### `MarshalPayload` ```go MarshalPayload(w io.Writer, models interface{}) error ``` Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalPayload) Writes a JSON API response, with related records sideloaded, into an `included` array. This method encodes a response for either a single record or many records. ##### Handler Example Code ```go func CreateBlog(w http.ResponseWriter, r *http.Request) { blog := new(Blog) if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // ...save your blog... w.Header().Set("Content-Type", jsonapi.MediaType) w.WriteHeader(http.StatusCreated) if err := jsonapi.MarshalPayload(w, blog); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } ``` ### Create Records Example #### `UnmarshalManyPayload` ```go UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) ``` Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalManyPayload) Takes an `io.Reader` and a `reflect.Type` representing the uniform type contained within the `"data"` JSON API member. ##### Handler Example Code ```go func CreateBlogs(w http.ResponseWriter, r *http.Request) { // ...create many blogs at once blogs, err := UnmarshalManyPayload(r.Body, reflect.TypeOf(new(Blog))) if err != nil { t.Fatal(err) } for _, blog := range blogs { b, ok := blog.(*Blog) // ...save each of your blogs } w.Header().Set("Content-Type", jsonapi.MediaType) w.WriteHeader(http.StatusCreated) if err := jsonapi.MarshalPayload(w, blogs); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } ``` ### Links If you need to include [link objects](http://jsonapi.org/format/#document-links) along with response data, implement the `Linkable` interface for document-links, and `RelationshipLinkable` for relationship links: ```go func (post Post) JSONAPILinks() *Links { return &Links{ "self": "href": fmt.Sprintf("https://example.com/posts/%d", post.ID), "comments": Link{ Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", post.ID), Meta: map[string]interface{}{ "counts": map[string]uint{ "likes": 4, }, }, }, } } // Invoked for each relationship defined on the Post struct when marshaled func (post Post) JSONAPIRelationshipLinks(relation string) *Links { if relation == "comments" { return &Links{ "related": fmt.Sprintf("https://example.com/posts/%d/comments", post.ID), } } return nil } ``` ### Meta If you need to include [meta objects](http://jsonapi.org/format/#document-meta) along with response data, implement the `Metable` interface for document-meta, and `RelationshipMetable` for relationship meta: ```go func (post Post) JSONAPIMeta() *Meta { return &Meta{ "details": "sample details here", } } // Invoked for each relationship defined on the Post struct when marshaled func (post Post) JSONAPIRelationshipMeta(relation string) *Meta { if relation == "comments" { return &Meta{ "this": map[string]interface{}{ "can": map[string]interface{}{ "go": []interface{}{ "as", "deep", map[string]interface{}{ "as": "required", }, }, }, }, } } return nil } ``` ### 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. #### `MarshalErrors` ```go MarshalErrors(w io.Writer, errs []*ErrorObject) error ``` Writes a JSON API response using the given `[]error`. #### `ErrorsPayload` ```go type ErrorsPayload struct { Errors []*ErrorObject `json:"errors"` } ``` ErrorsPayload is a serializer struct for representing a valid JSON API errors payload. #### `ErrorObject` ```go type ErrorObject struct { ... } // Error implements the `Error` interface. func (e *ErrorObject) Error() string { return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail) } ``` ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. The main idea behind this struct is that you can use it directly in your code as an error type and pass it directly to `MarshalErrors` to get a valid JSON API errors payload. ##### Errors Example Code ```go // An error has come up in your code, so set an appropriate status, and serialize the error. if err := validate(&myStructToValidate); err != nil { context.SetStatusCode(http.StatusBadRequest) // Or however you need to set a status. jsonapi.MarshalErrors(w, []*ErrorObject{{ Title: "Validation Error", Detail: "Given request body was invalid.", Status: "400", Meta: map[string]interface{}{"field": "some_field", "error": "bad type", "expected": "string", "received": "float64"}, }}) return } ``` ## Testing ### `MarshalOnePayloadEmbedded` ```go MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error ``` Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalOnePayloadEmbedded) This method is not strictly meant to for use in implementation code, although feel free. It was mainly created for use in tests; in most cases, your request payloads for create will be embedded rather than sideloaded for related records. This method will serialize a single struct pointer into an embedded json response. In other words, there will be no, `included`, array in the json; all relationships will be serialized inline with the data. However, in tests, you may want to construct payloads to post to create methods that are embedded to most closely model the payloads that will be produced by the client. This method aims to enable that. ### Example ```go out := bytes.NewBuffer(nil) // testModel returns a pointer to a Blog jsonapi.MarshalOnePayloadEmbedded(out, testModel()) h := new(BlogsHandler) w := httptest.NewRecorder() r, _ := http.NewRequest(http.MethodPost, "/blogs", out) h.CreateBlog(w, r) blog := new(Blog) jsonapi.UnmarshalPayload(w.Body, blog) // ... assert stuff about blog here ... ``` ## Alternative Installation I use git subtrees to manage dependencies rather than `go get` so that the src is committed to my repo. ``` git subtree add --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master ``` To update, ``` git subtree pull --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master ``` This assumes that I have my repo structured with a `src` dir containing a collection of packages and `GOPATH` is set to the root folder--containing `src`. ## Contributing Fork, Change, Pull Request *with tests*. jsonapi-1.0.0/constants.go000066400000000000000000000037301403265153500155250ustar00rootroot00000000000000package jsonapi const ( // StructTag annotation strings annotationJSONAPI = "jsonapi" annotationPrimary = "primary" annotationClientID = "client-id" annotationAttribute = "attr" annotationRelation = "relation" annotationOmitEmpty = "omitempty" annotationISO8601 = "iso8601" annotationRFC3339 = "rfc3339" annotationSeperator = "," iso8601TimeFormat = "2006-01-02T15:04:05Z" // MediaType is the identifier for the JSON API media type // // see http://jsonapi.org/format/#document-structure MediaType = "application/vnd.api+json" // Pagination Constants // // http://jsonapi.org/format/#fetching-pagination // KeyFirstPage is the key to the links object whose value contains a link to // the first page of data KeyFirstPage = "first" // KeyLastPage is the key to the links object whose value contains a link to // the last page of data KeyLastPage = "last" // KeyPreviousPage is the key to the links object whose value contains a link // to the previous page of data KeyPreviousPage = "prev" // KeyNextPage is the key to the links object whose value contains a link to // the next page of data KeyNextPage = "next" // QueryParamPageNumber is a JSON API query parameter used in a page based // pagination strategy in conjunction with QueryParamPageSize QueryParamPageNumber = "page[number]" // QueryParamPageSize is a JSON API query parameter used in a page based // pagination strategy in conjunction with QueryParamPageNumber QueryParamPageSize = "page[size]" // QueryParamPageOffset is a JSON API query parameter used in an offset based // pagination strategy in conjunction with QueryParamPageLimit QueryParamPageOffset = "page[offset]" // QueryParamPageLimit is a JSON API query parameter used in an offset based // pagination strategy in conjunction with QueryParamPageOffset QueryParamPageLimit = "page[limit]" // QueryParamPageCursor is a JSON API query parameter used with a cursor-based // strategy QueryParamPageCursor = "page[cursor]" ) jsonapi-1.0.0/doc.go000066400000000000000000000055271403265153500142640ustar00rootroot00000000000000/* 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 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. jsonapi uses StructField tags to annotate the structs fields that you already have and use in your app and then reads and writes jsonapi.org output based on the instructions you give the library in your jsonapi tags. Example structs using a Blog > Post > Comment structure, type Blog struct { ID int `jsonapi:"primary,blogs"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts"` CurrentPost *Post `jsonapi:"relation,current_post"` CurrentPostID int `jsonapi:"attr,current_post_id"` CreatedAt time.Time `jsonapi:"attr,created_at"` ViewCount int `jsonapi:"attr,view_count"` } type Post struct { ID int `jsonapi:"primary,posts"` BlogID int `jsonapi:"attr,blog_id"` Title string `jsonapi:"attr,title"` Body string `jsonapi:"attr,body"` Comments []*Comment `jsonapi:"relation,comments"` } type Comment struct { ID int `jsonapi:"primary,comments"` PostID int `jsonapi:"attr,post_id"` Body string `jsonapi:"attr,body"` } jsonapi Tag Reference Value, primary: "primary," This indicates that this is the primary key field for this struct type. Tag value arguments are comma separated. The first argument must be, "primary", and the second must be the name that should appear in the "type" field for all data objects that represent this type of model. Value, attr: "attr,[,]" 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 "attributes" hash for that record. The following extra arguments are also supported: "omitempty": excludes the fields value from the "attribute" hash. "iso8601": uses the ISO8601 timestamp format when serialising or deserialising the time.Time value. Value, relation: "relation," Relations are struct fields that represent a one-to-one or one-to-many to other structs. jsonapi will traverse the graph of relationships and marshal or unmarshal records. The first argument must be, "relation", and the second should be the name of the relationship, used as the key in the "relationships" hash for the record. Use the methods below to Marshal and Unmarshal jsonapi.org json payloads. Visit the readme at https://github.com/google/jsonapi */ package jsonapi jsonapi-1.0.0/errors.go000066400000000000000000000041251403265153500150240ustar00rootroot00000000000000package jsonapi import ( "encoding/json" "fmt" "io" ) // MarshalErrors writes a JSON API response using the given `[]error`. // // For more information on JSON API error payloads, see the spec here: // http://jsonapi.org/format/#document-top-level // and here: http://jsonapi.org/format/#error-objects. func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error { return json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects}) } // ErrorsPayload is a serializer struct for representing a valid JSON API errors payload. type ErrorsPayload struct { Errors []*ErrorObject `json:"errors"` } // ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. // // The main idea behind this struct is that you can use it directly in your code as an error type // and pass it directly to `MarshalErrors` to get a valid JSON API errors payload. // For more information on Golang errors, see: https://golang.org/pkg/errors/ // For more information on the JSON API spec's error objects, see: http://jsonapi.org/format/#error-objects type ErrorObject struct { // ID is a unique identifier for this particular occurrence of a problem. ID string `json:"id,omitempty"` // Title is a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization. Title string `json:"title,omitempty"` // Detail is a human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized. Detail string `json:"detail,omitempty"` // Status is the HTTP status code applicable to this problem, expressed as a string value. Status string `json:"status,omitempty"` // Code is an application-specific error code, expressed as a string value. Code string `json:"code,omitempty"` // Meta is an object containing non-standard meta-information about the error. Meta *map[string]interface{} `json:"meta,omitempty"` } // Error implements the `Error` interface. func (e *ErrorObject) Error() string { return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail) } jsonapi-1.0.0/errors_test.go000066400000000000000000000032251403265153500160630ustar00rootroot00000000000000package jsonapi import ( "bytes" "encoding/json" "fmt" "io" "reflect" "testing" ) func TestErrorObjectWritesExpectedErrorMessage(t *testing.T) { err := &ErrorObject{Title: "Title test.", Detail: "Detail test."} var input error = err output := input.Error() if output != fmt.Sprintf("Error: %s %s\n", err.Title, err.Detail) { t.Fatal("Unexpected output.") } } func TestMarshalErrorsWritesTheExpectedPayload(t *testing.T) { var marshalErrorsTableTasts = []struct { Title string In []*ErrorObject Out map[string]interface{} }{ { Title: "TestFieldsAreSerializedAsNeeded", In: []*ErrorObject{{ID: "0", Title: "Test title.", Detail: "Test detail", Status: "400", Code: "E1100"}}, Out: map[string]interface{}{"errors": []interface{}{ map[string]interface{}{"id": "0", "title": "Test title.", "detail": "Test detail", "status": "400", "code": "E1100"}, }}, }, { Title: "TestMetaFieldIsSerializedProperly", In: []*ErrorObject{{Title: "Test title.", Detail: "Test detail", Meta: &map[string]interface{}{"key": "val"}}}, Out: map[string]interface{}{"errors": []interface{}{ map[string]interface{}{"title": "Test title.", "detail": "Test detail", "meta": map[string]interface{}{"key": "val"}}, }}, }, } for _, testRow := range marshalErrorsTableTasts { t.Run(testRow.Title, func(t *testing.T) { buffer, output := bytes.NewBuffer(nil), map[string]interface{}{} var writer io.Writer = buffer _ = MarshalErrors(writer, testRow.In) json.Unmarshal(buffer.Bytes(), &output) if !reflect.DeepEqual(output, testRow.Out) { t.Fatalf("Expected: \n%#v \nto equal: \n%#v", output, testRow.Out) } }) } } jsonapi-1.0.0/examples/000077500000000000000000000000001403265153500147755ustar00rootroot00000000000000jsonapi-1.0.0/examples/app.go000066400000000000000000000073551403265153500161160ustar00rootroot00000000000000package main import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "time" "github.com/google/jsonapi" ) func main() { jsonapi.Instrumentation = func(r *jsonapi.Runtime, eventType jsonapi.Event, callGUID string, dur time.Duration) { metricPrefix := r.Value("instrument").(string) if eventType == jsonapi.UnmarshalStart { fmt.Printf("%s: id, %s, started at %v\n", metricPrefix+".jsonapi_unmarshal_time", callGUID, time.Now()) } if eventType == jsonapi.UnmarshalStop { fmt.Printf("%s: id, %s, stopped at, %v , and took %v to unmarshal payload\n", metricPrefix+".jsonapi_unmarshal_time", callGUID, time.Now(), dur) } if eventType == jsonapi.MarshalStart { fmt.Printf("%s: id, %s, started at %v\n", metricPrefix+".jsonapi_marshal_time", callGUID, time.Now()) } if eventType == jsonapi.MarshalStop { fmt.Printf("%s: id, %s, stopped at, %v , and took %v to marshal payload\n", metricPrefix+".jsonapi_marshal_time", callGUID, time.Now(), dur) } } exampleHandler := &ExampleHandler{} http.HandleFunc("/blogs", exampleHandler.ServeHTTP) exerciseHandler() } func exerciseHandler() { // list req, _ := http.NewRequest(http.MethodGet, "/blogs", nil) req.Header.Set(headerAccept, jsonapi.MediaType) w := httptest.NewRecorder() fmt.Println("============ start list ===========") http.DefaultServeMux.ServeHTTP(w, req) fmt.Println("============ stop list ===========") jsonReply, _ := ioutil.ReadAll(w.Body) fmt.Println("============ jsonapi response from list ===========") fmt.Println(string(jsonReply)) fmt.Println("============== end raw jsonapi from list =============") // show req, _ = http.NewRequest(http.MethodGet, "/blogs?id=1", nil) req.Header.Set(headerAccept, jsonapi.MediaType) w = httptest.NewRecorder() fmt.Println("============ start show ===========") http.DefaultServeMux.ServeHTTP(w, req) fmt.Println("============ stop show ===========") jsonReply, _ = ioutil.ReadAll(w.Body) fmt.Println("============ jsonapi response from show ===========") fmt.Println(string(jsonReply)) fmt.Println("============== end raw jsonapi from show =============") // create blog := fixtureBlogCreate(1) in := bytes.NewBuffer(nil) jsonapi.MarshalOnePayloadEmbedded(in, blog) req, _ = http.NewRequest(http.MethodPost, "/blogs", in) req.Header.Set(headerAccept, jsonapi.MediaType) w = httptest.NewRecorder() fmt.Println("============ start create ===========") http.DefaultServeMux.ServeHTTP(w, req) fmt.Println("============ stop create ===========") buf := bytes.NewBuffer(nil) io.Copy(buf, w.Body) fmt.Println("============ jsonapi response from create ===========") fmt.Println(buf.String()) fmt.Println("============== end raw jsonapi response =============") // echo blogs := []interface{}{ fixtureBlogCreate(1), fixtureBlogCreate(2), fixtureBlogCreate(3), } in = bytes.NewBuffer(nil) jsonapi.MarshalPayload(in, blogs) req, _ = http.NewRequest(http.MethodPut, "/blogs", in) req.Header.Set(headerAccept, jsonapi.MediaType) w = httptest.NewRecorder() fmt.Println("============ start echo ===========") http.DefaultServeMux.ServeHTTP(w, req) fmt.Println("============ stop echo ===========") buf = bytes.NewBuffer(nil) io.Copy(buf, w.Body) fmt.Println("============ jsonapi response from create ===========") fmt.Println(buf.String()) fmt.Println("============== end raw jsonapi response =============") responseBlog := new(Blog) jsonapi.UnmarshalPayload(buf, responseBlog) out := bytes.NewBuffer(nil) json.NewEncoder(out).Encode(responseBlog) fmt.Println("================ Viola! Converted back our Blog struct =================") fmt.Println(string(out.Bytes())) fmt.Println("================ end marshal materialized Blog struct =================") } jsonapi-1.0.0/examples/fixtures.go000066400000000000000000000016531403265153500172020ustar00rootroot00000000000000package main import "time" func fixtureBlogCreate(i int) *Blog { return &Blog{ ID: 1 * i, Title: "Title 1", CreatedAt: time.Now(), Posts: []*Post{ { ID: 1 * i, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1 * i, Body: "foo", }, { ID: 2 * i, Body: "bar", }, }, }, { ID: 2 * i, Title: "Fuubar", Body: "Bas", Comments: []*Comment{ { ID: 1 * i, Body: "foo", }, { ID: 3 * i, Body: "bas", }, }, }, }, CurrentPost: &Post{ ID: 1 * i, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1 * i, Body: "foo", }, { ID: 2 * i, Body: "bar", }, }, }, } } func fixtureBlogsList() (blogs []interface{}) { for i := 0; i < 10; i++ { blogs = append(blogs, fixtureBlogCreate(i)) } return blogs } jsonapi-1.0.0/examples/handler.go000066400000000000000000000056301403265153500167450ustar00rootroot00000000000000package main import ( "net/http" "strconv" "github.com/google/jsonapi" ) const ( headerAccept = "Accept" headerContentType = "Content-Type" ) // ExampleHandler is the handler we are using to demonstrate building an HTTP // server with the jsonapi library. type ExampleHandler struct{} func (h *ExampleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Header.Get(headerAccept) != jsonapi.MediaType { http.Error(w, "Unsupported Media Type", http.StatusUnsupportedMediaType) } var methodHandler http.HandlerFunc switch r.Method { case http.MethodPost: methodHandler = h.createBlog case http.MethodPut: methodHandler = h.echoBlogs case http.MethodGet: if r.FormValue("id") != "" { methodHandler = h.showBlog } else { methodHandler = h.listBlogs } default: http.Error(w, "Not Found", http.StatusNotFound) return } methodHandler(w, r) } func (h *ExampleHandler) createBlog(w http.ResponseWriter, r *http.Request) { jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.create") blog := new(Blog) if err := jsonapiRuntime.UnmarshalPayload(r.Body, blog); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // ...do stuff with your blog... w.WriteHeader(http.StatusCreated) w.Header().Set(headerContentType, jsonapi.MediaType) if err := jsonapiRuntime.MarshalPayload(w, blog); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *ExampleHandler) echoBlogs(w http.ResponseWriter, r *http.Request) { jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list") // ...fetch your blogs, filter, offset, limit, etc... // but, for now blogs := fixtureBlogsList() w.WriteHeader(http.StatusOK) w.Header().Set(headerContentType, jsonapi.MediaType) if err := jsonapiRuntime.MarshalPayload(w, blogs); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *ExampleHandler) showBlog(w http.ResponseWriter, r *http.Request) { id := r.FormValue("id") // ...fetch your blog... intID, err := strconv.Atoi(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.show") // but, for now blog := fixtureBlogCreate(intID) w.WriteHeader(http.StatusOK) w.Header().Set(headerContentType, jsonapi.MediaType) if err := jsonapiRuntime.MarshalPayload(w, blog); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *ExampleHandler) listBlogs(w http.ResponseWriter, r *http.Request) { jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list") // ...fetch your blogs, filter, offset, limit, etc... // but, for now blogs := fixtureBlogsList() w.Header().Set("Content-Type", jsonapi.MediaType) w.WriteHeader(http.StatusOK) if err := jsonapiRuntime.MarshalPayload(w, blogs); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } jsonapi-1.0.0/examples/handler_test.go000066400000000000000000000052631403265153500200060ustar00rootroot00000000000000package main import ( "bytes" "net/http" "net/http/httptest" "testing" "github.com/google/jsonapi" ) func TestExampleHandler_post(t *testing.T) { blog := fixtureBlogCreate(1) requestBody := bytes.NewBuffer(nil) jsonapi.MarshalOnePayloadEmbedded(requestBody, blog) r, err := http.NewRequest(http.MethodPost, "/blogs?id=1", requestBody) if err != nil { t.Fatal(err) } r.Header.Set(headerAccept, jsonapi.MediaType) rr := httptest.NewRecorder() handler := &ExampleHandler{} handler.ServeHTTP(rr, r) if e, a := http.StatusCreated, rr.Code; e != a { t.Fatalf("Expected a status of %d, got %d", e, a) } } func TestExampleHandler_put(t *testing.T) { blogs := []interface{}{ fixtureBlogCreate(1), fixtureBlogCreate(2), fixtureBlogCreate(3), } requestBody := bytes.NewBuffer(nil) jsonapi.MarshalPayload(requestBody, blogs) r, err := http.NewRequest(http.MethodPut, "/blogs", requestBody) if err != nil { t.Fatal(err) } r.Header.Set(headerAccept, jsonapi.MediaType) rr := httptest.NewRecorder() handler := &ExampleHandler{} handler.ServeHTTP(rr, r) if e, a := http.StatusOK, rr.Code; e != a { t.Fatalf("Expected a status of %d, got %d", e, a) } } func TestExampleHandler_get_show(t *testing.T) { r, err := http.NewRequest(http.MethodGet, "/blogs?id=1", nil) if err != nil { t.Fatal(err) } r.Header.Set(headerAccept, jsonapi.MediaType) rr := httptest.NewRecorder() handler := &ExampleHandler{} handler.ServeHTTP(rr, r) if e, a := http.StatusOK, rr.Code; e != a { t.Fatalf("Expected a status of %d, got %d", e, a) } } func TestExampleHandler_get_list(t *testing.T) { r, err := http.NewRequest(http.MethodGet, "/blogs", nil) if err != nil { t.Fatal(err) } r.Header.Set(headerAccept, jsonapi.MediaType) rr := httptest.NewRecorder() handler := &ExampleHandler{} handler.ServeHTTP(rr, r) if e, a := http.StatusOK, rr.Code; e != a { 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") } } jsonapi-1.0.0/examples/models.go000066400000000000000000000042751403265153500166170ustar00rootroot00000000000000package main import ( "fmt" "time" "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"` Posts []*Post `jsonapi:"relation,posts"` CurrentPost *Post `jsonapi:"relation,current_post"` CurrentPostID int `jsonapi:"attr,current_post_id"` CreatedAt time.Time `jsonapi:"attr,created_at"` 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"` Title string `jsonapi:"attr,title"` Body string `jsonapi:"attr,body"` 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"` } // 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{ "related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID), } } if relation == "current_post" { return &jsonapi.Links{ "related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID), } } return nil } // 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{ "detail": "posts meta information", } } if relation == "current_post" { return &jsonapi.Meta{ "detail": "current post meta information", } } return nil } jsonapi-1.0.0/go.mod000066400000000000000000000000401403265153500142570ustar00rootroot00000000000000module github.com/google/jsonapijsonapi-1.0.0/models_test.go000066400000000000000000000123031403265153500160270ustar00rootroot00000000000000package jsonapi import ( "fmt" "time" ) type BadModel struct { ID int `jsonapi:"primary"` } type ModelBadTypes struct { ID string `jsonapi:"primary,badtypes"` StringField string `jsonapi:"attr,string_field"` FloatField float64 `jsonapi:"attr,float_field"` TimeField time.Time `jsonapi:"attr,time_field"` TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"` } type WithPointer struct { ID *uint64 `jsonapi:"primary,with-pointers"` Name *string `jsonapi:"attr,name"` IsActive *bool `jsonapi:"attr,is-active"` IntVal *int `jsonapi:"attr,int-val"` FloatVal *float32 `jsonapi:"attr,float-val"` } 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 { ID *string `jsonapi:"primary,cars"` Make *string `jsonapi:"attr,make,omitempty"` Model *string `jsonapi:"attr,model,omitempty"` Year *uint `jsonapi:"attr,year,omitempty"` } type Post struct { Blog ID uint64 `jsonapi:"primary,posts"` BlogID int `jsonapi:"attr,blog_id"` ClientID string `jsonapi:"client-id"` Title string `jsonapi:"attr,title"` Body string `jsonapi:"attr,body"` Comments []*Comment `jsonapi:"relation,comments"` LatestComment *Comment `jsonapi:"relation,latest_comment"` } type Comment struct { ID int `jsonapi:"primary,comments"` ClientID string `jsonapi:"client-id"` PostID int `jsonapi:"attr,post_id"` Body string `jsonapi:"attr,body"` } type Book struct { ID uint64 `jsonapi:"primary,books"` Author string `jsonapi:"attr,author"` ISBN string `jsonapi:"attr,isbn"` Title string `jsonapi:"attr,title,omitempty"` Description *string `jsonapi:"attr,description"` Pages *uint `jsonapi:"attr,pages,omitempty"` PublishedAt time.Time Tags []string `jsonapi:"attr,tags"` } type Blog struct { ID int `jsonapi:"primary,blogs"` ClientID string `jsonapi:"client-id"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts"` CurrentPost *Post `jsonapi:"relation,current_post"` CurrentPostID int `jsonapi:"attr,current_post_id"` CreatedAt time.Time `jsonapi:"attr,created_at"` ViewCount int `jsonapi:"attr,view_count"` } func (b *Blog) JSONAPILinks() *Links { return &Links{ "self": fmt.Sprintf("https://example.com/api/blogs/%d", b.ID), "comments": Link{ Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", b.ID), Meta: Meta{ "counts": map[string]uint{ "likes": 4, "comments": 20, }, }, }, } } func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links { if relation == "posts" { return &Links{ "related": Link{ Href: fmt.Sprintf("https://example.com/api/blogs/%d/posts", b.ID), Meta: Meta{ "count": len(b.Posts), }, }, } } if relation == "current_post" { return &Links{ "self": fmt.Sprintf("https://example.com/api/posts/%s", "3"), "related": Link{ Href: fmt.Sprintf("https://example.com/api/blogs/%d/current_post", b.ID), }, } } return nil } func (b *Blog) JSONAPIMeta() *Meta { return &Meta{ "detail": "extra details regarding the blog", } } func (b *Blog) JSONAPIRelationshipMeta(relation string) *Meta { if relation == "posts" { return &Meta{ "this": map[string]interface{}{ "can": map[string]interface{}{ "go": []interface{}{ "as", "deep", map[string]interface{}{ "as": "required", }, }, }, }, } } if relation == "current_post" { return &Meta{ "detail": "extra current_post detail", } } return nil } type BadComment struct { ID uint64 `jsonapi:"primary,bad-comment"` Body string `jsonapi:"attr,body"` } func (bc *BadComment) JSONAPILinks() *Links { return &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"` } jsonapi-1.0.0/node.go000066400000000000000000000074461403265153500144460ustar00rootroot00000000000000package jsonapi import "fmt" // Payloader is used to encapsulate the One and Many payload types type Payloader interface { clearIncluded() } // OnePayload is used to represent a generic JSON API payload where a single // resource (Node) was included as an {} in the "data" key type OnePayload struct { Data *Node `json:"data"` Included []*Node `json:"included,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } func (p *OnePayload) clearIncluded() { p.Included = []*Node{} } // ManyPayload is used to represent a generic JSON API payload where many // resources (Nodes) were included in an [] in the "data" key type ManyPayload struct { Data []*Node `json:"data"` Included []*Node `json:"included,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } func (p *ManyPayload) clearIncluded() { p.Included = []*Node{} } // Node is used to represent a generic JSON API Resource type Node struct { Type string `json:"type"` ID string `json:"id,omitempty"` ClientID string `json:"client-id,omitempty"` Attributes map[string]interface{} `json:"attributes,omitempty"` Relationships map[string]interface{} `json:"relationships,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } // RelationshipOneNode is used to represent a generic has one JSON API relation type RelationshipOneNode struct { Data *Node `json:"data"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } // RelationshipManyNode is used to represent a generic has many JSON API // relation type RelationshipManyNode struct { Data []*Node `json:"data"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } // Links is used to represent a `links` object. // http://jsonapi.org/format/#document-links type Links map[string]interface{} func (l *Links) validate() (err error) { // Each member of a links object is a “link”. A link MUST be represented as // either: // - a string containing the link’s URL. // - an object (“link object”) which can contain the following members: // - href: a string containing the link’s URL. // - meta: a meta object containing non-standard meta-information about the // link. for k, v := range *l { _, isString := v.(string) _, isLink := v.(Link) if !(isString || isLink) { return fmt.Errorf( "The %s member of the links object was not a string or link object", k, ) } } return } // Link is used to represent a member of the `links` object. type Link struct { Href string `json:"href"` Meta Meta `json:"meta,omitempty"` } // Linkable is used to include document links in response data // e.g. {"self": "http://example.com/posts/1"} type Linkable interface { JSONAPILinks() *Links } // RelationshipLinkable is used to include relationship links in response data // e.g. {"related": "http://example.com/posts/1/comments"} type RelationshipLinkable interface { // JSONAPIRelationshipLinks will be invoked for each relationship with the corresponding relation name (e.g. `comments`) JSONAPIRelationshipLinks(relation string) *Links } // Meta is used to represent a `meta` object. // http://jsonapi.org/format/#document-meta type Meta map[string]interface{} // Metable is used to include document meta in response data // e.g. {"foo": "bar"} type Metable interface { JSONAPIMeta() *Meta } // RelationshipMetable is used to include relationship meta in response data type RelationshipMetable interface { // JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`) JSONAPIRelationshipMeta(relation string) *Meta } jsonapi-1.0.0/request.go000066400000000000000000000413131403265153500152000ustar00rootroot00000000000000package jsonapi import ( "bytes" "encoding/json" "errors" "fmt" "io" "reflect" "strconv" "strings" "time" ) const ( unsupportedStructTagMsg = "Unsupported jsonapi tag annotation, %s" ) var ( // ErrInvalidTime is returned when a struct has a time.Time type field, but // the JSON value was not a unix timestamp integer. ErrInvalidTime = errors.New("Only numbers can be parsed as dates, unix timestamps") // 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") // 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. // // Will Unmarshal embedded and sideloaded payloads. The latter is only possible if the // object graph is complete. That is, in the "relationships" data there are type and id, // keys that correspond to records in the "included" array. // // For example you could pass it, in, req.Body and, model, a BlogPost // struct instance to populate in an http handler, // // func CreateBlog(w http.ResponseWriter, r *http.Request) { // blog := new(Blog) // // if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil { // http.Error(w, err.Error(), 500) // return // } // // // ...do stuff with your blog... // // w.Header().Set("Content-Type", jsonapi.MediaType) // w.WriteHeader(201) // // if err := jsonapi.MarshalPayload(w, blog); err != nil { // http.Error(w, err.Error(), 500) // } // } // // // Visit https://github.com/google/jsonapi#create for more info. // // model interface{} should be a pointer to a struct. func UnmarshalPayload(in io.Reader, model interface{}) error { payload := new(OnePayload) if err := json.NewDecoder(in).Decode(payload); err != nil { return err } if payload.Included != nil { includedMap := make(map[string]*Node) for _, included := range payload.Included { key := fmt.Sprintf("%s,%s", included.Type, included.ID) includedMap[key] = included } return unmarshalNode(payload.Data, reflect.ValueOf(model), &includedMap) } return unmarshalNode(payload.Data, reflect.ValueOf(model), nil) } // 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) { payload := new(ManyPayload) if err := json.NewDecoder(in).Decode(payload); err != nil { return nil, err } models := []interface{}{} // will be populated from the "data" includedMap := map[string]*Node{} // will be populate from the "included" if payload.Included != nil { for _, included := range payload.Included { key := fmt.Sprintf("%s,%s", included.Type, included.ID) includedMap[key] = included } } for _, data := range payload.Data { model := reflect.New(t.Elem()) err := unmarshalNode(data, model, &includedMap) if err != nil { return nil, err } models = append(models, model.Interface()) } return models, nil } func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("data is not a jsonapi representation of '%v'", model.Type()) } }() modelValue := model.Elem() modelType := modelValue.Type() var er error for i := 0; i < modelValue.NumField(); i++ { fieldType := modelType.Field(i) tag := fieldType.Tag.Get("jsonapi") if tag == "" { continue } fieldValue := modelValue.Field(i) args := strings.Split(tag, ",") if len(args) < 1 { er = ErrBadJSONAPIStructTag break } annotation := args[0] if (annotation == annotationClientID && len(args) != 1) || (annotation != annotationClientID && len(args) < 2) { er = ErrBadJSONAPIStructTag break } if annotation == annotationPrimary { // Check the JSON API Type if data.Type != args[1] { er = fmt.Errorf( "Trying to Unmarshal an object of type %#v, but %#v does not match", data.Type, args[1], ) break } if data.ID == "" { continue } // ID will have to be transmitted as astring per the JSON API spec v := reflect.ValueOf(data.ID) // Deal with PTRS var kind reflect.Kind if fieldValue.Kind() == reflect.Ptr { kind = fieldType.Type.Elem().Kind() } else { kind = fieldType.Type.Kind() } // Handle String case if kind == reflect.String { assign(fieldValue, v) continue } // Value was not a string... only other supported type was a numeric, // which would have been sent as a float value. floatValue, err := strconv.ParseFloat(data.ID, 64) if err != nil { // Could not convert the value in the "id" attr to a float er = ErrBadJSONAPIID break } // Convert the numeric float to one of the supported ID numeric types // (int[8,16,32,64] or uint[8,16,32,64]) 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 break } assign(fieldValue, idValue) } else if annotation == annotationClientID { if data.ClientID == "" { continue } fieldValue.Set(reflect.ValueOf(data.ClientID)) } else if annotation == annotationAttribute { attributes := data.Attributes if attributes == nil || len(data.Attributes) == 0 { continue } attribute := attributes[args[1]] // continue if the attribute was not included in the request if attribute == nil { continue } structField := fieldType value, err := unmarshalAttribute(attribute, args, structField, fieldValue) if err != nil { er = err break } assign(fieldValue, value) } else if annotation == annotationRelation { isSlice := fieldValue.Type().Kind() == reflect.Slice if data.Relationships == nil || data.Relationships[args[1]] == nil { continue } if isSlice { // to-many relationship relationship := new(RelationshipManyNode) buf := bytes.NewBuffer(nil) json.NewEncoder(buf).Encode(data.Relationships[args[1]]) json.NewDecoder(buf).Decode(relationship) data := relationship.Data models := reflect.New(fieldValue.Type()).Elem() for _, n := range data { m := reflect.New(fieldValue.Type().Elem().Elem()) if err := unmarshalNode( fullNode(n, included), m, included, ); err != nil { er = err break } models = reflect.Append(models, m) } fieldValue.Set(models) } else { // to-one relationships relationship := new(RelationshipOneNode) buf := bytes.NewBuffer(nil) json.NewEncoder(buf).Encode( data.Relationships[args[1]], ) json.NewDecoder(buf).Decode(relationship) /* http://jsonapi.org/format/#document-resource-object-relationships http://jsonapi.org/format/#document-resource-object-linkage relationship can have a data node set to null (e.g. to disassociate the relationship) so unmarshal and set fieldValue only if data obj is not null */ if relationship.Data == nil { continue } m := reflect.New(fieldValue.Type().Elem()) if err := unmarshalNode( fullNode(relationship.Data, included), m, included, ); err != nil { er = err break } fieldValue.Set(m) } } else { er = fmt.Errorf(unsupportedStructTagMsg, annotation) } } return er } func fullNode(n *Node, included *map[string]*Node) *Node { includedKey := fmt.Sprintf("%s,%s", n.Type, n.ID) if included != nil && (*included)[includedKey] != nil { return (*included)[includedKey] } return n } // assign will take the value specified and assign it to the field; if // field is expecting a ptr assign will assign a ptr. func assign(field, value reflect.Value) { 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) } } 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 } jsonapi-1.0.0/request_test.go000066400000000000000000001015211403265153500162350ustar00rootroot00000000000000package jsonapi import ( "bytes" "encoding/json" "errors" "fmt" "io" "reflect" "sort" "strings" "testing" "time" ) func TestUnmarshall_attrStringSlice(t *testing.T) { out := &Book{} tags := []string{"fiction", "sale"} data := map[string]interface{}{ "data": map[string]interface{}{ "type": "books", "id": "1", "attributes": map[string]interface{}{"tags": tags}, }, } b, err := json.Marshal(data) if err != nil { t.Fatal(err) } if err := UnmarshalPayload(bytes.NewReader(b), out); err != nil { t.Fatal(err) } if e, a := len(tags), len(out.Tags); e != a { t.Fatalf("Was expecting %d tags, got %d", e, a) } sort.Strings(tags) sort.Strings(out.Tags) for i, tag := range tags { if e, a := tag, out.Tags[i]; e != a { t.Fatalf("At index %d, was expecting %s got %s", i, e, a) } } } func TestUnmarshalToStructWithPointerAttr(t *testing.T) { out := new(WithPointer) in := map[string]interface{}{ "name": "The name", "is-active": true, "int-val": 8, "float-val": 1.1, } if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil { t.Fatal(err) } if *out.Name != "The name" { t.Fatalf("Error unmarshalling to string ptr") } if !*out.IsActive { t.Fatalf("Error unmarshalling to bool ptr") } if *out.IntVal != 8 { t.Fatalf("Error unmarshalling to int ptr") } if *out.FloatVal != 1.1 { t.Fatalf("Error unmarshalling to float ptr") } } 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":{"type":"with-pointers"}}`), out); err != nil { t.Fatalf("Error unmarshalling to Foo: %v", err) } if out.ID != nil { t.Fatalf("Error unmarshalling; expected ID ptr to be nil") } } func TestUnmarshalPayloadWithPointerID(t *testing.T) { out := new(WithPointer) attrs := map[string]interface{}{} if err := UnmarshalPayload(sampleWithPointerPayload(attrs), out); err != nil { t.Fatalf("Error unmarshalling to Foo") } // these were present in the payload -- expect val to be not nil if out.ID == nil { t.Fatalf("Error unmarshalling; expected ID ptr to be not nil") } if e, a := uint64(2), *out.ID; e != a { t.Fatalf("Was expecting the ID to have a value of %d, got %d", e, a) } } func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) { out := new(WithPointer) in := map[string]interface{}{ "name": "The name", "is-active": true, } if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil { t.Fatalf("Error unmarshalling to Foo") } // these were present in the payload -- expect val to be not nil if out.Name == nil || out.IsActive == nil { t.Fatalf("Error unmarshalling; expected ptr to be not nil") } // these were absent in the payload -- expect val to be nil if out.IntVal != nil || out.FloatVal != nil { t.Fatalf("Error unmarshalling; expected ptr to be nil") } } func TestUnmarshalToStructWithPointerAttr_BadType_bool(t *testing.T) { out := new(WithPointer) in := map[string]interface{}{ "name": true, // This is the wrong type. } expectedErrorMessage := "jsonapi: Can't unmarshal true (bool) 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_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) { // Build Book payload description := "Hello World!" data := map[string]interface{}{ "data": map[string]interface{}{ "type": "books", "id": "5", "attributes": map[string]interface{}{ "author": "aren55555", "description": description, "isbn": "", }, }, } payload, err := json.Marshal(data) if err != nil { t.Fatal(err) } // Parse JSON API payload book := new(Book) if err := UnmarshalPayload(bytes.NewReader(payload), book); err != nil { t.Fatal(err) } if book.Description == nil { t.Fatal("Was not expecting a nil pointer for book.Description") } if expected, actual := description, *book.Description; expected != actual { t.Fatalf("Was expecting descript to be `%s`, got `%s`", expected, actual) } } func TestMalformedTag(t *testing.T) { out := new(BadModel) err := UnmarshalPayload(samplePayload(), out) if err == nil || err != ErrBadJSONAPIStructTag { t.Fatalf("Did not error out with wrong number of arguments in tag") } } func TestUnmarshalInvalidJSON(t *testing.T) { in := strings.NewReader("{}") out := new(Blog) err := UnmarshalPayload(in, out) if err == nil { t.Fatalf("Did not error out the invalid JSON.") } } func TestUnmarshalInvalidJSON_BadType(t *testing.T) { var badTypeTests = []struct { Field string BadValue interface{} Error error }{ // The `Field` values here correspond to the `ModelBadTypes` jsonapi fields. {Field: "string_field", BadValue: 0, Error: ErrUnknownFieldNumberType}, // Expected string. {Field: "float_field", BadValue: "A string.", Error: ErrInvalidType}, // Expected float64. {Field: "time_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected int64. {Field: "time_ptr_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected *time / int64. } for _, test := range badTypeTests { t.Run(fmt.Sprintf("Test_%s", test.Field), func(t *testing.T) { out := new(ModelBadTypes) in := map[string]interface{}{} in[test.Field] = test.BadValue expectedErrorMessage := test.Error.Error() err := UnmarshalPayload(samplePayloadWithBadTypes(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()) } }) } } func TestUnmarshalSetsID(t *testing.T) { in := samplePayloadWithID() out := new(Blog) if err := UnmarshalPayload(in, out); err != nil { t.Fatal(err) } if out.ID != 2 { t.Fatalf("Did not set ID on dst interface") } } func TestUnmarshal_nonNumericID(t *testing.T) { data := samplePayloadWithoutIncluded() data["data"].(map[string]interface{})["id"] = "non-numeric-id" payload, err := json.Marshal(data) if err != nil { t.Fatal(err) } in := bytes.NewReader(payload) out := new(Post) if err := UnmarshalPayload(in, out); err != ErrBadJSONAPIID { t.Fatalf( "Was expecting a `%s` error, got `%s`", ErrBadJSONAPIID, err, ) } } func TestUnmarshalSetsAttrs(t *testing.T) { out, err := unmarshalSamplePayload() if err != nil { t.Fatal(err) } if out.CreatedAt.IsZero() { t.Fatalf("Did not parse time") } if out.ViewCount != 1000 { t.Fatalf("View count not properly serialized") } } 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 }, }, { 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 }, }, { 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) } 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, err := json.Marshal(samplePayloadWithoutIncluded()) if err != nil { t.Fatal(err) } in := bytes.NewReader(data) out := new(Post) if err := UnmarshalPayload(in, out); err != nil { t.Fatal(err) } // Verify each comment has at least an ID for _, comment := range out.Comments { if comment.ID == 0 { t.Fatalf("The comment did not have an ID") } } } func TestUnmarshalRelationships(t *testing.T) { out, err := unmarshalSamplePayload() if err != nil { t.Fatal(err) } if out.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if out.CurrentPost.Title != "Bas" || out.CurrentPost.Body != "Fuubar" { t.Fatalf("Attributes were not set") } if len(out.Posts) != 2 { t.Fatalf("There should have been 2 posts") } } func TestUnmarshalNullRelationship(t *testing.T) { sample := map[string]interface{}{ "data": map[string]interface{}{ "type": "posts", "id": "1", "attributes": map[string]interface{}{ "body": "Hello", "title": "World", }, "relationships": map[string]interface{}{ "latest_comment": map[string]interface{}{ "data": nil, // empty to-one relationship }, }, }, } data, err := json.Marshal(sample) if err != nil { t.Fatal(err) } in := bytes.NewReader(data) out := new(Post) if err := UnmarshalPayload(in, out); err != nil { t.Fatal(err) } if out.LatestComment != nil { t.Fatalf("Latest Comment was not set to nil") } } func TestUnmarshalNullRelationshipInSlice(t *testing.T) { sample := map[string]interface{}{ "data": map[string]interface{}{ "type": "posts", "id": "1", "attributes": map[string]interface{}{ "body": "Hello", "title": "World", }, "relationships": map[string]interface{}{ "comments": map[string]interface{}{ "data": []interface{}{}, // empty to-many relationships }, }, }, } data, err := json.Marshal(sample) if err != nil { t.Fatal(err) } in := bytes.NewReader(data) out := new(Post) if err := UnmarshalPayload(in, out); err != nil { t.Fatal(err) } if len(out.Comments) != 0 { t.Fatalf("Wrong number of comments; Comments should be empty") } } func TestUnmarshalNestedRelationships(t *testing.T) { out, err := unmarshalSamplePayload() if err != nil { t.Fatal(err) } if out.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if out.CurrentPost.Comments == nil { t.Fatalf("Did not materialize nested records, comments") } if len(out.CurrentPost.Comments) != 2 { t.Fatalf("Wrong number of comments") } } func TestUnmarshalRelationshipsSerializedEmbedded(t *testing.T) { out := sampleSerializedEmbeddedTestModel() if out.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if out.CurrentPost.Title != "Foo" || out.CurrentPost.Body != "Bar" { t.Fatalf("Attributes were not set") } if len(out.Posts) != 2 { t.Fatalf("There should have been 2 posts") } if out.Posts[0].LatestComment.Body != "foo" { t.Fatalf("The comment body was not set") } } func TestUnmarshalNestedRelationshipsEmbedded(t *testing.T) { out := bytes.NewBuffer(nil) if err := MarshalOnePayloadEmbedded(out, testModel()); err != nil { t.Fatal(err) } model := new(Blog) if err := UnmarshalPayload(out, model); err != nil { t.Fatal(err) } if model.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if model.CurrentPost.Comments == nil { t.Fatalf("Did not materialize nested records, comments") } if len(model.CurrentPost.Comments) != 2 { t.Fatalf("Wrong number of comments") } if model.CurrentPost.Comments[0].Body != "foo" { t.Fatalf("Comment body not set") } } func TestUnmarshalRelationshipsSideloaded(t *testing.T) { payload := samplePayloadWithSideloaded() out := new(Blog) if err := UnmarshalPayload(payload, out); err != nil { t.Fatal(err) } if out.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if out.CurrentPost.Title != "Foo" || out.CurrentPost.Body != "Bar" { t.Fatalf("Attributes were not set") } if len(out.Posts) != 2 { t.Fatalf("There should have been 2 posts") } } func TestUnmarshalNestedRelationshipsSideloaded(t *testing.T) { payload := samplePayloadWithSideloaded() out := new(Blog) if err := UnmarshalPayload(payload, out); err != nil { t.Fatal(err) } if out.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if out.CurrentPost.Comments == nil { t.Fatalf("Did not materialize nested records, comments") } if len(out.CurrentPost.Comments) != 2 { t.Fatalf("Wrong number of comments") } if out.CurrentPost.Comments[0].Body != "foo" { t.Fatalf("Comment body not set") } } func TestUnmarshalNestedRelationshipsEmbedded_withClientIDs(t *testing.T) { model := new(Blog) if err := UnmarshalPayload(samplePayload(), model); err != nil { t.Fatal(err) } if model.Posts[0].ClientID == "" { t.Fatalf("ClientID not set from request on related record") } } func unmarshalSamplePayload() (*Blog, error) { in := samplePayload() out := new(Blog) if err := UnmarshalPayload(in, out); err != nil { return nil, err } return out, nil } func TestUnmarshalManyPayload(t *testing.T) { sample := map[string]interface{}{ "data": []interface{}{ map[string]interface{}{ "type": "posts", "id": "1", "attributes": map[string]interface{}{ "body": "First", "title": "Post", }, }, map[string]interface{}{ "type": "posts", "id": "2", "attributes": map[string]interface{}{ "body": "Second", "title": "Post", }, }, }, } data, err := json.Marshal(sample) if err != nil { t.Fatal(err) } in := bytes.NewReader(data) posts, err := UnmarshalManyPayload(in, reflect.TypeOf(new(Post))) if err != nil { t.Fatal(err) } 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) { firstPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=50" prevPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=0" nextPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=100" lastPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=500" sample := map[string]interface{}{ "data": []interface{}{ map[string]interface{}{ "type": "posts", "id": "1", "attributes": map[string]interface{}{ "body": "First", "title": "Post", }, }, map[string]interface{}{ "type": "posts", "id": "2", "attributes": map[string]interface{}{ "body": "Second", "title": "Post", }, }, }, "links": map[string]interface{}{ KeyFirstPage: firstPageURL, KeyPreviousPage: prevPageURL, KeyNextPage: nextPageURL, KeyLastPage: lastPageURL, }, } data, err := json.Marshal(sample) if err != nil { t.Fatal(err) } in := bytes.NewReader(data) payload := new(ManyPayload) if err = json.NewDecoder(in).Decode(payload); err != nil { t.Fatal(err) } if payload.Links == nil { t.Fatal("Was expecting a non nil ptr Link field") } links := *payload.Links first, ok := links[KeyFirstPage] if !ok { t.Fatal("Was expecting a non nil ptr Link field") } if e, a := firstPageURL, first; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyFirstPage, e, a) } prev, ok := links[KeyPreviousPage] if !ok { t.Fatal("Was expecting a non nil ptr Link field") } if e, a := prevPageURL, prev; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyPreviousPage, e, a) } next, ok := links[KeyNextPage] if !ok { t.Fatal("Was expecting a non nil ptr Link field") } if e, a := nextPageURL, next; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyNextPage, e, a) } last, ok := links[KeyLastPage] if !ok { t.Fatal("Was expecting a non nil ptr Link field") } if e, a := lastPageURL, last; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyLastPage, e, a) } } 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 , 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{}{ "type": "posts", "id": "1", "attributes": map[string]interface{}{ "body": "Hello", "title": "World", }, "relationships": map[string]interface{}{ "comments": map[string]interface{}{ "data": []interface{}{ map[string]interface{}{ "type": "comments", "id": "123", }, map[string]interface{}{ "type": "comments", "id": "456", }, }, }, "latest_comment": map[string]interface{}{ "data": map[string]interface{}{ "type": "comments", "id": "55555", }, }, }, }, } } func samplePayload() io.Reader { payload := &OnePayload{ Data: &Node{ Type: "blogs", Attributes: map[string]interface{}{ "title": "New blog", "created_at": 1436216820, "view_count": 1000, }, Relationships: map[string]interface{}{ "posts": &RelationshipManyNode{ Data: []*Node{ { Type: "posts", Attributes: map[string]interface{}{ "title": "Foo", "body": "Bar", }, ClientID: "1", }, { Type: "posts", Attributes: map[string]interface{}{ "title": "X", "body": "Y", }, ClientID: "2", }, }, }, "current_post": &RelationshipOneNode{ Data: &Node{ Type: "posts", Attributes: map[string]interface{}{ "title": "Bas", "body": "Fuubar", }, ClientID: "3", Relationships: map[string]interface{}{ "comments": &RelationshipManyNode{ Data: []*Node{ { Type: "comments", Attributes: map[string]interface{}{ "body": "Great post!", }, ClientID: "4", }, { Type: "comments", Attributes: map[string]interface{}{ "body": "Needs some work!", }, ClientID: "5", }, }, }, }, }, }, }, }, } out := bytes.NewBuffer(nil) json.NewEncoder(out).Encode(payload) return out } func samplePayloadWithID() io.Reader { payload := &OnePayload{ Data: &Node{ ID: "2", Type: "blogs", Attributes: map[string]interface{}{ "title": "New blog", "view_count": 1000, }, }, } out := bytes.NewBuffer(nil) json.NewEncoder(out).Encode(payload) return out } func samplePayloadWithBadTypes(m map[string]interface{}) io.Reader { payload := &OnePayload{ Data: &Node{ ID: "2", Type: "badtypes", Attributes: m, }, } out := bytes.NewBuffer(nil) json.NewEncoder(out).Encode(payload) return out } func sampleWithPointerPayload(m map[string]interface{}) io.Reader { payload := &OnePayload{ Data: &Node{ ID: "2", Type: "with-pointers", Attributes: m, }, } out := bytes.NewBuffer(nil) json.NewEncoder(out).Encode(payload) return out } func testModel() *Blog { return &Blog{ ID: 5, ClientID: "1", Title: "Title 1", CreatedAt: time.Now(), Posts: []*Post{ { ID: 1, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 2, Body: "bar", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, { ID: 2, Title: "Fuubar", Body: "Bas", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 3, Body: "bas", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, }, CurrentPost: &Post{ ID: 1, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 2, Body: "bar", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, } } func samplePayloadWithSideloaded() io.Reader { testModel := testModel() out := bytes.NewBuffer(nil) MarshalPayload(out, testModel) return out } func sampleSerializedEmbeddedTestModel() *Blog { out := bytes.NewBuffer(nil) MarshalOnePayloadEmbedded(out, testModel()) blog := new(Blog) UnmarshalPayload(out, 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) } } jsonapi-1.0.0/response.go000066400000000000000000000335201403265153500153470ustar00rootroot00000000000000package jsonapi import ( "encoding/json" "errors" "fmt" "io" "reflect" "strconv" "strings" "time" ) var ( // ErrBadJSONAPIStructTag is returned when the Struct field's JSON API // annotation is invalid. ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format") // ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field // was not a valid numeric type. ErrBadJSONAPIID = errors.New( "id should be either string, int(8,16,32,64) or uint(8,16,32,64)") // 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") // ErrUnexpectedType is returned when marshalling an interface; the interface // had to be a pointer or a slice; otherwise this error is returned. ErrUnexpectedType = errors.New("models should be a struct pointer or slice of struct pointers") ) // MarshalPayload writes a jsonapi response for one or many records. The // related records are sideloaded into the "included" array. If this method is // given a struct pointer as an argument it will serialize in the form // "data": {...}. If this method is given a slice of pointers, this method will // serialize in the form "data": [...] // // One Example: you could pass it, w, your http.ResponseWriter, and, models, a // ptr to a Blog to be written to the response body: // // func ShowBlog(w http.ResponseWriter, r *http.Request) { // blog := &Blog{} // // w.Header().Set("Content-Type", jsonapi.MediaType) // w.WriteHeader(http.StatusOK) // // if err := jsonapi.MarshalPayload(w, blog); err != nil { // http.Error(w, err.Error(), http.StatusInternalServerError) // } // } // // Many Example: you could pass it, w, your http.ResponseWriter, and, models, a // slice of Blog struct instance pointers to be written to the response body: // // func ListBlogs(w http.ResponseWriter, r *http.Request) { // blogs := []*Blog{} // // w.Header().Set("Content-Type", jsonapi.MediaType) // w.WriteHeader(http.StatusOK) // // if err := jsonapi.MarshalPayload(w, blogs); err != nil { // http.Error(w, err.Error(), http.StatusInternalServerError) // } // } // func MarshalPayload(w io.Writer, models interface{}) error { payload, err := Marshal(models) if err != nil { return err } return json.NewEncoder(w).Encode(payload) } // Marshal does the same as MarshalPayload except it just returns the payload // and doesn't write out results. Useful if you use your own JSON rendering // library. func Marshal(models interface{}) (Payloader, error) { switch vals := reflect.ValueOf(models); vals.Kind() { case reflect.Slice: m, err := convertToSliceInterface(&models) if err != nil { return nil, err } payload, err := marshalMany(m) if err != nil { return nil, err } if linkableModels, isLinkable := models.(Linkable); isLinkable { jl := linkableModels.JSONAPILinks() if er := jl.validate(); er != nil { return nil, er } payload.Links = linkableModels.JSONAPILinks() } if metableModels, ok := models.(Metable); ok { payload.Meta = metableModels.JSONAPIMeta() } return payload, nil case reflect.Ptr: // Check that the pointer was to a struct if reflect.Indirect(vals).Kind() != reflect.Struct { return nil, ErrUnexpectedType } return marshalOne(models) default: return nil, ErrUnexpectedType } } // MarshalPayloadWithoutIncluded writes a jsonapi response with one or many // records, without the related records sideloaded into "included" array. // If you want to serialize the relations into the "included" array see // MarshalPayload. // // models interface{} should be either a struct pointer or a slice of struct // pointers. func MarshalPayloadWithoutIncluded(w io.Writer, model interface{}) error { payload, err := Marshal(model) if err != nil { return err } payload.clearIncluded() return json.NewEncoder(w).Encode(payload) } // marshalOne does the same as MarshalOnePayload except it just returns the // payload and doesn't write out results. Useful is you use your JSON rendering // library. func marshalOne(model interface{}) (*OnePayload, error) { included := make(map[string]*Node) rootNode, err := visitModelNode(model, &included, true) if err != nil { return nil, err } payload := &OnePayload{Data: rootNode} payload.Included = nodeMapValues(&included) return payload, nil } // marshalMany does the same as MarshalManyPayload except it just returns the // payload and doesn't write out results. Useful is you use your JSON rendering // library. func marshalMany(models []interface{}) (*ManyPayload, error) { payload := &ManyPayload{ Data: []*Node{}, } included := map[string]*Node{} for _, model := range models { node, err := visitModelNode(model, &included, true) if err != nil { return nil, err } payload.Data = append(payload.Data, node) } payload.Included = nodeMapValues(&included) return payload, nil } // MarshalOnePayloadEmbedded - This method not meant to for use in // implementation code, although feel free. The purpose of this // method is for use in tests. In most cases, your request // payloads for create will be embedded rather than sideloaded for // related records. This method will serialize a single struct // pointer into an embedded json response. In other words, there // will be no, "included", array in the json all relationships will // be serailized inline in the data. // // However, in tests, you may want to construct payloads to post // to create methods that are embedded to most closely resemble // the payloads that will be produced by the client. This is what // this method is intended for. // // model interface{} should be a pointer to a struct. func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error { rootNode, err := visitModelNode(model, nil, false) if err != nil { return err } payload := &OnePayload{Data: rootNode} return json.NewEncoder(w).Encode(payload) } func visitModelNode(model interface{}, included *map[string]*Node, sideload bool) (*Node, error) { node := new(Node) var er error value := reflect.ValueOf(model) if value.IsNil() { return nil, nil } modelValue := value.Elem() modelType := value.Type().Elem() for i := 0; i < modelValue.NumField(); i++ { structField := modelValue.Type().Field(i) tag := structField.Tag.Get(annotationJSONAPI) if tag == "" { continue } fieldValue := modelValue.Field(i) fieldType := modelType.Field(i) args := strings.Split(tag, annotationSeperator) if len(args) < 1 { er = ErrBadJSONAPIStructTag break } annotation := args[0] if (annotation == annotationClientID && len(args) != 1) || (annotation != annotationClientID && len(args) < 2) { er = ErrBadJSONAPIStructTag break } if annotation == annotationPrimary { v := fieldValue // Deal with PTRS var kind reflect.Kind if fieldValue.Kind() == reflect.Ptr { kind = fieldType.Type.Elem().Kind() v = reflect.Indirect(fieldValue) } else { kind = fieldType.Type.Kind() } // Handle allowed types switch kind { case reflect.String: node.ID = v.Interface().(string) case reflect.Int: node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10) case reflect.Int8: node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10) case reflect.Int16: node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10) case reflect.Int32: node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10) case reflect.Int64: node.ID = strconv.FormatInt(v.Interface().(int64), 10) case reflect.Uint: node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10) case reflect.Uint8: node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10) case reflect.Uint16: node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10) case reflect.Uint32: node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10) case reflect.Uint64: node.ID = strconv.FormatUint(v.Interface().(uint64), 10) default: // We had a JSON float (numeric), but our field was not one of the // allowed numeric types er = ErrBadJSONAPIID } if er != nil { break } node.Type = args[1] } else if annotation == annotationClientID { clientID := fieldValue.String() if clientID != "" { node.ClientID = clientID } } else if annotation == annotationAttribute { var omitEmpty, iso8601, rfc3339 bool if len(args) > 2 { for _, arg := range args[2:] { switch arg { case annotationOmitEmpty: omitEmpty = true case annotationISO8601: iso8601 = true case annotationRFC3339: rfc3339 = true } } } if node.Attributes == nil { node.Attributes = make(map[string]interface{}) } if fieldValue.Type() == reflect.TypeOf(time.Time{}) { t := fieldValue.Interface().(time.Time) if t.IsZero() { continue } 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() } } else if fieldValue.Type() == reflect.TypeOf(new(time.Time)) { // A time pointer may be nil if fieldValue.IsNil() { if omitEmpty { continue } node.Attributes[args[1]] = nil } else { tm := fieldValue.Interface().(*time.Time) if tm.IsZero() && omitEmpty { continue } 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() } } } else { // Dealing with a fieldValue that is not a time emptyValue := reflect.Zero(fieldValue.Type()) // See if we need to omit this field if omitEmpty && reflect.DeepEqual(fieldValue.Interface(), emptyValue.Interface()) { continue } strAttr, ok := fieldValue.Interface().(string) if ok { node.Attributes[args[1]] = strAttr } else { node.Attributes[args[1]] = fieldValue.Interface() } } } else if annotation == annotationRelation { var omitEmpty bool //add support for 'omitempty' struct tag for marshaling as absent if len(args) > 2 { omitEmpty = args[2] == annotationOmitEmpty } isSlice := fieldValue.Type().Kind() == reflect.Slice if omitEmpty && (isSlice && fieldValue.Len() < 1 || (!isSlice && fieldValue.IsNil())) { continue } if node.Relationships == nil { node.Relationships = make(map[string]interface{}) } var relLinks *Links if linkableModel, ok := model.(RelationshipLinkable); ok { relLinks = linkableModel.JSONAPIRelationshipLinks(args[1]) } var relMeta *Meta if metableModel, ok := model.(RelationshipMetable); ok { relMeta = metableModel.JSONAPIRelationshipMeta(args[1]) } if isSlice { // to-many relationship relationship, err := visitModelNodeRelationships( fieldValue, included, sideload, ) if err != nil { er = err break } relationship.Links = relLinks relationship.Meta = relMeta if sideload { shallowNodes := []*Node{} for _, n := range relationship.Data { appendIncluded(included, n) shallowNodes = append(shallowNodes, toShallowNode(n)) } node.Relationships[args[1]] = &RelationshipManyNode{ Data: shallowNodes, Links: relationship.Links, Meta: relationship.Meta, } } else { node.Relationships[args[1]] = relationship } } else { // to-one relationships // Handle null relationship case if fieldValue.IsNil() { node.Relationships[args[1]] = &RelationshipOneNode{Data: nil} continue } relationship, err := visitModelNode( fieldValue.Interface(), included, sideload, ) if err != nil { er = err break } if sideload { appendIncluded(included, relationship) node.Relationships[args[1]] = &RelationshipOneNode{ Data: toShallowNode(relationship), Links: relLinks, Meta: relMeta, } } else { node.Relationships[args[1]] = &RelationshipOneNode{ Data: relationship, Links: relLinks, Meta: relMeta, } } } } else { er = ErrBadJSONAPIStructTag break } } if er != nil { return nil, er } if linkableModel, isLinkable := model.(Linkable); isLinkable { jl := linkableModel.JSONAPILinks() if er := jl.validate(); er != nil { return nil, er } node.Links = linkableModel.JSONAPILinks() } if metableModel, ok := model.(Metable); ok { node.Meta = metableModel.JSONAPIMeta() } return node, nil } func toShallowNode(node *Node) *Node { return &Node{ ID: node.ID, Type: node.Type, } } func visitModelNodeRelationships(models reflect.Value, included *map[string]*Node, sideload bool) (*RelationshipManyNode, error) { nodes := []*Node{} for i := 0; i < models.Len(); i++ { n := models.Index(i).Interface() node, err := visitModelNode(n, included, sideload) if err != nil { return nil, err } nodes = append(nodes, node) } return &RelationshipManyNode{Data: nodes}, nil } func appendIncluded(m *map[string]*Node, nodes ...*Node) { included := *m for _, n := range nodes { k := fmt.Sprintf("%s,%s", n.Type, n.ID) if _, hasNode := included[k]; hasNode { continue } included[k] = n } } func nodeMapValues(m *map[string]*Node) []*Node { mp := *m nodes := make([]*Node, len(mp)) i := 0 for _, n := range mp { nodes[i] = n i++ } return nodes } func convertToSliceInterface(i *interface{}) ([]interface{}, error) { vals := reflect.ValueOf(*i) if vals.Kind() != reflect.Slice { return nil, ErrExpectedSlice } var response []interface{} for x := 0; x < vals.Len(); x++ { response = append(response, vals.Index(x).Interface()) } return response, nil } jsonapi-1.0.0/response_test.go000066400000000000000000000605121403265153500164070ustar00rootroot00000000000000package jsonapi import ( "bytes" "encoding/json" "fmt" "reflect" "sort" "testing" "time" ) func TestMarshalPayload(t *testing.T) { book := &Book{ID: 1} books := []*Book{book, {ID: 2}} var jsonData map[string]interface{} // One out1 := bytes.NewBuffer(nil) MarshalPayload(out1, book) if err := json.Unmarshal(out1.Bytes(), &jsonData); err != nil { t.Fatal(err) } if _, ok := jsonData["data"].(map[string]interface{}); !ok { t.Fatalf("data key did not contain an Hash/Dict/Map") } // Many out2 := bytes.NewBuffer(nil) MarshalPayload(out2, books) if err := json.Unmarshal(out2.Bytes(), &jsonData); err != nil { t.Fatal(err) } if _, ok := jsonData["data"].([]interface{}); !ok { t.Fatalf("data key did not contain an Array") } } 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} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, b); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } jsonTags := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{})["tags"].([]interface{}) if e, a := len(tags), len(jsonTags); e != a { t.Fatalf("Was expecting tags of length %d got %d", e, a) } // Convert from []interface{} to []string jsonTagsStrings := []string{} for _, tag := range jsonTags { jsonTagsStrings = append(jsonTagsStrings, tag.(string)) } // Sort both sort.Strings(jsonTagsStrings) sort.Strings(tags) for i, tag := range tags { if e, a := tag, jsonTagsStrings[i]; e != a { t.Fatalf("At index %d, was expecting %s got %s", i, e, a) } } } func TestWithoutOmitsEmptyAnnotationOnRelation(t *testing.T) { blog := &Blog{} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, blog); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } relationships := jsonData["data"].(map[string]interface{})["relationships"].(map[string]interface{}) // Verifiy the "posts" relation was an empty array posts, ok := relationships["posts"] if !ok { t.Fatal("Was expecting the data.relationships.posts key/value to have been present") } postsMap, ok := posts.(map[string]interface{}) if !ok { t.Fatal("data.relationships.posts was not a map") } postsData, ok := postsMap["data"] if !ok { t.Fatal("Was expecting the data.relationships.posts.data key/value to have been present") } postsDataSlice, ok := postsData.([]interface{}) if !ok { t.Fatal("data.relationships.posts.data was not a slice []") } if len(postsDataSlice) != 0 { t.Fatal("Was expecting the data.relationships.posts.data value to have been an empty array []") } // Verifiy the "current_post" was a null currentPost, postExists := relationships["current_post"] if !postExists { t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted") } currentPostMap, ok := currentPost.(map[string]interface{}) if !ok { t.Fatal("data.relationships.current_post was not a map") } currentPostData, ok := currentPostMap["data"] if !ok { t.Fatal("Was expecting the data.relationships.current_post.data key/value to have been present") } if currentPostData != nil { t.Fatal("Was expecting the data.relationships.current_post.data value to have been nil/null") } } func TestWithOmitsEmptyAnnotationOnRelation(t *testing.T) { type BlogOptionalPosts struct { ID int `jsonapi:"primary,blogs"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts,omitempty"` CurrentPost *Post `jsonapi:"relation,current_post,omitempty"` } blog := &BlogOptionalPosts{ID: 999} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, blog); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } payload := jsonData["data"].(map[string]interface{}) // Verify relationship was NOT set if val, exists := payload["relationships"]; exists { t.Fatalf("Was expecting the data.relationships key/value to have been empty - it was not and had a value of %v", val) } } func TestWithOmitsEmptyAnnotationOnRelation_MixedData(t *testing.T) { type BlogOptionalPosts struct { ID int `jsonapi:"primary,blogs"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts,omitempty"` CurrentPost *Post `jsonapi:"relation,current_post,omitempty"` } blog := &BlogOptionalPosts{ ID: 999, CurrentPost: &Post{ ID: 123, }, } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, blog); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } payload := jsonData["data"].(map[string]interface{}) // Verify relationship was set if _, exists := payload["relationships"]; !exists { t.Fatal("Was expecting the data.relationships key/value to have NOT been empty") } relationships := payload["relationships"].(map[string]interface{}) // Verify the relationship was not omitted, and is not null if val, exists := relationships["current_post"]; !exists { t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted") } else if val.(map[string]interface{})["data"] == nil { t.Fatal("Was expecting the data.relationships.current_post value to have NOT been nil/null") } } 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{ ID: &id, Make: &make, Model: &model, } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, car); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } data := jsonData["data"].(map[string]interface{}) // attributes := data["attributes"].(map[string]interface{}) // Verify that the ID was sent val, exists := data["id"] if !exists { t.Fatal("Was expecting the data.id member to exist") } if val != id { t.Fatalf("Was expecting the data.id member to be `%s`, got `%s`", id, val) } } func TestMarshalOnePayload_omitIDString(t *testing.T) { type Foo struct { ID string `jsonapi:"primary,foo"` Title string `jsonapi:"attr,title"` } foo := &Foo{Title: "Foo"} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, foo); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } payload := jsonData["data"].(map[string]interface{}) // Verify that empty ID of type string gets omitted. See: // https://github.com/google/jsonapi/issues/83#issuecomment-285611425 _, ok := payload["id"] if ok { t.Fatal("Was expecting the data.id member to be omitted") } } func TestMarshall_invalidIDType(t *testing.T) { type badIDStruct struct { ID *bool `jsonapi:"primary,cars"` } id := true o := &badIDStruct{ID: &id} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, o); err != ErrBadJSONAPIID { t.Fatalf( "Was expecting a `%s` error, got `%s`", ErrBadJSONAPIID, err, ) } } func TestOmitsEmptyAnnotation(t *testing.T) { book := &Book{ Author: "aren55555", PublishedAt: time.Now().AddDate(0, -1, 0), } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, book); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } attributes := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{}) // Verify that the specifically omitted field were omitted if val, exists := attributes["title"]; exists { t.Fatalf("Was expecting the data.attributes.title key/value to have been omitted - it was not and had a value of %v", val) } if val, exists := attributes["pages"]; exists { 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 implicitly omitted fields were omitted if val, exists := attributes["PublishedAt"]; exists { 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 if _, exists := attributes["isbn"]; !exists { t.Fatal("Was expecting the data.attributes.isbn key/value to have NOT been omitted") } } func TestHasPrimaryAnnotation(t *testing.T) { testModel := &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), } 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.Type != "blogs" { t.Fatalf("type should have been blogs, got %s", data.Type) } if data.ID != "5" { t.Fatalf("ID not transferred") } } func TestSupportsAttributes(t *testing.T) { testModel := &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), } 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["title"] != "Title 1" { t.Fatalf("Attributes hash not populated using tags correctly") } } func TestOmitsZeroTimes(t *testing.T) { testModel := &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Time{}, } 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["created_at"] != nil { t.Fatalf("Created at was serialized even though it was a zero Time") } } func TestMarshal_Times(t *testing.T) { aTime := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC) 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) } } }) } } func TestSupportsLinkable(t *testing.T) { testModel := &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), } 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.Links == nil { t.Fatal("Expected data.links") } links := *data.Links self, hasSelf := links["self"] if !hasSelf { t.Fatal("Expected 'self' link to be present") } if _, isString := self.(string); !isString { t.Fatal("Expected 'self' to contain a string") } comments, hasComments := links["comments"] if !hasComments { t.Fatal("expect 'comments' to be present") } commentsMap, isMap := comments.(map[string]interface{}) if !isMap { t.Fatal("Expected 'comments' to contain a map") } commentsHref, hasHref := commentsMap["href"] if !hasHref { t.Fatal("Expect 'comments' to contain an 'href' key/value") } if _, isString := commentsHref.(string); !isString { t.Fatal("Expected 'href' to contain a string") } commentsMeta, hasMeta := commentsMap["meta"] if !hasMeta { t.Fatal("Expect 'comments' to contain a 'meta' key/value") } commentsMetaMap, isMap := commentsMeta.(map[string]interface{}) if !isMap { t.Fatal("Expected 'comments' to contain a map") } commentsMetaObject := Meta(commentsMetaMap) countsMap, isMap := commentsMetaObject["counts"].(map[string]interface{}) if !isMap { t.Fatal("Expected 'counts' to contain a map") } for k, v := range countsMap { if _, isNum := v.(float64); !isNum { t.Fatalf("Exepected value at '%s' to be a numeric (float64)", k) } } } func TestInvalidLinkable(t *testing.T) { testModel := &BadComment{ ID: 5, Body: "Hello World", } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err == nil { t.Fatal("Was expecting an error") } } func TestSupportsMetable(t *testing.T) { testModel := &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), } 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.Meta == nil { t.Fatalf("Expected data.meta") } meta := Meta(*data.Meta) if e, a := "extra details regarding the blog", meta["detail"]; e != a { t.Fatalf("Was expecting meta.detail to be %q, got %q", e, a) } } func TestRelations(t *testing.T) { testModel := testBlog() 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) } relations := resp.Data.Relationships if relations == nil { t.Fatalf("Relationships were not materialized") } if relations["posts"] == nil { t.Fatalf("Posts relationship was not materialized") } else { if relations["posts"].(map[string]interface{})["links"] == nil { t.Fatalf("Posts relationship links were not materialized") } if relations["posts"].(map[string]interface{})["meta"] == nil { t.Fatalf("Posts relationship meta were not materialized") } } if relations["current_post"] == nil { t.Fatalf("Current post relationship was not materialized") } else { if relations["current_post"].(map[string]interface{})["links"] == nil { t.Fatalf("Current post relationship links were not materialized") } if relations["current_post"].(map[string]interface{})["meta"] == nil { t.Fatalf("Current post relationship meta were not materialized") } } if len(relations["posts"].(map[string]interface{})["data"].([]interface{})) != 2 { t.Fatalf("Did not materialize two posts") } } func TestNoRelations(t *testing.T) { testModel := &Blog{ID: 1, Title: "Title 1", CreatedAt: time.Now()} 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) } if resp.Included != nil { t.Fatalf("Encoding json response did not omit included") } } func TestMarshalPayloadWithoutIncluded(t *testing.T) { data := &Post{ ID: 1, BlogID: 2, ClientID: "123e4567-e89b-12d3-a456-426655440000", Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 20, Body: "First", }, { ID: 21, Body: "Hello World", }, }, LatestComment: &Comment{ ID: 22, Body: "Cool!", }, } out := bytes.NewBuffer(nil) if err := MarshalPayloadWithoutIncluded(out, data); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } if resp.Included != nil { t.Fatalf("Encoding json response did not omit included") } } func TestMarshalPayload_many(t *testing.T) { data := []interface{}{ &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), Posts: []*Post{ { ID: 1, Title: "Foo", Body: "Bar", }, { ID: 2, Title: "Fuubar", Body: "Bas", }, }, CurrentPost: &Post{ ID: 1, Title: "Foo", Body: "Bar", }, }, &Blog{ ID: 6, Title: "Title 2", CreatedAt: time.Now(), Posts: []*Post{ { ID: 3, Title: "Foo", Body: "Bar", }, { ID: 4, Title: "Fuubar", Body: "Bas", }, }, CurrentPost: &Post{ ID: 4, Title: "Foo", Body: "Bar", }, }, } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, data); err != nil { t.Fatal(err) } resp := new(ManyPayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } d := resp.Data if len(d) != 2 { t.Fatalf("data should have two elements") } } func TestMarshalMany_WithSliceOfStructPointers(t *testing.T) { var data []*Blog for len(data) < 2 { data = append(data, testBlog()) } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, data); err != nil { t.Fatal(err) } resp := new(ManyPayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } d := resp.Data if len(d) != 2 { t.Fatalf("data should have two elements") } } func TestMarshalManyWithoutIncluded(t *testing.T) { var data []*Blog for len(data) < 2 { data = append(data, testBlog()) } out := bytes.NewBuffer(nil) if err := MarshalPayloadWithoutIncluded(out, data); err != nil { t.Fatal(err) } resp := new(ManyPayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } d := resp.Data if len(d) != 2 { t.Fatalf("data should have two elements") } if resp.Included != nil { t.Fatalf("Encoding json response did not omit included") } } func TestMarshalMany_SliceOfInterfaceAndSliceOfStructsSameJSON(t *testing.T) { structs := []*Book{ {ID: 1, Author: "aren55555", ISBN: "abc"}, {ID: 2, Author: "shwoodard", ISBN: "xyz"}, } interfaces := []interface{}{} for _, s := range structs { interfaces = append(interfaces, s) } // Perform Marshals structsOut := new(bytes.Buffer) if err := MarshalPayload(structsOut, structs); err != nil { t.Fatal(err) } interfacesOut := new(bytes.Buffer) if err := MarshalPayload(interfacesOut, interfaces); err != nil { t.Fatal(err) } // Generic JSON Unmarshal structsData, interfacesData := make(map[string]interface{}), make(map[string]interface{}) if err := json.Unmarshal(structsOut.Bytes(), &structsData); err != nil { t.Fatal(err) } if err := json.Unmarshal(interfacesOut.Bytes(), &interfacesData); err != nil { t.Fatal(err) } // Compare Result if !reflect.DeepEqual(structsData, interfacesData) { t.Fatal("Was expecting the JSON API generated to be the same") } } func TestMarshal_InvalidIntefaceArgument(t *testing.T) { out := new(bytes.Buffer) if err := MarshalPayload(out, true); err != ErrUnexpectedType { t.Fatal("Was expecting an error") } if err := MarshalPayload(out, 25); err != ErrUnexpectedType { t.Fatal("Was expecting an error") } if err := MarshalPayload(out, Book{}); err != ErrUnexpectedType { t.Fatal("Was expecting an error") } } func testBlog() *Blog { return &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), Posts: []*Post{ { ID: 1, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 2, Body: "bar", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, { ID: 2, Title: "Fuubar", Body: "Bas", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 3, Body: "bas", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, }, CurrentPost: &Post{ ID: 1, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 2, Body: "bar", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, } } jsonapi-1.0.0/runtime.go000066400000000000000000000067051403265153500152010ustar00rootroot00000000000000package jsonapi import ( "crypto/rand" "fmt" "io" "reflect" "time" ) // Event represents a lifecycle event in the marshaling or unmarshalling // process. type Event int const ( // UnmarshalStart is the Event that is sent when deserialization of a payload // begins. UnmarshalStart Event = iota // UnmarshalStop is the Event that is sent when deserialization of a payload // ends. UnmarshalStop // MarshalStart is the Event that is sent sent when serialization of a payload // begins. MarshalStart // MarshalStop is the Event that is sent sent when serialization of a payload // ends. MarshalStop ) // Runtime has the same methods as jsonapi package for serialization and // deserialization but also has a ctx, a map[string]interface{} for storing // state, designed for instrumenting serialization timings. type Runtime struct { ctx map[string]interface{} } // Events is the func type that provides the callback for handling event timings. type Events func(*Runtime, Event, string, time.Duration) // Instrumentation is a a global Events variable. This is the handler for all // timing events. var Instrumentation Events // NewRuntime creates a Runtime for use in an application. func NewRuntime() *Runtime { return &Runtime{make(map[string]interface{})} } // WithValue adds custom state variables to the runtime context. func (r *Runtime) WithValue(key string, value interface{}) *Runtime { r.ctx[key] = value return r } // Value returns a state variable in the runtime context. func (r *Runtime) Value(key string) interface{} { return r.ctx[key] } // Instrument is deprecated. func (r *Runtime) Instrument(key string) *Runtime { return r.WithValue("instrument", key) } func (r *Runtime) shouldInstrument() bool { return Instrumentation != nil } // UnmarshalPayload has docs in request.go for UnmarshalPayload. func (r *Runtime) UnmarshalPayload(reader io.Reader, model interface{}) error { return r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error { return UnmarshalPayload(reader, model) }) } // UnmarshalManyPayload has docs in request.go for UnmarshalManyPayload. 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 } // MarshalPayload has docs in response.go for MarshalPayload. 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 }