-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Flushing out response objects, simplifying API, more spec checks
- Loading branch information
Derek Dowling
committed
Nov 14, 2015
1 parent
4d142c2
commit ffaf161
Showing
11 changed files
with
517 additions
and
218 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,40 @@ | ||
Go JSON API | ||
Go JSON API Specification Handler | ||
--- | ||
|
||
[![GoDoc](https://godoc.org/github.com/derekdowling/go-json-spec-handler?status.png)](https://godoc.org/github.com/derekdowling/go-json-spec-handler)[![Build Status](https://travis-ci.org/derekdowling/go-json-spec-handler.svg?branch=master)](https://travis-ci.org/derekdowling/go-json-spec-handler) | ||
|
||
Go API helpers for achieving a [JSON API Specification](http://jsonapi.org/) | ||
Go JSON API helpers for achieving a [JSON API Specification](http://jsonapi.org/) | ||
compatible backend. | ||
|
||
## Mission | ||
## Features | ||
|
||
To make writing an API that matches the JSON Specification easy in Go. | ||
Implemented: | ||
|
||
- Handles both single object and array based JSON requests and responses | ||
- Input validation with HTTP 422 Status support via [go-validator](https://github.com/go-validator/validator) | ||
- Client request validation with HTTP 406 Status responses | ||
- Links, Relationship, Meta fields | ||
- Prepackaged error responses, easy to use Internal Service Error builder | ||
- Smart responses with correct HTTP Statuses based on Request Method and HTTP Headers | ||
|
||
TODO: | ||
|
||
- Reserved character checking | ||
|
||
Not Implenting: | ||
|
||
* These features aren't handled because they are beyond the scope of what | ||
this library is meant to be. In the future, I might build a framework | ||
utilizing this library to handle these complex features which require | ||
Router and ORM compatibility. | ||
|
||
- Relationship management | ||
- Sorting | ||
- Pagination | ||
- Filtering | ||
|
||
## Installation | ||
|
||
``` | ||
$ go get github.com/derekdowling/go-jsonapi | ||
$ go get github.com/derekdowling/go-json-spec-handler | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,112 @@ | ||
package jsh | ||
|
||
// Error represents a JSON Specification Error. | ||
// Error.Source.Pointer is used in 422 status responses to indicate validation | ||
// errors on a JSON Object attribute. | ||
import ( | ||
"fmt" | ||
"net/http" | ||
) | ||
|
||
// DefaultError can be customized in order to provide a more customized error | ||
// Detail message when an Internal Server Error occurs. Optionally, you can modify | ||
// a returned jsh.Error before sending it as a response as well. | ||
var DefaultErrorDetail = "Request failed, something went wrong." | ||
|
||
// DefaultTitle can be customized to provide a more customized ISE Title | ||
var DefaultErrorTitle = "Internal Server Error" | ||
|
||
// Error represents a JSON Specification Error. Error.Source.Pointer is used in 422 | ||
// status responses to indicate validation errors on a JSON Object attribute. | ||
// | ||
// ISE (internal server error) captures the server error internally to help with | ||
// logging/troubleshooting, but is never returned in a response. | ||
// | ||
// Once a jsh.Error is returned, and you have logged/handled it accordingly, you | ||
// can simply return it using jsh.SendError() or jsh.SendErrors() | ||
type Error struct { | ||
Title string `json:"title"` | ||
Detail string `json:"detail"` | ||
Status int `json:"status"` | ||
Source struct { | ||
Pointer string `json:"pointer"` | ||
} `json:"source"` | ||
ISE string `json:"-"` | ||
} | ||
|
||
// Prepare returns a response containing a prepared error list since the JSON | ||
// API specification requires that errors are returned as a list | ||
func (e *Error) Prepare(req *http.Request) (*Response, *Error) { | ||
list := &ErrorList{Errors: []*Error{e}} | ||
return list.Prepare(req) | ||
} | ||
|
||
// ErrorList is just a wrapped error array that implements Sendable | ||
type ErrorList struct { | ||
Errors []*Error | ||
} | ||
|
||
// Add first validates the error, and then appends it to the ErrorList | ||
func (e *ErrorList) Add(newError *Error) error { | ||
err := validateError(newError) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
e.Errors = append(e.Errors, newError) | ||
return nil | ||
} | ||
|
||
// Prepare first validates the errors, and then returns an appropriate response | ||
func (e *ErrorList) Prepare(req *http.Request) (*Response, *Error) { | ||
if len(e.Errors) == 0 { | ||
return nil, ISE("No errors provided for attempted error response.") | ||
} | ||
|
||
return &Response{Errors: e.Errors, HTTPStatus: e.Errors[0].Status}, nil | ||
} | ||
|
||
// validateError ensures that the error is ready for a response in it's current state | ||
func validateError(err *Error) error { | ||
|
||
if err.Status < 400 || err.Status > 600 { | ||
return fmt.Errorf("Invalid HTTP Status for error %+v\n", err) | ||
} else if err.Status == 422 && err.Source.Pointer == "" { | ||
return fmt.Errorf("Source Pointer must be set for 422 Status errors") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// ISE is a convenience function for creating a ready-to-go Internal Service Error | ||
// response. As previously mentioned, the Error.ISE field is for logging only, and | ||
// won't be returned to the end user. | ||
func ISE(err string) *Error { | ||
return &Error{ | ||
Title: DefaultErrorTitle, | ||
Detail: DefaultErrorDetail, | ||
Status: http.StatusInternalServerError, | ||
ISE: err, | ||
} | ||
} | ||
|
||
// InputError creates a properly formatted Status 422 error with an appropriate | ||
// user facing message, and a Status Pointer to the first attribute that | ||
func InputError(attribute string, detail string) *Error { | ||
err := &Error{ | ||
Title: "Invalid Attribute", | ||
Detail: detail, | ||
Status: 422, | ||
} | ||
|
||
// Assign this after the fact, easier to do | ||
err.Source.Pointer = fmt.Sprintf("data/attributes/%s", attribute) | ||
|
||
return err | ||
} | ||
|
||
// SpecificationError is used whenever the Client violates the JSON API Spec | ||
func SpecificationError(detail string) *Error { | ||
return &Error{ | ||
Title: "API Specification Error", | ||
Detail: detail, | ||
Status: http.StatusNotAcceptable, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package jsh | ||
|
||
import ( | ||
"log" | ||
"net/http" | ||
"net/http/httptest" | ||
"strconv" | ||
"testing" | ||
|
||
. "github.com/smartystreets/goconvey/convey" | ||
) | ||
|
||
func TestError(t *testing.T) { | ||
|
||
Convey("Error Tests", t, func() { | ||
|
||
writer := httptest.NewRecorder() | ||
request := &http.Request{} | ||
|
||
testError := &Error{ | ||
Status: http.StatusBadRequest, | ||
Title: "Fail", | ||
Detail: "So badly", | ||
} | ||
|
||
Convey("->validateError()", func() { | ||
|
||
Convey("should not fail for a valid Error", func() { | ||
validErr := ISE("Valid Error") | ||
err := validateError(validErr) | ||
So(err, ShouldBeNil) | ||
}) | ||
|
||
Convey("422 Status Formatting", func() { | ||
|
||
testError.Status = 422 | ||
|
||
Convey("should accept a properly formatted 422 error", func() { | ||
testError.Source.Pointer = "data/attributes/test" | ||
err := validateError(testError) | ||
So(err, ShouldBeNil) | ||
}) | ||
|
||
Convey("should error if Source.Pointer isn't set", func() { | ||
err := validateError(testError) | ||
So(err, ShouldNotBeNil) | ||
}) | ||
}) | ||
|
||
Convey("should fail for an out of range HTTP error status", func() { | ||
testError.Status = http.StatusOK | ||
err := validateError(testError) | ||
So(err, ShouldNotBeNil) | ||
}) | ||
}) | ||
|
||
Convey("->Send()", func() { | ||
|
||
testErrors := &ErrorList{Errors: []*Error{&Error{ | ||
Status: http.StatusForbidden, | ||
Title: "Forbidden", | ||
Detail: "Can't Go Here", | ||
}, testError}} | ||
|
||
Convey("->Send(Error)", func() { | ||
err := Send(request, writer, testError) | ||
So(err, ShouldBeNil) | ||
log.Printf("err = %+v\n", err) | ||
log.Printf("writer = %+v\n", writer) | ||
So(writer.Code, ShouldEqual, http.StatusBadRequest) | ||
}) | ||
|
||
Convey("should send a properly formatted JSON Error", func() { | ||
err := Send(request, writer, testErrors) | ||
|
||
So(err, ShouldBeNil) | ||
So(writer.Code, ShouldEqual, http.StatusForbidden) | ||
|
||
contentLength, convErr := strconv.Atoi(writer.HeaderMap.Get("Content-Length")) | ||
So(convErr, ShouldBeNil) | ||
So(contentLength, ShouldBeGreaterThan, 0) | ||
So(writer.HeaderMap.Get("Content-Type"), ShouldEqual, ContentType) | ||
}) | ||
|
||
}) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package jsh | ||
|
||
// Links is a top-level document field | ||
type Links struct { | ||
Self *Link `json:"self,omitempty"` | ||
Related *Link `json:"related,omitempty"` | ||
} | ||
|
||
// Link is a JSON format type | ||
type Link struct { | ||
HREF string `json:"href,omitempty"` | ||
Meta map[string]interface{} `json:"meta,omitempty"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package jsh | ||
|
||
import "net/http" | ||
|
||
// List is just a wrapper around an object array that implements Sendable | ||
type List struct { | ||
Objects []*Object `json:"data"` | ||
} | ||
|
||
// Prepare returns a success status response | ||
func (l List) Prepare(r *http.Request) (*Response, *Error) { | ||
return &Response{Data: l.Objects, HTTPStatus: http.StatusOK}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package jsh | ||
|
||
import ( | ||
"encoding/json" | ||
"net/http" | ||
"net/http/httptest" | ||
"strconv" | ||
"testing" | ||
|
||
. "github.com/smartystreets/goconvey/convey" | ||
) | ||
|
||
func TestList(t *testing.T) { | ||
|
||
Convey("List Tests", t, func() { | ||
|
||
testObject := &Object{ | ||
ID: "ID123", | ||
Type: "testConversion", | ||
Attributes: json.RawMessage(`{"foo":"bar"}`), | ||
} | ||
|
||
testList := &List{Objects: []*Object{testObject}} | ||
req := &http.Request{} | ||
|
||
Convey("->Prepare()", func() { | ||
response, err := testList.Prepare(req) | ||
So(err, ShouldBeNil) | ||
So(response.HTTPStatus, ShouldEqual, http.StatusOK) | ||
}) | ||
|
||
Convey("->Send(list)", func() { | ||
|
||
Convey("should send a properly formatted List response", func() { | ||
|
||
writer := httptest.NewRecorder() | ||
err := Send(req, writer, testList) | ||
|
||
So(err, ShouldBeNil) | ||
So(writer.Code, ShouldEqual, http.StatusOK) | ||
contentLength, convErr := strconv.Atoi(writer.HeaderMap.Get("Content-Length")) | ||
So(convErr, ShouldBeNil) | ||
So(contentLength, ShouldBeGreaterThan, 0) | ||
So(writer.HeaderMap.Get("Content-Type"), ShouldEqual, ContentType) | ||
|
||
closer := createIOCloser(writer.Body.Bytes()) | ||
responseList, err := ParseList(closer) | ||
So(len(responseList), ShouldEqual, 1) | ||
}) | ||
}) | ||
}) | ||
} |
Oops, something went wrong.