rest

package module
v0.0.0-...-6260bc3 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2021 License: MIT Imports: 12 Imported by: 10

README

Simple Generic REST controller

Build Status Go Report Card GoDoc Maintainability Coverage

This package provides a simple REST controller compatible with the JSON Server API "dialect". This package enables the creation of backends for the great React-admin framework using pure Go, but can be used in other scenarios where you need a simple REST server for your data.

To use it, you will need to provide an implementation of the Repository interface and a function to create such repository (the constructor). For a simple implementation of an in-memory repository, see /examples/sample_repository.go.

The controller was created to be used with Gorilla Pat, as it requires URL params to be parsed and set as query params. You can easily adapt it to work with other routers and frameworks using a custom middleware.

The functionality is provided by a set of handlers named after the REST verbs they handle: Get(), GetAll(), Put(), Post() and Delete(). Each of these functions receive a constructor for your repository, and an optional implementation of the Logger interface (compatible with Logrus). If no Logger is specified, the functions falls back to the default Go log package.

Example using Gorilla Pat:

	func NewThingsRepository(ctx context) rest.Repository {
		return &ThingsRepository{ctx: ctx}
	}

	func main() {
		router := pat.New()

		router.Get("/thing/{id}", rest.Get(NewThingsRepository))
		router.Get("/thing", rest.GetAll(NewThingsRepository))
		router.Post("/thing", rest.Post(NewThingsRepository))
		router.Put("/thing/{id}", rest.Put(NewThingsRepository))
		router.Delete("/thing/{id}", rest.Delete(NewThingsRepository))

		http.Handle("/", router)

		log.Print("Listening on 127.0.0.1:8000...")
		log.Fatal(http.ListenAndServe(":8000", nil))
	}

Example using chi router:

	func main() {
		router := chi.NewRouter()

		router.Route("/thing", func(r chi.Router) {
			r.Get("/", rest.GetAll(NewThingsRepository))
			r.Post("/", rest.Post(NewThingsRepository))
			r.Route("/{id:[0-9]+}", func(r chi.Router) {
				r.With(urlParams).Get("/", rest.Get(NewThingsRepository))
				r.With(urlParams).Put("/", rest.Put(NewThingsRepository))
				r.With(urlParams).Delete("/", rest.Delete(NewThingsRepository))
			})
		})

		http.Handle("/", router)

		log.Print("Listening on 127.0.0.1:8000...")
		log.Fatal(http.ListenAndServe(":8000", nil))
	}

	// Middleware to convert Chi URL params (from Context) to query params, as expected by our REST package
	func urlParams(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := chi.RouteContext(r.Context())
			parts := make([]string, 0)
			for i, key := range ctx.URLParams.Keys {
				value := ctx.URLParams.Values[i]
				if key == "*" {
					continue
				}
				parts = append(parts, url.QueryEscape(":"+key)+"="+url.QueryEscape(value))
			}
			q := strings.Join(parts, "&")
			if r.URL.RawQuery == "" {
				r.URL.RawQuery = q
			} else {
				r.URL.RawQuery += "&" + q
			}

			next.ServeHTTP(w, r)
		})
	}

Add an issue if you need examples for other routers/frameworks

Documentation

Overview

Package rest provides a simple REST controller compatible with the JSON Server API "dialect". This package enables the creation of backends for the great Admin-on-rest package using pure Go, but can be used in other scenarios where you need a simple REST server for your data.

To use it, you will need to provide an implementation of the Repository interface and a function to create such repository.

The controller was created to be used with Gorilla Pat, as it requires URL params to be parsed and set as query params. You can easily adapt it to work with other routers and frameworks using a custom middleware.

The functionality is provided by a set of handlers named after the REST verbs they handle: Get(), GetAll(), Put(), Post() and Delete(). Each of these functions receive a function used to construct your repository, and an optional implementation of Logger (compatible with Logrus). If no Logger is specified, the functions falls back to the default Go log package

Example using Gorilla Pat (https://github.com/gorilla/pat):

func NewThingsRepository(ctx context) rest.Repository {
	return &ThingsRepository{ctx: ctx}
}

func main() {
	router := pat.New()

	router.Get("/thing/{id}", rest.Get(NewThingsRepository))
	router.Get("/thing", rest.GetAll(NewThingsRepository))
	router.Post("/thing", rest.Post(NewThingsRepository))
	router.Put("/thing/{id}", rest.Put(NewThingsRepository))
	router.Delete("/thing/{id}", rest.Delete(NewThingsRepository))

	http.Handle("/", router)

	log.Print("Listening on 127.0.0.1:8000...")
	log.Fatal(http.ListenAndServe(":8000", nil))
}

Example using chi router (https://github.com/go-chi/chi):

func main() {
	router := chi.NewRouter()

	router.Route("/thing", func(r chi.Router) {
		r.Get("/", rest.GetAll(NewThingsRepository))
		r.Post("/", rest.Post(NewThingsRepository))
		r.Route("/{id:[0-9]+}", func(r chi.Router) {
			r.With(urlParams).Get("/", rest.Get(NewThingsRepository))
			r.With(urlParams).Put("/", rest.Put(NewThingsRepository))
			r.With(urlParams).Delete("/", rest.Delete(NewThingsRepository))
		})
	})

	http.Handle("/", router)

	log.Print("Listening on 127.0.0.1:8000...")
	log.Fatal(http.ListenAndServe(":8000", nil))
}

// Middleware to convert Chi URL params (from Context) to query params, as expected by our REST package
func urlParams(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := chi.RouteContext(r.Context())
		parts := make([]string, 0)
		for i, key := range ctx.URLParams.Keys {
			value := ctx.URLParams.Values[i]
			if key == "*" {
				continue
			}
			parts = append(parts, url.QueryEscape(":"+key)+"="+url.QueryEscape(value))
		}
		q := strings.Join(parts, "&")
		if r.URL.RawQuery == "" {
			r.URL.RawQuery = q
		} else {
			r.URL.RawQuery += "&" + q
		}

		next.ServeHTTP(w, r)
	})
}

For more info see:

JSON Server: https://github.com/typicode/json-server
admin-on-rest: https://marmelab.com/admin-on-rest/

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotFound will make the controller return a 404 error
	ErrNotFound = errors.New("data not found")

	// ErrPermissionDenied will make the controller return a 403 error
	ErrPermissionDenied = errors.New("permission denied")
)

Possible errors returned by a Repository implementation. Any error other than these will make the REST controller return a 500 http status code.

Functions

func Delete

func Delete(newRepository RepositoryConstructor, logger ...Logger) http.HandlerFunc

Delete handles the DELETE verb. Should be mapped to: DELETE /thing/:id

func Get

func Get(newRepository RepositoryConstructor, logger ...Logger) http.HandlerFunc

Get handles the GET verb for individual items. Should be mapped to: GET /thing/:id

func GetAll

func GetAll(newRepository RepositoryConstructor, logger ...Logger) http.HandlerFunc

GetAll handles the GET verb for the full collection. Should be mapped to: GET /thing For all query options available, see https://github.com/typicode/json-server

func Post

func Post(newRepository RepositoryConstructor, logger ...Logger) http.HandlerFunc

Post handles the POST verb. Should be mapped to: POST /thing

func Put

func Put(newRepository RepositoryConstructor, logger ...Logger) http.HandlerFunc

Put handles the PUT verb. Should be mapped to: PUT /thing/:id

func RespondWithError

func RespondWithError(w http.ResponseWriter, code int, message string) error

RespondWithError returns an error message formatted as a JSON object, and sets the http status to code

func RespondWithJSON

func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}) error

RespondWithJSON returns a message formatted as JSON, and sets the http status to code

Types

type Controller

type Controller struct {
	Repository Repository
	Logger     Logger
}

Controller implements a set of RESTful handlers, compatible with the JSON Server API "dialect". Please prefer to use the functions provided in the handler.go file instead of these.

func (*Controller) Delete

func (c *Controller) Delete(w http.ResponseWriter, r *http.Request)

Delete handles the DELETE verb

func (*Controller) Get

func (c *Controller) Get(w http.ResponseWriter, r *http.Request)

Get handles the GET verb for individual items.

func (*Controller) GetAll

func (c *Controller) GetAll(w http.ResponseWriter, r *http.Request)

GetAll handles the GET verb for the full collection

func (*Controller) Post

func (c *Controller) Post(w http.ResponseWriter, r *http.Request)

Post handles the POST verb

func (*Controller) Put

func (c *Controller) Put(w http.ResponseWriter, r *http.Request)

Put handles the PUT verb

type Logger

type Logger interface {
	Warnf(format string, args ...interface{})
	Errorf(format string, args ...interface{})
}

A Logger instance can be passed to the handlers provided by this package. This is compatible with Logrus, but also allows for full customization of the log system used. If you want to use a different logger, just implement a wrapper with the self-explanatory functions defined by this interface.

type Persistable

type Persistable interface {
	// Adds the entity to the repository and returns the newly created id
	Save(entity interface{}) (string, error)

	// Updates the entity identified by id. Optionally select the fields to be updated
	Update(id string, entity interface{}, cols ...string) error

	// Delete the entity identified by id
	Delete(id string) error
}

Persistable must be implemented by repositories in adition to the Repository interface, to allow the POST, PUT and DELETE methods. If this interface is not implemented by the repository, calls to these methods will return 405 - Method Not Allowed

type QueryOptions

type QueryOptions struct {
	// Comma separated list of fields to sort the data
	Sort string

	// Possible values: asc (default), desc
	Order string

	// Max records to return. Used for pagination
	Max int

	// Initial record to return. Used for pagination
	Offset int

	// Map of filters to apply to the query. Keys are field names and values are the filter
	// to be applied to the field. Eg.: {"age": 30, "name": "john"}
	// How the values of the filters are applied to the fields is implementation dependent
	// (you can implement substring, exact match, etc..)
	Filters map[string]interface{}
}

QueryOptions are optional query parameters that can be received by Count and ReadAll and are used to implement pagination, sorting and filtering.

type Repository

type Repository interface {
	// Returns the number of entities that matches the criteria specified by the options
	Count(options ...QueryOptions) (int64, error)

	// Returns the entity identified by id
	Read(id string) (interface{}, error)

	// Returns a slice of entities that matches the criteria specified by the options
	ReadAll(options ...QueryOptions) (interface{}, error)

	// Return the entity name (used for logs and messages)
	EntityName() string

	// Returns a newly created instance. Should be as simple as return &Thing{}
	NewInstance() interface{}
}

Repository is the interface that must be created for your data. See SampleRepository (in examples folder) for a simple in-memory map-based example.

type RepositoryConstructor

type RepositoryConstructor func(ctx context.Context) Repository

RepositoryConstructor needs to be implemented by your custom repository implementation, and it returns a fully initialized repository. It is meant to be called on every HTTP request, so you shouldn't keep state in your repository, and it should execute fast. You have access to the current HTTP request's context.

type ValidationError

type ValidationError struct {
	Errors map[string]string `json:"errors"`
}

ValidationError will make the controller return a 400 error, with the listed errors in the body

func (ValidationError) Error

func (m ValidationError) Error() string

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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