goldi

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Dec 9, 2018 License: MIT Imports: 5 Imported by: 9

README

Build Status Coverage Status GoDoc license

Goldi: lazy dependency injection framework for go.

This library enables you to build your applications based on a dependency injection container. It helps to make your code modular, flexible and ensures that you can reuse components easily.

If you are familiar with the Symfony dependency injection framework you should feel at home here.

The goldi API

Use go get to get the goldi API:

$ go get github.com/fgrosse/goldi

No additional dependencies are required to use the library. The full documentation is available at godoc.org. It is almost complete and includes a lot of examples on how to use goldi.

Usage

First you need to define the types you are going to use later

import (
    "github.com/fgrosse/goldi"
    "github.com/fgrosse/goldi/validation"
)

// create a new container when your application loads
registry := goldi.NewTypeRegistry()
config := map[string]interface{}{
    "some_parameter": "Hello World",
    "timeout":        42.7,
}
container := goldi.NewContainer(registry, config)

// now define the types you want to build using the di container
// you can use simple structs
container.RegisterType("logger", &SimpleLogger{})
container.RegisterType("api.geo.client", new(GeoClient), "http://example.com/geo:1234")

// you can also use factory functions and parameters
container.RegisterType("acme_corp.mailer", NewAwesomeMailer, "first argument", "%some_parameter%")

// dynamic or static parameters and references to other services can be used as arguments
container.RegisterType("renderer", NewRenderer, "@logger")

// closures and functions are also possible
container.Register("http_handler", goldi.NewFuncType(func(w http.ResponseWriter, r *http.Request) {
    // do amazing stuff
}))

// once you are done registering all your types you should probably validate the container
validator := validation.NewContainerValidator()
validator.MustValidate(container) // will panic, use validator.Validate to get the error

// whoever has access to the container can request these types now
logger := container.MustGet("logger").(LoggerInterface)
logger.DoStuff("...")

// in the tests you might want to exchange the registered types with mocks or other implementations
container.RegisterType("logger", NewNullLogger)

// if you already have an instance you want to be used you can inject it directly
myLogger := NewNullLogger()
container.InjectInstance("logger", myLogger)

The types are build lazily. This means that the logger will only be created when you ask the container for it the first time. Also all built types are singletons. This means that if you call container.Get("typeID")two times you will always get the same instance of whatever typeID stands for.

More detailed usage examples and a list of features will be available eventually.

The goldigen binary

If you are used to frameworks like Symfony you might want to define your types in an easy to maintain yaml file. You can do this using goldigen.

Use go get to install the goldigen binary:

$ go get github.com/fgrosse/goldi/goldigen

Goldigen depends on gopkg.in/yaml.v2 (LGPLv3) for the parsing of the yaml files and Kingpin (MIT licensed) for the command line flag parsing.

You then need to define your types like this:

types:
    logger:
        package: github.com/fgrosse/goldi-example/lib
        type: SimpleLogger

    my_fancy.client:
        package: github.com/fgrosse/goldi-example/lib
        type: Client
        factory: NewDefaultClient
        arguments:
            - "%client_base_url%"   # As in the API you can use parameters here
            - "@logger"             # You can also reference other types 

    time.clock:
        package: github.com/fgrosse/goldi-example/lib/mytime
        type: Clock
        factory: NewSystemClock
        
    http_handler:
        package: github.com/fgrosse/servo/example
        func:    HandleHTTP         # You can register functions as types using the "func" keyword

Now you have your type configuration file you can use goldigen like this:

$ goldigen --in config/types.yml --out lib/dependency_injection.go

This will generate the following output and write it to lib/dependency_injection.go:

//go:generate goldigen --in "../config/types.yml" --out "dependency_injection.go" --package github.com/fgrosse/goldi-example/lib --function RegisterTypes --overwrite --nointeraction
package lib

import (
	"github.com/fgrosse/goldi"
	"github.com/fgrosse/goldi-example/lib/mytime"
	"github.com/fgrosse/servo/example"
)

// RegisterTypes registers all types that have been defined in the file "../config/types.yml"
//
// DO NOT EDIT THIS FILE: it has been generated by goldigen v0.9.9.
// It is however good practice to put this file under version control.
// See https://github.com/fgrosse/goldi for what is going on here.
func RegisterTypes(types goldi.TypeRegistry) {
	types.RegisterAll(map[string]goldi.TypeFactory{
        "http_handler":    goldi.NewFuncType(example.HandleHTTP),
        "logger":          goldi.NewStructType(new(SimpleLogger)),
        "my_fancy.client": goldi.NewType(NewDefaultClient, "%client_base_url%", "@logger"),
        "time.clock":      goldi.NewType(mytime.NewSystemClock),
	})
}

As you might have noticed goldigen has created a go generate comment for you. Next time you want to update dependency_injection.go you can simply run go generate.

Goldigen tries its best to determine the output files package by looking into your GOPATH. In certain situations this might not be enough so you can set a package explicitly using the --package parameter.

For a full list of goldigens flags and parameters try:

$ goldigen --help

Now all you need to to is to create the di container as you would just using the goldi API and then somewhere in the bootstrapping of your application call.

RegisterTypes(registry)

If you have a serious error in your type registration (like returning more than one result from your type factory method) goldi defers error handling by return an invalid type. You can check for invalid types with the ContainerValidator or by using goldi.IsValid(TypeFactory) directly. Using the ContainerValidator is always the preferred option since it will check for a wide variety of bad configurations like undefined parameters or circular type dependencies.

Note that using goldigen is completely optional. If you do not like the idea of having an extra build step for your application just use goldis API directly.

License

Goldi is licensed under the the MIT license. Please see the LICENSE file for details.

Contributing

Any contributions are always welcome (use pull requests). For each pull request make sure that you covered your changes and additions with ginkgo tests. If you are unsure how to write those just drop me a message.

Please keep in mind that I might not always be able to respond immediately but I usually try to react within the week ☺.

Documentation

Overview

Package goldi implements a lazy dependency injection framework for go. Goldi is MIT-Licensed

Example
package main

import (
	"net/http"

	"github.com/fgrosse/goldi"
	"github.com/fgrosse/goldi/validation"
)

func main() {
	// create a new container when your application loads
	registry := goldi.NewTypeRegistry()
	config := map[string]interface{}{
		"some_parameter": "Hello World",
		"timeout":        42.7,
	}
	container := goldi.NewContainer(registry, config)

	// now define the types you want to build using the di container
	// you can use simple structs
	container.RegisterType("logger", &SimpleLogger{})
	container.RegisterType("api.geo.client", new(GeoClient), "http://example.com/geo:1234")

	// you can also use factory functions and parameters
	container.RegisterType("acme_corp.mailer", NewAwesomeMailer, "first argument", "%some_parameter%")

	// dynamic or static parameters and references to other services can be used as arguments
	container.RegisterType("renderer", NewRenderer, "@logger")

	// closures and functions are also possible
	container.Register("http_handler", goldi.NewFuncType(func(w http.ResponseWriter, r *http.Request) {
		// do amazing stuff
	}))

	// once you are done registering all your types you should probably validate the container
	validator := validation.NewContainerValidator()
	validator.MustValidate(container) // will panic, use validator.Validate to get the error

	// whoever has access to the container can request these types now
	logger := container.MustGet("logger").(LoggerInterface)
	logger.DoStuff("...")

	// in the tests you might want to exchange the registered types with mocks or other implementations
	container.RegisterType("logger", NewNullLogger)

	// if you already have an instance you want to be used you can inject it directly
	myLogger := NewNullLogger()
	container.InjectInstance("logger", myLogger)
}

type LoggerInterface interface {
	DoStuff(message string) string
}

type SimpleLogger struct {
	Name string
}

func (l *SimpleLogger) DoStuff(input string) string { return input }

type NullLogger struct{}

func NewNullLogger() *NullLogger {
	return &NullLogger{}
}

func (l *NullLogger) DoStuff(input string) string { return input }

type AwesomeMailer struct {
	arg1, arg2 string
}

func NewAwesomeMailer(arg1, arg2 string) *AwesomeMailer {
	return &AwesomeMailer{arg1, arg2}
}

type Renderer struct {
	logger *LoggerInterface
}

func NewRenderer(logger *LoggerInterface) *Renderer {
	return &Renderer{logger}
}

type GeoClient struct {
	BaseURL string
}
Output:

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsParameter

func IsParameter(p string) bool

IsParameter returns whether the given type ID represents a parameter. A goldi parameter is recognized by the leading and trailing percent sign Example: %foobar%

func IsParameterOrTypeReference

func IsParameterOrTypeReference(p string) bool

IsParameterOrTypeReference is a utility function that returns whether the given string represents a parameter or a reference to a type. See IsParameter and IsTypeReference for further details

func IsTypeReference

func IsTypeReference(p string) bool

IsTypeReference returns whether the given string represents a reference to a type. A goldi type reference is recognized by the leading @ sign. Example: @foobar

func IsValid

func IsValid(t TypeFactory) bool

IsValid checks if a given type factory is valid. This function can be used to check the result of functions like NewType

Types

type Container

type Container struct {
	TypeRegistry
	Config   map[string]interface{}
	Resolver *ParameterResolver
	// contains filtered or unexported fields
}

Container is the dependency injection container that can be used by your application to define and get types.

Basically this is just a TypeRegistry with access to the application configuration and the knowledge of how to build individual services. Additionally this implements the laziness of the DI using a simple in memory type cache

You must use goldi.NewContainer to get a initialized instance of a Container!

Example
registry := goldi.NewTypeRegistry()
config := map[string]interface{}{}
container := goldi.NewContainer(registry, config)

container.Register("logger", goldi.NewType(NewNullLogger))

l := container.MustGet("logger")
fmt.Printf("%T", l)
Output:

*goldi_test.NullLogger

func NewContainer

func NewContainer(registry TypeRegistry, config map[string]interface{}) *Container

NewContainer creates a new container instance using the provided arguments

func (*Container) Get

func (c *Container) Get(typeID string) (interface{}, error)

Get retrieves a previously defined type or an error. If the requested typeID has not been registered before or can not be generated Get will return an error.

For your dependency injection to work properly it is important that you do only try to assert interface types when you use Get(..). Otherwise it might be impossible to assert the correct type when you change the underlying type implementations. Also make sure your application is properly tested and defers some panic handling in case you forgot to define a service.

See also Container.MustGet

Example
registry := goldi.NewTypeRegistry()
config := map[string]interface{}{}
container := goldi.NewContainer(registry, config)

container.Register("logger", goldi.NewType(NewNullLogger))

l, err := container.Get("logger")
if err != nil {
	fmt.Println(err.Error())
	return
}

// do stuff with the logger. usually you need a type assertion
fmt.Printf("%T", l.(*NullLogger))
Output:

*goldi_test.NullLogger

func (*Container) MustGet

func (c *Container) MustGet(typeID string) interface{}

MustGet behaves exactly like Get but will panic instead of returning an error Since MustGet can only return interface{} you need to add a type assertion after the call:

container.MustGet("logger").(LoggerInterface)

type ParameterResolver

type ParameterResolver struct {
	Container *Container
}

The ParameterResolver is used by type factories to resolve the values of the dynamic factory arguments (parameters and other type references).

func NewParameterResolver

func NewParameterResolver(container *Container) *ParameterResolver

NewParameterResolver creates a new ParameterResolver and initializes it with the given Container. The container is used when resolving parameters and the type references.

func (*ParameterResolver) Resolve

func (r *ParameterResolver) Resolve(parameter reflect.Value, expectedType reflect.Type) (reflect.Value, error)

Resolve takes a parameter and resolves any references to configuration parameter values or type references. If the type of `parameter` is not a parameter or type reference it is returned as is. Parameters must always have the form `%my.beautiful.param%. Type references must have the form `@my_type.bla`. It is also legal to request an optional type using the syntax `@?my_optional_type`. If this type is not registered Resolve will not return an error but instead give you the null value of the expected type.

type StringSet added in v1.0.1

type StringSet map[string]struct{}

A StringSet represents a set of strings.

func (StringSet) Contains added in v1.0.1

func (s StringSet) Contains(value string) bool

Contains returns true if the given value is contained in this string set.

func (StringSet) Set added in v1.0.1

func (s StringSet) Set(value string)

Set adds a value to the set.

type TypeConfigurator

type TypeConfigurator struct {
	ConfiguratorTypeID string
	MethodName         string
}

The TypeConfigurator is used to configure a type after its instantiation. You can specify a function in another type that is known to the container. The type instance is passed to the configurator type, allowing the configurator to do whatever it needs to configure the type after its creation.

A TypeConfigurator can be used, for example, when you have a type that requires complex setup based on configuration settings coming from different sources. Using an external configurator, you can decouple the setup logic from the business logic of the corresponding type to keep it DRY and easy to maintain. Also this way its easy to exchange setup logic at run time for example on different environments.

Another interesting use case is when you have multiple objects that share a common configuration or that should be configured in a similar way at runtime.

func NewTypeConfigurator

func NewTypeConfigurator(configuratorTypeID, methodName string) *TypeConfigurator

NewTypeConfigurator creates a new TypeConfigurator

func (*TypeConfigurator) Configure

func (c *TypeConfigurator) Configure(thing interface{}, container *Container) error

Configure will get the configurator type and ass `thing` its configuration function. The method returns an error if thing is nil, the configurator type is not defined or the configurators function does not exist.

type TypeFactory

type TypeFactory interface {

	// Arguments returns all arguments that are used to generate the type.
	// This enables the container validator to check if all required parameters exist
	// and if there are circular type dependencies.
	Arguments() []interface{}

	// Generate will instantiate a new instance of the according type or return an error.
	Generate(parameterResolver *ParameterResolver) (interface{}, error)
}

A TypeFactory is used to instantiate a certain type.

func NewAliasType

func NewAliasType(typeID string) TypeFactory

NewAliasType create a new TypeFactory which just serves as alias to the given type ID.

A call to an alias type will retrieve the aliased type as if it was requested via container.Get(typeID) This method will always return a valid type and works bot for regular type references (without leading @) and references to type functions.

Goldigen yaml syntax example:

type_that_is_aliased:
    alias: "@some_type"  // container.Get("type_that_is_aliased") will now return "some_type" instead

Goldigen yaml syntax example with function reference:

func_type_that_is_aliased:
    alias: "@some_type::DoStuff"
Example
container := goldi.NewContainer(goldi.NewTypeRegistry(), map[string]interface{}{})

container.Register("logger", goldi.NewStructType(SimpleLogger{}))
container.Register("default_logger", goldi.NewAliasType("logger"))
container.Register("logging_func", goldi.NewAliasType("logger::DoStuff"))

fmt.Printf("logger:         %T\n", container.MustGet("logger"))
fmt.Printf("default_logger: %T\n", container.MustGet("default_logger"))
fmt.Printf("logging_func:   %T\n", container.MustGet("logging_func"))
Output:

logger:         *goldi_test.SimpleLogger
default_logger: *goldi_test.SimpleLogger
logging_func:   func(string) string

func NewConfiguredType

func NewConfiguredType(embeddedType TypeFactory, configuratorTypeID, configuratorMethod string) TypeFactory

NewConfiguredType creates a new TypeFactory that decorates a given TypeFactory. The returned configurator will use the decorated type factory first to create a type and then use the resolve the configurator by the given type ID and call the configured method with the instance.

Internally the goldi.TypeConfigurator is used.

The method removes any leading or trailing whitespace from configurator type ID and method. NewConfiguredType will return an invalid type when embeddedType is nil or the trimmed configurator typeID or method is empty.

Goldigen yaml syntax example:

my_type:
    package: github.com/fgrosse/foobar
    type:    MyType
    configurator: [ "@my_configurator", Configure ]
Example
container := goldi.NewContainer(goldi.NewTypeRegistry(), map[string]interface{}{})

// this example configurator accepts a Foo type and will set its Value field to the given value
configurator := &MyConfigurator{ConfiguredValue: "success!"}

// register the configurator under a type ID
container.Register("configurator_type", goldi.NewInstanceType(configurator))

// create the type that should be configured
embeddedType := goldi.NewStructType(Foo{})
container.Register("foo", goldi.NewConfiguredType(embeddedType, "configurator_type", "Configure"))

fmt.Println(container.MustGet("foo").(*Foo).Value)
Output:

success!

func NewFuncReferenceType

func NewFuncReferenceType(typeID, functionName string) TypeFactory

NewFuncReferenceType returns a TypeFactory that returns a method of another type as method value (function).

Goldigen yaml syntax example:

my_func_type:
    func: "@some_type::FancyAction"
Example
container := goldi.NewContainer(goldi.NewTypeRegistry(), map[string]interface{}{})

logger := new(SimpleLogger)
container.Register("logger", goldi.NewInstanceType(logger))
container.Register("log_func", goldi.NewFuncReferenceType("logger", "DoStuff"))

f := container.MustGet("log_func").(func(string) string)
fmt.Println(f("Hello World")) // executes logger.DoStuff
Output:

Hello World

func NewFuncType

func NewFuncType(function interface{}) TypeFactory

NewFuncType creates a new TypeFactory that will return a method value

Goldigen yaml syntax example:

my_func_type:
    package: github.com/fgrosse/foobar
    func:    DoStuff
Example
container := goldi.NewContainer(goldi.NewTypeRegistry(), map[string]interface{}{})

// define the type
container.Register("my_func", goldi.NewFuncType(func(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "test" {
		w.WriteHeader(http.StatusAccepted)
	}
}))

// generate it
result, err := container.Get("my_func")
if err != nil {
	return
}

// call it
f := result.(func(name string, age int) (bool, error))
ok, err := f("foo", 42)
if ok != true || err != nil {
	panic("!!!")
}
Output:

func NewInstanceType

func NewInstanceType(instance interface{}) TypeFactory

NewInstanceType creates a new TypeFactory which will return the given instance on each call to Generate. It will return an invalid type factory if the given instance is nil

You can not generate this type using goldigen

Example
container := goldi.NewContainer(goldi.NewTypeRegistry(), map[string]interface{}{})

myInstance := new(SimpleLogger)
myInstance.Name = "Foobar" // you can configure the instance in your code

// now register this instance as a type
container.Register("logger", goldi.NewInstanceType(myInstance))

// each reference to the "logger" type will now be resolved to that instance
fmt.Println(container.MustGet("logger").(*SimpleLogger).Name)
Output:

Foobar

func NewProxyType

func NewProxyType(typeID, functionName string, args ...interface{}) TypeFactory

NewProxyType returns a TypeFactory that uses a function of another type to generate a result.

Goldigen yaml syntax example:

logger:
    factory: "@logger_provider::GetLogger"
    args:    [ "My Logger" ]
Example

Let's assume that we have a LoggerProvider type that produces configured instances of a Logger each time we call LoggerProvider.GetLogger(loggerName string).

The example shows how to register a `logger` as proxy for a specific call to this LoggerProvider.

container := goldi.NewContainer(goldi.NewTypeRegistry(), map[string]interface{}{})

// register some type as always
container.Register("logger_provider", goldi.NewStructType(LoggerProvider{}))

// register a proxy type that references the method of previously defined type and append call arguments if any
container.Register("logger", goldi.NewProxyType("logger_provider", "GetLogger", "My logger"))

l := container.MustGet("logger").(*SimpleLogger)
fmt.Printf("%s: %T", l.Name, l)
Output:

My logger: *goldi_test.SimpleLogger

func NewStructType

func NewStructType(structT interface{}, structParameters ...interface{}) TypeFactory

NewStructType creates a TypeFactory that can be used to create a new instance of some struct type.

This function will return an invalid type if:

  • structT is no struct or pointer to a struct,
  • the number of given structParameters exceed the number of field of structT
  • the structParameters types do not match the fields of structT

Goldigen yaml syntax example:

logger:
    package: github.com/fgrosse/foobar
    type:    MyType
Example
container := goldi.NewContainer(goldi.NewTypeRegistry(), map[string]interface{}{})

// all of the following types are semantically identical
container.Register("foo_1", goldi.NewStructType(Foo{}))
container.Register("foo_2", goldi.NewStructType(&Foo{}))
container.Register("foo_3", goldi.NewStructType(new(Foo)))

// each reference to the "logger" type will now be resolved to that instance
fmt.Printf("foo_1: %T\n", container.MustGet("foo_1"))
fmt.Printf("foo_2: %T\n", container.MustGet("foo_2"))
fmt.Printf("foo_3: %T\n", container.MustGet("foo_3"))
Output:

foo_1: *goldi_test.Foo
foo_2: *goldi_test.Foo
foo_3: *goldi_test.Foo

func NewType

func NewType(factoryFunction interface{}, factoryParameters ...interface{}) TypeFactory

NewType creates a new TypeFactory.

This function will return an invalid type if:

  • the factoryFunction is nil or no function,
  • the factoryFunction returns zero or more than one parameter
  • the factoryFunctions return parameter is no pointer, interface or function type.
  • the number of given factoryParameters does not match the number of arguments of the factoryFunction

Goldigen yaml syntax example:

my_type:
    package: github.com/fgrosse/foobar
    factory: NewType
    args:
        - "Hello World"
        - true
Example
container := goldi.NewContainer(goldi.NewTypeRegistry(), map[string]interface{}{})

// register the type using the factory function NewMockTypeWithArgs and pass two arguments
container.Register("my_type", goldi.NewType(NewMockTypeWithArgs, "Hello World", true))

t := container.MustGet("my_type").(*MockType)
fmt.Printf("%#v", t)
Output:

&goldi_test.MockType{StringParameter:"Hello World", BoolParameter:true}

type TypeID

type TypeID struct {
	ID, Raw             string
	FuncReferenceMethod string
	IsOptional          bool
	IsFuncReference     bool
}

TypeID represents a parsed type identifier and associated meta data.

func NewTypeID

func NewTypeID(s string) *TypeID

NewTypeID creates a new TypeId. Trying to create a type ID from an empty string will panic

func (*TypeID) String

func (t *TypeID) String() string

String implements the fmt.Stringer interface by returning the raw representation of this type ID.

type TypeReferenceError

type TypeReferenceError struct {
	TypeID       string
	TypeInstance interface{}
	// contains filtered or unexported fields
}

A TypeReferenceError occurs if you tried to inject a type that does not match the function declaration of the corresponding method.

type TypeRegistry

type TypeRegistry map[string]TypeFactory

The TypeRegistry is effectively a map of typeID strings to TypeFactory

func NewTypeRegistry

func NewTypeRegistry() TypeRegistry

NewTypeRegistry creates a new empty TypeRegistry

func (TypeRegistry) InjectInstance

func (r TypeRegistry) InjectInstance(typeID string, instance interface{})

InjectInstance enables you to inject type instances. If instance is nil an error is returned

func (TypeRegistry) Register

func (r TypeRegistry) Register(typeID string, typeDef TypeFactory)

Register saves a type under the given symbolic typeID so it can be retrieved later. It is perfectly legal to call Register multiple times with the same typeID. In this case you overwrite existing type definitions with new once

func (TypeRegistry) RegisterAll

func (r TypeRegistry) RegisterAll(factories map[string]TypeFactory)

RegisterAll will register all given type factories under the mapped type ID It uses TypeRegistry.Register internally

func (TypeRegistry) RegisterType

func (r TypeRegistry) RegisterType(typeID string, factory interface{}, arguments ...interface{})

RegisterType is convenience method for TypeRegistry.Register It tries to create the correct TypeFactory and passes this to TypeRegistry.Register This function panics if the given generator function and arguments can not be used to create a new type factory.

type UnknownTypeReferenceError

type UnknownTypeReferenceError struct {
	TypeID string
	// contains filtered or unexported fields
}

The UnknownTypeReferenceError occurs if you try to get a type by an unknown type id (type has not been registered).

Directories

Path Synopsis
The goldigen binary See https://github.com/fgrosse/goldi#the-goldigen-binary
The goldigen binary See https://github.com/fgrosse/goldi#the-goldigen-binary
Package validation provides simple validation of goldi containers
Package validation provides simple validation of goldi containers

Jump to

Keyboard shortcuts

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