api2go

package module
v0.0.0-...-edb82c2 Latest Latest
Warning

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

Go to latest
Published: Apr 13, 2015 License: MIT Imports: 12 Imported by: 0

README

api2go

GoDoc Build Status

A JSON API Implementation for Go, to be used e.g. as server for Ember Data.

import "github.com/univedo/api2go"

api2go works, but we're still working on some rough edges. Things might change. Open an issue and join in!

** we are currently re-implementing a lot of stuff in a cleaner way with interfaces and upgrading to the RC3 (Final) Standard of jsonapi.org**

Note: if you only need the marshaling functionality, you can install the subpackage via

go get github.com/univedo/api2go/jsonapi

Examples

Examples can be found here.

Usage

Take the simple structs:

type Post struct {
  ID          int
  Title       string
  Comments    []Comment `json:"-"` // this will be ignored by the api2go marshaller
  CommentsIDs []int     `json:"-"` // it's only useful for our internal relationship handling
}

type Comment struct {
  ID   int
  Text string
}
Interfaces to implement

You must at least implement one interface for api2go to work, which is the one for marshalling/unmarshalling the primary ID of the struct that you want to marshal/unmarshal. This is because of the huge variety of types that you could use for the primary ID. For example a string, a UUID or a BSON Object for MongoDB etc...

If the struct already has a field named ID, or Id, it will be ignored automatically. If your ID field has a different name, please use the json ignore tag.

MarshalIdentifier
type MarshalIdentifier interface {
	GetID() string
}

Implement this interface to marshal a struct.

UnmarshalIdentifier
type UnmarshalIdentifier interface {
	SetID(string) error
}

This is the corresponding interface to MarshalIdentifier. Implement this interface in order to unmarshal incoming json into a struct.

Marshalling with References to other structs

For relationships to work, there are 3 Interfaces that you can use:

type MarshalReferences interface {
	GetReferences() []Reference
}

// MarshalLinkedRelations must be implemented if there are references and the reference IDs should be included
type MarshalLinkedRelations interface {
	MarshalReferences
	MarshalIdentifier
	GetReferencedIDs() []ReferenceID
}

// MarshalIncludedRelations must be implemented if referenced structs should be included
type MarshalIncludedRelations interface {
	MarshalReferences
	MarshalIdentifier
	GetReferencedStructs() []MarshalIdentifier
}

Here, you can choose what you want to implement too, but, you must at least implement MarshalReferences and MarshalLinkedRelations.

MarshalReferences must be implemented in order for api2go to know which relations are possible for your struct.

MarshalLinkedRelations must be implemented to retrieve the IDs of the relations that are connected to this struct. This method could also return an empty array, if there are currently no relations. This is why there is the MarshalReferences interface, so that api2go knows what is possible, even if nothing is referenced at the time.

In addition to that, you can implement MarshalIncludedRelations which exports the complete referenced structs and embeds them in the json result inside the included object.

We choose to do this because it gives you better flexibility and eliminates the conventions in the previous versions of api2go. You can now choose how you internally manage relations. So, there are no limits regarding the use of ORMs.

Unmarshalling with references to other structs

Incoming jsons can also contain reference IDs. In order to unmarshal them correctly, you have to implement the following interface

type UnmarshalLinkedRelations interface {
	SetReferencedIDs([]ReferenceID) error
}

If you need to know more about how to use the interfaces, look at our tests or at the example project.

Ignoring fields

api2go ignores all fields that are marked with the json"-" ignore tag. This is useful if your struct has some more fields which are only used internally to manage relations or data that needs to stay private, like a password field.

Manual marshaling / unmarshaling
comment1 = Comment{ID: 1, Text: "First!"}
comment2 = Comment{ID: 2, Text: "Second!"}
post = Post{ID: 1, Title: "Foobar", Comments: []Comment{comment1, comment2}}

json, err := api2go.MarshalJSON(post)

will yield

{
  "data": [
    {
      "id": "1",
      "type": "posts",
      "links": {
        "comments": {
          "linkage": [
            {
              "id": "1",
              "type": "comments"
            },
            {
              "id": "2",
              "type": "comments"
            }
          ],
          "resource": "\/posts\/1\/comments"
        }
      },
      "title": "Foobar"
    }
  ],
  "included": [
    {
      "id": "1",
      "type": "comments",
      "text": "First!"
    },
    {
      "id": "2",
      "type": "comments",
      "text": "Second!"
    }
  ]
}

Recover the structure from above using

var posts []Post
err := api2go.UnmarshalFromJSON(json, &posts)
// posts[0] == Post{ID: 1, Title: "Foobar", CommentsIDs: []int{1, 2}}

Note that when unmarshaling, api2go will always fill the CommentsIDs field, never the Comments field.

Building a REST API

First, write an implementation of api2go.DataSource. You have to implement 5 methods:

type fixtureSource struct {}

func (s *fixtureSource) FindAll(r api2go.Request) (interface{}, error) {
  // Return a slice of all posts as []Post
}

