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
|
||||
|
||||
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 {
|
||||
Data *Node `json:"data"`
|
||||
Included []*Node `json:"included,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 {
|
||||
Data []*Node `json:"data"`
|
||||
Included []*Node `json:"included,omitempty"`
|
||||
Links *map[string]string `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// Node is used to represent a generic JSON API Resource
|
||||
type Node struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
|
@ -20,11 +27,14 @@ type Node struct {
|
|||
Relationships map[string]interface{} `json:"relationships,omitempty"`
|
||||
}
|
||||
|
||||
// RelationshipOneNode is used to represent a generic has one JSON API relation
|
||||
type RelationshipOneNode struct {
|
||||
Data *Node `json:"data"`
|
||||
Links *map[string]string `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// RelationshipManyNode is used to represent a generic has many JSON API
|
||||
// relation
|
||||
type RelationshipManyNode struct {
|
||||
Data []*Node `json:"data"`
|
||||
Links *map[string]string `json:"links,omitempty"`
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
|
||||
const (
|
||||
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
|
||||
clientIDAnnotation = "client-id"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
111
response.go
111
response.go
|
@ -12,13 +12,22 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
// ErrBadJSONAPIStructTag is returned when the Struct field's JSON API
|
||||
// annotation is invalid.
|
||||
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.
|
||||
// This method encodes a response for a 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.
|
||||
// MarshalOnePayload writes a jsonapi response with one, with related records
|
||||
// sideloaded, into "included" array. This method encodes a response for a
|
||||
// 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.
|
||||
//
|
||||
|
@ -56,9 +65,9 @@ func MarshalOnePayloadWithoutIncluded(w io.Writer, model interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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)
|
||||
|
||||
|
@ -73,12 +82,14 @@ func MarshalOne(model interface{}) (*OnePayload, error) {
|
|||
return payload, nil
|
||||
}
|
||||
|
||||
// MarshalManyPayload writes a jsonapi response with many records, with related records sideloaded, into "included" array.
|
||||
// This method encodes a response for a slice of records, hence data will be an array of
|
||||
// records rather than a single record. To serialize a single record, see MarshalOnePayload
|
||||
// MarshalManyPayload writes a jsonapi response with many records, with related
|
||||
// records sideloaded, into "included" array. This method encodes a response for
|
||||
// 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
|
||||
// struct instance pointers as interface{}'s to write to the response,
|
||||
// For example you could pass it, w, your http.ResponseWriter, and, models, a
|
||||
// slice of Blog struct instance pointers as interface{}'s to write to the
|
||||
// response,
|
||||
//
|
||||
// func ListBlogs(w http.ResponseWriter, r *http.Request) {
|
||||
// // ... 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.
|
||||
//
|
||||
// models []interface{} should be a slice of struct pointers.
|
||||
func MarshalManyPayload(w io.Writer, models []interface{}) error {
|
||||
payload, err := MarshalMany(models)
|
||||
// models interface{} should be a slice of struct pointers.
|
||||
func MarshalManyPayload(w io.Writer, models interface{}) error {
|
||||
m, err := convertToSliceInterface(&models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := MarshalMany(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -109,9 +124,9 @@ func MarshalManyPayload(w io.Writer, models []interface{}) error {
|
|||
return 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.
|
||||
// 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) {
|
||||
var data []*Node
|
||||
included := make(map[string]*Node)
|
||||
|
@ -138,16 +153,17 @@ func MarshalMany(models []interface{}) (*ManyPayload, error) {
|
|||
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.
|
||||
// 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.
|
||||
// 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 {
|
||||
|
@ -190,7 +206,8 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
|
||||
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
|
||||
break
|
||||
}
|
||||
|
@ -213,7 +230,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
}
|
||||
|
||||
node.Type = args[1]
|
||||
} else if annotation == "client-id" {
|
||||
} else if annotation == clientIDAnnotation {
|
||||
clientID := fieldValue.String()
|
||||
if clientID != "" {
|
||||
node.ClientID = clientID
|
||||
|
@ -282,7 +299,12 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
}
|
||||
|
||||
if isSlice {
|
||||
relationship, err := visitModelNodeRelationships(args[1], fieldValue, included, sideload)
|
||||
relationship, err := visitModelNodeRelationships(
|
||||
args[1],
|
||||
fieldValue,
|
||||
included,
|
||||
sideload,
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
d := relationship.Data
|
||||
|
@ -294,7 +316,9 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
shallowNodes = append(shallowNodes, toShallowNode(n))
|
||||
}
|
||||
|
||||
node.Relationships[args[1]] = &RelationshipManyNode{Data: shallowNodes}
|
||||
node.Relationships[args[1]] = &RelationshipManyNode{
|
||||
Data: shallowNodes,
|
||||
}
|
||||
} else {
|
||||
node.Relationships[args[1]] = relationship
|
||||
}
|
||||
|
@ -303,13 +327,20 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
break
|
||||
}
|
||||
} else {
|
||||
relationship, err := visitModelNode(fieldValue.Interface(), included, sideload)
|
||||
relationship, err := visitModelNode(
|
||||
fieldValue.Interface(),
|
||||
included,
|
||||
sideload)
|
||||
if err == nil {
|
||||
if sideload {
|
||||
appendIncluded(included, relationship)
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{Data: toShallowNode(relationship)}
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||
Data: toShallowNode(relationship),
|
||||
}
|
||||
} else {
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{Data: relationship}
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||
Data: relationship,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
er = err
|
||||
|
@ -383,3 +414,15 @@ func nodeMapValues(m *map[string]*Node) []*Node {
|
|||
|
||||
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 (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
"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 {
|
||||
return &Blog{
|
||||
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 MarshalManyPayload(w, models)
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue