auwfg

package module
v0.0.0-...-29ec066 Latest Latest
Warning

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

Go to latest
Published: Nov 16, 2015 License: MIT Imports: 17 Imported by: 0

README

Another useless web framework for Go

AUWFG is an ugly web stack for Go used for building web services.

AUWFG has two goals.

First, to provide a generic framework while still levarging strong typing. Many Go-based frameworks are built as a black box resulting in the inability to be cleanly expanded and/or exposing untyped data (map[string]string) where it doesn't make any sense.

Second, to cleanly separate the framework infrastructure from the actual web application.

Ultimately, these goals result in actions which are clean, testable and look like:

package "sessions"
//context is specific to your application
//auwfg.Response is an interface which maps well http.ResponseWriter
func Create(context *Context) auwfg.Response {
  input := context.Body.(*LoginInput)
  usermame := input.Username
  password := input.Rassword
  remember := input.LongLived

  //or create your own response types
  return auwfg.Json(`{"token":"blah"}`).Status(201)
}

Configuration

You can start the server, using default settings, by calling auwfg.Run(auwfg.Configure()). However, Configure() exposes a fluent interface which lets you specify various settings:

c := auwfg.Configure().Address("127.0.0.1:3944").NotFound('{"error":404}')
auwfg.Run(c)

Possible configuration options are:

  • Address: the address to listen to
  • NotFound: the body to reply with when a route isn't found
  • NotFoundResponse: the response to reply with when a route isn't found (gives more control than the simpler NotFound). Note that once set, this response is available as auwfg.NotFound for use in your own application
  • BodyTooLarge: the body to reply with when an input body is too large (see BodyPool)
  • BodyTooLargeResponse: the response to reply with when an input body is too large (gives more control than the simpler BodyTooLarge)
  • InvalidFormat: the body to reply with when an input format is invalid (not valid json)
  • InvalidFormatResponse: the response to reply with when an input format is invalid (gives more control than the simpler InvalidFormat)
  • InternalServerError: the body to reply with when an error occurs
  • InternalServerErrorResponse: the response to reply with when an error occurs (gives more control than the simpler InternalServerError). Note that once set, this response is available as auwfg.InternalServerError for use in your own application
  • ContextFactory: explained below
  • Dispatcher: explained below
  • Route: explained below
  • BodyPool: explained below
  • InvalidPool: explained below
  • LoadRawBody: The raw/unparsed body will be available in the context (useful for debugging)
  • SetGeoIP: Sets the path where the GeoIP database can be found. By default it is "/opt/geoip/GeoIP.dat".

BaseContext

The base context of auwfg provides a series of useful attributes:

  • Query: (map[string]string) A hash map with query parameters found in the request
  • Ip: (string) The IP of the client that made the request. This is extracted from the HTTP headers x-forwarded-for, client-ip and remote-addr (the first one with an IP is the value of the remote IP).
  • Country: (string) The Country code of the client that made the request. This is extracted from the HTTP headers X-Geoip-Country-Code if present, otherwise resolved from the IP (see Ip) using a GeoIP database that is passed in the configuration.
  • Params: Parameters extracted from the URL. explained below
  • RawBody: The raw body bytes of the request. Only available if LoadRawBody configuration is set.

ContextFactory and Dispatcher

Having strongly-typed, application specific context relies on specifying a custom ContextFactory and Dispatcher.

The ContextFactory takes an instance of *auwfg.BaseContext and returns whatever context instance you care about. Whatever object you return need not have a relationship with the provided BaseContext. However, most usage will likely want to make use of Go's implicit composition (embedding):

type Context struct {
  User *User
  // more app-specific fields
  // ...
  *auwfg.BaseContext
}

func ContextFactory(c *auwfg.BaseContext) interface{} {
  return &Context{loadUser(c.Req), c}
}

c := auwfg.Configure().ContextFactory(ContextFactory)
auwfg.Run(c)

