chain

package
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: 0

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

Examples

Constants

This section is empty.

Variables

View Source
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

func (c Func) Arg(typeOrInterfacePtr interface{}) Func

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

func (c Func) Code(name, pkg string, w io.Writer)

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

func (c Func) Defer(handler interface{}) Func

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

func (c Func) OnErr(errorHandler interface{}) Func

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

func (c Func) Run(argValues ...interface{}) error

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

func (c Func) Set(value interface{}) Func

Set an immediate value. This cannot be used to provide an interface, instead use SetAs(...) or With(...) with a function that returns the interface.

func (Func) SetAs

func (c Func) SetAs(value, ifacePtr interface{}) Func

SetAs provides an immediate value as the specified interface type.

func (Func) Then

func (c Func) Then(handlers ...interface{}) Func

Then adds one or more handlers to the middleware chain. It may only accept args of types that have already been provided.

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

type PanicError struct {
	Val             interface{}
	RawStack        string
	MiddlewareStack []FuncInfo
}

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.

Jump to

Keyboard shortcuts

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