sandwich

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jan 5, 2023 License: MIT Imports: 13 Imported by: 1

README

Sandwich: Delicious HTTP Middleware Build Status Coverage Go Report Card GoDoc

Keep pilin' it on!

Sandwich is a middleware & routing framework that lets you write your handlers and middleware the way you want to and it takes care of tracking & validating dependencies.

Features

  • Keeps middleware and handlers simple and testable.
  • Consolidates error handling.
  • Ensures that middleware dependencies are safely provided -- avoids unsafe casting from generic context objects.
  • Detects missing dependencies during route construction (before the server starts listening!), not when the route is actually called.
  • Provides clear and helpful error messages.
  • Compatible with the http.Handler interface and lots of existing middleware.
  • Provides just a touch of magic: enough to make things easier, but not enough to induce a debugging nightmare.

Getting started

Here's a very simple example of using sandwich with the standard HTTP stack:

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/augustoroman/sandwich"
)

func main() {
	// Create a default sandwich middlware stack that includes logging and
	// a simple error handler.
	mux := sandwich.TheUsual()
	mux.Get("/", func(w http.ResponseWriter) {
		fmt.Fprintf(w, "Hello world!")
	})
	if err := http.ListenAndServe(":6060", mux); err != nil {
		log.Fatal(err)
	}
}

See the examples directory for:

Usage

Providing

Sandwich automatically calls your middleware with the necessary arguments to run them based on the types they require. These types can be provided by previous middleware or directly during the initial setup.

For example, you can use this to provide your database to all handlers:

func main() {
    db_conn := ConnectToDatabase(...)
    mux := sandwich.TheUsual()
    mux.Set(db_conn)
    mux.Get("/", Home)
}

func Home(w http.ResponseWriter, r *http.Request, db_conn *Database) {
    // process the request here, using the provided db_conn
}

Set(...) and SetAs(...) are excellent alternatives to using global values, plus they keep your functions easy to test!

Handlers

In many cases you want to initialize a value based on the request, for example extracting the user login:

func main() {
    mux := sandwich.TheUsual()
    mux.Get("/", ParseUserCookie, SayHi)
}
// You can write & test exactly this signature:
func ParseUserCookie(r *http.Request) (User, error) { ... }
// Then write your handler assuming User is available:
func SayHi(w http.ResponseWriter, u User) {
    fmt.Fprintf(w, "Hello %s", u.Name)
}

This starts to show off the real power of sandwich. For each request, the following occurs:

  • First ParseUserCookie is called. If it returns a non-nil error, sandwich's HandleError is called and the request is aborted. If the error is nil, processing continues.
  • Next SayHi is called with User returned from ParseUserCookie.

This allows you to write small, independently testable functions and let sandwich chain them together for you. Sandwich works hard to ensure that you don't get annoying run-time errors: it's structured such that it must always be possible to call your functions when the middleware is initialized rather than when the http handler is being executed, so you don't get surprised while your server is running.

Error Handlers

When a handler returns an error, sandwich aborts the middleware chain and looks for the most recently registered error handler and calls that. Error handlers may accept any types that have been provided so far in the middleware stack as well as the error type. They must not have any return values.

Here's an example of rendering errors with a custom error page:

type ErrorPageTemplate *template.Template
func main() {
    tpl := template.Must(template.ParseFiles("path/to/my/error_page.tpl"))
    mux := sandwich.TheUsual()
    mux.Set(ErrorPageTemplate(tpl))
    mux.OnErr(MyErrorHandler)
    ...
}
func MyErrorHandler(w http.ResponseWriter, t ErrorPageTemplate, l *sandwich.LogEntry, err error) {
    if err == sandwich.Done {  // sandwich.Done can be returned to abort middleware.
        return                 // It indicates there was no actual error, so just return.
    }
    // Unwrap to a sandwich.Error that has Code, ClientMsg, and internal LogMsg.
    e := sandwich.ToError(err)
    // If there's an internal log message, add it to the request log.
    e.LogIfMsg(l)
    // Respond with my custom html error page, including the client-facing msg.
    w.WriteHeader(e.Code)
    t.Execute(w, map[string]string{Msg: e.ClientMsg})
}

