presto

package module
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2020 License: Apache-2.0 Imports: 10 Imported by: 0

README

Presto 🎩

GoDoc Go Report Card Build Status Codecov Version

Magical REST interfaces for Go

Presto is a thin wrapper library that helps structure and simplify the REST interfaces you create in Go. Its purpose is to encapsulate all of the boilerplate code that is commonly required to publish a server-side service via a REST interface. Using Presto, your route configuration code looks like this:

main.go
// ROUTER CONFIGURATION

// Presto requires the echo router by LabStack.  So first, let's pass in a new instance of echo.
presto.UseRouter(echo.New())

// Define a new service to expose online as a REST collection. (Services, Factories, Scopes and Roles defined below)
presto.NewCollection(NoteFactory, "/notes").
    List().
    Post(role.InRoom).
    Get(role.InRoom).
    Put(role.InRoom, role.Owner).
    Patch(role.InRoom, role.Owner).
    Delete(role.InRoom, role.Owner).
    Method("action-name", customHandler, role.InRoom, role.CustomValue)

Design Philosophy

Clean Architecture

Presto lays the groundwork to implement a REST API according to the CLEAN architecture, first published by "Uncle Bob" Martin. This means decoupling business logic and databases, by injecting dependencies down through your application. To do this in a type-safe manner, Presto requires that your services and objects fit into its interfaces, which describe minimal behavior that each must support in order to be used by Presto.

Presto also uses the data package as an abstract representation of some common database concepts, such as query criteria. This allows you to swap in any database by building an adapter that implements the data interfaces. Once Presto is able to work with your business logic in an abstract way, the rest of the common code is repeated for each API endpoint you need to create.

REST API Design Rulebook

Presto works hard to implement REST APIs according to the patterns laid out in the "REST API Design Rulebook", by Mark Massé. This means:

  • Clear route names
  • Using HTTP methods (GET, PUT, POST, PATCH, DELETE) to determine the action being taken
  • Using POST and URL route parameters for other API endpoints that don't fit neatly into the standard HTTP method definitions.
Minimal Dependencies

Presto's only dependency is on the fast and fabulous Echo router, which is an open-source package for creating HTTP servers in Go. Our ultimate goal with this package is to remove this as a hard dependency eventually, and refactor this code to work with multiple routers in the Go ecosystem.

Services

Presto does not replace your application business logic. It only exposes your internal services via a REST API. Each endpoint must be linked to a corresponding service (that matches Presto's required interface) to handle the actual loading, saving, and deleting of objects.

Factories

The specific work of creating services and objects is pushed out to a Factory object, which provides a map of your complete domain. The factories also manage dependencies (such as a live database connection) for each service that requires it. Here's an example factory:

REST Endpoints: Defaults

Presto implements six standard REST endpoints that are defined in the REST API Design Rulebook, and should serve a majority of your needs.

List
Post
Get
Put
Patch
Delete

REST Endpoints: Custom Methods

There are many cases where these six default endpoints are not enough, such as when you have to initiate a specific transaction. A good example of this is a "checkout" function in a shopping cart. The REST API Design Rulebook labels these actions as "Methods", and states that these transactions should always be registered as a POST handler. Presto helps you to manage these functions as well, using the following calls:

main.go
// The following code will register a POST handler on the
// route `/cart/checkout`, using the function `CheckoutHandler`
presto.NewCollection(echo.Echo, factory.Cart, "/cart").
    Method("/checkout", CheckoutHandler, roles)

Scopes and Database Criteria

Your REST server should be able to limit the records accessed though the website, for instance, hiding records that have been virtually deleted, or limiting users in a multi-tenant database to only see the records for their virtual account. Presto accomplishes this using scopes, and ScopeFuncs which are functions that inspect the echo.Context and return a data.Expression that limits users access. The data package is used to create an intermediate representation of the query criteria that can then be interpreted into the specific formats used by your database system. Here's an example of some ScopeFunc functions.

main.go
// This overrides the default scoping function, and uses the
// NotDeleted function for all routes in your API instead.
presto.UseScope(scope.NotDeleted)

// This configures this specific collection to limit all
// database queries using the `ByUsername` scope, in addition
// to the globally defined `NotDeleted` scope declared above.
presto.NewCollection(e, PersonFactory, "/person").
    UseScope(scope.ByUsername)
scopes/scopes.go
// NotDeleted filters out all records that have not been
// "virtually deleted" from the database.
func NotDeleted(ctx echo.Context) (data.Expression, *derp.Error) {
    return data.Expression{{"journal.deleteDate", data.OperatorEqual, 0}}, nil
}

// ByPersonID uses the route Param "personId" to limit
// requests to records that include that personId only.
func Route(ctx echo.Context) (data.Expression, *derp.Error) {

    personID := ctx.Param("personId")

    // If the personID is empty, then return an error to the caller..
    if personID == "" {
        return data.Expression{}, derp.New(derp.CodeBadRequestError, "example.Route", "Empty PersonID", personID)
    }

    // Convert the parameter value into a bson.ObjectID and return the expression
    if personID, err := primitive.ObjectIDFromHex(personID); err != nil {
        return data.Expression{{"personId", data.OperatorEqual, personId}}, nil
    }

    // Fall through to here means that we couldn't convert the personID into a valid ObjectID.  Return an error.
    return data.Expression{}, derp.New(derp.CodeBadRequestError, "example.Route", "Invalid PersonID", personID)
}

User Roles

It's very likely that your API requires custom authentication and authorization for each endpoint. Since this is very custom to your application logic and environment, Presto can't automate this for you. But, Presto does make it very easy to organize the permissions for each endpoint into a single, readable location. Authorization requirements for each endpoint are baked into common functions called roles, and then passed in to Presto during system configuration.

main.go
// Sets up a new collection, where the user must have permissions
// to post into the Room.  This is handled by the `InRoom` function.
presto.NewCollection(echo.Echo, NoteFactory, "/notes").
    Post(role.InRoom)
roles/roles.go
// InRoom determines if the requester has access to the Room in
// which this object resides. If so, then access to it is valid,
// so return a TRUE.
func InRoom(ctx echo.Context, object Object) bool {

    // Get the list of rooms that this user has access to..
    // For example, using JWT tokens in the context request headers.
    allowedRoomIDs := getRoomListFromContext(ctx)

    // Uses a type switch to retrieve the roomID from the Object interface.
    roomID, err := getRoomIDFromObject(object)

    if err != nil {
        return false
    }

    // Try to find the object.RoomID in the list of allowed rooms.
    for _, allowedRoomID := range allowedRoomIDs {
        if allowedRoomID == roomID {
            return true // If so, then you're in.
        }
    }

    // Otherwise, you are not permitted to access this object.
    return false;
}

Performance: Caching, ETag Support

Presto uses ETags to dramatically improve performance and consistency of your REST API. This requires client support as well, so if your client does not include ETag information with your REST requests, then this code is effectively skipped.

304 Not Modified

HTTP includes a great way to minimize bandwidth and latency, using 304 Not Modified responses. Presto can use ETags to determine if a resource has not been changed since it was last delivered to the client, and will send 304 Not Modified responses when it can.

Pluggable Cache Engines

Presto provides an interface for you to plug in your own caching system. Caches only store resource URIs and the most recent ETag. If a request's ETags match the value in the cache, then Presto can skip the database load entirely and deliver a simple 304 status code.

Using ETags for Optimistic Locking

ETags are also useful to implement optimistic locking on records. If the client sends ETag information along with a PUT, PATCH, or DELETE method, then this ETag is compared with the current value in the record. If the ETags do not match, then the record has been modified since the client's last read, and the transaction is rejected.

Remember, this is an optional feature. If your client does not include ETags with these transactions, then the logic for optimistic locking is simply skipped.

Implementing ETags in your Domain Model

The data library includes an optional Journal object that implements most of the Object interface that Presto needs in order to operate. The data.Journal object also includes a simple mechanism for reading and writing ETags into every record you create. You're welcome to use this implementation, or to create one that suits your needs better.

Pull Requests Welcome

Original versions of this library have been used in production on commercial applications for years, and greatly reduced the amount of work required to create and maintain a well-structured REST API.

This new, open-sourced version of PRESTO will greatly benefit from your experience reports, use cases, and contributions. If you have an idea for making Rosetta better, send in a pull request. We're all in this together! 🎩

Documentation

Overview

Package presto gives you a strong foundation for creating REST interfaces using Go and the [Echo Router](http://echo.labstack.com)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Get added in v0.4.2

func Get(ctx Context, service Service, cache Cache, scopes ScopeFuncSlice, roles RoleFuncSlice) (int, data.Object)

Get does *most* of the work of handling an http.GET request. It checks scopes and roles, then loads an object and returns it along with a valid HTTP status code. You can use this as a shortcut in your own HTTP handler functions, and can wrap additional logic before and after this work.

func RequestInfo

func RequestInfo(context Context) map[string]string

RequestInfo inspects a request and returns any information that might be useful for debugging problems. It is primarily used by internal methods whenever there's a problem with a request.

func UseCache added in v0.3.0

func UseCache(cache Cache)

UseCache sets the global cache for all presto endpoints.

func UseRouter added in v0.3.0

func UseRouter(router *echo.Echo)

UseRouter sets the echo router that presto will use to register HTTP handlers.

func UseScopes added in v0.3.0

func UseScopes(scopes ...ScopeFunc)

UseScopes sets global settings for all collections that are managed by presto

Types

type Cache

type Cache interface {

	// Get returns the cache value (ETag) corresponding to the argument (objectID) provided.
	// If a value is not found, then Get returns empty string ("")
	Get(objectID string) string

	// Set updates the value in the cache, returning a derp.Error in case there was a problem.
	Set(objectID string, value string) *derp.Error
}

Cache maintains fast access to key/value pairs that are used to check ETags of incoming requests. By default, Presto uses a Null cache, that simply reports cache misses for every request. However, this can be extended by the user, with any external caching system that matches this interface.

type Collection

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

Collection provides all of the HTTP hanlers for a specific domain object, or collection of records

func NewCollection

func NewCollection(serviceFunc ServiceFunc, prefix string) *Collection

NewCollection returns a fully populated Collection object

func (*Collection) Delete

func (collection *Collection) Delete(roles ...RoleFunc) *Collection

Delete returns an HTTP handler that knows how to delete records from the collection

func (*Collection) Get

func (collection *Collection) Get(roles ...RoleFunc) *Collection

Get returns an HTTP handler that knows how to retrieve a single record from the collection

func (*Collection) List

func (collection *Collection) List(roles ...RoleFunc) *Collection

List returns an HTTP handler that knows how to list a series of records from the collection

func (*Collection) Method

func (collection *Collection) Method(name string, handler echo.HandlerFunc)

Method defines a custom "method"-style endpoint

func (*Collection) Patch

func (collection *Collection) Patch(roles ...RoleFunc) *Collection

Patch returns an HTTP handler that knows how to update in the collection

func (*Collection) Post

func (collection *Collection) Post(roles ...RoleFunc) *Collection

Post returns an HTTP handler that knows how to create new objects in the collection

func (*Collection) Put

func (collection *Collection) Put(roles ...RoleFunc) *Collection

Put returns an HTTP handler that knows how to update in the collection

func (*Collection) UseCache added in v0.3.0

func (collection *Collection) UseCache(cache Cache) *Collection

UseCache adds a local ETag cache for this collection only

func (*Collection) UseScopes added in v0.3.0

func (collection *Collection) UseScopes(scopes ...ScopeFunc) *Collection

UseScopes replaces the default scope with a new list of ScopeFuncs

func (*Collection) UseToken added in v0.3.0

func (collection *Collection) UseToken(token string) *Collection

UseToken overrides the default "token" variable that is appended to all GET, PUT, PATCH, and DELETE routes, and is used as the unique identifier of the record being created, read, updated, or deleted.

type Context added in v0.3.2

type Context interface {

	// Request returns the raw HTTP request object that we're responding to
	Request() *http.Request

	// Path returns the registered path for the handler.
	Path() string

	// RealIP returns the client's network address based on `X-Forwarded-For`
	// or `X-Real-IP` request header.
	RealIP() string

	// ParamNames returns a slice of route parameter names that are present in the request path
	ParamNames() []string

	// Param returns the value of an individual route parameter in the request path
	Param(name string) string

	// QueryParams returns the raw values of all query parameters passed in the request URI.
	QueryParams() url.Values

	// FormParams returns the raw values of all form parameters passed in the request body.
	FormParams() (url.Values, error)

	// Bind binds the request body into provided type `i`. The default binder
	// does it based on Content-Type header.
	Bind(interface{}) error

	// JSON sends a JSON response with status code.
	JSON(code int, value interface{}) error

	// HTML sends an HTTP response with status code.
	HTML(code int, html string) error

	// NoContent sends a response with no body and a status code.
	NoContent(code int) error
}

Context represents the minimum interface that a presto HTTP handler can depend on. It is essentially a subset of the Context interface, and adapters will be written in github.com/benpate/multipass to bridge this API over to other routers.

type ETagger added in v0.2.8

type ETagger interface {

	// ETag returns a version-unique string that helps determine if an object has changed or not.
	ETag() string
}

ETagger interface wraps the ETag function, which tells presto whether or not an object supports ETags. Presto uses ETags to automatically support optimistic locking of files, as well as saving time and bandwidth using 304: "Not Modified" responses when possible.

type RoleFunc

type RoleFunc func(context Context, object data.Object) bool

RoleFunc is a function signature that validates a user's permission to access a particular object

type RoleFuncSlice added in v0.4.2

type RoleFuncSlice []RoleFunc

RoleFuncSlice defines behaviors for a slice of RoleFuncs

func (RoleFuncSlice) Evaluate added in v0.4.2

func (roles RoleFuncSlice) Evaluate(ctx Context, object data.Object) bool

Evaluate resolves all of the RoleFuncs using a Context and a data.Object

type ScopeFunc added in v0.2.0

type ScopeFunc func(context Context) (expression.Expression, *derp.Error)

ScopeFunc is the function signature for a function that can limit database queries to a particular "scope". It inspects the provided context and returns criteria that will be passed to all database queries.

type ScopeFuncSlice added in v0.4.2

type ScopeFuncSlice []ScopeFunc

ScopeFuncSlice defines behaviors for a slice of Scopes

func (*ScopeFuncSlice) Add added in v0.8.0

func (scopes *ScopeFuncSlice) Add(new ...ScopeFunc)

func (ScopeFuncSlice) Evaluate added in v0.4.2

func (scopes ScopeFuncSlice) Evaluate(ctx Context) (expression.AndExpression, *derp.Error)

Evaluate resolves all scopes into an expression (or error) using the provided Context

type Service added in v0.2.0

type Service interface {

	// NewObject creates a newly initialized object that is ready to use
	NewObject() data.Object

	// ListObjects returns an iterator the returns all objects
	ListObjects(criteria expression.Expression, options ...option.Option) (data.Iterator, *derp.Error)

	// LoadObject retrieves a single object from the database
	LoadObject(criteria expression.Expression) (data.Object, *derp.Error)

	// SaveObject inserts/updates a single object in the database
	SaveObject(object data.Object, comment string) *derp.Error

	// DeleteObject removes a single object from the database
	DeleteObject(object data.Object, comment string) *derp.Error

	// Close cleans up any connections opened by the service.
	Close()
}

Service defines all of the functions that a service must provide to work with Presto. It relies on the generic Object interface to load and save objects of any type. GenericServices will likely include additional business logic that is triggered when a domain object is created, edited, or deleted, but this is hidden from presto.

type ServiceFunc added in v0.2.0

type ServiceFunc func(context.Context) Service

ServiceFunc is a function that can generate new services/sessions. Each session represents a single HTTP request, which can potentially span multiple database calls. This gives the factory an opportunity to initialize a new database session for each HTTP request.

Directories

Path Synopsis
cache
mongocache
Package mongocache implements the presto.Cache interface using a mongodb database for persistent storage.
Package mongocache implements the presto.Cache interface using a mongodb database for persistent storage.
nullcache
Package nullcache implements the presto.Cache interface with an empty data structure that never stores any data, and always reports a cache "miss".
Package nullcache implements the presto.Cache interface with an empty data structure that never stores any data, and always reports a cache "miss".

Jump to

Keyboard shortcuts

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