rack

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2021 License: MIT Imports: 10 Imported by: 0

README

Rack

Build Status codecov Go Report Card

Rack provides an opinionated wrapper for AWS Lambda handlers written in Go. The concept is similar to that offered by chop and aws-lambda-go-api-proxy, but without the integration with the standard HTTP modules.

The intention of the module is to remove a lot of the boilerplate involved in writing handler functions for scenarios that do not make use of HTTP routing. Typically this would be when an individual Lambda function is deployed for each resource in an API as opposed to using a router within a single function.

Getting Started

go get github.com/stevecallear/rack
import (
    "github.com/aws/aws-lambda-go/lambda"
	"github.com/stevecallear/rack"
)

func main() {
    h := rack.New(func(c rack.Context) error {
		t, err := store.GetTask(c.Context(), c.Path("id"))
		if err != nil {
			return err
		}

		return c.JSON(http.StatusOK, &t)
	})

    lambda.StartHandler(h)
}

Handler

A handler must satisfy the func(rack.Context) error signature. The supplied Context provides a number of request accessors and response writers for common operations.

Operations not available on the Context can be performed by accessing the canonical request and response objects using Request and Response respectively.

h := rack.New(func(c rack.Context) error {
    v := c.Request().Header.Get("X-Custom-Header")
    c.Response().Header.Set("X-Custom-Header", v)

    return c.NoContent(http.StatusOK)
})

The incoming event and Lamdba context are also available if required. The following example assumes that the event type is guaranteed. A type switch or equivalent should be used if the handler is handling multiple event types.

h := rack.NewWithConfig(cfg, func(c rack.Context) error {
    e := c.Request().Event.(*events.APIGatewayV2HTTPRequest)
    lc, _ := lambdacontext.FromContext(c.Context())

    return c.String(http.StatusOK, fmt.Sprintf("%s %s", e.RequestContext.AccountID, lc.AwsRequestID))
})

Configuration

Handler configuration can be optionally specified by using NewWithConfig.

Event Types

Rack supports API Gateway proxy integration, API Gateway V2 HTTP and ALB target group events. By default the event type is resolved at runtime, but this behaviour can be configured as required. The following example configures the handler to marshal to/from V2 HTTP events regardless of the payload.

cfg := rack.Config{
    Resolver:   rack.ResolveStatic(rack.APIGatewayV2HTTPEventProcessor),
}

h := rack.NewWithConfig(cfg, handler)
Middleware

Middleware can be specified by passing a MiddlewareFunc in the configuration. The Chain helper function allows multiple middleware functions to be combined into a single chain. Functions execute in the order they are specified as arguments.

cfg := rack.Config{
    Middleware: rack.Chain(errorLogging, extractClaims),
}

h := rack.NewWithConfig(cfg, handler)
Error Handling

By default Rack will only return a function error if the incoming our outgoing payloads cannot be marshalled. All handler errors will be written to the response as a JSON body. This behaviour can be customised by modifying the handler OnError function. The following example writes the error message to the response as a string.

cfg := rack.Config{
    OnError: func(c rack.Context, err error) error {
        return c.String(rack.StatusCode(err), err.Error())
    },
}

h := rack.NewWithConfig(cfg, handler)
Bind

The handler Context offers a Bind function to marshal the incoming JSON body into an object. It is possible to configure a post-bind operation, for example to perform validation.

cfg := rack.Config{
    OnBind: func(c rack.Context, v interface{}) error {
        if err := validate(v); err != nil {
            return rack.WrapError(http.StatusBadRequest, err)
        }

        return nil
    },
}

h := rack.NewWithConfig(cfg, func(c rack.Context) error {
    var t Task
    if err := c.Bind(&t); err != nil {
        return err
    }

    if err := store.CreateTask(c.Context(), t); err != nil {
        return err
    }

    return c.NoContent(http.StatusCreated)
})

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnsupportedEventType indicates that the supplied event payload is not supported
	ErrUnsupportedEventType = errors.New("unsupported event type")
)

