bottleneck

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2019 License: BSD-3-Clause Imports: 14 Imported by: 0

README

Bottleneck Go router

Build Status Azure DevOps tests (master) codecov Go Report Card GoDoc

Bottleneck is a web framework for Go with a focus on convenience over raw performance. Most routers try to be the best by being a few cpu cycles faster or using fewer allocations per request. I'm going to go out on a limb here by saying that in many real world applications and even more so in amateur projects the performance benefits of the faster libraries do not really make a difference.

Motivation

While working on different ideas involving web and Go I used quite a few of the libraries the community has to offer. All of them did something better than others, but never everything. Bottleneck is the aggregation of things I personally liked.

Context from gin-gonic/gin

Many routers use a pattern that is not compatible with the standard library, but is something I much prefer and first saw in gin. The pattern in question is to use a single "context" instead of *http.Request and http.ResponseWriter and provide some convenience methods on top of it. The advantage is to have lots of utilities available in every handler and to have a slightly shorter function signature.

Error handling from labstack/echo

Most routers define route handlers as functions that do not return anything. That starts to become annoying when you do a lot of if err != nil { return err } and maybe want to handle errors at a centralized place, for example through middleware.

Echo is the first framework I came across that returns an error in every handler and middleware which made it much more pleasant for me to write services.

Mounting subgroups from go-chi/chi

Many routers support grouping of routes, but most of them require them to be defined with the router. This makes defining groups of routes as a separate package a lot harder / less modular. Chi allows groups to be created separately and then be mounted onto a router later.

Bring your own context from gocraft/web

A common pattern for web services is to do some work or checks before a group of routes. This can be achieved with middleware in many of the existing routers, but storing something to be accessed down the chain often requires type assertions. gocraft/web allows you to bring your own context and therefore makes it possible to avoid type assertions by putting that burden on the web framework.

Automatic request unmarshalling

Handling a request is (almost) always the same. Unmarshal and possibly validate a request. Do some work. Marshal a response. Done.

Marshalling a response can be hidden behind the context, but unmarshalling and validating the request always requires some boilerplate at the start of each handler. I want Bottleneck to do that automatically and provide the resulting value as a parameter to handlers.

Example

package main

import (
	"net/http"

	"github.com/lukasdietrich/bottleneck"
)

// Context is a custom context
type Context struct {
	bottleneck.Context        // It must embed bottleneck.Context to be valid
	User               string // Store user during request handling
}

// AmIRequest is a request definition
type AmIRequest struct {
	Name string `json:"name" validate:"required"`
}

// AmIResponse is a response definition
type AmIResponse struct {
	Yes bool `json:"yes"`
}

func main() {
	r := bottleneck.NewRouter(Context{}) // Create a new router
	r.Mount(routes())                    // Mount the routes

	// Start a http server and use the router
	http.ListenAndServe(":8080", r)
}

// routes creates a group with all /api routes.
func routes() *bottleneck.Group {
	g := bottleneck.NewGroup().WithPrefix("/api")

	// store the current user for handlers
	g.Use(func(ctx *Context, next bottleneck.Next) error {
		user, _, ok := ctx.Request().BasicAuth()
		if !ok {
			return bottleneck.NewError(http.StatusUnauthorized)
		}

		ctx.User = user
		return next()
	})

	// test if the user knows his own name
	g.POST("/test", func(ctx *Context, req *AmIRequest) error {
		return ctx.JSON(http.StatusOK, AmIResponse{
			Yes: req.Name == ctx.User,
		})
	})

	return g
}

Credits

Bottleneck utilizes some great packages under the hood:

  1. dimfeld/httptreemux for request routing
  2. go-playground/validator for the default validation implemenation

Documentation

Index

Constants

View Source
const (
	HeaderAcceptEncoding  = "Accept-Encoding"
	HeaderContentEncoding = "Content-Encoding"
	HeaderContentType     = "Content-Type"
	HeaderVary            = "Vary"
)

Some well-known http header keys.