func (s *fixtureSource) FindOne(ID string, r api2go.Request) (interface{}, error) {
  // Return a single post by ID as Post
}

func (s *fixtureSource) FindMultiple(IDs []string, r api2go.Request) (interface{}, error) {
  // Return multiple posts by ID as []Post
  // For example for Requests like GET /posts/1,2,3
}

func (s *fixtureSource) Create(obj interface{}, r api2go.Request) (string, error) {
  // Save the new Post in `obj` and return its ID.
}

func (s *fixtureSource) Delete(id string, r api2go.Request) error {
  // Delete a post
}

func (s *fixtureSource) Update(obj interface{}, r api2go.Request) error {
  // Apply the new values in the Post in `obj`
}

As an example, check out the implementation of fixtureSource in api_test.go.

You can then create an API:

api := api2go.NewAPI("v1")
api.AddResource(Post{}, &PostsSource{})
http.ListenAndServe(":8080", api.Handler())

This generates the standard endpoints:

OPTIONS /v1/posts
OPTIONS /v1/posts/<id>
GET     /v1/posts
POST    /v1/posts
GET     /v1/posts/<id>
PUT     /v1/posts/<id>
DELETE  /v1/posts/<id>
GET     /v1/posts/<id>,<id>,...
GET     /v1/posts/<id>/comments
Query Params

To support all the features mentioned in the Fetching Resources section of Jsonapi: http://jsonapi.org/format/#fetching

If you want to support any parameters mentioned there, you can access them in your Resource via the api2go.Request Parameter. This currently supports QueryParams which holds all query parameters as map[string][]string unfiltered. So you can use it for:

  • Filtering
  • Inclusion of Linked Resources
  • Sparse Fieldsets
  • Sorting
  • Aything else you want to do that is not in the official Jsonapi Spec
type fixtureSource struct {}

func (s *fixtureSource) FindAll(req api2go.Request) (interface{}, error) {
  for key, values range req.QueryParams {
    ...
  }
  ...
}

If there are multiple values, you have to separate them with a comma. api2go automatically slices the values for you.

Example Request
GET /people?fields=id,name,age

req.QueryParams["fields"] contains values: ["id", "name", "age"]
Using Pagination

Api2go can automatically generate the required links for pagination. Currently there are 2 combinations of query parameters supported:

  • page[number], page[size]
  • page[offset], page[limit]

Pagination is optional. If you want to support pagination, you have to implement the PaginatedFindAll method in you resource struct. For an example, you best look into our example project.

Example request

GET /v0/users?page[number]=2&page[size]=2

would return a json with the top level links object

{
  "links": {
    "first": "http://localhost:31415/v0/users?page[number]=1&page[size]=2",
    "last": "http://localhost:31415/v0/users?page[number]=5&page[size]=2",
    "next": "http://localhost:31415/v0/users?page[number]=3&page[size]=2",
    "prev": "http://localhost:31415/v0/users?page[number]=1&page[size]=2"
  },
  "data": [...]
}

Api2go always creates a resource property for elements in the links property of the result. This is like it's specified on jsonapi.org. Post example:

{
  "data": [
    {
      "id": "1",
      "type": "posts",
      "title": "Foobar",
      "links": {
        "comments": {
          "resource": "\/v1\/posts\/1\/comments",
          "linkage": [
            {
              "id": "1",
              "type": "comments"
            },
            {
              "id": "2",
              "type": "comments"
            }
          ]
        }
      }
    }
  ]
}

If a client requests this resource url, the FindAll method of the comments resource will be called with a query parameter postsID.

So if you implement the FindAll method, do not forget to check for all possible query Parameters. This means you have to check all your other structs and if it references the one for that you are implementing FindAll, check for the query Paramter and only return comments that belong to it. In this example, return the comments for the Post.

Tests

go test
ginkgo                # Alternative
ginkgo watch -notify  # Watch for changes

Documentation

Overview

Package api2go enables building REST servers for the JSONAPI.org standard.

See https://github.com/univedo/api2go for usage instructions.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type API

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

API is a REST JSONAPI.

func NewAPI

func NewAPI(prefix string) *API

NewAPI returns an initialized API instance `prefix` is added in front of all endpoints.

func NewAPIWithBaseURL

func NewAPIWithBaseURL(prefix string, baseURL string) *API

NewAPIWithBaseURL does the same as NewAPI with the addition of a baseURL which get's added in front of all generated URLs. For example http://localhost/v1/myResource/abc instead of /v1/myResource/abc

func (*API) AddResource

func (api *API) AddResource(prototype interface{}, source CRUD)

AddResource registers a data source for the given resource At least the CRUD interface must be implemented, all the other interfaces are optional. `resource` should by an empty struct instance such as `Post{}`. The same type will be used for constructing new elements.

func (*API) Handler

func (api *API) Handler() http.Handler

Handler returns the http.Handler instance for the API.

func (*API) SetRedirectTrailingSlash

func (api *API) SetRedirectTrailingSlash(enabled bool)

SetRedirectTrailingSlash enables 307 redirects on urls ending with / when disabled, an URL ending with / will 404