Unfortunately, on its own, ContextFactory is not enough. While we've created a specific type, said type information is unknown to AUWFG itself. Rather than having each action deal with typeless data, we use a custom Dispatcher:

func Dispatcher(route *auwfg.Route, context interface{}) auwfg.Response {
  //you probably won't need to do anything with route
  return route.Action.(func(*Context) auwfg.Response)(context.(*Context))
}
...
c := auwfg.Configure().ContextFactory(ContextFactory).Dispatcher(Dispatcher)
auwfg.Run(c)

Your dispatcher knows that your actions take a parameter of type Context, and thus can safely cast both the action and the parameter. Also, the dispatcher is a great place to execute pre and post filters for all actions.

Routing

Routing is strict and configured via the Route method of the configuration. Every route must have a mimimum of 4 components:

  • method
  • version
  • resource
  • action

The first three are strings, the last is the action handler. Let's look at some configuration example:

c := auwfg.Configure()

//matches GET /v1/gholas.ext
c.Route(auwfg.R("LIST", "v1", "gholas", gholas.List))

//matches GET /v1/gholas/SOMEID.EXT
c.Route(auwfg.R("GET", "v1", "gholas", gholas.Show))

//matches POST /v1/gholas.ext
c.Route(auwfg.R("POST", "v1", "gholas", gholas.Create))

//matches PUT /v1/gholas/SOMEID.ext
c.Route(auwfg.R("PUT", "v1", "gholas", gholas.Update))

//matches DELETE /v1/gholas/SOMEID.ext
c.Route(auwfg.R("DELETE", "v1", "gholas", gholas.Delete))

Note that the method for a GET with no id is called LIST. The gholas.List|Show|Create|Update|Delete actions are function which can be invoked by your dispatcher.

The captured parameters, "v1", "gholas" and "SOMEID" are available in the BaseContext.Params (Version, Resource and Id fields respectively), which you've hopefully preserved in your own context.

Nested Routes

A nested route is configured as such:

c.Route(auwfg.R("LIST", "v1", "ghoas/history", gholas.Show))

Will match GET /v1/gholas/SOMEID/history.json

SOMEID is available in Params.ParentId

Parsing Request Bodies

Any route can have an associated BodyFactory, configured as:

c.Route(auwfg.R("POST", "v1", "gholas", gholas.Create).BodyFactory(func() { return new(GholaInput)} ))

A BodyFactory simply returns an instance of an object which will be used with the encoding/json package to convert the request body into the desired input.

Sadly, type information is lost; actions must currently cast the BaseContext.Body to the appropriate type:

func Create(context *Context) auwfg.Response {
  input := context.Body.(*GholaInput)
  ...
}

When no body is present, an empty instance is made available. If, however, there's an error parsing the input, and InvalidFormat is returned.

Pool Size

A fixed-length byte pool is used to parse input bodies. The size of this pool is configured with the BodyPool configuration method. For example, to specify a max input body size of 64K and to pre-allocate 512 slots, you'd use:

c := auwfg.Configure().BodyPool(512, 64 * 1024)

While the maximum size of the body is fixed (64K with the above configuration), the number of available buffers (512 above) will grow as needed.

Responses

A auwfg.Response must implement three members:

  • Body() []byte
  • Status() int
  • Header() http.Header
  • Close()

The Json(body string) builder should prove helpful.

The Fatal(err error) helper should be used when an InternalServerError should be returned and an error logged (using the standard logger)

The auwfg.Deleted variable is a response which returns 204 and a content-length of 0

Closing Responses

The reason for responses to implement Close is to make it possible to use buffer pools when generating responses. If you're creating your own Response, it would not be unusual for Close to do nothing. Furthermore, under normal conditions, AUWFG will take care of closing responses. However, if you intercept a auwfg.Response and return a different response, you should call Close() on it. For example:

if res, valid := validate(input); valid == false {
  res.Close() // <- since auwfg will never see this response, you need to take care of closing it yourself
  return Json("why?!")s
}

Validation

