forked from Mirrors/jsonapi
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:
parent
9d919b42a6
commit
3ea9ec4904
43
README.md
43
README.md
|
@ -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:
|
||||
|
|
|
@ -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 =================")
|
||||
}
|
||||
|
||||
|
|
20
request.go
20
request.go
|
@ -93,14 +93,16 @@ 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)
|
||||
|
@ -113,20 +115,6 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
|
|||
return models, nil
|
||||
}
|
||||
|
||||
var models []interface{}
|
||||
|
||||
for _, data := range payload.Data {
|
||||
model := reflect.New(t.Elem())
|
||||
err := unmarshalNode(data, model, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
models = append(models, model.Interface())
|
||||
}
|
||||
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
|
|
129
request_test.go
129
request_test.go
|
@ -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{}{
|
||||
|
|
Loading…
Reference in New Issue