type CRUD

type CRUD interface {
	// FindOne returns an object by its ID
	FindOne(ID string, req Request) (interface{}, error)

	// Create a new object and return its ID
	Create(obj interface{}, req Request) (string, error)

	// Delete an object
	Delete(id string, req Request) error

	// Update an object
	Update(obj interface{}, req Request) error
}

The CRUD interface MUST be implemented in order to use the api2go api.

type Error

type Error struct {
	ID     string `json:"id,omitempty"`
	Href   string `json:"href,omitempty"`
	Status string `json:"status,omitempty"`
	Code   string `json:"code,omitempty"`
	Title  string `json:"title,omitempty"`
	Detail string `json:"detail,omitempty"`
	Path   string `json:"path,omitempty"`
}

Error can be used for all kind of application errors e.g. you would use it to define form errors or any other semantical application problems for more information see http://jsonapi.org/format/#errors

func (Error) GetID

func (e Error) GetID() string

GetID returns the ID

type FindAll

type FindAll interface {
	// FindAll returns all objects
	FindAll(req Request) (interface{}, error)
}

The FindAll interface can be optionally implemented to fetch all records at once.

type FindMultiple

type FindMultiple interface {
	// FindMultiple returns all objects for the specified IDs
	FindMultiple(IDs []string, req Request) (interface{}, error)
}

The FindMultiple interface can be optionally implemented to fetch multiple records by their ID at once.

type HTTPError

type HTTPError struct {
	Errors []Error `json:"errors,omitempty"`
	// contains filtered or unexported fields
}

HTTPError is used for errors

func NewHTTPError

func NewHTTPError(err error, msg string, status int) HTTPError

NewHTTPError creates a new error with message and status code. `err` will be logged (but never sent to a client), `msg` will be sent and `status` is the http status code. `err` can be nil.

func (HTTPError) Error

func (e HTTPError) Error() string

Error returns a nice string represenation including the status

type PaginatedFindAll

type PaginatedFindAll interface {
	PaginatedFindAll(req Request) (obj interface{}, totalCount uint, err error)
}

The PaginatedFindAll interface can be optionally implemented to fetch a subset of all records. Pagination query parameters must be used to limit the result. Pagination URLs will automatically be generated by the api. You can use a combination of the following 2 query parameters: page[number] AND page[size] OR page[offset] AND page[limit]

type Request

type Request struct {
	PlainRequest *http.Request
	QueryParams  map[string][]string
	Header       http.Header
}

Request holds additional information for FindOne and Find Requests

Directories

Path Synopsis
Create a new user: `curl -X POST http://localhost:31415/v0/users -d '{"data" : [{"type" : "users" , "username" : "marvin"}]}'` List users: `curl -X GET http://localhost:31415/v0/users` List paginated users: `curl -X GET http://localhost:31415/v0/users?page[offset]=0&page[limit]=2` OR `curl -X GET http://localhost:31415/v0/users?page[number]=1&page[size]=2` Update: `curl -vX PUT http://localhost:31415/v0/users/1 -d '{ "data" : {"type" : "users", "username" : "better marvin", "id" : "1"}}'` Delete: `curl -vX DELETE http://localhost:31415/v0/users/2` FindMultiple (this only works if you've called create a bunch of times :) `curl -X GET http://localhost:31415/v0/users/3,4` Create a chocolate with the name sweet `curl -X POST http://localhost:31415/v0/chocolates -d '{"data" : [{"type" : "chocolates" , "name" : "Ritter Sport", "taste": "Very Good"}]}'` Link the sweet `curl -X POST http://localhost:31415/v0/users -d '{"data" : [{"type" : "users" , "username" : "marvin", "links": {"sweets": {"linkage": {"type": "chocolates", "id": "1"}}}}]}'`
Create a new user: `curl -X POST http://localhost:31415/v0/users -d '{"data" : [{"type" : "users" , "username" : "marvin"}]}'` List users: `curl -X GET http://localhost:31415/v0/users` List paginated users: `curl -X GET http://localhost:31415/v0/users?page[offset]=0&page[limit]=2` OR `curl -X GET http://localhost:31415/v0/users?page[number]=1&page[size]=2` Update: `curl -vX PUT http://localhost:31415/v0/users/1 -d '{ "data" : {"type" : "users", "username" : "better marvin", "id" : "1"}}'` Delete: `curl -vX DELETE http://localhost:31415/v0/users/2` FindMultiple (this only works if you've called create a bunch of times :) `curl -X GET http://localhost:31415/v0/users/3,4` Create a chocolate with the name sweet `curl -X POST http://localhost:31415/v0/chocolates -d '{"data" : [{"type" : "chocolates" , "name" : "Ritter Sport", "taste": "Very Good"}]}'` Link the sweet `curl -X POST http://localhost:31415/v0/users -d '{"data" : [{"type" : "users" , "username" : "marvin", "links": {"sweets": {"linkage": {"type": "chocolates", "id": "1"}}}}]}'`

Jump to

Keyboard shortcuts

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