Functions

func New

func New(h HandlerFunc) lambda.Handler

New returns a new lambda handler for the specified function

func NewWithConfig

func NewWithConfig(c Config, h HandlerFunc) lambda.Handler

NewWithConfig returns a new lambda handler for the specified function and configuration

func StatusCode

func StatusCode(err error) int

StatusCode returns the status code for the specified error

Types

type Config

type Config struct {
	Resolver        Resolver
	Middleware      MiddlewareFunc
	OnBind          func(Context, interface{}) error
	OnError         func(Context, error) error
	OnEmptyResponse HandlerFunc
}

Config represent handler configuration

type Context

type Context interface {
	// Context returns the function invocation context
	Context() context.Context

	// Request returns the canonical request
	Request() *Request

	// Response returns the canonical response
	Response() *Response

	// Get returns the stored value with the specified key
	Get(key string) interface{}

	// Set stores the specified value in the context
	Set(key string, v interface{})

	// Path returns the path parameter with the specified key
	// An empty string is returned if no parameter exists.
	Path(key string) string

	// Query returns the first query string parameter with the specified key
	// An empty string is returned if no parameter exists. If all query string values
	// are required, then the raw values can be accessed using Request().Query[key].
	Query(key string) string

	// Bind unmarshals the request body into the specified value
	// Currently only JSON request bodies are supported.
	Bind(v interface{}) error

	// NoContent writes the specified status code to the response without a body
	NoContent(code int) error

	// String writes the specified status code and value to the response
	String(code int, s string) error

	// JSON writes the specified status code and value to the response as JSON
	JSON(code int, v interface{}) error
}

Context represents a handler context

type HandlerFunc

type HandlerFunc func(Context) error

HandlerFunc represents a handler function

type MiddlewareFunc

type MiddlewareFunc func(HandlerFunc) HandlerFunc

MiddlewareFunc represents a middleware function

func Chain

func Chain(m ...MiddlewareFunc) MiddlewareFunc

Chain returns a middleware func that chains the specified funcs

type Processor

type Processor interface {
	// CanProcess returns true if the processor is valid for the payload
	CanProcess(payload []byte) bool

	// UnmarshalRequest unmarshals the specified payload into a canonical request
	UnmarshalRequest(payload []byte) (*Request, error)

	// MarshalResponse marshals the canonical response into a response payload
	MarshalResponse(res *Response) ([]byte, error)
}

Processor represents an event processor

