argmapper

package module
v0.2.4 Latest Latest
Warning

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

Go to latest
Published: Apr 20, 2022 License: MPL-2.0 Imports: 10 Imported by: 14

README

go-argmapper Godoc

go-argmapper is a dependency-injection library for Go that supports automatically chaining conversion functions to reach desired results. go-argmapper is designed for runtime, reflection-based dependency injection.

API Status: Mostly Stable. We have released HashiCorp products using this library successfully, so we don't think the API will change significantly. For the time being, we're retaining the 0.x version numbers to note that we may still change the API and to recognize that the library has only been used in the real world for a short period of time.

Features

Named parameter matching. go-argmapper can match on named arguments, so you can say that from int is different from to int when calling the same function.

Typed parameter matching. go-argmapper can match on types, including interfaces and interface implementations. This enables the common dependency-injection pattern of fulfilling an interface.

"Subtype" labels for overloaded types. Values can be labeled with a "subtype" key (a string) for more fine-grained matching. A real-world use case of this is protobuf Any values. The subtype of these values can be the protobuf message name. This enables separating name, type, and subtype for more fine-grained matching.

Automatic conversion function chaining. You can configure multiple "conversion functions" that can take some set of values and return another set of values and go-argmapper will automatically call them in the correct order if necessary to reach your desired function parameter types.

Function redefinition in terms of certain types. Functions can be "redefined" to take as input and/or output values that match user-provided filters. go-argmapper will automatically call proper conversion functions to reach the target function.

Type conversion API. In addition to function calling, you can use the automatic conversion function chaining to convert some input values to any target value. go-argmapper will tell you (via an error) if this is not possible.

Examples

Basic Dependency Injection

The example below shows common, basic dependency injection.

// This is our target function. It wants some Writer implementation.
target, err := argmapper.NewFunc(func(w io.Writer) {
	// ... use the writer ...
})

// This is a provider that provides our io.Writer. You can imagine that
// this may differ between test/prod, configs, etc.
provider := func() io.Writer { return bytes.NewBuffer(nil) }

// Call our function. This will call our provider to create an io.Writer
// and then call our target function.
result := target.Call(argmapper.Converter(provider))
if result.Err() != nil {
	panic(result.Err())
}

The key thing happening here is that we're registering the provider function as a "converter." argmapper will automatically find some converter to provide any values we're looking for.

Named and Typed Values

The example below shows both named and typed parameters in use.

target, err := argmapper.NewFunc(func(input struct {
	// This tells argmapper to fill the values in this struct rather
	// than provide a value for the entire struct.
	argmapper.Struct

	A int
	B int
	Prefix string
}) string {
	return fmt.Sprintf("%s: %d", in.Prefix, in.A*in.B)
})

result := target.Call(
	argmapper.Named("a", 21),
	argmapper.Named("b", 2),
	argmapper.Typed("our value is"),
)
if result.Err() != nil {
	panic(result.Err())
}

// This prints: "our value is: 42"
println(result.Out(0).(string))

Both A and B are of the same type, but are matched on their names. This lets us get the desired value of 42, rather than 21*21, 2*2, etc.

Note that Prefix is a named parameter, but we don't provide any inputs matching that name. In this case, argmapper by default falls back to treating it as a typed parameter, allowing our typed string input to match.

Explicitly Typed Values

The previous example showed Prefix implicitly using a typed-only match since there was no input named "Prefix". You can also explictly note that the name doesn't matter in two ways.

First, you can use struct tags:

target, err := argmapper.NewFunc(func(input struct {
	// This tells argmapper to fill the values in this struct rather
	// than provide a value for the entire struct.
	argmapper.Struct

	A int
	B int
	Prefix string `argmapper:",typeOnly"`
}) string {
	return fmt.Sprintf("%s: %d", in.Prefix, in.A*in.B)
})

You can also use a non-struct input. Go reflection doesn't reveal function parameter names so all function parameters are by definition type only:

target, err := argmapper.NewFunc(func(string) {})

You can mix and match named and typed parameters.

Conversion Function Chaining

The example below shows how conversion functions are automatically chained as necessary to reach your desired function.

// Trivial function that takes a string and just returns it.
target, err := argmapper.NewFunc(func(v string) string { return v })

result := target.Call(
	// "false" value
	argmapper.Typed(false),

	// bool to int
	argmapper.Converter(func(v bool) int {
		if v {
			return 1
		}

		return 0
	}),

	// int to string
	argmapper.Converter(func(v int) string {
		return strconv.Itoa(v)
	}),
)
if result.Err() != nil {
	// If we didn't have converters necessary to get us from bool => int => string
	// then this would fail.
	panic(result.Err())
}

// Prints "0"
println(result.Out(0).(string))

Typed converters preserve the name of their arguments. If the above input was Named("foo", false) rather than typed, then the name "foo" would be attached both the string and int values generated in case any target functions requested a named parameter. In the case of this example, the name is carried through but carries no consequence since the final target function is just a typed parameter.

Conversion Function Cycles

Cycles in conversion functions are completely allowed. The example below behaves as you would expect. This is a simple direct cycle, more complex cycles from chaining multiple converters will also behave correctly. This lets you register complex sets of bidirectional conversion functions with ease.

// Trivial function that takes a string and just returns it.
target, err := argmapper.NewFunc(func(v string) string { return v })

result := target.Call(
	argmapper.Typed(12),
	argmapper.Converter(func(v int) string { return strconv.Itoa(v) }),
	argmapper.Converter(func(v string) (int, error) { return strconv.Atoi(v) }),
)
if result.Err() != nil {
	// If we didn't have converters necessary to get us from bool => int => string
	// then this would fail.
	panic(result.Err())
}

// Prints "12"
println(result.Out(0).(string))
Conversion Errors

The example above has a converter that returns (int, error). If the final return type of a converter is error, go-argmapper treats that as a special value signaling if the conversion succeeded or failed.

If conversion fails, the target function call fails and the error is returned to the user.

In the future, we plan on retrying via other possible conversion paths if they are available.

Documentation

Overview

Package argmapper is a dependency-injection library for Go.

go-argmapper supports named values, typed values, automatically chaining conversion functions to reach desired types, and more. go-argmapper is designed for runtime, reflection-based dependency injection.

The primary usage of this library is via the Func struct. See Func for more documentation.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Convert

func Convert(target reflect.Type, opts ...Arg) (interface{}, error)

Convert converts the input arguments to the given target type. Convert will use any of the available arguments and converters to reach the given target type.

Types

type Arg

type Arg func(*argBuilder) error

Arg is an option to Func.Call that sets the state for the function call. This can be a direct named arg or a converter that could be used if necessary to reach the target.

func Converter

func Converter(fs ...interface{}) Arg

Converter specifies one or more converters to use if necessary. A converter will be used if an argument type doesn't match exactly.

func ConverterFunc

func ConverterFunc(fs ...*Func) Arg

ConverterFunc is the same as Converter but takes an already created Func value. Any nil arguments are ignored. This appends to the list of converters.

func ConverterGen

func ConverterGen(fs ...ConverterGenFunc) Arg

ConverterGen registers a converter generator. A converter generator generates a converter dynamically based on some set values. This can be used to generate type conversions for example. The returned func can have more requirements.

If the function returns a nil Func, then no converter is generated.

func FilterInput

func FilterInput(f FilterFunc) Arg

FilterInput is used by Func.Redefine to define what inputs are valid. This will replace any previously set FilterInput value. This has no effect unless Func.Redefine is being called.

func FilterOutput

func FilterOutput(f FilterFunc) Arg

FilterOutput is identical to FilterInput but for output values. If this is not set, then Redefine will allow any output values. This behavior is the same as if FilterInput were not specified.

func FuncName

func FuncName(n string) Arg

FuncName sets the function name. This is used only with NewFunc.

func FuncOnce added in v0.2.0

func FuncOnce() Arg

FuncOnce configures the function to be called at most once. The result of a function call will be memoized and any future calls to the function will return the memoized function.

This is particularly useful if there is a complex converter that may be required multiple times in a function call chain.

The downside to this is that the result is memoized regardless of the input arguments. Therefore, if the input arguments change, this function will still not be called again. Users of this should be ABSOLUTELY SURE that they want this function to run exactly once regardless of arguments and return the same result every time.

func Logger

func Logger(l hclog.Logger) Arg

Logger specifies a logger to be used during operations with these arguments. If this isn't specified, the default hclog.L() logger is used.

func Named

func Named(n string, v interface{}) Arg

Named specifies a named argument with the given value. This will satisfy any requirement where the name matches AND the value is assignable to the struct.

If the name is an empty string, this is equivalent to calling Typed.

func NamedSubtype

func NamedSubtype(n string, v interface{}, st string) Arg

NamedSubtype is the same as Named but specifies a subtype for the value.

If the name is an empty string, this is the equivalent to calling TypedSubtype.

func Typed

func Typed(vs ...interface{}) Arg

Typed specifies a typed argument with the given value. This will satisfy any requirement where the type is assignable to a required value. The name can be anything of the required value.

func TypedSubtype

func TypedSubtype(v interface{}, st string) Arg

TypedSubtype is the same as Typed but specifies a subtype key for the value. If the subtype is empty, this is equivalent to calling Typed.

type ConverterGenFunc

type ConverterGenFunc func(Value) (*Func, error)

ConverterGenFunc is called with a value and should return a non-nil Func if it is able to generate a converter on the fly based on this value.

type ErrArgumentUnsatisfied added in v0.2.1

type ErrArgumentUnsatisfied struct {
	// Func is the target function call that was attempted.
	Func *Func

	// Args are the args that aren't satisfied. Note that this won't have
	// the "Value" field set because an unsatisfied argument by definition
	// is missing a value.
	Args []*Value

	// Inputs is the list of values that were provided directly to the
	// function call that we could use to populate arguments.
	Inputs []*Value

	// Converters is the list of converter functions available for use.
	Converters []*Func
}

ErrArgumentUnsatisfied is the value returned when there is an argument to a target function that cannot be satisfied given the inputs and mappers.

func (*ErrArgumentUnsatisfied) Error added in v0.2.1

func (e *ErrArgumentUnsatisfied) Error() string

type FilterFunc

type FilterFunc func(Value) bool

func FilterAnd

func FilterAnd(fs ...FilterFunc) FilterFunc

FilterAnd returns a FilterFunc that returns true if any of the given filter functions return true.

func FilterOr

func FilterOr(fs ...FilterFunc) FilterFunc

FilterOr returns a FilterFunc that returns true if any of the given filter functions return true.

func FilterType

func FilterType(t reflect.Type) FilterFunc

FilterType filters values based on matching the given type. If the type is an interface value then any types that implement the interface will also pass.

type Func

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

Func represents both a target function you want to execute as well as a function that can be used to provide values, convert types, etc. for calling another Func.

A Func can take any number of arguments and return any number of values. Direct function arguments are matched via type. You may use a struct that embeds the Struct type (see Struct) for named value matching. Go reflection doesn't enable accessing direct function parameter names, so a struct is required for named matching.

Structs that do not embed the Struct type are matched as typed.

Converter Basics

A Func also can act as a converter for another function call when used with the Converter Arg option.

Converters are used if a direct match argument isn't found for a Func call. If a converter exists (or a chain of converts) to go from the input arguments to the desired argument, then the chain will be called and the result used.

Like any typical Func, converters can take as input zero or more values of any kind. Converters can return any number of values as a result. Note that while no return values are acceptable, such a converter would never be called since it provides no value to the target function call.

Converters can output both typed and named values. Similar to inputs, outputting a name value requires using a struct with the Struct type embedded.

Converter Errors

A final return type of "error" can be used with converters to signal that conversion failed. If this occurs, the full function call attempt fails and the error is reported to the user.

If there is only one return value and it is of type "error", then this is still considered the error result. A function can't return a non-erroneous error value without returning more than one result value.

Converter Priorities

When multiple converters are available to reach some desired type, Func will determine which converter to call using an implicit "cost" associated with the converter. The cost is calculated across multiple dimensions:

  • When converting from one named value to another, such as "Input int" to "Input string", conversion will favor any converters that explicitly use the equivalent name (but different type). So if there are two converters, one `func(int) string` and another `func(Input int) string`, then the latter will be preferred.

  • Building on the above, if there is only one converter `func(int) string` but there are multiple `int` inputs available, an input with a matching name is preferred. Therefore, if an input named `Input` is available, that will be used for the conversion.

  • Converters that have less input values are preferred. This isn't a direct parameter count on the function, but a count on the input values which includes struct members and so on.

func BuildFunc

func BuildFunc(input, output *ValueSet, cb func(in, out *ValueSet) error, opts ...Arg) (*Func, error)

BuildFunc builds a function based on the specified input and output value sets. When called, this will call the cb with a valueset matching input and output with the argument values set. The cb should return a populated ValueSet.

func MustFunc added in v0.2.0

func MustFunc(f *Func, err error) *Func

MustFunc can be called around NewFunc in order to force success and panic if there is any error.

func NewFunc

func NewFunc(f interface{}, opts ...Arg) (*Func, error)

NewFunc creates a new Func from the given input function f.

For more details on the format of the function f, see the package docs.

Additional opts can be provided. These will always be set when calling Call. Any conflicting arguments given on Call will override these args. This can be used to provide some initial values, converters, etc.

func NewFuncList

func NewFuncList(fs []interface{}, opts ...Arg) ([]*Func, error)

NewFuncList initializes multiple Funcs at once. This is the same as calling NewFunc for each f.

func (*Func) Call

func (f *Func) Call(opts ...Arg) Result

Call calls the function. Use the various Arg functions to set the state for the function call. More details on how Call works are on the Func struct documentation directly.

func (*Func) Func

func (f *Func) Func() interface{}

Func returns the function pointer that this Func is built around.

func (*Func) Input

func (f *Func) Input() *ValueSet

Input returns the input ValueSet for this function, representing the values that this function requires as input.

func (*Func) Name

func (f *Func) Name() string

Name returns the name of the function.

This will return the configured name if one was given on NewFunc. If not, this will attempt to look up the function name using the pointer. If no friendly name can be found, then this will default to the function type signature.

func (*Func) Output

func (f *Func) Output() *ValueSet

Output returns the output ValueSet for this function, representing the values that this function produces as an output.

func (*Func) Redefine

func (f *Func) Redefine(opts ...Arg) (*Func, error)

Redefine returns a new func where the requirements are what is missing to satisfy the original function given the arguments here. Therefore, args may be incomplete, and this will return a function that only depends on the missing arguments.

Redefine also allows the usage of FilterInput and FilterOutput Arg values. These can be used to further restrict what values can be provided as an input or returned as an output, respectively. This can be used for example to try to redefine a function to only take Go primitives. In the case where Filter is used, converters must be specified that enable going to and from filtered values.

Currently, FilterOutput will just return an error if the functions outputs don't match what is expected. In the future, we plan on enabling FilterOutput to also map through converters to return the desired matches.

If it is impossible to redefine the function according to the given constraints, an error will be returned.

func (*Func) String

func (f *Func) String() string

String returns the name for this function. See Name.

type Result

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

Result is returned from a Call with the results of the function call.

This structure lets you access multiple results values. If the function call had a final return value type "error", this is treated specially and is present via the Err call and not via Out.

func (*Result) Err

func (r *Result) Err() error

Err returns any error that occurred as part of the call. This can be an error in the process of calling or it can be an error from the result of the call.

func (*Result) Len

func (r *Result) Len() int

Len returns the number of outputs, excluding any final error output.

Len does not include the "error" type if it was the final output type. For example, a function returning (error), (int, error), (int, bool, error) would have a length of 0, 1, and 2 respectively.

func (*Result) Out

func (r *Result) Out(i int) interface{}

Out returns the i'th result (zero-indexed) of the function. This will panic if i >= Len so for safety all calls to Out should check Len.

Similar to Len, Out does not include any final "error" type. This can only be accessed using Err().

type Struct

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

Struct should be embedded into any struct where the parameters are populated. This lets argmapper differentiate between arguments where you want the full struct provided or fields within the struct.

Example:

type MyParams {
  argmapper.Struct

  // A and B will be populated through injection.
  A, B int
}

If the embedded Struct was left out, argmapper would look for a full MyParams type to inject.

Named Parameters

By default, the field name is the name of the parameter. In the example above, MyParams expects parameters named "A" and "B", both of type int.

Parameter names are case insensitive.

Parameters can be renamed using a struct tag. The example below renames the field "A" to "B".

type MyParams {
  argmapper.Struct

  A int `argmapper:"B"`
}

Typed Parameters

A field in the struct can be marked as typed only using struct tags. The field name of a typed field is ignored and argmapper will match it to any matching type.

