jsh

package module
v0.0.0-...-42497ca Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 19, 2016 License: MIT Imports: 13 Imported by: 5

README

A Go JSONAPI Specification Handler

GoDoc Travis CI Go Report Card TestCoverage

A (de)serialization handler for writing JSON API Specification compatible software in Go. Works with Ember/Ember-Data too!

Contents

  1. JSH
  1. JSC
  2. JSH-API

jsh - JSON Specification Handler

For streamlined JSONAPI object serialization. Uses govalidator for input validation.

import github.com/derekdowling/go-json-spec-handler

type User struct {
  // valid from github.com/asaskevich/govalidator gives us input validation
  // when object.Unmarshal() is invoked on this type
  Name string `json:"name" valid:"alphanum"`
}

// example http.HandlerFunc for PATCH /users/1
func PatchUser(w http.ResponseWriter, r *http.Request) {

  // performs Specification checks against the request
  object, err := jsh.ParseObject(r)
  if err != nil {
    // jsh returns API friendly errors, that are easy to respond with
    jsh.Send(w, r, err)
    return
  }

  // use object.ID to look up user/do business logic
  user := &User{}
  
  // unmarshal data into relevant internal types if govalidator passes, otherwise
  // return the pre-formatted HTTP 422 error to signify how the input failed
  err = object.Unmarshal("users", user)
  if err != nil {
    jsh.Send(w, r, err)
    return
  }

  // modify your resource object
  user.Name = "Bob"

  // repackage and send the updated resource as a response
  err = object.Marshal(user)
  if err != nil {
    jsh.Send(w, r, err)
    return
  }

  jsh.Send(w, r, object)
}

Motivation for JSH

JSH was written for tackling the issue of dealing with Ember-Data within a pre-existing API server. In sticking with Go's philosophy of modules over frameworks, it is intended to be a drop in serialization layer focusing only on parsing, validating, and sending JSONAPI compatible responses.

Features

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
- HTTP Client for GET, POST, DELETE, PATCH

TODO:

- [Reserved character checking](http://jsonapi.org/format/upcoming/#document-member-names-reserved-characters)

Not Implementing:

* These features aren't handled because they are beyond the scope of what
  this module is meant to achieve. See [jshapi](https://github.com/EtixLabs/jsh-api)
  for a full-fledged API solution that solves many of these problems.

- Routing
- Sorting
- Pagination
- Filtering
- ORM

Stability

jsh has a mostly stabilized core data document model. At this point in time I am not yet ready to declare v1, but am actively trying to avoid breaking the public API. The areas most likely to receive improvement include relationship, link, and metadata management. At this point in time I can confidentally suggest you use jsh without risking major upgrade incompatibility going forward!

jsc - JSON Specification Client

A HTTP JSONAPI Client for making outbound server requests. Built on top of http.Client and jsh:

import github.com/derekdowling/go-json-spec-handler/client

// GET http://your.api/users/1
document, resp, err := jsc.Fetch("http://your.api/", "users", "1")
object := doc.First()

user := &yourUser{}
err := object.Unmarshal("users", user)

JSH-API

If you're looking for a good place to start with a new API, I've since created jshapi which builds on top of Goji 2 and jsh in order to handle the routing structure that JSON API requires as well as a number of other useful tools for testing and mocking APIs as you develop your own projects. JSHAPI is similar in spirit to this project as it inteniontally focuses on eliminating boilerplate from your projects without trying to solve all of the world's problems.

Examples

There are lots of great examples in the tests themselves that show exactly how jsh works. The godocs as linked above have a number of examples in them as well.

Documentation

Overview

Package jsh (JSON API Specification Handler) makes it easy to parse JSON API requests and send responses that match the JSON API Specification: http://jsonapi.org/ from your server.

For a request client, see: jsc: https://godoc.org/github.com/EtixLabs/go-json-spec-handler/client

For a full http.Handler API builder see jshapi: https://godoc.org/github.com/EtixLabs/jsh-api

Index

Constants

View Source
const (
	// ContentType is the data encoding of choice for HTTP Request and Response Headers
	ContentType = "application/vnd.api+json"
)
View Source
const JSONAPIVersion = "1.1"

JSONAPIVersion is version of JSON API Spec that is currently compatible: http://jsonapi.org/format/1.1/

Variables

View Source
var DefaultErrorDetail = "Request failed, something went wrong"

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.

View Source
var DefaultErrorTitle = "Internal Server Error"

DefaultTitle can be customized to provide a more customized ISE Title

View Source
var IncludeJSONAPIVersion = true

IncludeJSONAPIVersion is an option that allows consumers to include/remove the `jsonapi` top-level member from server responses.

Functions

func AttributePointer

func AttributePointer(attribute string) string

AttributePointer returns a JSON pointer to the given attribute in a JSON API document.

func CreateReadCloser

func CreateReadCloser(data []byte) io.ReadCloser

CreateReadCloser is a helper function for dealing with creating HTTP requests

func NewObject

func NewObject(id string, resourceType string, attributes interface{}) (*Object, *Error)

NewObject prepares a new JSON Object for an API response. Whatever is provided as attributes will be marshalled to JSON.

func ParseDoc

func ParseDoc(r *http.Request, mode DocumentMode) (*Document, *Error)

ParseDoc parses and returns a top level jsh.Document. In most cases, using "ParseList" or "ParseObject" is preferable.

func ParseList

func ParseList(r *http.Request) (List, *Error)

ParseList validates the HTTP request and returns a list of resource objects parsed from the request Body. Use just like ParseObject.

func ParseObject

func ParseObject(r *http.Request) (*Object, *Error)

ParseObject validates the HTTP request and returns a JSON object for a given io.ReadCloser containing a raw JSON payload. Here's an example of how to use it as part of your full flow.

func Handler(w http.ResponseWriter, r *http.Request) {
	obj, error := jsh.ParseObject(r)
	if error != nil {
		// log your error
		err := jsh.Send(w, r, error)
		return
	}

	yourType := &YourType{}

	err := object.Unmarshal("yourtype", &yourType)
	if err != nil {
		err := jsh.Send(w, r, err)
		return
	}

	yourType.ID = obj.ID
	// do business logic

	err := object.Marshal(yourType)
	if err != nil {
		// log error
		err := jsh.Send(w, r, err)
		return
	}

	err := jsh.Send(w, r, object)
}

func ParseRelationship

func ParseRelationship(r *http.Request) (*IDObject, *Error)

ParseRelationship validates the HTTP request and returns a relationship object. Use just like ParseObject.

func ParseRelationshipList

func ParseRelationshipList(r *http.Request) (IDList, *Error)

ParseRelationshipList validates the HTTP request and returns a list of relationship objects parsed from the request Body. Use just like ParseList.

func RelationshipPointer

func RelationshipPointer(relationship string) string

RelationshipPointer returns a JSON pointer to the given primary resource relationship in a JSON API document.

Types

type Document

type Document struct {
	Data List `json:"data"`
	// Object   *Object     `json:"-"`
	Errors   ErrorList   `json:"errors,omitempty"`
	Links    *Links      `json:"links,omitempty"`
	Included []*Object   `json:"included,omitempty"`
	Meta     interface{} `json:"meta,omitempty"`
	JSONAPI  *JSONAPI    `json:"jsonapi,omitempty"`
	// Status is an HTTP Status Code
	Status int `json:"-"`
	// DataMode to enforce for the document
	Mode DocumentMode `json:"-"`
	// contains filtered or unexported fields
}

Document represents a top level JSON formatted Document. Refer to the JSON API Specification for a full descriptor of each attribute: http://jsonapi.org/format/#document-structure

func Build

func Build(payload Sendable) *Document

Build creates a Sendable Document with the provided sendable payload, either Data or errors. Build also assumes you've already validated your data with .Validate() so it should be used carefully.

func New

func New() *Document

New instantiates a new JSON Document object.

func Ok

func Ok() *Document

Ok makes it simple to return a 200 OK response via jsh:

jsh.Send(w, r, jsh.Ok())

func (*Document) AddError

func (d *Document) AddError(newErr *Error) *Error

AddError adds an error to the Document. It will also set the document Mode to "ErrorMode" if not done so already.

func (*Document) AddObject

func (d *Document) AddObject(object *Object) *Error

AddObject adds another object to the JSON Document.

func (*Document) Error

func (d *Document) Error() string

Error implements error for the Document type.

func (*Document) First

func (d *Document) First() *Object

First will return the first object from the document data if possible.

func (*Document) HasData

func (d *Document) HasData() bool

HasData will return true if the JSON document's Data field is set

func (*Document) HasErrors

func (d *Document) HasErrors() bool

HasErrors will return true if the Errors attribute is not nil.

func (*Document) MarshalJSON

func (d *Document) MarshalJSON() ([]byte, error)

MarshalJSON handles the custom serialization case caused by case where the "data" element of a document might be either a single resource object, or a collection of them.

func (*Document) Validate

func (d *Document) Validate(r *http.Request, isResponse bool) *Error

Validate performs document level checks against the JSONAPI specification. It is assumed that if this call returns without an error, your document is valid and can be sent as a request or response.

type DocumentMode

type DocumentMode int

DocumentMode allows different specification settings to be enforced based on the specified mode.

const (
	// ObjectMode enforces fetch request/response specifications
	ObjectMode DocumentMode = iota
	// ListMode enforces listing request/response specifications
	ListMode
	// ErrorMode enforces error response specifications
	ErrorMode
)

type Error

type Error struct {
	Status int          `json:"status,string"`
	Code   string       `json:"code,omitempty"`
	Title  string       `json:"title,omitempty"`
	Detail string       `json:"detail,omitempty"`
	Source *ErrorSource `json:"source,omitempty"`
	ISE    string       `json:"-"`
}

Error consists of a number of contextual attributes to make conveying certain error type simpler as per the JSON API specification: http://jsonapi.org/format/#error-objects

error := &jsh.Error{
	Title: "Authentication Failure",
	Detail: "Category 4 Username Failure",
	Status: 401
}

jsh.Send(w, r, error)

func BadRequestError

func BadRequestError(msg string, detail string) *Error

BadRequestError is a convenience function to return a 400 Bad Request response.

func ConflictError

func ConflictError(resourceType string, id string) *Error

ConflictError returns a 409 Conflict error.

func ForbiddenError

func ForbiddenError(msg string) *Error

ForbiddenError is used whenever an attempt to do a forbidden operation is made.

func ISE

func ISE(internalMessage string) *Error

ISE is a convenience function for creating a ready-to-go Internal Service Error response. The message you pass in is set to the ErrorObject.ISE attribute so you can gracefully log ISE's internally before sending them.

func InputError

func InputError(msg string, attribute string) *Error

InputError creates a properly formatted HTTP Status 422 error with an appropriate user safe message. The parameter "attribute" will format err.Source.Pointer to be "/data/attributes/<attribute>".

func NotFound

func NotFound(resourceType string, id string) *Error

NotFound returns a 404 formatted error.

func NotImplemented

func NotImplemented(internalMessage string) *Error

NotImplemented is a convenience function similar to ISE except if generates a 501 response.

func ParameterError

func ParameterError(msg string, param string) *Error

ParameterError creates a properly formatted HTTP Status 400 error with an appropriate user safe message. The err.Source.Parameter field will be set to the parameter "param".

func RelationshipError

func RelationshipError(msg string, relationship string) *Error

RelationshipError creates a properly formatted HTTP Status 422 error with an appropriate user safe message. The parameter "relationship" will format err.Source.Pointer to be "/data/relationship/<attribute>".

func Send

func Send(w http.ResponseWriter, r *http.Request, payload Sendable) *Error

Send will respond with the given JSON payload to the client. If the payload response validation fails, it will respond with the validation error and will return it. Send is designed to always send a response, but will also return the last error it encountered to help with debugging in the event of an Internal Server Error.

func SpecificationError

func SpecificationError(detail string) *Error

SpecificationError returnss a 406 Not Acceptable. It is used whenever the Client violates the JSON API Spec.

func TopLevelError

func TopLevelError(field string) *Error

TopLevelError is used whenever the client sends a JSON payload with a missing top-level field.

func (*Error) Error

func (e *Error) Error() string

Error will print an internal server error if set, or default back to the SafeError() format if not. As usual, err.Error() should not be considered safe for presentation to the end user, use err.SafeError() instead.

func (*Error) StatusCode

func (e *Error) StatusCode() int

StatusCode (HTTP) for the error. Defaults to 0.

func (*Error) Validate

func (e *Error) Validate(r *http.Request, response bool) *Error

Validate ensures that the error meets all JSON API criteria.

type ErrorList

type ErrorList []*Error

ErrorList is wraps an Error Array so that it can implement Sendable

func (ErrorList) Error

func (e ErrorList) Error() string

Fulfills the default error interface

func (ErrorList) StatusCode

func (e ErrorList) StatusCode() int

StatusCode (HTTP) of the first error in the list. Defaults to 0 if the list is empty or one has not yet been set for the first error.

func (ErrorList) Validate

func (e ErrorList) Validate(r *http.Request, response bool) *Error

Validate checks all errors within the list to ensure that they are valid

type ErrorSource

type ErrorSource struct {
	Pointer   string `json:"pointer,omitempty"`
	Parameter string `json:"parameter,omitempty"`
}

ErrorSource represents the source of a JSONAPI error, either by a pointer or a query parameter name.

type ErrorType

type ErrorType interface {
	// Error returns a formatted error and allows it to conform to the stdErr
	// interface.
	Error() string
	// Validate checks that the error is valid in the context of JSONAPI
	Validate(r *http.Request, response bool) *Error
	// StatusCode returns the first encountered HTTP Status Code for the error type.
	// Returns 0 if none is set.
	StatusCode() int
}

ErrorType represents the common interface requirements that libraries may specify if they would like to accept either a single error or a list.

type IDList

type IDList []*IDObject

IDList is a wrapper around a resource identifier slice that implements Sendable and Unmarshaler. IDList also implements sort.Interface for []*IDObject based on the ID field.

func (IDList) Len

func (l IDList) Len() int

func (IDList) Less

func (l IDList) Less(i, j int) bool

func (IDList) Swap

func (l IDList) Swap(i, j int)

func (IDList) ToList

func (list IDList) ToList() List

ToList returns a list with a new object instance for each resource object.

func (*IDList) UnmarshalJSON

func (l *IDList) UnmarshalJSON(data []byte) error

UnmarshalJSON allows us to manually decode a the resource linkage via the json.Unmarshaler interface.

func (IDList) Validate

func (list IDList) Validate(r *http.Request, response bool) *Error

Validate ensures that the relationship list is JSON API compatible.

type IDObject

type IDObject struct {
	Type string `json:"type" valid:"required"`
	ID   string `json:"id" valid:"required"`
}

IDObject identifies an individual resource.

func NewIDObject

func NewIDObject(tp, id string) *IDObject

NewIDObject creates a new resource identifier object instance.

func (*IDObject) ToObject

func (obj *IDObject) ToObject() *Object

ToObject returns a new object instance from the given resource object.

func (*IDObject) Validate

func (obj *IDObject) Validate(r *http.Request, response bool) *Error

Validate ensures that the relationship is JSON API compatible.

type JSONAPI

type JSONAPI struct {
	Version string `json:"version"`
}

JSONAPI is the top-level member of a JSONAPI document that includes the server compatible version of the JSONAPI specification.

type Link struct {
	HREF string                 `json:"href,omitempty"`
	Meta map[string]interface{} `json:"meta,omitempty"`
}

Link is a resource link that can encode as a string or as an object as per the JSON API specification.

func NewLink(href string) *Link

NewLink creates a new link encoded as a string.

func NewMetaLink(href string, meta map[string]interface{}) *Link

NewMetaLink creates a new link with metadata encoded as an object.

func NewRelationshipLink(id interface{}, resource, name string, relationship bool) *Link

NewRelationshipLink creates a new relationship link encoded as a string.

func NewSelfLink(id interface{}, resource string) *Link

NewSelfLink creates a new self link encoded as a string.

func (*Link) MarshalJSON

func (l *Link) MarshalJSON() ([]byte, error)

MarshalJSON implements the Marshaler interface for Link.

func (*Link) UnmarshalJSON

func (l *Link) UnmarshalJSON(data []byte) error

UnmarshalJSON implements the Unmarshaler interface for Link.

type Links struct {
	Self    *Link `json:"self,omitempty"`
	Related *Link `json:"related,omitempty"`
}

Links is a top-level document field

func NewRelationshipLinks(id interface{}, resource, name string) *Links

NewRelationshipLinks creates a new pair of relationship links encoded as a string.

type List

type List []*Object

List is a wrapper around an object slice that implements Sendable. List implements sort.Interface for []*Object based on the ID field.

func (List) Len

func (l List) Len() int

func (List) Less

func (l List) Less(i, j int) bool

func (List) Swap

func (l List) Swap(i, j int)

func (*List) UnmarshalJSON

func (list *List) UnmarshalJSON(rawData []byte) error

UnmarshalJSON allows us to manually decode a list via the json.Unmarshaler interface.

func (List) Validate

func (list List) Validate(r *http.Request, response bool) *Error

Validate ensures that List is JSON API compatible.

type Object

type Object struct {
	Type          string                   `json:"type" valid:"required"`
	ID            string                   `json:"id"`
	Attributes    json.RawMessage          `json:"attributes,omitempty"`
	Links         map[string]*Link         `json:"links,omitempty"`
	Relationships map[string]*Relationship `json:"relationships,omitempty"`
	Meta          map[string]interface{}   `json:"meta,omitempty"`
	// Status is the HTTP Status Code that should be associated with the object
	// when it is sent.
	Status int `json:"-"`
}

Object represents the default JSON spec for objects

func (o *Object) AddRelationshipLinks(name string)

AddRelationshipLinks creates a new relationship link and adds it to the resource object relationships.

func (*Object) AddRelationshipMany

func (o *Object) AddRelationshipMany(name string, linkage IDList)

AddRelationshipMany sets the resource linkage of the resource object for the given to-many relationship.

func (*Object) AddRelationshipOne

func (o *Object) AddRelationshipOne(name string, linkage *IDObject)

AddRelationshipOne sets the resource linkage of the resource object for the given to-one relationship.

func (o *Object) AddSelfLink()

AddSelfLink creates a new self link and adds it to the resource object links.

func (*Object) Marshal

func (o *Object) Marshal(attributes interface{}) *Error

Marshal allows you to load a modified payload back into an object to preserve all of the data it has.

func (*Object) ProcessCreate

func (o *Object) ProcessCreate(resourceType string, model interface{}) ([]string, ErrorList)

ProcessCreate unmarshals the object to the given struct (see Object.Unmarshal) and uses JSH tags to validate that there is no missing attributes or forbidden ones.

Simply define your struct with jsh tags to allow for the model to be created with the tagged attributes.

struct {
	Username string `json:"username" jsh:"create"`
}

You can also add a required option to ensure a specific attribute is non-zero.

struct {
	Username string `json:"username" jsh:"create/required"`
}

The model must be a non-nil pointer to a struct. If valid, the model contains the valid request attributes after the call (even on validation error). Relationship fields, if any, are set to the IDObject values in object.Relationships.

See the documentation of Validator.Validate for more detailed information.

The string slice returned contains the names of the attributes and relationships that were unmarshaled to the model.

func (*Object) ProcessUpdate

func (o *Object) ProcessUpdate(resourceType string, model interface{}) ([]string, ErrorList)

ProcessUpdate behaves just like ProcessCreate but uses the update tag for validation. It also adds the constraint of requiring at least one field to be updated.

func (*Object) String

func (o *Object) String() string

String prints a formatted string representation of the object

func (*Object) ToIDObject

func (o *Object) ToIDObject() *IDObject

ToIDObject returns a resource identifier object created with the object type and ID.

func (*Object) Unmarshal

func (o *Object) Unmarshal(resourceType string, target interface{}) ErrorList

Unmarshal puts an Object's Attributes into a more useful target resourceType defined by the user. A correct object resourceType specified must also be provided otherwise an error is returned to prevent hard to track down situations.

Optionally, used https://github.com/go-validator/validator for request input validation. Simply define your struct with valid input tags:

struct {
	Username string `json:"username" valid:"required,alphanum"`
}

As the final action, the Unmarshal function will run govalidator on the unmarshal result. If the validator fails, a Sendable error response of HTTP Status 422 will be returned containing each validation error with a populated Error.Source.Pointer specifying each struct attribute that failed. In this case, all you need to do is:

errors := obj.Unmarshal("mytype", &myType)
if errors != nil {
	// log errors via error.ISE
	jsh.Send(w, r, errors)
}

func (*Object) Validate

func (o *Object) Validate(r *http.Request, response bool) *Error

Validate ensures that an object is JSON API compatible. Has a side effect of also setting the Object's Status attribute to be used as the Response HTTP Code if one has not already been set.

type Parser

type Parser struct {
	Method  string
	Headers http.Header
}

Parser is an abstraction layer that helps to support parsing JSON payload from many types of sources, and allows other libraries to leverage this if desired.

func NewParser

func NewParser(request *http.Request) *Parser

NewParser creates a parser from an http.Request

func (*Parser) Document

func (p *Parser) Document(payload io.ReadCloser, mode DocumentMode) (*Document, *Error)

Document returns a single JSON data object from the parser. In the process it will also validate any data objects against the JSON API.

type Relationship

type Relationship struct {
	Links *Links                 `json:"links,omitempty"`
	Data  IDList                 `json:"data,omitempty"`
	Meta  map[string]interface{} `json:"meta,omitempty"`
}

Relationship represents a reference from the resource object in which it's defined to other resource objects.

type Sendable

type Sendable interface {
	Validate(r *http.Request, response bool) *Error
}

Sendable implements functions that allows different response types to produce a sendable JSON Response format

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

Validator provides validation features for resource modeling.

func NewValidator

func NewValidator(obj *Object, action string) *Validator

NewValidator returns a new instance of a JSH validator.

func (*Validator) Validate

func (v *Validator) Validate(model interface{}) ([]string, ErrorList)

Validate validates that the given struct has no missing/forbidden fields and relationships for the jsh action (i.e. create, update) according to JSH rules.

Customers must use JSH tags "create" and "update" to allow each field to be provided for create and update requests. An optional "/required" can be added to the tag to require the field to be provided.

Additionally, relationships fields must fulfill the following requirements:

  • The field must be tagged "one" or "many".
  • The JSON tag of the field should be "-" to prevent it from being included in attributes.
  • If the relationship is tagged "one", the type of the field must be *jsh.IDObject.
  • If the relationship is tagged "many", the type of the field must be either: map[string]*jsh.IDObject or map[int]*jsh.IDObject. The map must be non-nil.

Example model:

type User struct {
	Group *jsh.IDObject `json:"-"        jsh:"one,create,update"`
	Name  string        `json:"username" jsh:"create/required"`
	Email string        `json:"email"    jsh:"create,update"`
}

The given model should be the result of the unmarshaling of the internal object attributes. The validator will automatically update the relationship fields during validation.

Directories

Path Synopsis
Package jsc (JSON Specification Client) is an http client that makes sending HTTP requests that match the JSON Specification: http://jsonapi.org/ simple.
Package jsc (JSON Specification Client) is an http client that makes sending HTTP requests that match the JSON Specification: http://jsonapi.org/ simple.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL