rip

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: May 21, 2024 License: BSD-3-Clause Imports: 14 Imported by: 0

README

RIP ⚰ Go Reference Go Report Card

REST in peace

gopher resting in peace

Why?

Creating RESTful API in Go is in a way simple and fun in the first time, but also repetitive and error prone the more resources you handle.
Copy pasting nearly the same code for each resource you want to GET or POST to except for the request and response types is not that cool, and interface{} neither.
Let's get the best of both worlds with GENERICS 🎆 everybody screams 😱

How?

The idea would be to use the classic net/http package with handlers created from Go types.

http.HandleFunc(rip.HandleEntities("/users", NewUserProvider(), nil)

and it would generate all the necessary boilerplate to have some sane (IMO) HTTP routes.

// HandleEntities associates an urlPath with an entity provider, and handles all HTTP requests in a RESTful way:
//
//	POST   /entities/    : creates the entity
//	GET    /entities/:id : get the entity
//	PUT    /entities/:id : updates the entity (needs to pass the full entity data)
//	DELETE /entities/:id : deletes the entity
//	GET    /entities/    : lists the entities
//
// It also handles fields
//
//	GET    /entities/:id/name : get only the name field of the entity
//	PUT    /entities/:id/name : updates only the name entity field

given that UserProvider implements the rip.EntityProvider interface

type EntityProvider[Ent Entity] interface {
	Create(ctx context.Context, ent Ent) (Ent, error)
	Get(ctx context.Context, id Entity) (Ent, error)
	Update(ctx context.Context, ent Ent) error
	Delete(ctx context.Context, id Entity) error
	ListAll(ctx context.Context) ([]Ent, error)
}

and your resource implements the Entity interface

type Entity interface {
	IDString() string
	IDFromString(s string) error
}

Right now, it can talk several encoding in reading and writing: JSON, protobuf, XML, YAML, msgpack, HTML and HTML form. Based on Accept and Content-Type headers, you can be asymmetrical in encoding: send JSON and read XML.

HTML/HTML Forms allows you to edit your resources directly from your web browser. It's very basic for now.

⚠️: Disclaimer, the API is not stable yet, use or contribute at your own risks

* Final code may differ from actual shown footage

Play with it

go run github.com/dolanor/rip/examples/srv-example@latest
// open your browser to http://localhost:8888/users/ and start editing users

Features

  • support for multiple encoding automatically selected with Accept and Content-Type headers, or entity extension /entities/1.json
    • JSON
    • protobuf
    • YAML
    • XML
    • msgpack
    • HTML (read version)
    • HTML forms (write version)
  • middlewares
  • automatic generation of HTML forms for live editing of entities
Encoding

You can add your own encoding for your own mime type (I plan on adding some domain type encoding for specific entities, see #13). It is quite easy to create if your encoding API follows generic standard library encoding packages like encoding/json. Here is how encoding/json codec is implemented for RIP

Talks

I gave a talk at GoLab 2023. I presented it again at FOSDEM 2024.

The slides are in my talks repository

(The FOSDEM talk present the more up-to-date API (per-route handler options), demo video (instead of live coding), + a live 3D demo, BUT, I couldn't display my note, so a lot of hesitation and parasite words, sorry about that)

TODO

  • middleware support
  • I'd like to have more composability in the entity provider (some are read-only, some can't list, some are write only…), haven't figured out the right way to design that, yet.
  • it should work for nested entities
  • improve the error API
  • support for hypermedia discoverability
  • support for multiple data representation

Thanks

  • logo from Thierry Pfeiffer

Documentation

Overview

Example
package main

import (
	"context"
	"net/http"
	"time"

	"github.com/dolanor/rip/encoding/html"
	"github.com/dolanor/rip/encoding/json"
)

func main() {
	up := newUserProvider()
	ro := NewRouteOptions().
		WithCodecs(json.Codec, html.NewEntityCodec("/users/"))
	http.HandleFunc(HandleEntities("/users/", up, ro))

	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}

type user struct {
	Name         string    `json:"name" xml:"name"`
	EmailAddress string    `json:"email_address" xml:"email_address"`
	BirthDate    time.Time `json:"birth_date" xml:"birth_date"`
}

func (u user) IDString() string {
	return u.Name
}

func (u *user) IDFromString(s string) error {
	u.Name = s

	return nil
}

type UserProvider struct {
	mem map[string]user
}

func newUserProvider() *UserProvider {
	return &UserProvider{
		mem: map[string]user{},
	}
}

func (up *UserProvider) Create(ctx context.Context, u *user) (*user, error) {
	up.mem[u.Name] = *u
	return u, nil
}

func (up UserProvider) Get(ctx context.Context, idString string) (*user, error) {
	u, ok := up.mem[idString]
	if !ok {
		return &user{}, ErrNotFound
	}
	return &u, nil
}

func (up *UserProvider) Delete(ctx context.Context, idString string) error {
	_, ok := up.mem[idString]
	if !ok {
		return ErrNotFound
	}

	delete(up.mem, idString)
	return nil
}

func (up *UserProvider) Update(ctx context.Context, u *user) error {
	_, ok := up.mem[u.Name]
	if !ok {
		return ErrNotFound
	}
	up.mem[u.Name] = *u

	return nil
}

func (up *UserProvider) List(ctx context.Context, offset, limit int) ([]*user, error) {
	var users []*user

	max := len(up.mem)
	if offset > max {
		offset = max
	}

	if offset+limit > max {
		limit = max - offset
	}

	for _, u := range up.mem {
		// we copy to avoid referring the same pointer that would get updated
		u := u
		users = append(users, &u)
	}

	return users[offset : offset+limit], nil
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotFound represents when a resource is not found.
	// It can also be used if a user without proper authorization
	// should not know if a resource exists or not.
	ErrNotFound = Error{
		Code:   ErrorCodeNotFound,
		Status: http.StatusNotFound,
		Detail: "entity not found",
	}

	// ErrNotImplemented communicates if a specific entity function is not
	// implemented.
	ErrNotImplemented = Error{
		Code:   ErrorCodeNotImplemented,
		Status: http.StatusNotImplemented,
		Detail: "not implemented",
	}
)

Functions

func Handle

func Handle[
	Input, Output any,
](
	method string, f InputOutputFunc[Input, Output],
	options *RouteOptions,
) http.HandlerFunc

Handle is a generic HTTP handler that maps an HTTP method to a InputOutputFunc f.

func HandleEntities added in v0.1.4

func HandleEntities[
	Ent any,
	EP EntityProvider[Ent],
](
	urlPath string,
	ep EP,
	options *RouteOptions,
) (path string, handler http.HandlerFunc)

HandleEntities associates an urlPath with an entity provider, and handles all HTTP requests in a RESTful way:

POST   /entities/    : creates the entity
GET    /entities/:id : get the entity
PUT    /entities/:id : updates the entity (needs to pass the full entity data)
DELETE /entities/:id : deletes the entity
GET    /entities/    : lists the entities (accepts page and page_size query param)

It also handles fields

GET    /entities/:id/name : get only the name field of the entity
PUT    /entities/:id/name : updates only the name entity field

Types

type EntityProvider added in v0.1.4

type EntityProvider[Ent any] interface {

	// Create creates a resource that can be identified (an entity).
	Create(ctx context.Context, ent Ent) (Ent, error)

	// Get gets a entity with its id.
	Get(ctx context.Context, id string) (Ent, error)

	// Update updates an entity.
	Update(ctx context.Context, ent Ent) error

	// Delete deletes a entity with its id.
	Delete(ctx context.Context, id string) error

	// List lists a group of entities.
	List(ctx context.Context, offset, limit int) ([]Ent, error)
}

EntityProvider provides entities. An entity is an identifiable resource. Its id should be marshalable as string.

type Error

type Error struct {
	// ID is a unique identifier for this particular occurrence of the problem.
	ID string `json:"id,omitempty"`

	// Links can contains an About Link or a Type Link.
	Links []ErrorLink `json:"links,omitempty"`

	// Status is the HTTP status code applicable to this problem. This SHOULD be provided.
	Status int `json:"status,omitempty"`

	// Code is an application-specific error code.
	Code ErrorCode `json:"code,omitempty"`

	// Title is a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
	Title string `json:"title,omitempty"`

	// Detail is a human-readable explanation specific to this occurrence of the problem
	Detail string `json:"detail,omitempty"`

	// Source is an object containing references to the primary source of the error. It SHOULD include one of its member or be omitted.
	Source ErrorSource `json:"source,omitempty"`

	// Debug contains debug information, not to be read by a user of the app, but by a technical user trying to fix problems.
	Debug string `json:"debug,omitempty"`
}

Error is the error returned by rip. It is inspired by JSON-API.

func (Error) Error

func (e Error) Error() string

type ErrorCode

type ErrorCode int

ErrorCode maps errors from the ResourceProvider implementation to HTTP status code.

const (
	// ErrorCodeNotFound happens when a resource with an id is not found.
	ErrorCodeNotFound ErrorCode = http.StatusNotFound

	// ErrorCodeNotImplemented is when the endpoint is not implemented.
	ErrorCodeNotImplemented ErrorCode = http.StatusNotImplemented
)
type ErrorLink struct {
	// HRef is a URI-reference [RFC3986 Section 4.1] pointing to the link’s target.
	HRef string `json:"href,omitempty"`

	// Rel indicates the link’s relation type. The string MUST be a valid link relation type.
	Rel string `json:"rel,omitempty"`

	// DescribedBy is a link to a description document (e.g. OpenAPI or JSON Schema) for the link target.
	DescribedBy *ErrorLink `json:"describedby,omitempty"`

	// Title serves as a label for the destination of a link such that it can be used as a human-readable identifier (e.g., a menu entry).
	Title string `json:"title,omitempty"`

	// Type indicates the media type of the link’s target.
	Type string `json:"type,omitempty"`

	// HRefLang indicates the language(s) of the link’s target. An array of strings indicates that the link’s target is available in multiple languages. Each string MUST be a valid language tag [RFC5646].
	HRefLang []string `json:"hreflang,omitempty"`
}

ErrorLink represents a RFC8288 web link.

type ErrorSource added in v0.2.0

type ErrorSource struct {
	// Pointer is a JSON Pointer [RFC6901] to the value in the request document
	// that caused the error [e.g. "/data" for a primary data object,
	// or "/data/attributes/title" for a specific attribute].
	// This MUST point to a value in the request document that exists;
	// if it doesn’t, the client SHOULD simply ignore the pointer.
	Pointer string `json:"pointer,omitempty"`

	// Parameter indicates which URI query parameter caused the error.
	Parameter string `json:"parameter,omitempty"`

	// Header indicates the name of a single request header which caused the error.
	Header string `json:"header,omitempty"`
}

ErrorSource indicates the source error. It is based on the JSON API specification: https://jsonapi.org/format/#error-objects

type ErrorSourceHeader added in v0.7.0

type ErrorSourceHeader interface {
	// ErrorSourceHeader returns the request header name that is creating the error.
	//
	// e.g.: "X-App-My-Header" for an HTTP request with this header
	ErrorSourceHeader() string
}

ErrorSourcePointer allows for a user to document the request header that is creating the error.

type ErrorSourceParameter added in v0.7.0

type ErrorSourceParameter interface {
	// ErrorSourceParameter returns the query parameter name that is creating the error.
	//
	// e.g.: "page" for a http://host/users/?page=2
	ErrorSourceParameter() string
}

ErrorSourcePointer allows for a user to document the query parameter that is creating the error.

type ErrorSourcePointer added in v0.7.0

type ErrorSourcePointer interface {
	// ErrorSourcePointer returns the data field name that is creating the error.
	//
	// e.g.: "OwnerID" for a
	//
	//	type Asset struct {
	//		OwnerID int
	//	}
	ErrorSourcePointer() string
}

ErrorSourcePointer allows for a user to document the field that is creating the error.

type InputOutputFunc added in v0.1.4

type InputOutputFunc[
	Input, Output any,
] func(ctx context.Context, input Input) (output Output, err error)

InputOutputFunc is a function that takes a ctx and an input, and it can return an output or an err. It should model any generic backend function that takes input, processes it and returns an output or an error.

type Middleware added in v0.1.4

type Middleware = func(http.HandlerFunc) http.HandlerFunc

Middleware is an HTTP Middleware that you can add to your handler to handle specific actions like logging, authentication, authorization, metrics, ….

type RouteOptions added in v0.2.0

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

RouteOptions allows to pass options to the route handler. It make each route able to have its own set of middlewares or codecs. It also allows to be reused betwenn multiple routes.

func NewRouteOptions added in v0.2.0

func NewRouteOptions() *RouteOptions

func (*RouteOptions) WithCodecs added in v0.2.0

func (ro *RouteOptions) WithCodecs(codecs ...encoding.Codec) *RouteOptions

func (*RouteOptions) WithErrors added in v0.7.0

func (ro *RouteOptions) WithErrors(statusMap StatusMap) *RouteOptions

WithErrors maps errors with an HTTP status code.

func (*RouteOptions) WithListPageSize added in v0.7.0

func (ro *RouteOptions) WithListPageSize(pageSize int) *RouteOptions

WithListPageSize configures the number of entities displayed in a list page.

func (*RouteOptions) WithListPageSizeMax added in v0.7.0

func (ro *RouteOptions) WithListPageSizeMax(pageSizeMax int) *RouteOptions

WithListPageSizeMax configures the maximum number of entities displayed in a list page.

func (*RouteOptions) WithMiddlewares added in v0.2.0

func (ro *RouteOptions) WithMiddlewares(middlewares ...Middleware) *RouteOptions

type StatusMap added in v0.7.0

type StatusMap map[error]int

Jump to

Keyboard shortcuts

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