AUWFG has some basic input validation facilities. Validation works in two phases. The first phase is to define rules. The second phase is to validate the actual data:

import (
  "github.com/viki-org/auwfg"
  "github.com/viki-org/auwfg/validation"
)

func init() {
  // definition id, field name, error message, vadation.Rule object
  auwfg.Define("user.username", "username", "Username is required", validation.Required())
  auwfg.Define("user.password", "password", "Password must be 6 or more characters", validation.MinLen(6))
}

func Create(context *Context) auwfg.Response {
  input := context.Body.(*LoginInput)
  if res, valid := validate(input); valid == false {
    return res
  }
  ...
}

func validate(input *LoginInput) (auwfg.Response, bool) {
  validator := auwfg.Validator()
  validator.Validate(input.UserName, "user.username")
  validator.Validate(input.Password, "user.password")
  return validator.Response()
}

Rules, such as the above RequiredRule and MinLenRule implement the validation.Rule interface. This interface defines a single method: Verify(value interface{}) bool.

The following validation methods are currently support:

  • Required()
  • Len(min, max int)
  • MinLen(min int)
  • MaxLen(max int)
  • Pattern(p *regexp.Pattern)
  • Email() (\S+@\S+.\S+)

In addition to calling Response, which returns (auwfg.Response, bool), IsValid() bool is also available.

Pool Size

A fixed-length byte pool is used to generate validation response. The size of this pool is configured with the InvalidPool configuration method. For example, to specify a max response size of 32 and to pre-allocate 256 slots, you'd use:

c := auwfg.Configure().InvalidPool(256, 32 * 1024)

While the maximum size is fixed (32K with the above configuration), the number of available buffers (256 above) will grow as needed.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	NotFound            = newNormalResponse([]byte(`{"error":"not found","code":404}`), 404)
	InternalServerError = newNormalResponse([]byte(`{"error":"internal server error","code":500}`), 500)
	Deleted             = &NormalResponse{204, []byte{}, http.Header{"Content-Length": []string{"0"}}}
)
View Source
var (
	GeoIP *gi.GeoIP
)
View Source
var (
	JsonHeader = []string{"application/json; charset=utf-8"}
)
View Source
var Stats = &stats{}

Functions

func IsIpPrivate

func IsIpPrivate(ipAddr string) bool

func Run

func Run(config *Configuration)

func Validator

func Validator() *validation.Validator

Types

type BaseContext

type BaseContext struct {
	Route   *Route
	Params  *Params
	RawBody []byte
	Body    interface{}
	Req     *http.Request
	Query   map[string]string
	Ip      string
	Country string
}

type BodyFactory

type BodyFactory func() interface{}

type ByteCloser

type ByteCloser interface {
	Len() int
	Close() error
	Bytes() []byte
}

type ClosableResponse

type ClosableResponse struct {
	S int
	H http.Header
	B ByteCloser
}

func (*ClosableResponse) Close

func (r *ClosableResponse) Close() error

func (*ClosableResponse) GetBody

func (r *ClosableResponse) GetBody() []byte

func (*ClosableResponse) GetHeader

func (r *ClosableResponse) GetHeader() http.Header

func (*ClosableResponse) GetStatus

func (r *ClosableResponse) GetStatus() int

func (*ClosableResponse) Length

func (r *ClosableResponse) Length() int

func (*ClosableResponse) SetStatus

func (r *ClosableResponse) SetStatus(status int)

type Configuration

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

func Configure

func Configure() *Configuration

func (*Configuration) Address

func (c *Configuration) Address(address string) *Configuration

func (*Configuration) BodyPool

func (c *Configuration) BodyPool(poolSize int, bufferSize int) *Configuration

func (*Configuration) BodyTooLarge

func (c *Configuration) BodyTooLarge(body string) *Configuration

func (*Configuration) BodyTooLargeResponse

func (c *Configuration) BodyTooLargeResponse(r Response) *Configuration

func (*Configuration) ContextFactory

