axon

package module
v0.5.3 Latest Latest
Warning

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

Go to latest
Published: Jun 8, 2019 License: Apache-2.0 Imports: 3 Imported by: 26

README

axon

Go Report Card

A simple, lightweight, and lazy-loaded DI (really just a singleton management) library influenced by multiple DI libraries but more specifically Google's Guice.

Install

go get github.com/eddieowens/axon

Usage

Basic
package main

import (
    "fmt"
    "github.com/eddieowens/axon"
)

func main() {
    binder := axon.NewBinder(axon.NewPackage(
        axon.Bind("AnswerToTheUltimateQuestion").To().Int(42),
    ))
    
    injector := axon.NewInjector(binder)
    
    fmt.Print(injector.GetInt("AnswerToTheUltimateQuestion")) // Prints 42
}

In the above, I created a new Binder by passing in a Package which stores my int value (42). A Binder is a series of Packages and these Packages allow you to define what is stored in the Injector at runtime. You define a Package by using different Bindings. In this case, we bound the key AnswerToTheUltimateQuestion to the int value 42. Now on all subsequent injector.GetInt("AnswerToTheUltimateQuestion") calls, 42 will be returned.

Injecting dependencies

Now the above isn't very interesting on its own but what if you wanted to pass in the AnswerToTheUltimateQuestion value to everything that depended on it? It would be tedious to create a global variable and pass that along wherever you needed it. Instead, you can inject it as a dependency.

package main

import (
    "fmt"
    "github.com/eddieowens/axon"
)

func main() {
    type MyStruct struct {
        IntField int `inject:"AnswerToTheUltimateQuestion"`
    }
    
    binder := axon.NewBinder(axon.NewPackage(
        axon.Bind("MyStruct").To().StructPtr(new(MyStruct)),
        axon.Bind("AnswerToTheUltimateQuestion").To().Int(42),
    ))
    
    injector := axon.NewInjector(binder)
    
    fmt.Print(injector.GetStructPtr("MyStruct").(*MyStruct).IntField) // Prints 42
}

Now, you have a struct called MyStruct which is bound to the key MyStruct as an Instance. An Instance is the wrapper that axon uses around everything it manages. It holds metadata and allows the injector to interact with everything you've defined in your Binder efficiently and safely. All interactions between your raw data and the injector will be done via an Instance.

You may have also noticed the inject tag. Well this is how axon delivers your dependencies to your struct at runtime and allows you to not have to worry about creating it yourself. The inject tag takes a single string value which is the key you defined within your Package and will automatically pass the value in on a call to injector.GetStructPtr("MyStruct").

Utilizing interfaces

In the above example, you have to write quite a bit of code to do something that could be done pretty succinctly in just raw go. Admittedly, axon does not shine when injecting simple constants (although it does make managing them far easier). Where axon really shines is injecting deep dependency trees and using interfaces.

package main

import (
    "fmt"
    "github.com/eddieowens/axon"
)

type Starter interface {
    Start()
}

type Car struct {
    Engine Starter `inject:"Engine"`
}

func (c *Car) Start() {
    fmt.Println("Starting the Car!")
    c.Engine.Start()
}

type Engine struct {
    FuelInjector Starter `inject:"FuelInjector"`
}

func (e *Engine) Start() {
    fmt.Println("Starting the Engine!")
    e.FuelInjector.Start()
}

type FuelInjector struct {
}

func (*FuelInjector) Start() {
    fmt.Println("Starting the FuelInjector!")
}

func CarFactory(_ axon.Injector, _ axon.Args) axon.Instance {
    fmt.Println("Hey, a new Car is being made!")
    return axon.StructPtr(new(Car))
}

func main() {
    binder := axon.NewBinder(axon.NewPackage(
        axon.Bind("Car").To().Factory(CarFactory).WithoutArgs(),
        axon.Bind("Engine").To().StructPtr(new(Engine)),
        axon.Bind("FuelInjector").To().StructPtr(new(FuelInjector)),
    ))
    
    injector := axon.NewInjector(binder)
    
    // Prints:
    // Hey, a new Car is being made!
    // Starting the Car!
    // Starting the Engine!
    // Starting the FuelInjector!
    injector.GetStructPtr("Car").(Starter).Start()
}

Here we have a Car which depends on an Engine which depends on a FuelInjector. Things are getting a bit messy and managing all of these structs is becoming tedious and cumbersome. Rather than managing everything ourselves, axon can manage these dependencies for us. Now whenever you call the Start() function on the Car, all of the required dependencies will be automatically added and this will be consistent throughout your codebase.

Factories

The above also introduces the use of a Factory to create an Instance. You use a Factory when you want the construction of your Instance to be managed by axon. For instance, let's say you require some parameters for your Car that aren't provided until runtime and aren't managed by axon.

...
type Car struct {
    LockCode string
}

func CarFactory(_ axon.Injector, args axon.Args) axon.Instance {
    return &Car{
        LockCode: args.String(0),
    }
}
...
binder := axon.NewBinder(axon.NewPackage(
    axon.Bind("Car").To().Factory(CarFactory).WithArgs(axon.Args{os.Getenv("CAR_LOCK_CODE")}),
    ...
))

injector := axon.NewInjector(binder)

car := injector.GetStructPtr("Car").(*Car)
fmt.Println(car.LockCode) // Prints the value of env var CAR_LOCK_CODE

FYI, if the Arg passed into your Instance is overriding a field that is tagged with inject, the arg will always take precedence and will not be overwritten.

Testing

What I think to be the most useful functionality that axon affords you is the ability to very easily and precisely test your code. Let's say you create a test for your Engine. You don't want to also test the functionality of the FuelInjector so you make a mock of the Starter interface.

import (
    "fmt"
    "github.com/stretchr/testify/mock"
    "testing"
)

type MockFuelInjector struct {
    mock.Mock
}

func (m *MockFuelInjector) Start() {
    fmt.Println("I'm a mock FuelInjector!")
}

Then you add it to the Injector within your test.

func TestEngine(t *testing.T) {
    injector := axon.NewInjector(binder)
	
    injector.Add("FuelInjector", axon.StructPtr(new(MockFuelInjector)))
    
    // Prints:
    // Starting the Engine!
    // I'm a mock FuelInjector!
    injector.GetStructPtr("Engine").(Starter).Start()
}

The injector.Add() method will replace the Instance held by the key with the mocked FuelInjector allowing you to unit test your code efficiently and exactly.

To note, the Injector keeps track of every Instance's dependencies and will clear the dependencies of a particular key when the Add() method is called. This means all subsequent injector.Get() method calls will return the correct Instance. For example, if you call injector.Get("Car") in the above test, it will store an Engine which will store a MockFuelInjector.

Docs

License

Apache

Documentation

Overview

A simple, lightweight, and lazy-loaded DI (really just a singleton management) library.

An Instance of data stored in the Injector. This is what will be retrieved from and added to the Injector.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Args

type Args []interface{}

Args that are passed into a Factory upon creation (call to injector.Get*(...)). These Args will take precedence over any and all other entities managed by axon. For instance, if you have a field that is tagged with inject, and is also instantiated via Args passed in a Factory, the value provided by the Arg will always remain.

Add Args through a Binding like so.

