Testing and Documentation for UnmarshalManyPayload (#66)

* Added a test for UnmarshalManyPayload

* Document UnmarshalManyPayload in the Readme

* Update the example app to have a UnmarshalManyPayload usage.

* DRY the UnmarshalManyPayload implementation.

* Add a test for grabbing the Links out of a ManyPayload
This commit is contained in:
Aren Patel 2017-01-26 15:25:13 -08:00 committed by GitHub
parent 9d919b42a6
commit 3ea9ec4904
4 changed files with 233 additions and 23 deletions

View File

@ -222,7 +222,7 @@ Writes a JSON API response, with related records sideloaded, into an
only. If you want to serialize many records, see,
[MarshalManyPayload](#marshalmanypayload).
#### Handler Example Code
##### Handler Example Code
```go
func CreateBlog(w http.ResponseWriter, r *http.Request) {
@ -270,7 +270,6 @@ blogInterface := make([]interface{}, len(blogs))
for i, blog := range blogs {
blogInterface[i] = blog
}
```
Alternatively, you can insert your `Blog`s into a slice of `interface{}`
@ -283,7 +282,7 @@ this,
func FetchBlogs() ([]interface{}, error)
```
#### Handler Example Code
##### Handler Example Code
```go
func ListBlogs(w http.ResponseWriter, r *http.Request) {
@ -300,6 +299,44 @@ func ListBlogs(w http.ResponseWriter, r *http.Request) {
}
```
### 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.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", jsonapi.MediaType)
if err := jsonapi.MarshalManyPayload(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:

View File

@ -8,6 +8,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"reflect"
"regexp"
"strconv"
"time"
@ -72,6 +73,33 @@ func showBlog(w http.ResponseWriter, r *http.Request) {
}
}
func echoBlogs(w http.ResponseWriter, r *http.Request) {
jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.echo")
// Fetch the blogs from the HTTP request body
data, err := jsonapiRuntime.UnmarshalManyPayload(r.Body, reflect.TypeOf(new(Blog)))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
// Type assert the []interface{} to []*Blog
blogs := []*Blog{}
for _, b := range data {
blog, ok := b.(*Blog)
if !ok {
http.Error(w, "Unexpected type", http.StatusInternalServerError)
}
blogs = append(blogs, blog)
}
// Echo the blogs to the response body
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", jsonapi.MediaType)
if err := jsonapiRuntime.MarshalManyPayload(w, blogs); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func main() {
jsonapi.Instrumentation = func(r *jsonapi.Runtime, eventType jsonapi.Event, callGUID string, dur time.Duration) {
metricPrefix := r.Value("instrument").(string)
@ -101,6 +129,8 @@ func main() {
if r.Method == http.MethodPost {
createBlog(w, r)
} else if r.Method == http.MethodPut {
echoBlogs(w, r)
} else if r.FormValue("id") != "" {
showBlog(w, r)
} else {
@ -207,7 +237,7 @@ func exerciseHandler() {
jsonReply, _ = ioutil.ReadAll(w.Body)
fmt.Println("\n============ jsonapi response from show ===========")
fmt.Println("============ jsonapi response from show ===========")
fmt.Println(string(jsonReply))
fmt.Println("============== end raw jsonapi from show =============")
@ -229,7 +259,33 @@ func exerciseHandler() {
buf := bytes.NewBuffer(nil)
io.Copy(buf, w.Body)
fmt.Println("\n============ jsonapi response from create ===========")
fmt.Println("============ jsonapi response from create ===========")
fmt.Println(buf.String())
fmt.Println("============== end raw jsonapi response =============")
// echo
blogs := []interface{}{
testBlogForCreate(1),
testBlogForCreate(2),
testBlogForCreate(3),
}
in = bytes.NewBuffer(nil)
jsonapi.MarshalManyPayload(in, blogs)
req, _ = http.NewRequest(http.MethodPut, "/blogs", in)
req.Header.Set("Accept", jsonapi.MediaType)
w = httptest.NewRecorder()
fmt.Println("============ start echo ===========")
http.DefaultServeMux.ServeHTTP(w, req)
fmt.Println("============ stop echo ===========")
buf = bytes.NewBuffer(nil)
io.Copy(buf, w.Body)
fmt.Println("============ jsonapi response from create ===========")
fmt.Println(buf.String())
fmt.Println("============== end raw jsonapi response =============")
@ -240,8 +296,8 @@ func exerciseHandler() {
out := bytes.NewBuffer(nil)
json.NewEncoder(out).Encode(responseBlog)
fmt.Println("\n================ Viola! Converted back our Blog struct =================")
fmt.Printf("%s\n", out.Bytes())
fmt.Println("================ Viola! Converted back our Blog struct =================")
fmt.Println(string(out.Bytes()))
fmt.Println("================ end marshal materialized Blog struct =================")
}

View File

@ -93,31 +93,19 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
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 {
includedMap := make(map[string]*Node)
for _, included := range payload.Included {
key := fmt.Sprintf("%s,%s", included.Type, included.ID)
includedMap[key] = included
}
var models []interface{}
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
}
var models []interface{}
for _, data := range payload.Data {
model := reflect.New(t.Elem())
err := unmarshalNode(data, model, nil)
err := unmarshalNode(data, model, &includedMap)
if err != nil {
return nil, err
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"io"
"reflect"
"strings"
"testing"
"time"
@ -504,6 +505,134 @@ func unmarshalSamplePayload() (*Blog, error) {
return out, nil
}
func TestUnmarshalManyPayload(t *testing.T) {
sample := map[string]interface{}{
"data": []interface{}{
map[string]interface{}{
"type": "posts",
"id": "1",
"attributes": map[string]interface{}{
"body": "First",
"title": "Post",
},
},
map[string]interface{}{
"type": "posts",
"id": "2",
"attributes": map[string]interface{}{
"body": "Second",
"title": "Post",
},
},
},
}
data, err := json.Marshal(sample)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(data)
posts, err := UnmarshalManyPayload(in, reflect.TypeOf(new(Post)))
if err != nil {
t.Fatal(err)
}
if len(posts) != 2 {
t.Fatal("Wrong number of posts")
}
for _, p := range posts {
_, ok := p.(*Post)
if !ok {
t.Fatal("Was expecting a Post")
}
}
}
func TestManyPayload_withLinks(t *testing.T) {
firstPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=50"
prevPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=0"
nextPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=100"
lastPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=500"
sample := map[string]interface{}{
"data": []interface{}{
map[string]interface{}{
"type": "posts",
"id": "1",
"attributes": map[string]interface{}{
"body": "First",
"title": "Post",
},
},
map[string]interface{}{
"type": "posts",
"id": "2",
"attributes": map[string]interface{}{
"body": "Second",
"title": "Post",
},
},
},
"links": map[string]interface{}{
KeyFirstPage: firstPageURL,
KeyPreviousPage: prevPageURL,
KeyNextPage: nextPageURL,
KeyLastPage: lastPageURL,
},
}
data, err := json.Marshal(sample)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(data)
payload := new(ManyPayload)
if err = json.NewDecoder(in).Decode(payload); err != nil {
t.Fatal(err)
}
if payload.Links == nil {
t.Fatal("Was expecting a non nil ptr Link field")
}
links := *payload.Links
first, ok := links[KeyFirstPage]
if !ok {
t.Fatal("Was expecting a non nil ptr Link field")
}
if e, a := firstPageURL, first; e != a {
t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyFirstPage, e, a)
}
prev, ok := links[KeyPreviousPage]
if !ok {
t.Fatal("Was expecting a non nil ptr Link field")
}
if e, a := prevPageURL, prev; e != a {
t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyPreviousPage, e, a)
}
next, ok := links[KeyNextPage]
if !ok {
t.Fatal("Was expecting a non nil ptr Link field")
}
if e, a := nextPageURL, next; e != a {
t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyNextPage, e, a)
}
last, ok := links[KeyLastPage]
if !ok {
t.Fatal("Was expecting a non nil ptr Link field")
}
if e, a := lastPageURL, last; e != a {
t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyLastPage, e, a)
}
}
func samplePayloadWithoutIncluded() map[string]interface{} {
return map[string]interface{}{
"data": map[string]interface{}{