From a1fa2c84a08fd4c2e527925c858edf34360e6b2c Mon Sep 17 00:00:00 2001 From: Aren Patel Date: Mon, 12 Sep 2016 22:12:42 -0700 Subject: [PATCH] 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. --- node.go | 10 +++++ request.go | 1 - response.go | 111 ++++++++++++++++++++++++++++++++--------------- response_test.go | 73 +++++++++++++++++++++++++++++++ runtime.go | 2 +- 5 files changed, 161 insertions(+), 36 deletions(-) diff --git a/node.go b/node.go index a8f00f3..ef8fbaf 100644 --- a/node.go +++ b/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"` diff --git a/request.go b/request.go index d1c83f5..ecdaf06 100644 --- a/request.go +++ b/request.go @@ -14,7 +14,6 @@ import ( const ( unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s" - clientIDAnnotation = "client-id" ) var ( diff --git a/response.go b/response.go index 9fcdef4..680be2f 100644 --- a/response.go +++ b/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 +} diff --git a/response_test.go b/response_test.go index 57b7729..be780f5 100644 --- a/response_test.go +++ b/response_test.go @@ -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, diff --git a/runtime.go b/runtime.go index ef451eb..08cd203 100644 --- a/runtime.go +++ b/runtime.go @@ -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) })