diff options
| author | mo khan <mo@mokhan.ca> | 2025-05-11 21:12:57 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-05-11 21:12:57 -0600 |
| commit | 60440f90dca28e99a31dd328c5f6d5dc0f9b6a2e (patch) | |
| tree | 2f54adf55086516f162f0a55a5347e6b25f7f176 /vendor/github.com/google/jsonapi | |
| parent | 05ca9b8d3a9c7203a3a3b590beaa400900bd9007 (diff) | |
chore: vendor go dependencies
Diffstat (limited to 'vendor/github.com/google/jsonapi')
| -rw-r--r-- | vendor/github.com/google/jsonapi/.gitignore | 1 | ||||
| -rw-r--r-- | vendor/github.com/google/jsonapi/.travis.yml | 13 | ||||
| -rw-r--r-- | vendor/github.com/google/jsonapi/LICENSE | 21 | ||||
| -rw-r--r-- | vendor/github.com/google/jsonapi/README.md | 477 | ||||
| -rw-r--r-- | vendor/github.com/google/jsonapi/constants.go | 56 | ||||
| -rw-r--r-- | vendor/github.com/google/jsonapi/doc.go | 70 | ||||
| -rw-r--r-- | vendor/github.com/google/jsonapi/errors.go | 52 | ||||
| -rw-r--r-- | vendor/github.com/google/jsonapi/node.go | 121 | ||||
| -rw-r--r-- | vendor/github.com/google/jsonapi/request.go | 656 | ||||
| -rw-r--r-- | vendor/github.com/google/jsonapi/response.go | 538 | ||||
| -rw-r--r-- | vendor/github.com/google/jsonapi/runtime.go | 129 |
11 files changed, 2134 insertions, 0 deletions
diff --git a/vendor/github.com/google/jsonapi/.gitignore b/vendor/github.com/google/jsonapi/.gitignore new file mode 100644 index 0000000..19b1e1c --- /dev/null +++ b/vendor/github.com/google/jsonapi/.gitignore @@ -0,0 +1 @@ +/examples/examples diff --git a/vendor/github.com/google/jsonapi/.travis.yml b/vendor/github.com/google/jsonapi/.travis.yml new file mode 100644 index 0000000..abc7d1b --- /dev/null +++ b/vendor/github.com/google/jsonapi/.travis.yml @@ -0,0 +1,13 @@ +language: 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 diff --git a/vendor/github.com/google/jsonapi/LICENSE b/vendor/github.com/google/jsonapi/LICENSE new file mode 100644 index 0000000..c97912c --- /dev/null +++ b/vendor/github.com/google/jsonapi/LICENSE @@ -0,0 +1,21 @@ +The 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. diff --git a/vendor/github.com/google/jsonapi/README.md b/vendor/github.com/google/jsonapi/README.md new file mode 100644 index 0000000..8dfb943 --- /dev/null +++ b/vendor/github.com/google/jsonapi/README.md @@ -0,0 +1,477 @@ +# jsonapi + +[](https://travis-ci.org/google/jsonapi) +[](https://goreportcard.com/report/github.com/google/jsonapi) +[](http://godoc.org/github.com/google/jsonapi) +[](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,<type field output>"` +``` + +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,<key name in attributes hash>,<optional: omitempty>"` +``` + +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,<key name in relationships hash>,<optional: omitempty>"` +``` + +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*. diff --git a/vendor/github.com/google/jsonapi/constants.go b/vendor/github.com/google/jsonapi/constants.go new file mode 100644 index 0000000..35bbe05 --- /dev/null +++ b/vendor/github.com/google/jsonapi/constants.go @@ -0,0 +1,56 @@ +package 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]" +) diff --git a/vendor/github.com/google/jsonapi/doc.go b/vendor/github.com/google/jsonapi/doc.go new file mode 100644 index 0000000..ba4068a --- /dev/null +++ b/vendor/github.com/google/jsonapi/doc.go @@ -0,0 +1,70 @@ +/* +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,<type field output>" + +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,<key name in attributes hash>[,<extra arguments>]" + +These fields' values should end up in the "attribute" hash for a record. The first +argument must be, "attr', and the second should be the name for the key to display in +the "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,<key name in relationships hash>" + +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 diff --git a/vendor/github.com/google/jsonapi/errors.go b/vendor/github.com/google/jsonapi/errors.go new file mode 100644 index 0000000..798fed0 --- /dev/null +++ b/vendor/github.com/google/jsonapi/errors.go @@ -0,0 +1,52 @@ +package 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) +} diff --git a/vendor/github.com/google/jsonapi/node.go b/vendor/github.com/google/jsonapi/node.go new file mode 100644 index 0000000..a58488c --- /dev/null +++ b/vendor/github.com/google/jsonapi/node.go @@ -0,0 +1,121 @@ +package 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 +} diff --git a/vendor/github.com/google/jsonapi/request.go b/vendor/github.com/google/jsonapi/request.go new file mode 100644 index 0000000..f665857 --- /dev/null +++ b/vendor/github.com/google/jsonapi/request.go @@ -0,0 +1,656 @@ +package 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 +} diff --git a/vendor/github.com/google/jsonapi/response.go b/vendor/github.com/google/jsonapi/response.go new file mode 100644 index 0000000..b44e4e9 --- /dev/null +++ b/vendor/github.com/google/jsonapi/response.go @@ -0,0 +1,538 @@ +package 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 +} diff --git a/vendor/github.com/google/jsonapi/runtime.go b/vendor/github.com/google/jsonapi/runtime.go new file mode 100644 index 0000000..db2d9f2 --- /dev/null +++ b/vendor/github.com/google/jsonapi/runtime.go @@ -0,0 +1,129 @@ +package 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 +} |