axon.NewPackage(axon.Bind("MyService").To().Factory(MyServiceFactory).WithArgs(axon.Args{"arg1"})

And access Args within a Factory like so.

func MyServiceFactory(args axon.Args) axon.Instance {
  return &myServiceImpl{StringField: args.String(0)} // The arg returned is the string "arg1".
}

func (Args) Any

func (f Args) Any(idx int) (val interface{})

Gets an interface{} from the Args. If the passed in index does not exist, a zero interface{} is returned

func (Args) Bool

func (f Args) Bool(idx int) (val bool)

Gets a bool from the Args. If the passed in index is not a bool or does not exist, false is returned

func (Args) Float32

func (f Args) Float32(idx int) (val float32)

Gets a float32 from the Args. If the passed in index is not a float32 or does not exist, a zero float32 is returned

func (Args) Float64

func (f Args) Float64(idx int) (val float64)

Gets a float64 from the Args. If the passed in index is not a float64 or does not exist, a zero float64 is returned

func (Args) Int

func (f Args) Int(idx int) (val int)

Gets a int from the Args. If the passed in index is not a int or does not exist, a zero int is returned

func (Args) Int64

func (f Args) Int64(idx int) (val int64)

Gets a int64 from the Args. If the passed in index is not a int64 or does not exist, a zero int64 is returned

func (Args) String

func (f Args) String(idx int) (val string)

Gets a string from the Args. If the passed in index is not a string or does not exist, a zero string is returned

func (Args) UInt

func (f Args) UInt(idx int) (val uint)

Gets a uint from the Args. If the passed in index is not a uint or does not exist, a zero uint is returned

type Binder

type Binder interface {
	// The slice of Packages that this Binder stores.
	Packages() []Package
}

A collection of Packages. The Binder is the top-level definition of what the Injector will store and how it will store it. The Bindings provided by this Binder's Packages will not be evaluated until an injector.Get*(string) method is called.

func NewBinder

func NewBinder(packages ...Package) Binder

Create a new Binder from a series of Packages. If no Packages are passed in, the Injector will essentially have no Bindings defined and will be functionally useless but no error will be returned.

axon.NewBinder(axon.NewPackage(...), axon.NewPackage(...))

type Binding

type Binding interface {
	// Retrieves the Instance that this Binding manages. This field is mutually exclusive with the Provider stored
	// in the Binding.
	GetInstance() Instance

	// Retrieves the Provider that this Binding manages. This field is mutually exclusive with the Instance stored
	// in the Binding. The Key tied to the Provider will ultimately be tied to the Instance that the Provider produces,
	// not the Provider itself.
	GetProvider() Provider

	// The key that is bound to the Provider or Instance.
	GetKey() string
}

A "binder" between a Key and an Injectable Entity (Instance or Provider) within an Injector. A Binding will never hold both an Instance and a Provider.

type BindingBuilder

type BindingBuilder interface {
	// Passthrough for defining an Injectable Entity (Provider or Instance).
	To() InjectableEntityBindingBuilder
}

An intermediary builder for a Binding. Offers no functional impact on the Binding. Is only comprised of prepositions to allow the flow of the builder to be more intuitive.

func Bind

func Bind(key string) BindingBuilder

Defines a Binding in a Package from a specified key to an Instance or a Provider.

axon.NewBinder(axon.NewPackage(
    axon.Bind("UserService").To().Instance(axon.StructPtr(new(UserService))),
    axon.Bind("MyString").To().String("hello world"),
    axon.Bind("DB").To().Factory(DBFactory).WithoutArgs(),
))

type ConstantBindingBuilder

type ConstantBindingBuilder interface {
	// Binds the provided Key to a string value.
	String(s string) Binding

	// Binds the provided Key to an int value.
	Int(i int) Binding

	// Binds the provided Key to an int32 value.
	Int32(i int32) Binding

	// Binds the provided Key to an int64 value.
	Int64(i int64) Binding

	// Binds the provided Key to a float32 value.
	Float32(f float32) Binding

	// Binds the provided Key to a float64 value.
	Float64(f float64) Binding

	// Binds the provided Key to a bool value.
	Bool(b bool) Binding

	// Binds the provided Key to a ptr to a struct value.
	StructPtr(s interface{}) Binding

	// Binds the provided Key to the value that is passed in.
	Any(s interface{}) Binding
}

Builds Bindings which utilize constants.

type Factory

type Factory func(inj Injector, args Args) Instance

Function that constructs an Instance. The args field are provided via the Provider listed within a Binding. The Injector provided is the state of the Injector at the time of calling the factory. This should be used carefully as a user can end up in an infinite loop

type FactoryArgsBindingBuilder

type FactoryArgsBindingBuilder interface {
	// Pass in arguments into your Factory.
	//  Bind("service").To().Factory(ServiceFactory).WithArgs(axon.Args{"my arg"})
	// All arguments passed in here will take precedence over injected values. These will not be
	// overwritten.
	WithArgs(args Args) Binding

	// Specifies that the Factory within the Binding does not have Args.
	WithoutArgs() Binding
}

type FactoryBindingBuilder

type FactoryBindingBuilder interface {
	// Binds a Key to a provided Factory.
	Factory(factory Factory) FactoryArgsBindingBuilder
}

Builds Bindings which utilize a Factory.

type InjectableEntityBindingBuilder

type InjectableEntityBindingBuilder interface {
	FactoryBindingBuilder
	ConstantBindingBuilder

	// Bind the provided Key to an Instance.
	Instance(instance Instance) Binding
}

Builder for a binding to an Injectable Entity (an Instance or a Provider).

type Injector

type Injector interface {
	// Gets a specific Instance within the Injector. If the Instance does not yet exist (created via a Provider) it is
	// constructed here. If the key is not found, nil is returned.
	Get(key string) Instance

	// Get a struct Instance from the injector. This will always return a pointer to whatever struct
	// was passed in via the Binding. If not found, nil is returned.
	GetStructPtr(key string) interface{}

	// Get a bool Instance from the Injector. If not found, false is returned.
	GetBool(key string) bool

	// Get a int Instance from the Injector. If not found, 0 is returned.
	GetInt(key string) int

	// Get a string Instance from the Injector. If not found, "" is returned.
	GetString(key string) string

	// Get a float32 Instance from the Injector. If not found, 0 is returned.
	GetFloat32(key string) float32

	// Get a float64 Instance from the Injector. If not found, 0 is returned.
	GetFloat64(key string) float64

	// Get a int64 Instance from the Injector. If not found, 0 is returned.
	GetInt64(key string) int64

	// Get a int32 Instance from the Injector. If not found, 0 is returned.
	GetInt32(key string) int32

	// Add an Instance to the Injector. If the Instance with the specified key already exists, it is replaced.
	//
	// Every time this is called, all dependencies of the Instance will be rebuilt on subsequent Get*(string)
	// calls.
	//
	// WARNING: Do not use this method at runtime within source code. This method is for TESTING purposes in order to
	// provide mocks without having to define a completely separate Binder. If you use this within source code, it will
	// work within a synchronous environment but has undefined behavior in an asynchronous environment.
	Add(key string, instance Instance)

	// Add a Provider to the Injector. If the Instance with the specified key already exists, it is replaced.
	//
	// Every time this is called, all dependencies of the Instance will be rebuilt on subsequent Get*(string)
	// calls.
	//
	// WARNING: Do not use this method at runtime within source code. This method is for TESTING purposes in order to
	// provide mocks without having to define a completely separate Binder. If you use this within source code, it will
	// work within a synchronous environment but has undefined behavior in an asynchronous environment.
	AddProvider(key string, provider Provider)
}

An Injector is, in essence, a map of a string Key to an Injectable Entity (Instance or a Provider). When a user calls injector.Get(key string) they are providing the key into the map which will either simply return the instance, or construct it via the Provider's Factory.

This is the main object you will interact with to get whatever you place into the injector via a Binder.

func NewInjector

func NewInjector(binder Binder) Injector

Creates and instantiates an Injector via a Binder provided by the user.

binder := axon.NewBinder(axon.NewPackage(
  axon.Bind("Car").To().Factory(CarFactory).WithoutArgs()),
  axon.Bind("Engine").To().Instance(axon.StructPtr(new(EngineImpl)),
  axon.Bind("LockCode").To().String(os.Getenv("LOCK_CODE")),
))

injector := axon.NewInjector(binder)

The user can now retrieve their Instances via a call to the Injector's Get*(string) methods.

fmt.Println(injector.GetString("LockCode")) // Prints the value of the env var "LOCK_CODE"

Or the Binding can be replaced before running a test via the injector.Add(...) method.

injector.Add("Engine", axon.StructPtr(new(EngineMock)))

type Instance

type Instance interface {
	// Get the reflect Kind of the Instance. To note, this reflect.Kind is consistent within
	// the axon codebase but may not be consistent with the raw Go type. For instance, a Struct
	// type is actually a reflect.Ptr.
	GetKind() reflect.Kind

	// The pointer to the struct that the Instance is managing.
	GetStructPtr() interface{}

	// The int that the Instance is managing.
	GetInt() int

	// The int32 that the Instance is managing.
	GetInt32() int32

	// The int64 that the Instance is managing.
	GetInt64() int64

	// The bool that the Instance is managing.
	GetBool() bool

	// The string that the Instance is managing.
	GetString() string

	// The float32 that the Instance is managing.
	GetFloat32() float32

	// The float64 that the Instance is managing.
	GetFloat64() float64

	// The raw value that the Instance is managing.
	GetValue() interface{}
	// contains filtered or unexported methods
}

func Any

func Any(instance interface{}) Instance

An Instance of any type. This should be used as sparingly as possible as more reflection is needed to manage it (which is slower). Use this if none of the other Instance types can fit your needs.

func Bool

func Bool(instance bool) Instance

An Instance of a bool type.

func Float32

func Float32(instance float32) Instance

An Instance of a float32 type.

func Float64

func Float64(instance float64) Instance

An Instance of a float64 type.

func Int

func Int(instance int) Instance

An Instance of an int type.

func Int32

func Int32(instance int32) Instance

An Instance of an int32 type.

func Int64

func Int64(instance int64) Instance

An Instance of an int64 type.

func String

func String(instance string) Instance

An Instance of a string type.

func StructPtr

func StructPtr(instance interface{}) Instance

An Instance of a struct ptr type. This type MUST be a ptr to a struct value. If it is not, a panic will occur. Good:

func MyStructFactory(_ axon.Injector, _ axon.Args) axon.Instance {
  return axon.StructPtr(new(MyStruct))
}

Bad:

func MyStructFactory(_ axon.Injector, _ axon.Args) axon.Instance {
  return axon.StructPtr(MyStruct{})
}

Will be stored within the Injector simply as &MyStruct{}

type Package

type Package interface {
	// Returns all of the Bindings that this Package stores.
	Bindings() []Binding
}

A group of Bindings that are conceptually tied together. A Package only serves as a way to separate Binding definitions into different conceptual chunks. A Package does not provide any functional difference within the Injector

func NewPackage

func NewPackage(packageBindings ...Binding) Package

Creates a new Package to pass into your Binder. You can also define a Package as a struct by implementing the Package interface like so

 type ConfigPackage struct {
 }

 type ConfigPackage struct {
 }

 func (*ConfigPackage) Entries() []Binding {
	 return []Binding{
     Bind("Dev").To().Factory(TestServiceFactory).WithoutArgs(),
   }
 }
 ...
 axon.NewBinder(new(ConfigPackage), ...)

type Provider

type Provider interface {
	// The Factory that the Provider will use to construct your Instance when called upon by the injector.Get(key string)
	// method.
	GetFactory() Factory

	// Arguments that will be passed into your Factory when it's called.
	GetArgs() Args
}

Defines how to "provide" an Instance to the Injector via a Factory. A Provider's Factory will only ever be called once by the Injector. After the Provider's Factory creates your Instance, the injector will stop referring to the Provider and will only refer to the Instance that the Provider's Factory produced.

func NewProvider

func NewProvider(factory Factory, args ...interface{}) Provider

Creates a new Provider from the passed in Args and Factory. This function should mainly be used for adding Mock Providers into your tests. Rather than calling this function directly within source code, use Bindings instead like so

axon.Bind("MyService").To().Factory(ServiceFactory).WithoutArgs()

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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