nject

package
v0.0.0-...-7490695 Latest Latest
Warning

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

Go to latest
Published: Feb 1, 2021 License: MIT Imports: 7 Imported by: 0

README

nject - dependency injection

GoDoc

Install:

go get github.com/BlueOwlOpenSource/nject

This package provides type-safe dependency injection without requiring users to do type assertions.

Main APIs

It provides two main APIs: Bind() and Run().

Bind() is used when performance matters: given a chain of providers, it will write two functions: one to initialize the chain and another to invoke it. As much as possible, all dependency injection work is done at the time of binding and initialization so that the invoke function operates with very little overhead. The chain is initialized when the initialize function is called. The chain is run when the invoke function is called. Bind() does not run the chain.

Run() is used when ad-hoc injection is desired and performance is not critical. Run is appropriate when starting servers and running tests. It is not reccomended for http endpoint handlers. Run exectes the chain immediately.

Identified by type

Rather than naming values, inputs and outputs are identified by their types.
Since Go makes it easy to create new types, this turns out to be quite easy to use.

Types of providers

Multiple types of providers are supported.

Literal values

You can provide a constant if you have one.

Injectors

Regular functions can provide values. Injectors will be called at initialization time when they're marked as cacheable or at invocation time if they're not.

Injectors can be memoized.

Injectors can return a special error type that stops the chain.

Injectors can use data produced by earlier injectors simply by having a function parameter that matches the type of a return value of an earlier injector.

Wrappers

Wrappers are special functions that are responsible for invoking the part of the injection chain that comes after themselves. They do this by calling an inner() function that the nject framework defines for them.

Any arguments to the inner() function are injected as values available further down the chain. Any return values from inner() must be returned by the final function in the chain or from another wrapper futher down the chain.

Composition

Collections of injectors may be composed by including them in other collections.

Documentation

Overview

Package nject is a general purpose lightweight dependency injection framework. It provides wrapping, pruning, and indirect variable passing. It is type safe and using it requires no type assertions. There are two main injection APIs: Run and Bind. Bind is designed to be used at program initialization and does as much work as possible then rather than during main execution.

The basic idea is to assemble a Collection of providers and then use that collection to supply inputs for functions that may use some or all of the provided types.

The biggest win from dependency injection with nject is the ability to reshape various different functions into a single signature. For example, having a bunch of functions with different APIs all bound as http.HandlerFunc is easy.

Every provider produces or consumes data. The data is distinguished by its type. If you want to three different strings, then define three different types:

type myFirst string
type mySecond string
type myThird string

Then you can have a function that does things with the three types:

func myStringFunc(first myFirst, second mySecond) myThird {
	return myThird(string(first) + string(second))
}

The above function would be a valid injector or final function in a provider Collection. For example:

var result string
Sequence("example sequence",
	func() mySecond {
		return "2nd"
	}
	myStringFunc,
).Run("example run",
	func(s myThird) {
		result = string(s)
	},
	myFirst("1st"))
fmt.Println(result)

This creates a sequence and executes it. Run injects a myFirst value and the sequence of providers runs: genSecond() injects a mySecond and myStringFunc() combines the myFirst and mySecond to create a myThird. Then the function given in run saves that final value. The expected output is

1st2nd

Collections

Providers are grouped as into linear sequences. When building an injection chain, the providers are grouped into several sets: LITERAL, STATIC, RUN. The LITERAL and STATIC sets run once per binding. The RUN set runs once per invoke. Providers within a set are executed in the order that they were originally specified. Providers whose outputs are not consumed are omitted unless they are marked Required().

The LITERAL set is just the literal values in the collection.

The STATIC set is composed of the cacheable injectors.

The RUN set if everything else.

Injectors

All injectors have the following type signature:

func(input value(s)) output values(s)

None of the input or output parameters may be anonymously-typed functions. An anoymously-typed function is a function without a named type.

Injectors whose output values are not used by a downstream handler are dropped from the handler chain. They are not invoked. Injectors that have no output values are a special case and they are always retained in the handler chain.

Cached injectors

In injector that is annotated as Cacheable() may promoted to the STATIC set. An injector that is annotated as MustCache() or Memoize() must be promoted to the STATIC set: if it cannot be promoted then the colection is deemed invalid.

An injector may not be promoted to the STATIC set if it takes as input data that comes from a provider that is not in the STATIC or LITERAL sets. For example, when using Bind(), if the invoke function takes an int as one of its inputs, then no injector that takes an int as an argument may be promoted to the STATIC set.

Memoized injectors

Injectors in the STATIC set are only run for initialization. For some things, like opening a database, that may still be too often. Injectors that are marked Memoized must be promoted to the static set.

Memoized injectors are only run once per combination of inputs. Their outputs are remembered. If called enough times with different arguments, memory will be exhausted.

Memoized injectors may not have more than 30 inputs.

Memoized injectors may not have any inputs that are go maps, slices, or functions. Arrays, structs, and interfaces are okay. This requirement is recursive so a struct that that has a slice in it is not okay.

Fallible injectors

Fallible injectors are injectors that return a value of type TerminalError.

func(input value(s)) (output values(s), TerminalError)

The TerminalError does not have to be the last return value. The nject package converts TerminalError objects into error objects so only the fallible injector should use TerminalError. Anything that consumes the TerminalError should do so by consuming error instead.

Fallible injectors can be in both the STATIC set and the RUN set. Their behavior is a bit different.

If a non-nil value is returned as the TerminalError from a fallible injector in the RUN set, none of the downstream providers will be called. The provider chain returns from that point with the TerminalError as a return value. Since all return values must be consumed by a middleware provider or the bound invoke function, fallible injectors must come downstream from a middleware handler that takes TerminalError as a returned value if the invoke function does not return error. If a fallible injector returns nil for the TerminalError, the other output values are made available for downstream handlers to consume. The other output values are not considered return values and are not available to be consumed by upstream middleware handlers. The error returned by a fallible injector is not available downstream.

If a non-nil value is returned as the TerminalError from a fallible injector in the STATIC set, the rest of the STATIC set will be skipped. If there is an init function and it returns error, then the value returned by the fallible injector will be returned via init fuction. Unlike fallible injectors in the RUN set, the error output by a fallible injector in the STATIC set is available downstream (but only in the RUN set -- nothing else in the STATIC set will execute).

Some examples:

func staticInjector(i int, s string) int { return i+7 }

func injector(r *http.Request) string { return r.FormValue("x") }

func fallibleInjector(i int) nject.TerminalError {
	if i > 10 {
		return fmt.Errorf("limit exceeded")
	}
	return nil
}

Wrap functions

A wrap function interrupts the linear sequence of providers. It may or may invoke the remainder of the sequence that comes after it. The remainder of the sequence is provided to the wrap function as a function that it may call. The type signature of a wrap function is a function that receives an function as its first parameter. That function must be of an anonymous type:

// wrapFunction
func(innerFunc, input value(s)) return value(s)

// innerFunc
func(output value(s)) returned value(s)

For example:

func wrapper(inner func(string) int, i int) int {
	j := inner(fmt.Sprintf("%d", i)
	return j * 2
}

When this wrappper function runs, it is responsible for invoking the rest of the provider chain. It does this by calling inner(). The parameters to inner are available as inputs to downstream providers. The value(s) returned by inner come from the return values of other wrapper functions and from the return value(s) of the final function.

Wrap functions can call inner() zero or more times.

The values returned by wrap functions must be consumed by another upstream wrap function or by the init function (if using Bind()).

Final functions

Final functions are simply the last provider in the chain. They look like regular Go functions. Their input parameters come from other providers. Their return values (if any) must be consumed by an upstream wrapper function or by the init function (if using Bind()).

func(input value(s)) return values(s)

Literal values

Literal values are values in the provider chain that are not functions.

Invalid provider chains

Provider chains can be invalid for many reasons: inputs of a type not provided earlier in the chain; annotations that cannot be honored (eg. MustCache & Memoize); return values that are not consumed; functions that take or return functions with an anymous type other than wrapper functions; A chain that does not terminate with a function; etc. Bind() and Run() will return error when presented with an invalid provider chain.

Panics

Bind() and Run() will return error rather than panic. After Bind()ing an init and invoke function, calling them will not panic unless a provider panic()s

Chain evaluation

Bind() uses a complex and somewhat expensive O(n^2) set of rules to evaluate which providers should be included in a chain and which can be dropped. The goal is to keep the ones you want and remove the ones you don't want. Bind() tries to figure this out based on the dependencies and the annotations.

MustConsume, not Desired: Only include if at least one output is transitively consumed by a Required or Desired chain element and all outputs are consumed by some other provider.

Not MustConsume, not Desired: only include if at least one output is transitively consumed by a Required or Desired provider.

Not MustConsume, Desired: Include if all inputs are available.

MustConsume, Desired: Only include if all outputs are transitively consumed by a required or Desired chain element.

When there are multiple providers of a type, Bind() tries to get it from the closest provider.

Providers that have unmet dependencies will be eliminated from the chain unless they're Required.

Example

Example shows what gets included and what does not for several injection chains. These examples are meant to show the subtlety of what gets included and why.

// This demonstrates displaying the elements of a chain using an error
// returned by the final element.
fmt.Println(Run("empty-chain",
	Provide("Names", func(d *Debugging) error {
		return errors.New(strings.Join(d.NamesIncluded, ", "))
	})))

// This demonstrates that wrappers will be included if they are closest
// provider of a return type that is required.  Names is included in
// the upwards chain even though ReflectError could provide the error that
// Run() wants.
fmt.Println(Run("overwrite",
	Required(Provide("InjectErrorDownward", func() error { return errors.New("overwrite me") })),
	Provide("Names", func(inner func() error, d *Debugging) error {
		inner()
		return errors.New(strings.Join(d.NamesIncluded, ", "))
	}),
	Provide("ReflectError", func(err error) error { return err })))

// This demonstrates that the closest provider will be chosen over one farther away.
// Otherwise InInjector would be included instead of BoolInjector and IntReinjector.
fmt.Println(Run("multiple-choices",
	Provide("IntInjector", func() int { return 1 }),
	Provide("BoolInjector", func() bool { return true }),
	Provide("IntReinjector", func(bool) int { return 2 }),
	Provide("IntConsumer", func(i int, d *Debugging) error {
		return errors.New(strings.Join(d.NamesIncluded, ", "))
	})))
Output:

Debugging, empty-chain invoke func, Run()error, Names
Debugging, overwrite invoke func, Run()error, InjectErrorDownward, Names, ReflectError
Debugging, multiple-choices invoke func, Run()error, BoolInjector, IntReinjector, IntConsumer

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func DetailedError

func DetailedError(err error) string

DetailedError transforms errors into strings. If the error happens to be an error returned by Bind() or something that called Bind() then it will return a much more detailed error than just calling err.Error()

func ExampleNotCacheable

func ExampleNotCacheable()

ExampleNotCacheable is a function to demonstrate the use of NotCacheable and MustConsume.

func MustBind

func MustBind(c *Collection, invokeFunc interface{}, initFunc interface{})

MustBind is a wrapper for Collection.Bind(). It panic()s if Bind() returns error.

func MustBindSimple

func MustBindSimple(c *Collection, name string) func()

MustBindSimple binds a collection with an invoke function that takes no arguments and returns no arguments. It panic()s if Bind() returns error.

func MustBindSimpleError

func MustBindSimpleError(c *Collection, name string) func() error

MustBindSimpleError binds a collection with an invoke function that takes no arguments and returns error.

func MustRun

func MustRun(name string, providers ...interface{})

MustRun is a wrapper for Run(). It panic()s if Run() returns error.

func MustSetCallback

func MustSetCallback(c *Collection, binderFunction interface{})

MustSetCallback is a wrapper for Collection.SetCallback(). It panic()s if SetCallback() returns error.

func Run

func Run(name string, providers ...interface{}) error

Run is different from bind: the provider chain is run, not bound to functions.

The only return value from the final function that is captured by Run() is error. Run will return that error value. If the final function does not return error, then run will return nil if it was able to execute the collection and function. Run can return error because the final function returned error or because the provider chain was not valid.

Nothing is pre-computed with Run(): the run-time cost from nject is higher than calling an invoke function defined by Bind().

Predefined Collection objects are considered providers along with InjectItems, functions, and literal values.

Example

Run is the simplest way to use the nject framework. Run simply executes the provider chain that it is given.

providerChain := Sequence("example sequence",
	"a literal string value",
	func(s string) int {
		return len(s)
	})
Run("example",
	providerChain,
	func(i int, s string) {
		fmt.Println(i, len(s))
	})
Output:

22 22

Types

type Collection

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

Collection holds a sequence of providers and sub-collections

func Sequence

func Sequence(name string, providers ...interface{}) *Collection

Sequence creates a Collection of providers. Each collection must have a name. The providers can be: functions, variables, or *Collections, or *Providers.

Functions must match one of the expected patterns.

Injectors specified here will be separated into two sets: ones that are run once per bound chain (STATIC); and ones that are run for each invocation (RUN). Memoized functions are in the STATIC chain but they only get run once per input combination. Literal values are inserted before the STATIC chain starts.

Each set will run in the order they were given here. Providers whose output is not consumed are skipped unless they are marked with Required. Providers that produce no output are always run.

Previsously created *Collection objects are considered providers along with *Provider, named functions, anoymous functions, and literal values.

func (*Collection) Append

func (c *Collection) Append(name string, funcs ...interface{}) *Collection

Append appends additional providers onto an existing collection to create a new collection. The additional providers may be value literals, functions, Providers, or *Collections.

func (*Collection) Bind

func (c *Collection) Bind(invokeFunc interface{}, initFunc interface{}) error

Bind expects to receive two function pointers for functions that are not yet defined. Bind defines the functions. The first function is called to invoke the Collection of providers.

The inputs to the invoke function are passed into the provider chain. The value returned from the invoke function comes from the values returned by the provider chain (from middleware and from the final func).

The second function is optional. It is called to initialize the provider chain. Once initialized, any further calls to the initialize function are ignored.

The inputs to the initialization function are injected into the head of the provider chain. The static portion of the provider chain will run once. The values returned from the initialization function come from the values available after the static portion of the provider chain runs.

Bind pre-computes as much as possible so that the invokeFunc is fast.

Example

Bind does as much work before invoke as possible.

providerChain := Sequence("example sequence",
	func(s string) int {
		return len(s)
	},
	func(i int, s string) {
		fmt.Println(s, i)
	})

var aInit func(string)
var aInvoke func()
providerChain.Bind(&aInvoke, &aInit)
aInit("string comes from init")
aInit("ignored since invoke is done")
aInvoke()
aInvoke()

var bInvoke func(string)
providerChain.Bind(&bInvoke, nil)
bInvoke("string comes from invoke")
bInvoke("not a constant")
Output:

string comes from init 22
string comes from init 22
string comes from invoke 24
not a constant 14

func (*Collection) SetCallback

func (c *Collection) SetCallback(setCallbackFunc interface{}) error

SetCallback expects to receive a function as an argument. SetCallback() will call that function. That function in turn should take one or two functions as arguments. The first argument must be an invoke function (see Bind). The second argument (if present) must be an init function. The invoke func (and the init func if present) will be created by SetCallback() and passed to the function SetCallback calls.

type Debugging

type Debugging struct {
	// Included is a list of the providers included in the chain.
	//
	// The format is:
	// "${groupName} ${className} ${providerNameShape}"
	Included []string

	// NamesIncluded is a list of the providers included in the chain.
	// The format is:
	// "${providerName}
	NamesIncluded []string

	// IncludeExclude is a list of all of the providers supplied to
	// ceate the chain.  Why each was included or not explained.
	// "INCLUDED ${groupName} ${className} ${providerNameShape} BECAUSE ${whyProviderWasInclude}"
	// "EXCLUDED ${groupName} ${className} ${providerNameShape} BECAUSE ${whyProviderWasExcluded}"
	IncludeExclude []string

	// Trace is an nject internal debugging trace that details the
	// decision process to decide which providers are included in the
	// chain.
	Trace string

	// Reproduce is a Go source string that attempts to somewhat anonymize
	// a provider chain as a unit test.  This output is nearly runnable
	// code.  It may need a bit of customization to fully capture a situation.
	Reproduce string
}

Debugging is provided to help diagnose injection issues. *Debugging is injected into every chain that consumes it.

type Provider

type Provider interface {
	// contains filtered or unexported methods
}

Provider is an individual injector (function, constant, or wrapper). Functions that take injectors, take interface{}. Functions that return invjectors return Provider so that methods can be attached.

func Cacheable

func Cacheable(fn interface{}) Provider

Cacheable creates an inject item and annotates it as allowed to be in the STATIC chain. Without this annotation, MustCache, or Memoize, a provider will be in the RUN chain.

When used on an existing Provider, it creates an annotated copy of that provider.

func ConsumptionOptional

func ConsumptionOptional(fn interface{}) Provider

ConsumptionOptional creates a new provider and annotates it as allowed to have some of its return values ignored. Without this annotation, a wrap function will not be included if some of its return values are not consumed.

In the downward direction, optional consumption is the default.

When used on an existing Provider, it creates an annotated copy of that provider.

func Desired

func Desired(fn interface{}) Provider

Desired creates a new provider and annotates it as desired: it will be included in the provider chain unless doing so creates an un-met dependency.

Injectors and wrappers that have no outputs are automatically considered desired.

When used on an existing Provider, it creates an annotated copy of that provider.

func Loose

func Loose(fn interface{}) Provider

Loose annotates a wrap function to indicate that when trying to match types against the outputs and return values from this provider, an in-exact match is acceptable. This matters when inputs and returned values are specified as interfaces. With the Loose annotation, an interface can be matched to the outputs and/or return values of this provider if the output/return value implements the interface.

By default, an exact match of types is required for all providers.

func Memoize

func Memoize(fn interface{}) Provider

Memoize creates a new InjectItem that is tagged as Cacheable further annotated so that it only executes once per input parameter values combination. This cache is global among all Sequences. Memoize can only be used on functions whose inputs are valid map keys (interfaces, arrays (not slices), structs, pointers, and primitive types).

Memoize is further restricted in that it only works in the STATIC provider set.

When used on an existing Provider, it creates an annotated copy of that provider.

func MustCache

func MustCache(fn interface{}) Provider

MustCache creates an Inject item and annotates it as required to be in the STATIC set. If it cannot be placed in the STATIC set then any collection that includes it is invalid.

func MustConsume

func MustConsume(fn interface{}) Provider

MustConsume creates a new provider and annotates it as needing to have all of its output values consumed. If any of its output values cannot be consumed then the provider will be excluded from the chain even if that renders the chain invalid.

A that is received by a provider and then provided by that same provider is not considered to have been consumed by that provider.

For example:

// All outputs of A must be consumed
Provide("A", MustConsume(func() string) { return "" } ),

// Since B takes a string and provides a string it
// does not count as suming the string that A provided.
Provide("B", func(string) string { return "" }),

// Since C takes a string but does not provide one, it
// counts as consuming the string that A provided.
Provide("C", func(string) int { return 0 }),

MustConsume works only in the downward direction of the provider chain. In the upward direction (return values) all values must be consumed.

When used on an existing Provider, it creates an annotated copy of that provider.

func NotCacheable

func NotCacheable(fn interface{}) Provider

NotCacheable creates an inject item and annotates it as not allowed to be in the STATIC chain. With this annotation, Cacheable is ignored and MustCache causes an invalid chain.

When used on an existing Provider, it creates an annotated copy of that provider.

func Provide

func Provide(name string, fn interface{}) Provider

Provide wraps an individual provider. It allows the provider to be named. The return value is chainable with with annotations like Cacheable() and Required(). It can be included in a collection. When providers are not named, they get their name from their position in their collection combined with the name of the collection they are in.

When used on an existing Provider, it creates an annotated copy of that provider.

func Required

func Required(fn interface{}) Provider

Required creates a new provider and annotates it as required: it will be included in the provider chain even if its outputs are not used.

When used on an existing Provider, it creates an annotated copy of that provider.

type TerminalError

type TerminalError interface {
	error
}

TerminalError is a standard error interface. For fallible injectors, TerminalError must one of the return values.

A non-nil return value terminates the handler call chain. The TerminalError return value gets converted to a regular error value and (like other return values) it must be consumed by an upstream handler or the invoke function.

Jump to

Keyboard shortcuts

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