type MyParams {
  argmapper.Struct

  A int `argmapper:",typeOnly"`
}

Note the comma before the "typeOnly" string. The comma is necessary so tell argmapper you're setting an option versus renaming a field.

type Value

type Value struct {

	// Name is the name of the value. This may be empty if this is a type-only
	// value. If the name is set, then we will satisfy this input with an arg
	// with this name and type.
	Name string

	// Type is the type of the value. This must be set.
	Type reflect.Type

	// Subtype is a key that specifies a unique "subtype" for the type.
	// This can be used to identify dynamic values such as protobuf Any types
	// where the full type isn't available. This is optional. For full details
	// on subtype matching see the package docs.
	Subtype string

	// Value is the known value. This is only ever set if using Func.Redefine
	// with an input that was given. Otherwise, this value is invalid.
	Value reflect.Value
	// contains filtered or unexported fields
}

Value represents an input or output of a Func. In normal operation, you do not need to interact with Value objects directly. This structure is exposed for users who are trying to introspect on functions or manually build functions. This is an advanced operation.

A Value represents multiple types of values depending on what fields are set. Please read the documentation carefully and use the exported methods to assist with checking value types.

func (*Value) Arg

func (v *Value) Arg() Arg

Arg returns an Arg that can be used with Func.Call to send this value. This only works if the Value's Value field is set.

func (*Value) Kind

func (v *Value) Kind() ValueKind

Kind returns the ValueKind that this Value represents.

func (*Value) String

func (v *Value) String() string

type ValueKind

type ValueKind uint

ValueKind is returned by Value.Kind to designate what kind of value this is: a value expecting a type and name, a value with just type matching, etc.

const (
	ValueInvalid ValueKind = iota
	ValueNamed
	ValueTyped
)

func (ValueKind) String

func (i ValueKind) String() string

type ValueSet

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

ValueSet tracks the values either accepted or returned as part of a function or converter.

Internally, argmapper converts all functions to a signature of `func(Struct) (Struct, error)`. This lets the internals simplify a lot by expecting to only set struct fields. On the edges (when calling functions or returning values) we convert to and from the true expected arguments.

func NewValueSet

func NewValueSet(vs []Value) (*ValueSet, error)

NewValueSet creates a ValueSet from a list of expected values.

This is primarily used alongside BuildFunc to dynamically build a Func.

func (*ValueSet) Args added in v0.2.0

func (vs *ValueSet) Args() []Arg

Args returns all of the values in this ValueSet as a slice of Arg to make it easier to pass to Call. This is equivalent to iterating over the Values result and accumulating Arg results.

func (*ValueSet) FromResult

func (vs *ValueSet) FromResult(r Result) error

FromResult sets the values in this set based on a Result. This will return an error if the result represents an error.

func (*ValueSet) FromSignature

func (vs *ValueSet) FromSignature(values []reflect.Value) error

FromSignature sets the values in this ValueSet based on the values list. The values list must match the type signature returned from vs.Signature. This usually comes from calling a function directly.

func (*ValueSet) Named

func (vs *ValueSet) Named(n string) *Value

Named returns a pointer to the value with the given name, or nil if it doesn't exist.

func (*ValueSet) Signature

func (vs *ValueSet) Signature() []reflect.Type

Signature returns the type signature that this ValueSet will map to/from. This is used for making dynamic types with reflect.FuncOf to take or return this valueset.

func (*ValueSet) SignatureValues

func (vs *ValueSet) SignatureValues() []reflect.Value

SignatureValues returns the values that match the Signature type list, based on the values set in this set. If a value isn't set, the zero value is used.

func (*ValueSet) Typed

func (vs *ValueSet) Typed(t reflect.Type) *Value

Typed returns a pointer to the value with the given type, or nil if it doesn't exist. If there is no typed value directly, a random type with the matching subtype will be chosen. If you want an exact match with no subtype, use TypedSubtype.

func (*ValueSet) TypedSubtype

func (vs *ValueSet) TypedSubtype(t reflect.Type, st string) *Value

TypedSubtype returns a pointer to the value that matches the type and subtype exactly.

func (*ValueSet) Values

func (vs *ValueSet) Values() []Value

Values returns the values in this ValueSet. This does not return pointers so any modifications to the values will not impact any values in this set. Please call Named, Typed, etc. directly to make modifications.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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