func (c *Configuration) ContextFactory(cf ContextFactory) *Configuration

func (*Configuration) Dispatcher

func (c *Configuration) Dispatcher(d Dispatcher) *Configuration

func (*Configuration) InternalServerError

func (c *Configuration) InternalServerError(body string) *Configuration

func (*Configuration) InternalServerErrorResponse

func (c *Configuration) InternalServerErrorResponse(r Response) *Configuration

func (*Configuration) InvalidFormat

func (c *Configuration) InvalidFormat(body string) *Configuration

func (*Configuration) InvalidFormatResponse

func (c *Configuration) InvalidFormatResponse(r Response) *Configuration

func (*Configuration) InvalidPool

func (c *Configuration) InvalidPool(poolSize int, bufferSize int) *Configuration

func (*Configuration) LoadRawBody

func (c *Configuration) LoadRawBody() *Configuration

func (*Configuration) NotFound

func (c *Configuration) NotFound(body string) *Configuration

func (*Configuration) NotFoundResponse

func (c *Configuration) NotFoundResponse(r Response) *Configuration

func (*Configuration) Route

func (*Configuration) SetGeoIP

func (c *Configuration) SetGeoIP(path string) *Configuration

func (*Configuration) StatsFile

func (c *Configuration) StatsFile(file string) *Configuration

func (*Configuration) StatsSleep

func (c *Configuration) StatsSleep(sleep time.Duration) *Configuration

type ContextFactory

type ContextFactory func(*BaseContext) interface{}

type Dispatcher

type Dispatcher func(route *Route, context interface{}) Response

type FatalResponse

type FatalResponse struct {
	Response
	// contains filtered or unexported fields
}

type NormalResponse

type NormalResponse struct {
	S int
	B []byte
	H http.Header
}

func (*NormalResponse) Close

func (r *NormalResponse) Close() error

func (*NormalResponse) GetBody

func (r *NormalResponse) GetBody() []byte

func (*NormalResponse) GetHeader

func (r *NormalResponse) GetHeader() http.Header

func (*NormalResponse) GetStatus

func (r *NormalResponse) GetStatus() int

func (*NormalResponse) SetStatus

func (r *NormalResponse) SetStatus(status int)

type Params

type Params struct {
	Id             string
	Version        string
	Resource       string
	ParentId       string
	ParentResource string
}

type Response

type Response interface {
	SetStatus(status int)

	GetStatus() int
	GetBody() []byte
	GetHeader() http.Header
	Close() error
}

func Fatal

func Fatal(err error) Response

type ResponseBuilder

type ResponseBuilder struct {
	Response Response
}

func Json

func Json(body interface{}) *ResponseBuilder

func (*ResponseBuilder) Cache

func (b *ResponseBuilder) Cache(duration int) *ResponseBuilder

func (*ResponseBuilder) Close

func (b *ResponseBuilder) Close() error

func (*ResponseBuilder) GetBody

func (b *ResponseBuilder) GetBody() []byte

func (*ResponseBuilder) GetHeader

func (b *ResponseBuilder) GetHeader() http.Header

func (*ResponseBuilder) GetStatus

func (b *ResponseBuilder) GetStatus() int

func (*ResponseBuilder) Header

func (b *ResponseBuilder) Header(key, value string) *ResponseBuilder

func (*ResponseBuilder) SetStatus

func (b *ResponseBuilder) SetStatus(status int)

func (*ResponseBuilder) Status

func (b *ResponseBuilder) Status(status int) *ResponseBuilder

type Route

type Route struct {
	Action      interface{}
	BodyFactory BodyFactory
}

type RouteBuilder

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

func R

func R(method, version, resource string, action interface{}) *RouteBuilder

func (*RouteBuilder) BodyFactory

func (r *RouteBuilder) BodyFactory(bf func() interface{}) *RouteBuilder

type Router

type Router struct {
	*Configuration
	// contains filtered or unexported fields
}

func (*Router) ServeHTTP

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

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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