View Source
const (
	MIMEApplicationForm            = "application/x-www-form-urlencoded"
	MIMEApplicationJSON            = "application/json"
	MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + charsetUTF8
	MIMEApplicationXML             = "application/xml"
	MIMEApplicationXMLCharsetUTF8  = MIMEApplicationXML + "; " + charsetUTF8
	MIMEMultipartForm              = "multipart/form-data"
	MIMEOctetStream                = "application/octet-stream"
	MIMETextPlain                  = "text/plain"
	MIMETextPlainCharsetUTF8       = MIMETextPlain + "; " + charsetUTF8
	MIMETextXML                    = "text/xml"
	MIMETextXMLCharsetUTF8         = MIMETextXML + "; " + charsetUTF8
)

Some well-known content types.

See https://www.iana.org/assignments/media-types/media-types.xhtml.

Variables

View Source
var (
	// ErrInvalidContext indicates the use of a context that does not match the required type.
	ErrInvalidContext = errors.New("invalid context type")
)

Functions

This section is empty.

Types

type Binder

type Binder interface {
	// Bind unmarshalls an incoming request. The first argument is the raw *http.Request.The second argument is the
	// target struct.
	Bind(*http.Request, interface{}) error
}

A Binder is used to unmarshal incoming requests.

var (
	// DefaultBinder is the default Binder implementation.
	// It handels unmarshalling of JSON and XML encoded payloads depending on the Content-Type header of the request.
	// If the Content-Type is neither JSON nor XML ErrBindSupportedContentType is returned.
	DefaultBinder Binder = defaultBinder{}

	// ErrBindUnsupportedContentType indicates that a request could not be bound, because the Content-Type is not
	// supported by the Binder implementation.
	ErrBindUnsupportedContentType = errors.New("cannot bind content type")
)

type Context

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

Context is the base for custom contexts. It is a container for the raw http request and response and provides convenience methods to access request-data and to write responses.

func (*Context) JSON

func (c *Context) JSON(status int, value interface{}) error

JSON writes a response using the JSONRenderer.

func (*Context) Param

func (c *Context) Param(key string) string

Param returns the path parameter of the current matched route. If the parameter does not exist, an empty string is returned instead.

router.GET("/users/:name", func(ctx *Context) error {
  return ctx.String(http.StatusOK, ctx.Param("name"))
})

func (*Context) Query

func (c *Context) Query(key string) string

Query returns the query value for a given key. If the key does not exist, an empty string is returned instead.

// curl "localhost:8080/search?input=Does router performance matter in Go?"
router.GET("/search", func(ctx *Context) error {
  return ctx.String(http.StatusOK, ctx.Query("input"))
})

func (*Context) Render

func (c *Context) Render(status int, r Renderer) error

Render writes a generic response using the provided Renderer after the status-code is set.

func (*Context) Request

func (c *Context) Request() *http.Request

Request returns the raw http request.

func (*Context) Response

func (c *Context) Response() *Response

Response returns the raw http response.

func (*Context) Stream

func (c *Context) Stream(status int, contentType string, reader io.Reader) error

Stream writes a response using the StreamRenderer.

func (*Context) String

func (c *Context) String(status int, value string) error

String writes a response using the StringRenderer.

func (*Context) XML

func (c *Context) XML(status int, value interface{}) error

XML writes a response using the XMLRenderer.

type Error

type Error struct {
	Status  int    `json:"status"`
	Message string `json:"message"`
	Cause   error  `json:"-"`
}

Error is a user displayable error that is returned during request handling.

func NewError

func NewError(status int) *Error

NewError creates a new Error and sets the http status code, which will be set when not handeled manually.

func (*Error) Error

func (e *Error) Error() string

Error formats the error as readable text.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the wrapped cause. This is useful to test for specific errors.

err := NewError(http.StatusInternalServerError).WithCause(io.EOF)
errors.Is(err, io.EOF) // true

func (*Error) WithCause

