forked from Mirrors/jsonapi
Feature/marshal many interface (#42)
* add support for direct use of slice of struct pointers for MarshalMany * Added a test to ensure that whether MarshalManyPayload is called with a []interface{} or []*Struct it will produce the same desired result. * Added a test to show how the new error type may be returned. * Ensure the outside accessible Errs have comments. * Page char width cleanup. Addressed warnings about duplicated "client-id" definition; using the const.
This commit is contained in:
parent
a70d58d3c8
commit
a1fa2c84a0
10
node.go
10
node.go
|
@ -1,17 +1,24 @@
|
||||||
package jsonapi
|
package jsonapi
|
||||||
|
|
||||||
|
const clientIDAnnotation = "client-id"
|
||||||
|
|
||||||
|
// 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 {
|
type OnePayload struct {
|
||||||
Data *Node `json:"data"`
|
Data *Node `json:"data"`
|
||||||
Included []*Node `json:"included,omitempty"`
|
Included []*Node `json:"included,omitempty"`
|
||||||
Links *map[string]string `json:"links,omitempty"`
|
Links *map[string]string `json:"links,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
type ManyPayload struct {
|
||||||
Data []*Node `json:"data"`
|
Data []*Node `json:"data"`
|
||||||
Included []*Node `json:"included,omitempty"`
|
Included []*Node `json:"included,omitempty"`
|
||||||
Links *map[string]string `json:"links,omitempty"`
|
Links *map[string]string `json:"links,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Node is used to represent a generic JSON API Resource
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
@ -20,11 +27,14 @@ type Node struct {
|
||||||
Relationships map[string]interface{} `json:"relationships,omitempty"`
|
Relationships map[string]interface{} `json:"relationships,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RelationshipOneNode is used to represent a generic has one JSON API relation
|
||||||
type RelationshipOneNode struct {
|
type RelationshipOneNode struct {
|
||||||
Data *Node `json:"data"`
|
Data *Node `json:"data"`
|
||||||
Links *map[string]string `json:"links,omitempty"`
|
Links *map[string]string `json:"links,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RelationshipManyNode is used to represent a generic has many JSON API
|
||||||
|
// relation
|
||||||
type RelationshipManyNode struct {
|
type RelationshipManyNode struct {
|
||||||
Data []*Node `json:"data"`
|
Data []*Node `json:"data"`
|
||||||
Links *map[string]string `json:"links,omitempty"`
|
Links *map[string]string `json:"links,omitempty"`
|
||||||
|
|
|
@ -14,7 +14,6 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
||||||
clientIDAnnotation = "client-id"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
111
response.go
111
response.go
|
@ -12,13 +12,22 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// ErrBadJSONAPIStructTag is returned when the Struct field's JSON API
|
||||||
|
// annotation is invalid.
|
||||||
ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format")
|
ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format")
|
||||||
ErrBadJSONAPIID = errors.New("id should be either string or int")
|
// 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 or uint")
|
||||||
|
// ErrExpectedSlice is returned when a variable or arugment 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")
|
||||||
)
|
)
|
||||||
|
|
||||||
// MarshalOnePayload writes a jsonapi response with one, with related records sideloaded, into "included" array.
|
// MarshalOnePayload writes a jsonapi response with one, with related records
|
||||||
// This method encodes a response for a single record only. Hence, data will be a single record rather
|
// sideloaded, into "included" array. This method encodes a response for a
|
||||||
// than an array of records. If you want to serialize many records, see, MarshalManyPayload.
|
// single record only. Hence, data will be a single record rather than an array
|
||||||
|
// of records. If you want to serialize many records, see, MarshalManyPayload.
|
||||||
//
|
//
|
||||||
// See UnmarshalPayload for usage example.
|
// See UnmarshalPayload for usage example.
|
||||||
//
|
//
|
||||||
|
@ -56,9 +65,9 @@ func MarshalOnePayloadWithoutIncluded(w io.Writer, model interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalOne does the same as MarshalOnePayload except it just returns the payload
|
// MarshalOne does the same as MarshalOnePayload except it just returns the
|
||||||
// and doesn't write out results.
|
// payload and doesn't write out results. Useful is you use your JSON rendering
|
||||||
// Useful is you use your JSON rendering library.
|
// library.
|
||||||
func MarshalOne(model interface{}) (*OnePayload, error) {
|
func MarshalOne(model interface{}) (*OnePayload, error) {
|
||||||
included := make(map[string]*Node)
|
included := make(map[string]*Node)
|
||||||
|
|
||||||
|
@ -73,12 +82,14 @@ func MarshalOne(model interface{}) (*OnePayload, error) {
|
||||||
return payload, nil
|
return payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalManyPayload writes a jsonapi response with many records, with related records sideloaded, into "included" array.
|
// MarshalManyPayload writes a jsonapi response with many records, with related
|
||||||
// This method encodes a response for a slice of records, hence data will be an array of
|
// records sideloaded, into "included" array. This method encodes a response for
|
||||||
// records rather than a single record. To serialize a single record, see MarshalOnePayload
|
// a slice of records, hence data will be an array of records rather than a
|
||||||
|
// single record. To serialize a single record, see MarshalOnePayload
|
||||||
//
|
//
|
||||||
// For example you could pass it, w, your http.ResponseWriter, and, models, a slice of Blog
|
// For example you could pass it, w, your http.ResponseWriter, and, models, a
|
||||||
// struct instance pointers as interface{}'s to write to the response,
|
// slice of Blog struct instance pointers as interface{}'s to write to the
|
||||||
|
// response,
|
||||||
//
|
//
|
||||||
// func ListBlogs(w http.ResponseWriter, r *http.Request) {
|
// func ListBlogs(w http.ResponseWriter, r *http.Request) {
|
||||||
// // ... fetch your blogs and filter, offset, limit, etc ...
|
// // ... fetch your blogs and filter, offset, limit, etc ...
|
||||||
|
@ -95,9 +106,13 @@ func MarshalOne(model interface{}) (*OnePayload, error) {
|
||||||
//
|
//
|
||||||
// Visit https://github.com/google/jsonapi#list for more info.
|
// Visit https://github.com/google/jsonapi#list for more info.
|
||||||
//
|
//
|
||||||
// models []interface{} should be a slice of struct pointers.
|
// models interface{} should be a slice of struct pointers.
|
||||||
func MarshalManyPayload(w io.Writer, models []interface{}) error {
|
func MarshalManyPayload(w io.Writer, models interface{}) error {
|
||||||
payload, err := MarshalMany(models)
|
m, err := convertToSliceInterface(&models)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload, err := MarshalMany(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -109,9 +124,9 @@ func MarshalManyPayload(w io.Writer, models []interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalMany does the same as MarshalManyPayload except it just returns the payload
|
// MarshalMany does the same as MarshalManyPayload except it just returns the
|
||||||
// and doesn't write out results.
|
// payload and doesn't write out results. Useful is you use your JSON rendering
|
||||||
// Useful is you use your JSON rendering library.
|
// library.
|
||||||
func MarshalMany(models []interface{}) (*ManyPayload, error) {
|
func MarshalMany(models []interface{}) (*ManyPayload, error) {
|
||||||
var data []*Node
|
var data []*Node
|
||||||
included := make(map[string]*Node)
|
included := make(map[string]*Node)
|
||||||
|
@ -138,16 +153,17 @@ func MarshalMany(models []interface{}) (*ManyPayload, error) {
|
||||||
return payload, nil
|
return payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalOnePayloadEmbedded - This method not meant to for use in implementation code, although feel
|
// MarshalOnePayloadEmbedded - This method not meant to for use in
|
||||||
// free. The purpose of this method is for use in tests. In most cases, your
|
// implementation code, although feel free. The purpose of this method is for
|
||||||
// request payloads for create will be embedded rather than sideloaded for related records.
|
// use in tests. In most cases, your request payloads for create will be
|
||||||
// This method will serialize a single struct pointer into an embedded json
|
// embedded rather than sideloaded for related records. This method will
|
||||||
// response. In other words, there will be no, "included", array in the json
|
// serialize a single struct pointer into an embedded json response. In other
|
||||||
// all relationships will be serailized inline in the data.
|
// 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
|
// However, in tests, you may want to construct payloads to post to create
|
||||||
// that are embedded to most closely resemble the payloads that will be produced by
|
// methods that are embedded to most closely resemble the payloads that will be
|
||||||
// the client. This is what this method is intended for.
|
// produced by the client. This is what this method is intended for.
|
||||||
//
|
//
|
||||||
// model interface{} should be a pointer to a struct.
|
// model interface{} should be a pointer to a struct.
|
||||||
func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
|
func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
|
||||||
|
@ -190,7 +206,8 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
||||||
|
|
||||||
annotation := args[0]
|
annotation := args[0]
|
||||||
|
|
||||||
if (annotation == "client-id" && len(args) != 1) || (annotation != "client-id" && len(args) < 2) {
|
if (annotation == clientIDAnnotation && len(args) != 1) ||
|
||||||
|
(annotation != clientIDAnnotation && len(args) < 2) {
|
||||||
er = ErrBadJSONAPIStructTag
|
er = ErrBadJSONAPIStructTag
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -213,7 +230,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
||||||
}
|
}
|
||||||
|
|
||||||
node.Type = args[1]
|
node.Type = args[1]
|
||||||
} else if annotation == "client-id" {
|
} else if annotation == clientIDAnnotation {
|
||||||
clientID := fieldValue.String()
|
clientID := fieldValue.String()
|
||||||
if clientID != "" {
|
if clientID != "" {
|
||||||
node.ClientID = clientID
|
node.ClientID = clientID
|
||||||
|
@ -282,7 +299,12 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSlice {
|
if isSlice {
|
||||||
relationship, err := visitModelNodeRelationships(args[1], fieldValue, included, sideload)
|
relationship, err := visitModelNodeRelationships(
|
||||||
|
args[1],
|
||||||
|
fieldValue,
|
||||||
|
included,
|
||||||
|
sideload,
|
||||||
|
)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
d := relationship.Data
|
d := relationship.Data
|
||||||
|
@ -294,7 +316,9 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
||||||
shallowNodes = append(shallowNodes, toShallowNode(n))
|
shallowNodes = append(shallowNodes, toShallowNode(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
node.Relationships[args[1]] = &RelationshipManyNode{Data: shallowNodes}
|
node.Relationships[args[1]] = &RelationshipManyNode{
|
||||||
|
Data: shallowNodes,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
node.Relationships[args[1]] = relationship
|
node.Relationships[args[1]] = relationship
|
||||||
}
|
}
|
||||||
|
@ -303,13 +327,20 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
relationship, err := visitModelNode(fieldValue.Interface(), included, sideload)
|
relationship, err := visitModelNode(
|
||||||
|
fieldValue.Interface(),
|
||||||
|
included,
|
||||||
|
sideload)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if sideload {
|
if sideload {
|
||||||
appendIncluded(included, relationship)
|
appendIncluded(included, relationship)
|
||||||
node.Relationships[args[1]] = &RelationshipOneNode{Data: toShallowNode(relationship)}
|
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||||
|
Data: toShallowNode(relationship),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
node.Relationships[args[1]] = &RelationshipOneNode{Data: relationship}
|
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||||
|
Data: relationship,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
er = err
|
er = err
|
||||||
|
@ -383,3 +414,15 @@ func nodeMapValues(m *map[string]*Node) []*Node {
|
||||||
|
|
||||||
return nodes
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package jsonapi
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -320,6 +321,78 @@ func TestMarshalMany(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarshalMany_WithSliceOfStructPointers(t *testing.T) {
|
||||||
|
var data []*Blog
|
||||||
|
for len(data) < 2 {
|
||||||
|
data = append(data, testBlog())
|
||||||
|
}
|
||||||
|
|
||||||
|
out := bytes.NewBuffer(nil)
|
||||||
|
if err := MarshalManyPayload(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_SliceOfInterfaceAndSliceOfStructsSameJSON(t *testing.T) {
|
||||||
|
structs := []*Book{
|
||||||
|
&Book{ID: 1, Author: "aren55555", ISBN: "abc"},
|
||||||
|
&Book{ID: 2, Author: "shwoodard", ISBN: "xyz"},
|
||||||
|
}
|
||||||
|
interfaces := []interface{}{}
|
||||||
|
for _, s := range structs {
|
||||||
|
interfaces = append(interfaces, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform Marshals
|
||||||
|
structsOut := new(bytes.Buffer)
|
||||||
|
if err := MarshalManyPayload(structsOut, structs); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
interfacesOut := new(bytes.Buffer)
|
||||||
|
if err := MarshalManyPayload(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 TestMarshalMany_InvalidIntefaceArgument(t *testing.T) {
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
if err := MarshalManyPayload(out, true); err != ErrExpectedSlice {
|
||||||
|
t.Fatal("Was expecting an error")
|
||||||
|
}
|
||||||
|
if err := MarshalManyPayload(out, 25); err != ErrExpectedSlice {
|
||||||
|
t.Fatal("Was expecting an error")
|
||||||
|
}
|
||||||
|
if err := MarshalManyPayload(out, Book{}); err != ErrExpectedSlice {
|
||||||
|
t.Fatal("Was expecting an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testBlog() *Blog {
|
func testBlog() *Blog {
|
||||||
return &Blog{
|
return &Blog{
|
||||||
ID: 5,
|
ID: 5,
|
||||||
|
|
|
@ -66,7 +66,7 @@ func (r *Runtime) MarshalOnePayload(w io.Writer, model interface{}) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runtime) MarshalManyPayload(w io.Writer, models []interface{}) error {
|
func (r *Runtime) MarshalManyPayload(w io.Writer, models interface{}) error {
|
||||||
return r.instrumentCall(MarshalStart, MarshalStop, func() error {
|
return r.instrumentCall(MarshalStart, MarshalStop, func() error {
|
||||||
return MarshalManyPayload(w, models)
|
return MarshalManyPayload(w, models)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue