olive

package module
v0.0.0-...-9542b16 Latest Latest
Warning

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

Go to latest
Published: Dec 7, 2022 License: Apache-2.0, MIT Imports: 16 Imported by: 1

README

olive - REST APIs with Martini godoc reference

olive is tiny, opinonated scaffolding to rapidly build standards-compliant REST APIs on top of Martini. olive handles content negotiation, parameter deserialization, request-scoped logging, panic recovery, and RFC-appropriate error responses so that you can focus on writing your business logic.

Because it's just Martini, you can easily plug in any middleware from the martini community for additional functionality like setting CORS headers.

olive is currently experimental.

Here's a simple API which calculates factorials that handles input and output in JSON or XML.

package main
import (
    "net/http"
    "github.com/inconshreveable/olive"
)

func main() {
    o := olive.Martini()
    o.Debug = true
    o.Post("/fact", o.Endpoint(factorial).Param(Input{}))
    http.ListenAndServe(":8080", o)
}

type Input struct {
    Num     int `json:"num" xml:"Num"`
    Timeout int `json:"timeout" xml:"Timeout"`
}

type Output struct {
    Factorial int `json:"answer" xml:"Answer"`
}

func factorial(r olive.Response, in *Input) {
    r.Info("computing factorial", "num", in.Num, "timeout", in.Timeout)
    ans, err := computeFactorial(in.Num, time.Duration(in.Timeout) * time.Second)
    if err != nil {
        r.Abort(err)
    }
    r.Encode(Output{Factorial: ans})
}
olive.Response

All olive endpoint handlers are injected with an olive.Response object with methods that makes it easy to respond to the caller.

The most common way you will do this is with Encode method. This serializes a structure by automatically choosing an appropriate ContentEncoder based on the requests Accept header. By default, olive only understands how to serialize JSON and XML, but it can be extended to handle arbitrary content types.

func helloWorldEndpoint(r olive.Response) {
    r.Encode([]string{"hello", "world"})
}

olive.Response also includes an Abort() function for terminating a handler when an error has been encountered:

func handler(r olive.Response) {
    if err := doSomething(); err != nil {
        r.Abort(err)
    }
}

The olive.Response interface also embeds a log15.Logger for easy logging and the http.ResponseWriter interface for writing custom status codes:

func handler(r olive.Response) {
    r.Warn("about to write a failing response", "code", 404)
    r.WriteHeader(404)
}
Param injection

When you create an olive endpoint, you can specify a Param that it expects. If you do, olive will attempt to deserialize the request body by examing the Content-Type and then map it into the structure you passed as the Param. It then takes the mapped structure and injects it into the martini.Context for easy use in your handler.

func main() {
    o := olive.Martini()
    o.Post("/table", o.Endpoint(createTable).Param(NewTable{}))
    // etc
}

type NewTable struct {
    Width int
    Height int
    Depth int
}
func createTable(r olive.Response, nt *NewTable) {
    // create the table and respond
}

Documentation

Overview

olive is a a tiny framework built on top of martini for rapid development of robust REST APIs.

olive handles content type negotation, serialization, deserialization, unique request-id logging and panic recovery leaving you free to write just your application's business logic.

Simple example:

package main
import (
	"net/http"
	"github.com/inconshreveable/olive"
)

func main() {
	o := olive.Martini()
	o.Debug = true
	o.Post("/fact", o.Endpoint(factorial).Param(Input{}))
	http.ListenAndServe(":8080", o)
}

type Input struct {
	Num     int `json:"num" xml:"Num"`
	Timeout int `json:"timeout" xml:"Timeout"`
}

type Output struct {
	Factorial int `json:"answer" xml:"Answer"`
}

func factorial(r olive.Response, in *Input) {
	r.Info("computing factorial", "num", in.Num, "timeout", in.Timeout)
	ans, err := computeFactorial(in.Num, time.Duration(in.Timeout) * time.Second)
	if err != nil {
		r.Abort(err)
	}
	r.Encode(Output{Factorial: ans})
}

The above API will appropriately deserialize a POST body of XML, JSON, or x-www-form-urlencoded depending on the Content-Type header. Based on the client's Accept header, the result will be serialized in either XML or JSON. Appropriate failures are returned for invalid client requests. The logger assigns a unique id to each request for easy tracing purposes:

INFO[11-21|15:33:58] start                                    pg=/fact id=e416b6cc83f386bc
INFO[11-21|15:33:58] computing factorial                      pg=/fact id=e416b6cc83f386bc num=4 timeout=5
INFO[11-21|15:33:58] end                                      pg=/fact id=e416b6cc83f386bc status=200 dur=371.98us

A more advanced example explaining features in detail:

package main

import (
	"net/http"

	"github.com/go-martini/martini"
	"github.com/inconshreveable/olive"
)

func main() {
	o := olive.Martini()
	o.Post("/accounts", o.Endpoint(createAccount).Param(CreateAccountParam{}))
	o.Get("/accounts", o.Endpoint(getAccounts).Param(GetAccountsParam{}))
	o.Get("/accounts/:id", o.Endpoint(getAccount)).Name("accountInstance")

	// serve the API
	http.ListenAndServe(":8080", o)
}

// This is the expected request payload for the createAccount endpoint
// It will automatically be deserialized appropriately depending on
// the Content-Type header sent by the client
type CreateAccountParam struct {
	Name  string `json:"name" xml:"Name"`
	Email string `json:"email" xml:"Email"`
}

// If a struct is specified with Endpoint's Param() function, the request body
// is deserialized and a pointer to the result is dependency injected
func createAccount(r olive.Response, param *CreateAccountParam) {
	// this is all business logic
	ac, err := account.Create(param.Name, param.Email)
	if err != nil {
		// Abort fails the request immediately, there is no need to return.
		// If the standard 'error' interface is passed in, we return a
		// 500 internal server error. see below for fine-grained control
		r.Abort(err)
	}

	// custom status codes need a call to WriteHeader first
	r.WriteHeader(201)

	// serialize output
	r.Encode(ac)
}

type GetAccountsParam struct {
	Email string `param:"email"`
}

// Unlike createAccount, GetAccountsParam is deserialized from the query URI instead
// of from the request body because this is a GET request
func getAccounts(r olive.Response, param *GetAccountsParam) {
	acs, err := account.GetAccountsForEmail(param.Email)
	if err != nil {
		r.Abort(err)
	}

	// the Response interface embeds a log15.Logger that you can use for easy logging
	// every request has a unique ID included in the log line
	r.Debug("fetched accounts", "email", param.Email, "count", len(acs))

	r.Encode(acs)
}