func (e *Error) WithCause(cause error) *Error

WithCause adds an actual error to the Error, which can later be unwrapped and tested for.

func (*Error) WithMessage

func (e *Error) WithMessage(message string) *Error

WithMessage adds a custom message to the Error. By default http.StatusText is used to create a message.

type FileHandlerOptions

type FileHandlerOptions struct {
	// Fs is used to resolve files.
	Fs http.FileSystem
	// NotFound is the filename to use, when no file exists. If empty 404 is returned.
	NotFound string
}

FileHandlerOptions define how static files are handled.

type Group

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

A Group is a collection of routes. A Group may have a prefix, that is shared across all routes.

func NewGroup

func NewGroup() *Group

NewGroup creates a new and empty Group.

func (*Group) Add

func (g *Group) Add(method, path string, handler Handler, middleware ...Middleware) *Group

Add adds a Handler to the Group.

func (*Group) DELETE

func (g *Group) DELETE(path string, handler Handler, middleware ...Middleware) *Group

DELETE adds a Handler to the Group with the "DELETE" http method.

func (*Group) Files

func (g *Group) Files(path string, opts FileHandlerOptions) *Group

Files adds a Handler for static files using http.ServeContent.

func (*Group) GET

func (g *Group) GET(path string, handler Handler, middleware ...Middleware) *Group

GET adds a Handler to the Group with the "GET" http method.

func (*Group) HEAD

func (g *Group) HEAD(path string, handler Handler, middleware ...Middleware) *Group

HEAD adds a Handler to the Group with the "HEAD" http method.

func (*Group) Mount

func (g *Group) Mount(subgroups ...*Group) *Group

Mount adds all routes of the subgroups to this Group.

func (*Group) OPTIONS

func (g *Group) OPTIONS(path string, handler Handler, middleware ...Middleware) *Group

OPTIONS adds a Handler to the Group with the "OPTIONS" http method.

func (*Group) POST

func (g *Group) POST(path string, handler Handler, middleware ...Middleware) *Group

POST adds a Handler to the Group with the "POST" http method.

func (*Group) PUT

func (g *Group) PUT(path string, handler Handler, middleware ...Middleware) *Group

PUT adds a Handler to the Group with the "PUT" http method.

func (*Group) Use

func (g *Group) Use(middleware ...Middleware) *Group

Use adds a list of Middleware to all routes of the Group. Middleware added with this method are only used for routes that are added afterwards.

func (*Group) WithPrefix

func (g *Group) WithPrefix(prefix string) *Group

WithPrefix prepends the prefix to all routes of the Group.

type Handler

type Handler interface{}

A Handler must be a func with either 1 or 2 arguments and returning an error. The first must be either *bottleneck.Context or a pointer to a struct, that embeds bottleneck.Context. The second is optional and, if provided, must be a pointer to a struct, which is used to unmarshal and validate request payloads.

type LoginRequest struct {
  Username string `json:"username"`
  Password string `json:"password"`
}

type LoginResponse struct {
  Message string `json:"message"`
}

func login(ctx *bottleneck.Context, req *LoginRequest) error {
  if req.Username == "AzureDiamond" && req.Password == "hunter2" {
    return ctx.JSON(http.StatusOK, LoginResponse{
      Message: "Welcome home, AzureDiamond!"
    })
  }

  return ctx.JSON(http.StatusUnauthorized, LoginResponse{
    Message: "Nice try!"
  })
}

type JSONRenderer

type JSONRenderer struct {
	Value interface{}
}

JSONRenderer implements the Renderer interface.

func (JSONRenderer) Header

func (JSONRenderer) Header(h http.Header)

Header sets the Content-Type to "application/json; charset=UTF8".

func (JSONRenderer) Render

func (r JSONRenderer) Render(w io.Writer) error

Render marshals the Value as JSON and then writes it to w.

type Middleware

type Middleware interface{}

A Middleware must be a func with exactly two arguments. The first must be either *bottleneck.Context or a pointer to a struct, that embeds bottleneck.Context. The second must be a Next func, that has to be called to continue the route handling.

type SessionContext struct {
  bottleneck.Context

  Username string
}

func checkLogin(ctx *SessionContext, next bottleneck.Next) error {
  if ctx.Username != "AzureDiamond" {
    return ctx.String(http.StatusForbidden, "Wrong person.")
  }

  return next()
}

type Next

type Next func() error

Next is the second argument for Middleware. When called it will continue the route handling and return future errors.

type Renderer

type Renderer interface {
	// Header is called before Render and is used to set http headers.
	// The standard implementations use this method to set the response Content-Type.
	Header(http.Header)

	// Render is called after Header and is used to write the raw http body.
	Render(io.Writer) error
}

A Renderer is a container type that wraps an abstract http response.

type Response

type Response struct {
	// Status is the http status code that is set during WriteHeader.
	Status int
	// Writer is the raw http.ResponseWriter. It can be set in a middleware to change the way data is written.
	Writer http.ResponseWriter
}

Response wraps a raw http.ResponseWriter and stores additional information, which is not tracked by the standard library.

func (*Response) Header

func (r *Response) Header() http.Header

Header returns the header map that will be sent by WriteHeader.

See https://golang.org/pkg/net/http/#ResponseWriter

func (*Response) Write

func (r *Response) Write(b []byte) (int, error)

Write writes the data to the connection as part of an HTTP reply.

See https://golang.org/pkg/net/http/#ResponseWriter

func (*Response) WriteHeader

func (r *Response) WriteHeader(status int)

WriteHeader sends an HTTP response header with the provided status code.

See https://golang.org/pkg/net/http/#ResponseWriter

type Router

type Router struct {
	Binder    Binder
	Validator Validator
	// contains filtered or unexported fields
}

A Router is a multiplexer for http requests.

func NewRouter

func NewRouter(contextValue interface{}) *Router

NewRouter creates a new Router for a custom context. The provided contextValue is an example instance of the context, which is used to get its type.

type CustomContext struct {
  bottleneck.Context
}

router := NewRouter(CustomContext{})
router.Listen(":8080")

func (*Router) Mount

func (r *Router) Mount(g *Group) *Router

Mount adds all routes of a Group to the Router.

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(res http.ResponseWriter, req *http.Request)

ServeHTTP implements the http.Handler interface.

type StreamRenderer

type StreamRenderer struct {
	ContentType string
	Reader      io.Reader
}

StreamRenderer implements the Renderer interface.

func (StreamRenderer) Header

func (r StreamRenderer) Header(h http.Header)

Header sets the Content-Type to the value of ContentType.

func (StreamRenderer) Render

func (r StreamRenderer) Render(w io.Writer) error

Render pipes the contents of Reader to w.

type StringRenderer

type StringRenderer struct {
	// String is the exact value written in Render
	String string
}

StringRenderer implements the Renderer interface.

func (StringRenderer) Header

func (StringRenderer) Header(h http.Header)

Header sets the Content-Type to "text/plain; charset=UTF8".

func (StringRenderer) Render

func (r StringRenderer) Render(w io.Writer) error

Render writes the String as is to w.

type Validator

type Validator interface {
	// Validate validates an incoming request. The first argument is the raw *http.Request. The second argument is the
	// unmarshalled payload.
	Validate(*http.Request, interface{}) error
}

A Validator is used to validate incoming requests.

var DefaultValidator Validator = defaultValidator{validator.New()}

DefaultValidator is the default Validator implementation using https://github.com/go-playground/validator.

type XMLRenderer

type XMLRenderer struct {
	Value interface{}
}

XMLRenderer implements the Renderer interface.

func (XMLRenderer) Header

func (XMLRenderer) Header(h http.Header)

Header sets the Content-Type to "text/xml; charset=UTF8".

func (XMLRenderer) Render

func (r XMLRenderer) Render(w io.Writer) error

Render marshals the Value as XML and then writes it w.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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