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,
|
only. If you want to serialize many records, see,
|
||||||
[MarshalManyPayload](#marshalmanypayload).
|
[MarshalManyPayload](#marshalmanypayload).
|
||||||
|
|
||||||
#### Handler Example Code
|
##### Handler Example Code
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func CreateBlog(w http.ResponseWriter, r *http.Request) {
|
func CreateBlog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -270,7 +270,6 @@ blogInterface := make([]interface{}, len(blogs))
|
||||||
for i, blog := range blogs {
|
for i, blog := range blogs {
|
||||||
blogInterface[i] = blog
|
blogInterface[i] = blog
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can insert your `Blog`s into a slice of `interface{}`
|
Alternatively, you can insert your `Blog`s into a slice of `interface{}`
|
||||||
|
@ -283,7 +282,7 @@ this,
|
||||||
func FetchBlogs() ([]interface{}, error)
|
func FetchBlogs() ([]interface{}, error)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Handler Example Code
|
##### Handler Example Code
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func ListBlogs(w http.ResponseWriter, r *http.Request) {
|
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
|
### 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:
|
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"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"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() {
|
func main() {
|
||||||
jsonapi.Instrumentation = func(r *jsonapi.Runtime, eventType jsonapi.Event, callGUID string, dur time.Duration) {
|
jsonapi.Instrumentation = func(r *jsonapi.Runtime, eventType jsonapi.Event, callGUID string, dur time.Duration) {
|
||||||
metricPrefix := r.Value("instrument").(string)
|
metricPrefix := r.Value("instrument").(string)
|
||||||
|
@ -101,6 +129,8 @@ func main() {
|
||||||
|
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
createBlog(w, r)
|
createBlog(w, r)
|
||||||
|
} else if r.Method == http.MethodPut {
|
||||||
|
echoBlogs(w, r)
|
||||||
} else if r.FormValue("id") != "" {
|
} else if r.FormValue("id") != "" {
|
||||||
showBlog(w, r)
|
showBlog(w, r)
|
||||||
} else {
|
} else {
|
||||||
|
@ -207,7 +237,7 @@ func exerciseHandler() {
|
||||||
|
|
||||||
jsonReply, _ = ioutil.ReadAll(w.Body)
|
jsonReply, _ = ioutil.ReadAll(w.Body)
|
||||||
|
|
||||||
fmt.Println("\n============ jsonapi response from show ===========")
|
fmt.Println("============ jsonapi response from show ===========")
|
||||||
fmt.Println(string(jsonReply))
|
fmt.Println(string(jsonReply))
|
||||||
fmt.Println("============== end raw jsonapi from show =============")
|
fmt.Println("============== end raw jsonapi from show =============")
|
||||||
|
|
||||||
|
@ -229,7 +259,33 @@ func exerciseHandler() {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
io.Copy(buf, w.Body)
|
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(buf.String())
|
||||||
fmt.Println("============== end raw jsonapi response =============")
|
fmt.Println("============== end raw jsonapi response =============")
|
||||||
|
|
||||||
|
@ -240,8 +296,8 @@ func exerciseHandler() {
|
||||||
out := bytes.NewBuffer(nil)
|
out := bytes.NewBuffer(nil)
|
||||||
json.NewEncoder(out).Encode(responseBlog)
|
json.NewEncoder(out).Encode(responseBlog)
|
||||||
|
|
||||||
fmt.Println("\n================ Viola! Converted back our Blog struct =================")
|
fmt.Println("================ Viola! Converted back our Blog struct =================")
|
||||||
fmt.Printf("%s\n", out.Bytes())
|
fmt.Println(string(out.Bytes()))
|
||||||
fmt.Println("================ end marshal materialized Blog struct =================")
|
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
|
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 {
|
if payload.Included != nil {
|
||||||
includedMap := make(map[string]*Node)
|
|
||||||
for _, included := range payload.Included {
|
for _, included := range payload.Included {
|
||||||
key := fmt.Sprintf("%s,%s", included.Type, included.ID)
|
key := fmt.Sprintf("%s,%s", included.Type, included.ID)
|
||||||
includedMap[key] = included
|
includedMap[key] = included
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var models []interface{}
|
|
||||||
for _, data := range payload.Data {
|
for _, data := range payload.Data {
|
||||||
model := reflect.New(t.Elem())
|
model := reflect.New(t.Elem())
|
||||||
err := unmarshalNode(data, model, &includedMap)
|
err := unmarshalNode(data, model, &includedMap)
|
||||||
|
@ -110,20 +112,6 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
|
||||||
models = append(models, model.Interface())
|
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)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
models = append(models, model.Interface())
|
|
||||||
}
|
|
||||||
|
|
||||||
return models, nil
|
return models, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
129
request_test.go
129
request_test.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -504,6 +505,134 @@ func unmarshalSamplePayload() (*Blog, error) {
|
||||||
return out, nil
|
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{} {
|
func samplePayloadWithoutIncluded() map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"data": map[string]interface{}{
|
"data": map[string]interface{}{
|
||||||
|
|
Loading…
Reference in New Issue