var (
	// APIGatewayProxyEventProcessor is an api gateway proxy event processor
	APIGatewayProxyEventProcessor Processor = &processor{
		canProcess: func(payload []byte) bool {
			pv := gjson.GetManyBytes(payload, "version", "requestContext.apiId")
			return !pv[0].Exists() && pv[1].Exists()
		},
		unmarshalRequest: func(payload []byte) (*Request, error) {
			e := new(events.APIGatewayProxyRequest)
			if err := json.Unmarshal(payload, e); err != nil {
				return nil, err
			}

			q := url.Values(e.MultiValueQueryStringParameters)
			h := http.Header(e.MultiValueHeaders)

			return &Request{
				Method:  e.HTTPMethod,
				RawPath: e.Path,
				Path:    e.PathParameters,
				Query:   q,
				Header:  h,
				Body:    e.Body,
				Event:   e,
			}, nil
		},
		marshalResponse: func(r *Response) ([]byte, error) {
			return json.Marshal(&events.APIGatewayProxyResponse{
				StatusCode:        r.StatusCode,
				Headers:           reduceHeaders(r.Headers),
				MultiValueHeaders: r.Headers,
				Body:              r.Body,
				IsBase64Encoded:   false,
			})
		},
	}

	// APIGatewayV2HTTPEventProcessor is an api gateway v2 http event processor
	APIGatewayV2HTTPEventProcessor Processor = &processor{
		canProcess: func(payload []byte) bool {
			pv := gjson.GetManyBytes(payload, "version", "requestContext.apiId")
			return pv[0].String() == "2.0" && pv[1].Exists()
		},
		unmarshalRequest: func(payload []byte) (*Request, error) {
			e := new(events.APIGatewayV2HTTPRequest)
			if err := json.Unmarshal(payload, e); err != nil {
				return nil, err
			}

			q := url.Values{}
			for k, ps := range e.QueryStringParameters {
				for _, v := range strings.Split(ps, ",") {
					q.Add(k, v)
				}
			}

			h := http.Header{}
			mergeMaps(e.Headers, nil, h.Add)

			return &Request{
				Method:  e.RequestContext.HTTP.Method,
				RawPath: e.RequestContext.HTTP.Path,
				Path:    e.PathParameters,
				Query:   q,
				Header:  h,
				Body:    e.Body,
				Event:   e,
			}, nil
		},
		marshalResponse: func(r *Response) ([]byte, error) {
			return json.Marshal(&events.APIGatewayV2HTTPResponse{
				StatusCode:        r.StatusCode,
				Headers:           reduceHeaders(r.Headers),
				MultiValueHeaders: r.Headers,
				Body:              r.Body,
				IsBase64Encoded:   false,
				Cookies:           []string{},
			})
		},
	}

	// ALBTargetGroupEventProcessor is an alb target group event processor
	ALBTargetGroupEventProcessor Processor = &processor{
		canProcess: func(payload []byte) bool {
			return gjson.GetBytes(payload, "requestContext.elb").Exists()
		},
		unmarshalRequest: func(payload []byte) (*Request, error) {
			e := new(events.ALBTargetGroupRequest)
			if err := json.Unmarshal(payload, e); err != nil {
				return nil, err
			}

			q := url.Values{}
			mergeMaps(e.QueryStringParameters, e.MultiValueQueryStringParameters, q.Add)

			h := http.Header{}
			mergeMaps(e.Headers, e.MultiValueHeaders, h.Add)

			return &Request{
				Method:  e.HTTPMethod,
				RawPath: e.Path,
				Path:    map[string]string{},
				Query:   q,
				Header:  h,
				Body:    e.Body,
				Event:   e,
			}, nil
		},
		marshalResponse: func(r *Response) ([]byte, error) {
			return json.Marshal(&events.ALBTargetGroupResponse{
				StatusCode:        r.StatusCode,
				StatusDescription: http.StatusText(r.StatusCode),
				Headers:           reduceHeaders(r.Headers),
				MultiValueHeaders: r.Headers,
				Body:              r.Body,
				IsBase64Encoded:   false,
			})
		},
	}
)

type Request

type Request struct {
	Method  string
	RawPath string
	Path    map[string]string
	Query   url.Values
	Header  http.Header
	Body    string
	Event   interface{}
}

Request represents a canonical request type

type Resolver

type Resolver interface {
	Resolve(payload []byte) (Processor, error)
}

Resolver represents an event processor resolver

func ResolveConditional

func ResolveConditional(p ...Processor) Resolver

ResolveConditional returns a new conditional event processor resolver The first applicable processor will be returned, based on the incoming payload.

func ResolveStatic

func ResolveStatic(p Processor) Resolver

ResolveStatic returns a new static event processor resolver The supplied processor will be invoked for marshal/unmarshal operations, regardless of the incoming payload.

type Response

type Response struct {
	StatusCode int
	Headers    http.Header
	Body       string
}

Response represents a canonical response type

type StatusError

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

StatusError represents a status code error

func WrapError

func WrapError(code int, err error) *StatusError

WrapError wraps the specified error

func (*StatusError) Code

func (e *StatusError) Code() int

Code returns the error status code

func (*StatusError) Error

func (e *StatusError) Error() string

Error returns the error message

func (*StatusError) Unwrap

func (e *StatusError) Unwrap() error

Unwrap returns the wrapped error

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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