func getAccount(r olive.Response, p martini.Params) {
	// access to URL parameters is the same as Martini
	accountId := p["id"]
	s, err := account.GetById(accountId)
	switch {
	case err == account.NotFoundError:
		// if you pass an olive.Error to Abort(), you can exert more sophisticated
		// control over the returned response
		r.Abort(&olive.Error{
			StatusCode: 404, // http status code
			ErrorCode:  102, // unique error code for this failure ("account not found")
			Message:    "account not found",
			Details:    olive.M{"id": accountId},
		})
	case err != nil:
		r.Abort(err)
	}
	r.Encode(s)
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ContentEncoder

type ContentEncoder struct {
	ContentType string
	Encoder
}

A ContentEncoder is an Encoder that can encode a resource to the representation described by the ContentType mimetype. Requests with an Accept header that match the ContentType will use that ContentEncoder.

type Decoder

type Decoder interface {
	Decode(rd io.Reader, v interface{}) error
}

type Encoder

type Encoder interface {
	Encode(wr io.Writer, v interface{}) error
}

type Endpoint

type Endpoint interface {
	// stucture of the request input, deserialized either from the request body or query string
	// if set, a pointer to a value of this type will be dependency-injected into the handler
	Param(interface{}) Endpoint

	// overload the allowed decoders
	Decoders(map[string]Decoder) Endpoint

	// customize the allowed encoders for this endpoint
	Encoders([]ContentEncoder) Endpoint

	// debug determines if error stack traces are printed to the client
	Debug(bool) Endpoint

	// returns the handlers that make up the endpoint
	Handlers() []martini.Handler
}

An Endpoint describes an Endpoint in an olive REST API. Callers may customize an endpoint's behavior by chaining calls that manipulate its state. After the Endpoint is built, the caller can use the Handlers() function to get the set of martini.Handlers that implement the API endpoint.

o := olive.Martini()
e := o.Endpoint(listTables).Param(TableFilter{}).Debug(true)
o.Get("/tables", e.Handlers()...)

type Error

type Error struct {
	ErrorCode  int    `json:"error_code,omitempty" xml:",omitempty"` // unique error code
	StatusCode int    `json:"status_code"`                           // http status code
	Message    string `json:"msg"`                                   // user-facing error message
	Details    M      `json:"details" xml:"-"`                       // extra error context for client, XXX should work in XML
}

A structure with details about an error that occurred while handling a request. Passing this structure to an ErrEncoder's Abort() method gives the caller complete control over the error response shape and status code.

func (*Error) Error

func (e *Error) Error() string

type M

type M map[string]interface{}

a Map of extra error details

type Olive

type Olive struct {
	Encoders []ContentEncoder   // default set of ContentEncoders used by a new Endpoint
	Decoders map[string]Decoder // default map of Decoders used by a new Endpoint
	Debug    bool               // default debug flag of a new Endpoint
	// contains filtered or unexported fields
}

Olive creates API Endpoints. Customizing the properties of the Olive changes the defaults of the created Endpoints.

func New

func New(rt martini.Router) *Olive

Returns a new Olive API creating endpoints that can be mapped onto the given martini.Router.

rt := martini.NewRouter()
o := olive.New(rt)
e := o.Endpoint(showTables)
rt.Get(e.Handlers()...)

func (*Olive) Any

func (o *Olive) Any(pattern string, e Endpoint) martini.Route

func (*Olive) Delete

func (o *Olive) Delete(pattern string, e Endpoint) martini.Route

func (*Olive) Endpoint

func (o *Olive) Endpoint(hs ...martini.Handler) Endpoint

func (*Olive) Get

func (o *Olive) Get(pattern string, e Endpoint) martini.Route

func (*Olive) Head

func (o *Olive) Head(pattern string, e Endpoint) martini.Route

func (*Olive) Options

func (o *Olive) Options(pattern string, e Endpoint) martini.Route

func (*Olive) Patch

func (o *Olive) Patch(pattern string, e Endpoint) martini.Route

func (*Olive) Post

func (o *Olive) Post(pattern string, e Endpoint) martini.Route

func (*Olive) Put

func (o *Olive) Put(pattern string, e Endpoint) martini.Route

type OliveMartini

type OliveMartini struct {
	*martini.Martini
	*Olive
	Router martini.Router
}

A convenient pairing of an Olive and Martini which can be used to define and customize an Olive API.

func Martini

func Martini() *OliveMartini

Returns an *OliveMartini that has both an Olive router and *martini.Martini appropriately wired together and ready for use.

type Response

type Response interface {
	martini.ResponseWriter
	log.Logger

	// Encode uses the negotiated codec to serialize and write the value to the response.
	Encode(v interface{}) error

	// Abort terminates a handler immediately with an error and no further processing is done.
	//
	// If the error is of type *olive.Error, the properties of the *olive.Error will be used to
	// determine the status code and shape of the error response. Otherwise, the response will
	// be a 500 internal server error which includes the error argument as one of its details.
	Abort(error)
}

Response is a composition of the most common interfaces needed when handling a request.

Jump to

Keyboard shortcuts

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