Error handlers allow you consolidate the error handling of your web app. You can customize the error page, assign user-facing error codes, detect and fire alerts for certain errors, and control which errors get logged -- all in one place.

By default, sandwich never sends internal error details to the client and insteads logs the details.

Wrapping Handlers

Sandwich also allows registering handlers to run during AND after the middleware (and error handling) stack has completed. This is especially useful for handles such as logging or gzip wrappers. Once the before handle is run, the 'after' handlers are queued to run and will be run regardless of whether an error aborts any subsequent middleware handlers.

Typically this is done with the first function creating and initializing some state to pass to the deferred handler. For example, the logging handlers are:

// NewLogEntry creates a *LogEntry and initializes it with basic request
// information.
func NewLogEntry(r *http.Request) *LogEntry {
    return &LogEntry{Start: time.Now(), ...}
}

// Commit fills in the remaining *LogEntry fields and writes the entry out.
func (entry *LogEntry) Commit(w *ResponseWriter) {
    entry.Elapsed = time.Since(entry.Start)
    ...
    WriteLog(*entry)
}

and are added to the chain using:

var LogRequests = Wrap{NewLogEntry, (*LogEntry).Commit}

In this case, NewLogEntry returns a *LogEntry that is then provided to downstream handlers, including the deferred Commit handler -- in this case a method expression that takes the *LogEntry as its value receiver.

Providing Interfaces

Unfortunately, set interface values is a little tricky. Since interfaces in Go are only used for static typing, the encapsulation isn't passed to functions that accept interface{}, like Set().

This means that if you have an interface and a concrete implementation, such as:

type UserDatabase interface{
    GetUserProfile(u User) (Profile, error)
}
type userDbImpl struct { ... }
func (u *userDbImpl) GetUserProfile(u User) (Profile, error) { ... }

You cannot provide this to handlers directly via the Set() call.

udb := &userDbImpl{...}
// DOESN'T WORK: this will provide *userDbImpl, not UserDatabase
mux.Set(udb)
mux.Set((UserDatabase)(udb)) // DOESN'T WORK EITHER
udb_iface := UserDatabase(udb)
mux.Set(&udb_iface)          // STILL DOESN'T WORK!

Instead, you have to either use SetAs() or a dedicated middleware function that returns the interface:

udb := &userDbImpl{...}
// either use SetAs() with a pointer to the interface
mux.SetAs(udb, (*UserDatabase)(nil))
// or add a handler that returns the interface
mux.Use(func() UserDatabase { return udb })

It's a bit silly, but there you are.

FAQ

Sandwich uses reflection-based dependency-injection to call the middleware functions with the parameters they need.

Q: OMG reflection and dependency-injection, isn't that terrible and slow and non-idiomatic go?!

Whoa, nelly. Let's deal with those one at time, m'kay?

Q: Isn't reflection slow?

Not compared to everything else a webserver needs to do.

Yes, sandwich's reflection-based dependency-injection code is slower than middleware code that directly calls functions, however the vast majority of server code (especially during development) is not impacted by time spent calling a few functions, but rather by HTTP network I/O, request parsing, database I/O, response marshalling, etc.

Q: Ok, but aren't both reflection and dependency-injection non-idiomatic Go?

Sorta. The use of reflection in and of itself isn't non-idiomatic, but the use of magical dependency injection is: Go eschews magic.

However, one of the major goals of this library is to allow the HTTP handler code (and all middleware) to be really clean, idiomatic go functions that are testable by themselves. The idea is that the magic is small, contained, doesn't leak, and provides substantial benefit.

Q: But wait, don't you get annoying run-time "dependency-not-found" errors with dependency-injection?

While it's true that you can't get the same compile-time checking that you do with direct-call-based middleware, sandwich works really hard to ensure that you don't get surprises while running your server.

At the time each middleware function is added to the stack, the library ensures that it's dependencies have been explicitly provided. One of the features of sandwich is that you can't arbitrary inject values -- they need to have an explicit provisioning source.

Q: Doesn't the http.Request.Context in go 1.7 solve the middleware dependency problem?

Have a request-scoped context allows you to pass values between middleware handlers, it's true. However, there's no guarantee that the values are available, so you get the same run-time bugs that you might get with a naive dependency-injection framework. In addition, you have to do type-assertions to get your values, so there's another possible source of bugs. One of the goals of sandwich is to avoid these two types of bugs.

Q: Why do I have to use two functions (before & after) to wrap a request. Why can't I just have one with a next() function?

Many middleware frameworks provide the capability to wrap a request via a next() function. Sometimes it's part of a context object (martini's Context.Next(), gin's Context.Next()) and sometimes it's directly provided (negroni's third handler arg).

While implementing sandwich, I initially included a next() function until I realized it was impossible to validate the dependencies with such a function. Sandwich guarantees that dependencies can be supplied, and therefore next() had to go.

Instead, I took a tip from go and instead implemented defer. The wrap interface simply makes it obvious that there's a before and after. This allows me to keep my dependency guarantee.

Q: I don't know, it's still scary and terrible!

Don't get scared off. Take a look at the library, try it out, and I hope you enjoy it. If you don't, there are lots of great alternatives.

Documentation

Overview

Package sandwich is a middleware framework for go that lets you write testable web servers.

Sandwich allows writing robust middleware handlers that are easily tested:

  • Avoid globals, instead propagate per-request state automatically from one handler to the next.
  • Write your handlers to accept the parameters they need rather than type-asserting from an untyped per-request context.
  • Abort request handling by returning an error.

Sandwich is provides a basic PAT-style router.

Example

Here's a simple complete program using sandwich:

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/augustoroman/sandwich"
)

func main() {
    mux := sandwich.TheUsual()
    mux.Get("/", func(w http.ResponseWriter) {
        fmt.Fprintf(w, "Hello world!")
    })
    if err := http.ListenAndServe(":6060", mux); err != nil {
        log.Fatal(err)
    }
}

Providing

Sandwich automatically calls your middleware with the necessary arguments to run them based on the types they require. These types can be provided by previous middleware or directly during the initial setup.

For example, you can use this to provide your database to all handlers:

func main() {
    db_conn := ConnectToDatabase(...)
    mux := sandwich.TheUsual()
    mux.Set(db_conn)
    mux.Get("/", Home)
}

func Home(w http.ResponseWriter, r *http.Request, db_conn *Database) {
    // process the request here, using the provided db_conn
}

Set(...) and SetAs(...) are excellent alternatives to using global values, plus they keep your functions easy to test!

Handlers

In many cases you want to initialize a value based on the request, for example extracting the user login:

func main() {
    mux := sandwich.TheUsual()
    mux.Get("/", ParseUserCookie, SayHi)
}
// You can write & test exactly this signature:
func ParseUserCookie(r *http.Request) (User, error) { ... }
// Then write your handler assuming User is available:
func SayHi(w http.ResponseWriter, u User) {
    fmt.Fprintf(w, "Hello %s", u.Name)
}

This starts to show off the real power of sandwich. For each request, the following occurs:

  • First ParseUserCookie is called. If it returns a non-nil error, sandwich's HandleError is called the request is aborted. If the error is nil, processing continues.
  • Next SayHi is called with the User value returned from ParseUserCookie.

This allows you to write small, independently testable functions and let sandwich chain them together for you. Sandwich works hard to ensure that you don't get annoying run-time errors: it's structured such that it must always be possible to call your functions when the middleware is initialized rather than when the http handler is being executed, so you don't get surprised while your server is running.

Error Handlers

When a handler returns an error, sandwich aborts the middleware chain and looks for the most recently registered error handler and calls that. Error handlers may accept any types that have been provided so far in the middleware stack as well as the error type. They must not have any return values.

Wrapping Handlers

Sandwich also allows registering handlers to run during AND after the middleware (and error handling) stack has completed. This is especially useful for handles such as logging or gzip wrappers. Once the before handle is run, the 'after' handlers are queued to run and will be run regardless of whether an error aborts any subsequent middleware handlers.

Typically this is done with the first function creating and initializing some state to pass to the deferred handler. For example, the logging handlers are:

// StartLog creates a *LogEntry and initializes it with basic request
// information.
func NewLogEntry(r *http.Request) *LogEntry {
  return &LogEntry{Start: time.Now(), ...}
}

// Commit fills in the remaining *LogEntry fields and writes the entry out.
func (entry *LogEntry) Commit(w *ResponseWriter) {
  entry.Elapsed = time.Since(entry.Start)
  ...
  WriteLog(*entry)
}

and are added to the chain using:

var LogRequests = Wrap{NewLogEntry, (*LogEntry).Commit}

In this case, the `Wrap` executes NewLogEntry during middleware processing that returns a *LogEntry which is provided to downstream handlers, including the deferred Commit handler -- in this case a method expression (https://golang.org/ref/spec#Method_expressions) that takes the *LogEntry as its value receiver.

Providing Interfaces

Unfortunately, providing interfaces is a little tricky. Since interfaces in Go are only used for static typing, the encapsulation isn't passed to functions that accept interface{}, like Set().

This means that if you have an interface and a concrete implementation, such as:

type UserDatabase interface{
    GetUserProfile(u User) (Profile, error)
}
type userDbImpl struct { ... }
func (u *userDbImpl) GetUserProfile(u User) (Profile, error) { ... }

You cannot provide this to handlers directly via the Set() call.

udb := &userDbImpl{...}
// DOESN'T WORK: this will provide *userDbImpl, not UserDatabase
mux.Set(udb)
// STILL DOESN'T WORK
mux.Set((UserDatabase)(udb))
// *STILL* DOESN'T WORK
udb_iface := UserDatabase(udb)
mux.Set(&udb_iface)

Instead, you have to either use SetAs() or a dedicated middleware function:

udb := &userDbImpl{...}
mux.SetAs(udb, (*UserDatabase)(nil))        // either use SetAs() with a pointer to the interface
mux.Use(func() UserDatabase { return udb }) // or add a handler that returns the interface

It's a bit silly, but that's how it is.

Index

Examples

Constants

This section is empty.

Variables

View Source
var Done = errors.New("<done>")

Done is a sentinel error value that can be used to interrupt the middleware chain without triggering the default error handling. HandleError will not attempt to write any status code or client message, nor will it add the error to the log.

View Source
var Gzip = Wrap{provideGZipWriter, (*gZipWriter).Flush}

Gzip wraps a sandwich.Middleware to add gzip compression to the output for all subsequent handlers.

For example, to gzip everything you could use:

router.Use(sandwich.Gzip)
...use as normal...

Or, to gzip just a particular route you could do:

router.Get("/foo/bar", sandwich.Gzip, MyHandleFooBar)

Note that this does NOT auto-detect the content and disable compression for already-compressed data (e.g. jpg images).

View Source
var LogRequests = Wrap{NewLogEntry, (*LogEntry).Commit}

LogRequests is a middleware wrap that creates a log entry during middleware processing and then commits the log entry after the middleware has executed.

View Source
var WriteLog = func(e LogEntry) {
	if e.Quiet {
		return
	}
	col, reset := logColors(e)
	fmt.Fprintf(os_Stderr, "%s%s %s \"%s %s\" (%d %dB %s) %s%s\n",
		col,
		e.Start.Format(time.RFC3339), e.RemoteIp,
		e.Request.Method, e.Request.RequestURI,
		e.StatusCode, e.ResponseSize, e.Elapsed,
		e.NotesAndError(),
		reset)
}

WriteLog is called to actually write a LogEntry out to the log. By default, it writes to stderr and colors normal requests green, slow requests yellow, and errors red. You can replace the function to adjust the formatting or use whatever logging library you like.

Functions

func HandleError

func HandleError(w http.ResponseWriter, r *http.Request, l *LogEntry, err error)

HandleError is the default error handler included in sandwich.TheUsual. If the error is a sandwich.Error, it responds with the specified status code and client message. Otherwise, it responds with a 500. In both cases, the underlying error is added to the request log.

If the error is sandwich.Done, HandleError does nothing.

func HandleErrorJson

func HandleErrorJson(w http.ResponseWriter, r *http.Request, l *LogEntry, err error)

HandleErrorJson is identical to HandleError except that it responds to the client as JSON instead of plain text. Again, detailed error info is added to the request log.

If the error is sandwich.Done, HandleErrorJson does nothing.

func NoLog

func NoLog(e *LogEntry)

NoLog is a middleware function that suppresses log output for this request. For example:

// suppress logging of the favicon request to reduce log spam.
router.Get("/favicon.ico", sandwich.NoLog, staticHandler)

This depends on WriteLog respecting the Quiet flag, which the default implementation does.

func ServeFS

func ServeFS(
	f fs.FS,
	fsRoot string,
	pathParam string,
) func(w http.ResponseWriter, r *http.Request, p Params)

ServeFS is a simple helper that will serve static files from an fs.FS filesystem. It allows serving files identified by a sandwich path parameter out of a subdirectory of the filesystem. This is especially useful when embedding static files:

//go:embed server_files
var all_files embed.FS

mux.Get("/css/:path*", sandwich.ServeFS(all_files, "static/css", "path"))
mux.Get("/js/:path*", sandwich.ServeFS(all_files, "dist/js", "path"))
mux.Get("/i/:path*", sandwich.ServeFS(all_files, "static/images", "path"))

Types

type ChainMutation

type ChainMutation interface {
	// Modify the provided chain and return the modified chain.
	Apply(c chain.Func) chain.Func
}

ChainMutation is a special type that allows modifying the chain directly when added to a router. This allows advanced usage and should generally not be used unless you know what you're doing. In particular, don't add `Arg`s to the chain, that will break the router.

type Error

type Error struct {
	Code      int
	ClientMsg string
	LogMsg    string
	Cause     error
}

Error is an error implementation that provides the ability to specify three things to the sandwich error handler:

  • The HTTP status code that should be used in the response.
  • The client-facing message that should be sent. Typically this is a sanitized error message, such as "Internal Server Error".
  • Internal debugging detail including a log message and the underlying error that should be included in the server logs.

Note that Cause may be nil.

The sandwich standard Error handlers (HandleError and HandleErrorJson) will respect these Errors and respond with the appropriate status code and client message. Additionally, the sandwich standard log handling will log LogMsg.

func ToError

func ToError(err error) Error

ToError will convert a generic non-nil error to an explicit sandwich.Error type. If err is already a sandwich.Error, it will be returned. Otherwise, a generic 500 Error (internal server error) will be initialized and returned. Note that if err is nil, it will still return a generic 500 Error.

func (Error) Error

func (e Error) Error() string

func (Error) LogIfMsg

func (e Error) LogIfMsg(l *LogEntry)

LogIfMsg will set the Error field on the LogEntry if the Error's LogMsg field has something.

type LogEntry

type LogEntry struct {
	RemoteIp     string
	Start        time.Time
	Request      *http.Request
	StatusCode   int
	ResponseSize int
	Elapsed      time.Duration
	Error        error
	Note         map[string]string
	// set to true to suppress logging this request
	Quiet bool
}

LogEntry is the information tracked on a per-request basis for the sandwich Logger. All fields other than Note are automatically filled in. The Note field is a generic key-value string map for adding additional per-request metadata to the logs. You can take *sandwich.LogEntry to your functions to add fields to Note.

For example:

func MyAuthCheck(r *http.Request, e *sandwich.LogEntry) (User, error) {
    user, err := decodeAuthCookie(r)
    if user != nil {
        e.Note["user"] = user.Id()  // indicate which user is auth'd
    }
    return user, err
}

func NewLogEntry

func NewLogEntry(r *http.Request) *LogEntry

NewLogEntry creates a *LogEntry and initializes it with basic request information.

func (*LogEntry) Commit

func (entry *LogEntry) Commit(w *ResponseWriter)

Commit fills in the remaining *LogEntry fields and writes the entry out.

func (LogEntry) NotesAndError

func (l LogEntry) NotesAndError() string

NotesAndError formats the Note values and error (if any) for logging.

type Params

type Params map[string]string

type ResponseWriter

type ResponseWriter struct {
	http.ResponseWriter
	Size int // The size of the response written so far, in bytes.
	Code int // The status code of the response, or 0 if not written yet.
}

ResponseWriter wraps http.ResponseWriter to add tracking of the response size and response code.

func WrapResponseWriter

func WrapResponseWriter(w http.ResponseWriter) (http.ResponseWriter, *ResponseWriter)

WrapResponseWriter creates a ResponseWriter and returns it as both an http.ResponseWriter and a *ResponseWriter. The double return is redundant for native Go code, but is a necessary hint to the dependency injection.

func (*ResponseWriter) Flush

func (w *ResponseWriter) Flush()

func (*ResponseWriter) Hijack

func (w *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error)

func (*ResponseWriter) Write

func (w *ResponseWriter) Write(p []byte) (int, error)

func (*ResponseWriter) WriteHeader

func (w *ResponseWriter) WriteHeader(code int)

type Router

type Router interface {
	// Set a value that will be available to all handlers subsequent referenced.
	// This is typically used for concrete values. For interfaces to be correctly
	// provided to subsequent middleware, use SetAs.
	Set(vals ...any)
	// SetAs sets a value as the specified interface that will be available to all
	// handlers.
	//
	// Example:
	//    type DB interface { ... }
	//    var db DB = ...
	//    mux.SetAs(db, (*DB)(nil))
	//
	// That is functionally equivalent to using a middleware function that returns
	// the desired interface instance:
	//    type DB interface { ... }
	//    var db DB = ...
	//    mux.Use(func() DB { return db })
	SetAs(val, ifacePtr any)

	// Use adds middleware to be invoked for all routes registered by the
	// returned Router. The current router is not affected. This is equivalent to
	// adding the specified middelwareHandlers to each registered route.
	Use(middlewareHandlers ...any)

	// On will register a handler for the given method and path.
	On(method, path string, handlers ...any)

	// Get registers handlers for the specified path for the 'GET' HTTP method.
	// Get is shorthand for `On("GET", ...)`.
	Get(path string, handlers ...any)
	// Put registers handlers for the specified path for the 'PUT' HTTP method.
	// Put is shorthand for `On("PUT", ...)`.
	Put(path string, handlers ...any)
	// Post registers handlers for the specified path for the 'POST' HTTP method.
	// Post is shorthand for `On("POST", ...)`.
	Post(path string, handlers ...any)
	// Patch registers handlers for the specified path for the 'PATCH' HTTP
	// method. Patch is shorthand for `On("PATCH", ...)`.
	Patch(path string, handlers ...any)
	// Delete registers handlers for the specified path for the 'DELETE' HTTP
	// method. Delete is shorthand for `On("DELETE", ...)`.
	Delete(path string, handlers ...any)
	// Any registers a handlers for the specified path for any HTTP method. This
	// will always be superceded by dedicated method handlers. For example, if the
	// path '/users/:id/' is registered for Get, Put and Any, GET and PUT requests
	// will be handled by the Get(...) and Put(...) registrations, but DELETE,
	// CONNECT, or HEAD would be handled by the Any(...) registration. Any is a
	// shortcut for `On("*", ...)`.
	Any(path string, handlers ...any)

	// OnErr uses the specified error handler to handle any errors that occur on
	// any routes in this router.
	OnErr(handler any)

	// SubRouter derives a router that will called for all suffixes (and methods)
	// for the specified path. For example, `sub := root.SubRouter("/api")` will
	// create a router that will handle `/api/`, `/api/foo`.
	SubRouter(pathPrefix string) Router

	// ServeHTTP implements the http.Handler interface for the router.
	ServeHTTP(w http.ResponseWriter, r *http.Request)
}

Router implements the sandwich middleware chaining and routing functionality.

Example
package main

import (
	"encoding/json"
	"fmt"
	"io/fs"
	"net/http"

	"github.com/augustoroman/sandwich"
)

type UserID string

type User struct{}
type UserDB interface {
	Get(UserID) (*User, error)
	New(*User) (UserID, error)
	Del(UserID) error
	List() ([]*User, error)
}

func main() {
	var db UserDB // = NewUserDB

	root := sandwich.TheUsual()
	root.SetAs(db, &db)

	api := root.SubRouter("/api")
	api.OnErr(sandwich.HandleErrorJson)

	apiUsers := api.SubRouter("/users")
	apiUsers.Get("/:uid", UserIDFromParam, UserDB.Get, SendUser)
	apiUsers.Delete("/:uid", UserIDFromParam, UserDB.Del)
	apiUsers.Get("/", UserDB.List, SendUserList)
	apiUsers.Post("/", UserFromCreateRequest, UserDB.New, SendUserID)

	var staticFS fs.FS
	root.Get("/home/", GetLoggedInUser, UserDB.Get, Home)
	root.Get("/:path", http.FileServer(http.FS(staticFS)))

}

func GetLoggedInUser(r *http.Request) (UserID, error) {
	token := r.Header.Get("user-token")
	uid := UserID(token) // decode the token to get the user info
	if uid == "" {
		return "", sandwich.Error{Code: http.StatusUnauthorized, ClientMsg: "invalid user token"}
	}
	return uid, nil
}

func Home(w http.ResponseWriter, u *User) {
	fmt.Fprintf(w, "Hello %v", u)
}

func UserIDFromParam(p sandwich.Params) (UserID, error) {
	uid := UserID(p["id"])
	if uid == "" {
		return "", sandwich.Error{Code: 400, ClientMsg: "Missing UID param", LogMsg: "Request missing UID param"}
	}
	return "", nil
}

func UserFromCreateRequest(r *http.Request) (*User, error) {
	u := &User{}
	return u, json.NewDecoder(r.Body).Decode(u)
}

func SendUserList(w http.ResponseWriter, users []*User) error { return SendJson(w, users) }
func SendUser(w http.ResponseWriter, user *User) error        { return SendJson(w, user) }
func SendUserID(w http.ResponseWriter, id UserID) error       { return SendJson(w, id) }

func SendJson(w http.ResponseWriter, val interface{}) error {
	w.Header().Set("Content-Type", "application/json")
	return json.NewEncoder(w).Encode(val)
}
Output:

func BuildYourOwn

func BuildYourOwn() Router

BuildYourOwn returns a minimal router that has no initial middleware handling.

func TheUsual

func TheUsual() Router

TheUsual returns a router initialized with useful middleware.

type Wrap

type Wrap struct {
	// `Before` is run in the normal course of middleware evaluation. Any returned
	// types from this will be available to the defer'd After handler. If Before
	// itself returns an error, After will not run.
	Before any
	// `After` is defer`d and run after the normal course of middleware has
	// completed, in reverse order of any registered `defer` handlers. Defer`d
	// handlers will always be executed if `Before` was executed, even in the case
	// of errors. The `After` handler may accept the `error` type -- that will be
	// nil unless a subsequent handler has returned an error.
	After any
}

Wrap provides a mechanism to add two handlers: one that runs during the normal course of middleware handling (Before) and one that is defer'd and runs after the main set of middleware has executed (After). The defer'd handler may accept the `error` type and handle or ignore errors as desired.

This is generally useful for specifying operations that need to run before and after subsequent middleware, such as timing, logging, or allocation/cleanup operations.

func (Wrap) Apply

func (w Wrap) Apply(c chain.Func) chain.Func

Apply modifies the chain to add Before and After.

Directories

Path Synopsis
Package chain is a reflection-based dependency-injected chain of functions that powers the sandwich middleware framework.
Package chain is a reflection-based dependency-injected chain of functions that powers the sandwich middleware framework.
examples
1-simple
1-simple is a demo webserver for the sandwich middleware package demonstrating basic usage.
1-simple is a demo webserver for the sandwich middleware package demonstrating basic usage.
2-advanced
2-advanced is a demo webserver for the sandwich middleware package demonstrating advanced usage.
2-advanced is a demo webserver for the sandwich middleware package demonstrating advanced usage.

Jump to

Keyboard shortcuts

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