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:
Aren Patel 2016-09-12 22:12:42 -07:00 committed by GitHub
parent a70d58d3c8
commit a1fa2c84a0
5 changed files with 161 additions and 36 deletions

10
node.go
View File

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

View File

@ -14,7 +14,6 @@ import (
const (
unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s"
clientIDAnnotation = "client-id"
)
var (

View File

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

View File

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

View File

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