Added tests for the REST endpoints exposed in our example. Added a script for running the example app. Split the example into multiple files.

This commit is contained in:
Aren Patel 2017-02-16 18:47:43 -08:00
parent 4898d2ff2a
commit 6958c3a8be
6 changed files with 326 additions and 218 deletions

View File

@ -8,98 +8,11 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"regexp"
"strconv"
"time" "time"
"github.com/google/jsonapi" "github.com/google/jsonapi"
) )
func createBlog(w http.ResponseWriter, r *http.Request) {
jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.create")
blog := new(Blog)
if err := jsonapiRuntime.UnmarshalPayload(r.Body, blog); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ...do stuff with your blog...
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", jsonapi.MediaType)
if err := jsonapiRuntime.MarshalOnePayload(w, blog); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func listBlogs(w http.ResponseWriter, r *http.Request) {
jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list")
// ...fetch your blogs, filter, offset, limit, etc...
// but, for now
blogs := testBlogsForList()
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 showBlog(w http.ResponseWriter, r *http.Request) {
id := r.FormValue("id")
// ...fetch your blog...
intID, err := strconv.Atoi(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.show")
// but, for now
blog := testBlogForCreate(intID)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", jsonapi.MediaType)
if err := jsonapiRuntime.MarshalOnePayload(w, blog); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
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)
@ -121,91 +34,11 @@ func main() {
} }
} }
http.HandleFunc("/blogs", func(w http.ResponseWriter, r *http.Request) { exampleHandler := &ExampleHandler{}
if !regexp.MustCompile(`application/vnd\.api\+json`).Match([]byte(r.Header.Get("Accept"))) { http.HandleFunc("/blogs", exampleHandler.ServeHTTP)
http.Error(w, "Unsupported Media Type", http.StatusUnsupportedMediaType)
return
}
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 {
listBlogs(w, r)
}
})
exerciseHandler() exerciseHandler()
} }
func testBlogForCreate(i int) *Blog {
return &Blog{
ID: 1 * i,
Title: "Title 1",
CreatedAt: time.Now(),
Posts: []*Post{
&Post{
ID: 1 * i,
Title: "Foo",
Body: "Bar",
Comments: []*Comment{
&Comment{
ID: 1 * i,
Body: "foo",
},
&Comment{
ID: 2 * i,
Body: "bar",
},
},
},
&Post{
ID: 2 * i,
Title: "Fuubar",
Body: "Bas",
Comments: []*Comment{
&Comment{
ID: 1 * i,
Body: "foo",
},
&Comment{
ID: 3 * i,
Body: "bas",
},
},
},
},
CurrentPost: &Post{
ID: 1 * i,
Title: "Foo",
Body: "Bar",
Comments: []*Comment{
&Comment{
ID: 1 * i,
Body: "foo",
},
&Comment{
ID: 2 * i,
Body: "bar",
},
},
},
}
}
func testBlogsForList() []interface{} {
blogs := make([]interface{}, 0, 10)
for i := 0; i < 10; i++ {
blogs = append(blogs, testBlogForCreate(i))
}
return blogs
}
func exerciseHandler() { func exerciseHandler() {
// list // list
req, _ := http.NewRequest(http.MethodGet, "/blogs", nil) req, _ := http.NewRequest(http.MethodGet, "/blogs", nil)
@ -242,7 +75,7 @@ func exerciseHandler() {
fmt.Println("============== end raw jsonapi from show =============") fmt.Println("============== end raw jsonapi from show =============")
// create // create
blog := testBlogForCreate(1) blog := fixtureBlogCreate(1)
in := bytes.NewBuffer(nil) in := bytes.NewBuffer(nil)
jsonapi.MarshalOnePayloadEmbedded(in, blog) jsonapi.MarshalOnePayloadEmbedded(in, blog)
@ -265,9 +98,9 @@ func exerciseHandler() {
// echo // echo
blogs := []interface{}{ blogs := []interface{}{
testBlogForCreate(1), fixtureBlogCreate(1),
testBlogForCreate(2), fixtureBlogCreate(2),
testBlogForCreate(3), fixtureBlogCreate(3),
} }
in = bytes.NewBuffer(nil) in = bytes.NewBuffer(nil)
jsonapi.MarshalManyPayload(in, blogs) jsonapi.MarshalManyPayload(in, blogs)
@ -300,48 +133,3 @@ func exerciseHandler() {
fmt.Println(string(out.Bytes())) fmt.Println(string(out.Bytes()))
fmt.Println("================ end marshal materialized Blog struct =================") fmt.Println("================ end marshal materialized Blog struct =================")
} }
type Blog struct {
ID int `jsonapi:"primary,blogs"`
Title string `jsonapi:"attr,title"`
Posts []*Post `jsonapi:"relation,posts"`
CurrentPost *Post `jsonapi:"relation,current_post"`
CurrentPostID int `jsonapi:"attr,current_post_id"`
CreatedAt time.Time `jsonapi:"attr,created_at"`
ViewCount int `jsonapi:"attr,view_count"`
}
type Post struct {
ID int `jsonapi:"primary,posts"`
BlogID int `jsonapi:"attr,blog_id"`
Title string `jsonapi:"attr,title"`
Body string `jsonapi:"attr,body"`
Comments []*Comment `jsonapi:"relation,comments"`
}
type Comment struct {
ID int `jsonapi:"primary,comments"`
PostID int `jsonapi:"attr,post_id"`
Body string `jsonapi:"attr,body"`
}
// Blog Links
func (blog Blog) JSONAPILinks() *map[string]interface{} {
return &map[string]interface{}{
"self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID),
}
}
func (blog Blog) JSONAPIRelationshipLinks(relation string) *map[string]interface{} {
if relation == "posts" {
return &map[string]interface{}{
"related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID),
}
}
if relation == "current_post" {
return &map[string]interface{}{
"related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID),
}
}
return nil
}

66
examples/fixtures.go Normal file
View File

@ -0,0 +1,66 @@
package main
import "time"
func fixtureBlogCreate(i int) *Blog {
return &Blog{
ID: 1 * i,
Title: "Title 1",
CreatedAt: time.Now(),
Posts: []*Post{
&Post{
ID: 1 * i,
Title: "Foo",
Body: "Bar",
Comments: []*Comment{
&Comment{
ID: 1 * i,
Body: "foo",
},
&Comment{
ID: 2 * i,
Body: "bar",
},
},
},
&Post{
ID: 2 * i,
Title: "Fuubar",
Body: "Bas",
Comments: []*Comment{
&Comment{
ID: 1 * i,
Body: "foo",
},
&Comment{
ID: 3 * i,
Body: "bas",
},
},
},
},
CurrentPost: &Post{
ID: 1 * i,
Title: "Foo",
Body: "Bar",
Comments: []*Comment{
&Comment{
ID: 1 * i,
Body: "foo",
},
&Comment{
ID: 2 * i,
Body: "bar",
},
},
},
}
}
func fixtureBlogsList() (blogs []interface{}) {
for i := 0; i < 10; i++ {
blogs = append(blogs, fixtureBlogCreate(i))
}
return blogs
}

113
examples/handler.go Normal file
View File

@ -0,0 +1,113 @@
package main
import (
"net/http"
"strconv"
"github.com/google/jsonapi"
)
const (
headerAccept = "Accept"
headerContentType = "Content-Type"
)
// ExampleHandler is the handler we are using to demonstrate building an HTTP
// server with the jsonapi library.
type ExampleHandler struct{}
func (h *ExampleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(headerAccept) != jsonapi.MediaType {
http.Error(w, "Unsupported Media Type", http.StatusUnsupportedMediaType)
}
var methodHandler http.HandlerFunc
switch r.Method {
case http.MethodPost:
methodHandler = h.createBlog
case http.MethodPut:
methodHandler = h.echoBlogs
case http.MethodGet:
if r.FormValue("id") != "" {
methodHandler = h.showBlog
} else {
methodHandler = h.listBlogs
}
default:
http.Error(w, "Not Found", http.StatusNotFound)
return
}
methodHandler(w, r)
}
func (h *ExampleHandler) createBlog(w http.ResponseWriter, r *http.Request) {
jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.create")
blog := new(Blog)
if err := jsonapiRuntime.UnmarshalPayload(r.Body, blog); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ...do stuff with your blog...
w.WriteHeader(http.StatusCreated)
w.Header().Set(headerContentType, jsonapi.MediaType)
if err := jsonapiRuntime.MarshalOnePayload(w, blog); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (h *ExampleHandler) echoBlogs(w http.ResponseWriter, r *http.Request) {
jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list")
// ...fetch your blogs, filter, offset, limit, etc...
// but, for now
blogs := fixtureBlogsList()
w.WriteHeader(http.StatusOK)
w.Header().Set(headerContentType, jsonapi.MediaType)
if err := jsonapiRuntime.MarshalManyPayload(w, blogs); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (h *ExampleHandler) showBlog(w http.ResponseWriter, r *http.Request) {
id := r.FormValue("id")
// ...fetch your blog...
intID, err := strconv.Atoi(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.show")
// but, for now
blog := fixtureBlogCreate(intID)
w.WriteHeader(http.StatusOK)
w.Header().Set(headerContentType, jsonapi.MediaType)
if err := jsonapiRuntime.MarshalOnePayload(w, blog); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (h *ExampleHandler) listBlogs(w http.ResponseWriter, r *http.Request) {
jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list")
// ...fetch your blogs, filter, offset, limit, etc...
// but, for now
blogs := fixtureBlogsList()
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)
}
}

86
examples/handler_test.go Normal file
View File

@ -0,0 +1,86 @@
package main
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/google/jsonapi"
)
func TestExampleHandler_post(t *testing.T) {
blog := fixtureBlogCreate(1)
requestBody := bytes.NewBuffer(nil)
jsonapi.MarshalOnePayloadEmbedded(requestBody, blog)
r, err := http.NewRequest(http.MethodPost, "/blogs?id=1", requestBody)
if err != nil {
t.Fatal(err)
}
r.Header.Set(headerAccept, jsonapi.MediaType)
rr := httptest.NewRecorder()
handler := &ExampleHandler{}
handler.ServeHTTP(rr, r)
if e, a := http.StatusCreated, rr.Code; e != a {
t.Fatalf("Expected a status of %d, got %d", e, a)
}
}
func TestExampleHandler_put(t *testing.T) {
blogs := []interface{}{
fixtureBlogCreate(1),
fixtureBlogCreate(2),
fixtureBlogCreate(3),
}
requestBody := bytes.NewBuffer(nil)
jsonapi.MarshalManyPayload(requestBody, blogs)
r, err := http.NewRequest(http.MethodPut, "/blogs", requestBody)
if err != nil {
t.Fatal(err)
}
r.Header.Set(headerAccept, jsonapi.MediaType)
rr := httptest.NewRecorder()
handler := &ExampleHandler{}
handler.ServeHTTP(rr, r)
if e, a := http.StatusOK, rr.Code; e != a {
t.Fatalf("Expected a status of %d, got %d", e, a)
}
}
func TestExampleHandler_get_show(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "/blogs?id=1", nil)
if err != nil {
t.Fatal(err)
}
r.Header.Set(headerAccept, jsonapi.MediaType)
rr := httptest.NewRecorder()
handler := &ExampleHandler{}
handler.ServeHTTP(rr, r)
if e, a := http.StatusOK, rr.Code; e != a {
t.Fatalf("Expected a status of %d, got %d", e, a)
}
}
func TestExampleHandler_get_list(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "/blogs", nil)
if err != nil {
t.Fatal(err)
}
r.Header.Set(headerAccept, jsonapi.MediaType)
rr := httptest.NewRecorder()
handler := &ExampleHandler{}
handler.ServeHTTP(rr, r)
if e, a := http.StatusOK, rr.Code; e != a {
t.Fatalf("Expected a status of %d, got %d", e, a)
}
}

51
examples/models.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"fmt"
"time"
)
type Blog struct {
ID int `jsonapi:"primary,blogs"`
Title string `jsonapi:"attr,title"`
Posts []*Post `jsonapi:"relation,posts"`
CurrentPost *Post `jsonapi:"relation,current_post"`
CurrentPostID int `jsonapi:"attr,current_post_id"`
CreatedAt time.Time `jsonapi:"attr,created_at"`
ViewCount int `jsonapi:"attr,view_count"`
}
type Post struct {
ID int `jsonapi:"primary,posts"`
BlogID int `jsonapi:"attr,blog_id"`
Title string `jsonapi:"attr,title"`
Body string `jsonapi:"attr,body"`
Comments []*Comment `jsonapi:"relation,comments"`
}
type Comment struct {
ID int `jsonapi:"primary,comments"`
PostID int `jsonapi:"attr,post_id"`
Body string `jsonapi:"attr,body"`
}
// Blog Links
func (blog Blog) JSONAPILinks() *map[string]interface{} {
return &map[string]interface{}{
"self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID),
}
}
func (blog Blog) JSONAPIRelationshipLinks(relation string) *map[string]interface{} {
if relation == "posts" {
return &map[string]interface{}{
"related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID),
}
}
if relation == "current_post" {
return &map[string]interface{}{
"related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID),
}
}
return nil
}

4
script/example Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
set -e
go run examples/app.go examples/handler.go examples/fixtures.go examples/models.go