Documentation ¶
Overview ¶
Package chain is a reflection-based dependency-injected chain of functions that powers the sandwich middleware framework.
A Func chain represents a sequence of functions to call along with an initial input. The parameters to each function are automatically provided from either the initial inputs or return values of earlier functions in the sequence.
In contrast to other dependency-injection frameworks, chain does not automatically determine how to provide dependencies -- it merely uses the most recently-provided value. This enables chains to report errors immediately during the chain construction and, if successfully constructed, a chain can always be executed.
HTTP Middleware Example ¶
As a common example, chains in http handling middleware typically start with the http.ResponseWriter and *http.Request provided by the http framework:
base := chain.Func{}. Arg((*http.ResponseWriter)(nil)). // declared as an arg when Run Arg((*http.Request)(nil)). // declared as an arg when Run
Given the following functions:
func GetDB() (*UserDB, error) {...} func GetUserFromRequest(db *UserDB, req *http.Request) (*User, error) {...} func SendUserAsJSON(w http.ResponseWriter, u *User) error {...} func GetUserID(r *http.Request) (UserID, error) {...} func (db *UserDB) Lookup(UserID) (*User, error) { ... } func SendProjectAsJSON(w http.ResponseWriter, p *Project) error {...}
then these chains would work fine:
base.Then( GetDB, // takes no args ✅, provides *UserDB to later funcs GetUserFromRequest, // takes *UserDB ✅ and *Request ✅, provides *User SendUserAsJSON, // takes ResponseWriter ✅ and *User ✅ ) base.Then( GetDB, // takes no args ✅, provides *UserDB to later funcs GetUserID, // takes *Request ✅, provides UserID (*UserDB).Lookup, // takes *UserDB ✅ and UserID ✅, provides *User SendUserAsJSON, // takes ResponseWriter ✅ and *User ✅ )
but these chains would fail:
base.Then( GetUserFromRequest, // takes *UserDB ❌ and *Request ✅ GetDB, // this *UserDB isn't available yet. SendUserAsJSON, // ) base.Then( GetDB, // takes no args ✅, provides *UserDB to later funcs GetUserID, // takes *Request ✅, provides UserID (*UserDB).Lookup, // takes *UserDB ✅ and UserID ✅, provides *User SendProjectAsJSON, // takes ResponseWriter ✅ and *Project ❌ ) base.Then( GetUserFromRequest, // takes *UserDB ❌ and *Request ✅ SendUserAsJSON, // )
Index ¶
- Variables
- type Func
- func (c Func) Arg(typeOrInterfacePtr interface{}) Func
- func (c Func) Code(name, pkg string, w io.Writer)
- func (c Func) Defer(handler interface{}) Func
- func (c Func) MustRun(argValues ...interface{})
- func (c Func) OnErr(errorHandler interface{}) Func
- func (c Func) Run(argValues ...interface{}) error
- func (c Func) Set(value interface{}) Func
- func (c Func) SetAs(value, ifacePtr interface{}) Func
- func (c Func) Then(handlers ...interface{}) Func
- type FuncInfo
- type PanicError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var DefaultErrorHandler interface{} = func(err error) { panic(err) }
DefaultErrorHandler is called when an error in the chain occurs and no error handler has been registered. Warning! The default error handler is not checked to verify that it's arguments can be provided. It's STRONGLY recommended to keep this as absolutely simple as possible.
Functions ¶
This section is empty.
Types ¶
type Func ¶
type Func struct {
// contains filtered or unexported fields
}
Func defines the chain of functions to invoke when Run. Each Func is immutable: all operations will return a new Func chain.
Example ¶
package main import ( "fmt" "os" "time" "github.com/augustoroman/sandwich/chain" ) func main() { example := chain.Func{}. // Indicate that the chain will will receive a time.Duration as the first // arg when it's executed. Arg(time.Duration(0)). // When the chain is executed, it will first call time.Now which takes no // arguments but will return a time.Time value that will be available to // later calls. Then(time.Now). // Next, time.Sleep will be invoked, which requires a time.Duration // parameter. That's available since it's provided as an input to the chain. Then(time.Sleep). // Next, time.Since will be invoked, which requires a time.Time value that // was provided by the earlier time.Now call. It will return a time.Duration // value that will overwrite the input the chain. Then(time.Since). // Finally, we'll print out the stored time.Duration value. Then(func(dt time.Duration) { // Round to the nearest 10ms -- this makes the test not-flaky since the // sleep duration will not have been exact. dt = dt.Truncate(10 * time.Millisecond) fmt.Println("elapsed:", dt) }) example.MustRun(time.Duration(30 * time.Millisecond)) // Print the equivalent code: fmt.Println("Generated code is:") example.Code("example", "main", os.Stdout) }
Output: elapsed: 30ms Generated code is: func example( ) func( duration time.Duration, ) { return func( duration time.Duration, ) { var time_Time time.Time time_Time = time.Now() time.Sleep(duration) duration = time.Since(time_Time) chain_test.ExampleFunc.func1(duration) } }
Example (File) ¶
package main import ( "fmt" "os" "github.com/augustoroman/sandwich/chain" ) func main() { // Chains can be used to do file operations! writeToFile := chain.Func{}. Arg(""). // filename Arg([]byte(nil)). // data Then(os.Create). Then((*os.File).Write). Then((*os.File).Close) // This never fails -- any errors in creating the file or writing to it will // be handled by the default error handler that logs a message, but the `Run` // itself doesn't fail unless the args are incorrect. writeToFile.MustRun("test.txt", []byte("the data")) content, err := os.ReadFile("test.txt") panicOnErr(err) fmt.Printf("test.txt: %s\n", content) panicOnErr(os.Remove("test.txt")) } func panicOnErr(err error) { if err != nil { panic(err) } }
Output: test.txt: the data
func (Func) Arg ¶
Arg indicates that a value with the specified type will be a parameter to Run when the Func is invoked. This is typically necessary to start the chain for a given middleware framework. Arg should not be exposed to users of sandwich since it bypasses the causal checks and risks runtime errors.
func (Func) Code ¶
Code writes the Go code for the current chain out to w assuming it lives in package "pkg" with the specified handler function name.
func (Func) Defer ¶
Defer adds a deferred handler to be executed after all normal handlers and error handlers have been called. Deferred handlers are executed in reverse order that they were registered (most recent first). Deferred handlers can accept the error type even if it hasn't been explicitly provided yet. If no error has occurred, it will be nil.
func (Func) MustRun ¶
func (c Func) MustRun(argValues ...interface{})
MustRun will function chain with the provided args and panic if the args don't match the expected arg values.
func (Func) OnErr ¶
OnErr registers an error handler to be called for failures of subsequent handlers. It may only accept args of types that have already been provided.
func (Func) Run ¶
Run executes the function chain. All declared args must be provided in the order than they were declared. This will return an error only if the arguments do not exactly correspond to the declared args. Interface values must be passed as pointers to the interface.
Important note: The returned error is NOT related to whether any the calls of chain returns an error -- any errors returned by functions in the chain are handled by the registered error handlers.
func (Func) Set ¶
Set an immediate value. This cannot be used to provide an interface, instead use SetAs(...) or With(...) with a function that returns the interface.
type FuncInfo ¶
type FuncInfo struct { Name string // fully-qualified name, e.g.: github.com/foo/bar.FuncName File string Line int Func reflect.Value }
FuncInfo describes a registered middleware function.
type PanicError ¶
PanicError is the error that is returned if a handler panics. It includes the panic'd value (Val), the raw Go stack trace (RawStack), and the middleware execution history (MiddlewareStack) that shows what middleware functions have already been called.
func (PanicError) Error ¶
func (p PanicError) Error() string
func (PanicError) FilteredStack ¶
func (p PanicError) FilteredStack() []string
FilteredStack returns the stack trace without some internal chain.* functions and without reflect.Value.call stack frames, since these are generally just noise. The reflect.Value.call removal could affect user stack frames.
TODO(aroman): Refine filtering so that it only removes reflect.Value.call frames due to sandwich.