remy

package module
v1.8.2 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2024 License: MIT Imports: 7 Imported by: 11

README

Remy

To install in your Go project, you must use the 1.18 version of the language, and have the environment variable GO111MODULE=on.

go get github.com/wrapped-owls/goremy-di/remy

How it works

All the instances and/or closures must be saved somewhere in-memory, so it will be available to be requested later in the program execution. When the app starts, it must register all dependencies in a given injector, which can be the default global injector or a custom one. This injector will hold and delegate how to instantiate an object/interface requested.

The Injector

Remy can generate multiple injector instances, and by default it can configure a default global instance. To generate a new Injector instance, the function NewInjector must be called passing a Config struct, which have the attributes:

  • AllowOverride - Determines if a element bind can be override
  • GenerifyInterface - Go interfaces are not exactly unique, so when an element is registered with an interface A and it has the same methods signatures as interface B, this flag will tell to remy to treat both as the same.
  • UseReflectionType - Use reflection to get the type of the object/interface. This is useful when your program has a type with the same package and type name from another module or subpackage.
  • ParentInjector - Make possible to pass an existing Injector as a parent injector, so the new injector can access all elements in its parent. Is good to know, that all binds registered in sub-injector can't be accessed by the parent injector, it is scope safe.
package core

import (
  "log"

  "github.com/wrapped-owls/goremy-di/remy"
)

var Injector remy.Injector

// create a new instance of the injector
func init() {
  log.Println("Initializing injector")
  Injector = remy.NewInjector()
}

Global Injector

The easiest way to register and retrieve a bind, is using the globalInjector, that can be defined in two different ways, bt setting a custom one using the method SetGlobalInjector, or let it be created automatically by the remy package.

To use the global injector, you must pass a nil as the Injector parameter in Get[T]/Register[T]/GetGen[T] functions.

Register bind elements

Use the function Register or one of its variants. When using the default Register function, a Bind generator function, with a Binder function parameter, should be passed as the second parameter. The Binder function receive a DependencyRetriever as parameter, which can be used to get another values that was also injected.

package main

import (
  "database/sql"

  "github.com/wrapped-owls/goremy-di/examples/basic/core"
  "github.com/wrapped-owls/goremy-di/remy"
)

// Create an instance of the database connection
func init() {
  remy.Register(
    core.Injector,
    remy.Singleton(
      func(retriever remy.DependencyRetriever) (*sql.DB, error) {
        return sql.Open("sqlite3", "file:locked.sqlite?cache=shared&mode=memory")
      },
    ),
  )
}

In this example, we register a database connection using the Register function, a Bind function and the Binder closure. To make it cleaner to read, we can rewrite this using the RegisterSingleton or RegisterInstance functions:

package main

import (
  "database/sql"

  "github.com/wrapped-owls/goremy-di/examples/basic/core"
  "github.com/wrapped-owls/goremy-di/remy"
)

// Create an instance of the database connection
func init() {
  remy.RegisterSingleton(
    core.Injector, func(retriever remy.DependencyRetriever) (*sql.DB, error) {
      return sql.Open("sqlite3", "file:locked.sqlite?cache=shared&mode=memory")
    },
  )
}

Bind types

Singleton is not the only bind that can be used, in total is possible to register 4 different types of bind:

  • Singleton: Build an instance only once when the bind is registered.
  • LazySingleton: Build an instance only once when it is requested.
  • Factory: Build an instance on demand.
  • Instance: Adds an existing instance.

The main difference about Instance and Singleton is that the singleton bind was made to be used when the object needs to be generated using the value in other binds, in this way we still have an instance, but it will be more easily to generate and inject.

Using the DependencyRetriever

While registering a bind with a Binder closure, is possible to retrieve other registered binds by using the DependencyRetriever parameter. So, it can be used in the same way the injector is used to retrieve registered elements, it is, using the Get[T] function.

Retrieve injected elements

Using the main feature added in Go1.18, we can retrieve all closures/instances using directly the type, instead of a key. In order to retrieve the element, you must use the function Get[T], which will search the bind in the given injector, and then return the element in the type T.

As for an example, if we want to retrieve a database connection to create a factory of repositories, we can do this by calling the Get function:

package main

import (
  "database/sql"

  "github.com/wrapped-owls/goremy-di/examples/basic/core"
  "github.com/wrapped-owls/goremy-di/remy"
)

func init() {
  remy.Register(
    core.Injector,
    remy.Factory(
      func(retriever remy.DependencyRetriever) (repo core.GenericRepository, err error) {
        repo = repositories.NewGenericDbRepository(remy.Get[*sql.DB](retriever))
        return
      },
    ),
  )
}

You can also use the registered element in any place of your application, using either the global injector or a _ local_ one.

package main

import (
  "database/sql"
  "log"

  "github.com/wrapped-owls/goremy-di/examples/basic/core"
  "github.com/wrapped-owls/goremy-di/remy"
)

func main() {
  // Executing create table query
  dbConn := remy.Get[*sql.DB](core.Injector)
  if _, err := dbConn.Exec("CREATE TABLE programming_languages(id INTEGER, name VARCHAR(60))"); err != nil {
    log.Fatalln(err)
  }
}

Passing parameters to bind injector

Sometimes you may want to generate a bind that must receive some additional arguments in the Binder function, but these arguments are variables and depend on the caller. So you may think that the solution for this is to dynamically during the program runtime, call the Register[T] function and register these parameters, but this thought is wrong, because the bind registration is not concurrent safe. This means that while the register of a single string, another thread can register the same element, and then the bind will retrieve the values in a wrong way.

This works by creating automatically a sub-injector that will be used to add instance binds that will be used to generate the factory bind requested.

REMINDER: It only works with the Factory and LazySingleton binds.

Currently, exists two ways to do this, by using an array of InstancePair or by using a function and registering the values directly in it.

Using as example a factory bind registered in the init function:

package main

import "github.com/wrapped-owls/goremy-di/remy"

func init() {
	remy.Register(
		nil, remy.Factory(
			func(injector remy.DependencyRetriever) (result string, err error) {
				result = fmt.Sprintf(
					"I love %s, yes this is %v, as the answer %d",
					remy.Get[string](injector, "lang"), remy.Get[bool](injector), remy.Get[uint8](injector),
				)
				return
			},
		),
	)
}

The requested values can be passed by two forms:

Using InstancePair array

This is the most straightforward way to register temporary binds that will only be used in the Get call. When use this way to register interface types, double of the attention is required, as it doesn't have compile-time type assertion.

package main

import (
  "log"

  "github.com/wrapped-owls/goremy-di/remy"
)

func main() {
  result := remy.GetGen[string](
    injector,
    []remy.InstancePairAny{
      {
        Value: uint8(42),
      },
      {
        Value: "Go",
        Key:   "lang",
      },
      {
        Value: true,
      },
      {
        InstanceValue: (*error)(nil),
      },
    },
  )

  log.Println(result)
}

Using callback to register the values
package main

import "github.com/wrapped-owls/goremy-di/remy"

func main() {
	remy.GetGenFunc[string](
		injector, func(injector remy.Injector) error {
			remy.Register(ij, remy.Instance[uint8](42))
			remy.Register(ij, remy.Instance("Go"), "lang")
			remy.Register(ij, remy.Instance(true))
			return nil
		},
	)
}
Cycle dependencies problem

As you can use the binds in a dynamic way, by generating factories that will run during runtime, it may happen to a bind request some other bind that depends on the first bind, this is a dependency cycle. The main problem with a cycle like this, is that is really hard to detect during code/compile time, and once the code is running, it can end in a Stack Overflow, which causes a panic to the program, and may be harm to it.

So, to enable the possibility to test and detect for a dependency-cycle, you can use the CycleDetectorInjector, which can be called using the constructor "NewCycleDetectorInjector". The main question during it's use is that it creates a wrap in the StandardInjector, and create an internal graph for each dependency that was requested to the injector. This functionality is much slower than using the StandardInjector, so it is only recommended to use it in test files, to make sure that no dependency cycle was created.


package main

import (
	"testing"

	"github.com/wrapped-owls/goremy-di/remy"
)

func createInjections(injector remy.Injector) {
	// ...
}

func TestCycles(t *testing.T) {
	ij := remy.NewCycleDetectorInjector()
	createInjections(ij)
	if _, err := remy.DoGet[string](ij); err != nil {
		t.Error(err)
	}
}

Important Note

When using the CycleDetectorInjector is important that in Binds, all Get methods used call the given DependencyRetriever, if the same injector is used inside the function, as a clojure, it will not be able to detect cycles.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DoGet added in v1.2.0

func DoGet[T any](i DependencyRetriever, keys ...string) (result T, err error)

DoGet directly access a retriever and returns the type that was bound in it. Additionally, it returns an error which indicates if the bind was found or not.

Receives: DependencyRetriever (required); key (optional)

func DoGetAll added in v1.8.0

func DoGetAll[T any](i DependencyRetriever, keys ...string) (result []T, err error)

DoGetAll directly access a retriever and returns a list of element that match requested types that was bound in it. Additionally, it returns an error which indicates if the instance was found or not.

Receives: DependencyRetriever (required); key (optional)

func DoGetGen added in v1.2.0

func DoGetGen[T any](
	i DependencyRetriever, elements []InstancePairAny,
	keys ...string,
) (result T, err error)

DoGetGen creates a sub-injector and access the retriever to generate and return a Factory bind Additionally, it returns an error which indicates if the bind was found or not.

Receives: DependencyRetriever (required); []InstancePairAny (required); key (optional)

func DoGetGenFunc added in v1.2.0

func DoGetGenFunc[T any](
	i DependencyRetriever, binder func(Injector) error,
	keys ...string,
) (result T, err error)

DoGetGenFunc creates a sub-injector and access the retriever to generate and return a Factory bind Additionally, it returns an error which indicates if the bind was found or not.

Receives: DependencyRetriever (required); func(Injector) (required); key (optional)

func Get

func Get[T any](i DependencyRetriever, keys ...string) T

Get directly access a retriever and returns the type that was bound in it.

Receives: DependencyRetriever (required); key (optional)

func GetAll added in v1.8.0

func GetAll[T any](i DependencyRetriever, keys ...string) []T

GetAll directly access a retriever and returns all instance types that was bound in it and match qualifier.

Receives: DependencyRetriever (required); key (optional)

func GetGen added in v1.1.0

func GetGen[T any](i DependencyRetriever, elements []InstancePairAny, keys ...string) T

GetGen creates a sub-injector and access the retriever to generate and return a Factory bind

Receives: DependencyRetriever (required); []InstancePairAny (required); key (optional)

func GetGenFunc added in v1.1.0

func GetGenFunc[T any](i DependencyRetriever, binder func(Injector) error, keys ...string) T

GetGenFunc creates a sub-injector and access the retriever to generate and return a Factory bind

Receives: DependencyRetriever (required); func(Injector) (required); key (optional)

func NewCycleDetectorInjector added in v1.4.0

func NewCycleDetectorInjector(configs ...Config) *cycleDetectorInjector

NewCycleDetectorInjector creates a new Injector that is able to check for cycle dependencies during runtime.

As it is much slower that the injector.StandardInjector, it is only recommended to be used in test files.

func Override added in v1.3.0

func Override[T any](i Injector, bind Bind[T], keys ...string)

Override works like the Register function, allowing to register a bind that was already registered. It also must be called during binds setup, because the library doesn't support registering dependencies while get at same time.

This is not supported in multithreading applications because it does not have race protection

func Register

func Register[T any](i Injector, bind Bind[T], keys ...string)

Register must be called first, because the library doesn't support registering dependencies while get at same time. This is not supported in multithreading applications because it does not have race protection

func RegisterConstructor added in v1.8.2

func RegisterConstructor[T any](
	i Injector, bindFunc func(binder types.Binder[T]) Bind[T],
	constructor func() T, keys ...string,
)

RegisterConstructor registers a constructor function that returns a value of type T without an error.

func RegisterConstructorArgs1 added in v1.8.2

func RegisterConstructorArgs1[T, K any](
	i Injector, bindFunc func(binder types.Binder[T]) Bind[T],
	constructor func(K) T, keys ...string,
)

RegisterConstructorArgs1 registers a constructor function with one argument that returns a value of type T without an error.

func RegisterConstructorArgs1Err added in v1.8.2

func RegisterConstructorArgs1Err[T, K any](
	i Injector, bindFunc func(binder types.Binder[T]) Bind[T],
	constructor func(K) (T, error), keys ...string,
)

RegisterConstructorArgs1Err registers a constructor function with one argument that returns a value of type T and an error.

func RegisterConstructorArgs2 added in v1.8.2

func RegisterConstructorArgs2[T, K, P any](
	i Injector, bindFunc func(binder types.Binder[T]) Bind[T],
	constructor func(K, P) T, keys ...string,
)

RegisterConstructorArgs2 registers a constructor function with two arguments that returns a value of type T without an error.

func RegisterConstructorArgs2Err added in v1.8.2

func RegisterConstructorArgs2Err[T, K, P any](
	i Injector, bindFunc func(binder types.Binder[T]) Bind[T],
	constructor func(K, P) (T, error), keys ...string,
)

RegisterConstructorArgs2Err registers a constructor function with two arguments that returns a value of type T and an error.

func RegisterConstructorErr added in v1.8.2

func RegisterConstructorErr[T any](
	i Injector, bindFunc func(binder types.Binder[T]) Bind[T],
	constructor func() (T, error), keys ...string,
)

RegisterConstructorErr registers a constructor function that returns a value of type T and an error.

func RegisterInstance

func RegisterInstance[T any](i Injector, value T, keys ...string)

RegisterInstance directly generates an instance bind without needing to write it.

Receives: Injector (required); value (required); key (optional)

func RegisterSingleton

func RegisterSingleton[T any](i Injector, binder types.Binder[T], keys ...string)

RegisterSingleton directly generates a singleton bind without needing to write it.

Receives: Injector (required); Binder (required); key (optional)

func SetGlobalInjector added in v1.1.0

func SetGlobalInjector(i Injector)

SetGlobalInjector receives a custom injector and saves it to be used as a global injector

Types

type Bind

type Bind[T any] interface {
	types.Bind[T]
}

Bind is directly copy from types.Bind

func Factory

func Factory[T any](binder types.Binder[T]) Bind[T]

Factory generates a bind that will be generated everytime a dependency with its type is requested by the Injector.

This bind don't hold any object or pointer references, and will use the given types.Binder function everytime to generate a new instance of the requested type.

As this bind doesn't hold any pointer and/or objects, there is no problem to use it in multiples goroutines at once. Just be careful with calls of the DependencyRetriever, if try to get and modify values from an Instance bind, it can end in a race-condition.

func Instance

func Instance[T any](element T) Bind[T]

Instance generates a bind that will be registered as a single instance during bind register in the Injector.

This bind type has no protection over concurrency, so it's not recommended to be used a struct that performs some operation that will modify its attributes.

It's recommended to use it only for read value injections. For concurrent mutable binds is recommended to use Singleton or LazySingleton

func LazySingleton

func LazySingleton[T any](binder types.Binder[T]) Bind[T]

LazySingleton generates a bind that works the same as the Singleton, with the only difference being that it's a lazy bind. So, it only will generate the singleton instance on the first Get call.

It is useful in cases that you want to instantiate heavier objects only when it's needed.

func Singleton

func Singleton[T any](binder types.Binder[T]) Bind[T]

Singleton generates a bind during the registration process. At the end of the registration, it holds the same object instance across application lifetime.

If you don't want to generate the bind immediately at its registration, you can use the LazySingleton bind.

type BindKey added in v1.5.0

type BindKey = types.BindKey

BindKey is the internal type used to generate all type keys, and used to retrieve all types from the injector. Is not supposed to use directly without the remy library, as this remove the main use of the remy-generics methods

type Config

type Config struct {
	// ParentInjector defines an Injector that will be used as a parent one, which will make possible to access it's
	// registered binds.
	ParentInjector Injector

	// CanOverride defines if a bind can be overridden if it is registered twice.
	CanOverride bool

	// DuckTypeElements informs to the injector that it can try to discover if the requested type
	// is on one of the already registered one.
	//
	// CAUTION: It costly a lot, since it will try to discover all registered elements
	DuckTypeElements bool

	// GenerifyInterfaces defines the method to check for interface binds.
	// If this parameter is true, then an interface that is defined in two different packages,
	// but has the same signature methods, will generate the same key. If is false, all interfaces will generate
	// a different key.
	GenerifyInterfaces bool

	// UseReflectionType defines the injector to use reflection when saving and retrieving types.
	// This parameter is useful when you want to use types with different modules but the same name and package names.
	//
	// Optional, default is false.
	UseReflectionType bool
}

Config defines needed configuration to instantiate a new injector

type ConstructorArg1 added in v1.8.2

type ConstructorArg1[T, K any] func(K) (T, error)

ConstructorArg1 defines a constructor function with one argument of type K that returns a value of type T and an error.

func (ConstructorArg1[T, K]) Binder added in v1.8.2

func (cons ConstructorArg1[T, K]) Binder(retriever types.DependencyRetriever) (value T, err error)

Binder retrieves the dependency of type K, then calls the constructor function for ConstructorArg1 and returns the constructed value and any error encountered.

type ConstructorArg2 added in v1.8.2

type ConstructorArg2[T, K, P any] func(K, P) (T, error)

ConstructorArg2 defines a constructor function with two arguments of types K and P that returns a value of type T and an error.

func (ConstructorArg2[T, K, P]) Binder added in v1.8.2

func (cons ConstructorArg2[T, K, P]) Binder(retriever types.DependencyRetriever) (value T, err error)

Binder retrieves the dependencies of types K and P, then calls the constructor function for ConstructorArg2 and returns the constructed value and any error encountered.

type ConstructorEmpty added in v1.8.2

type ConstructorEmpty[T any] func() (T, error)

ConstructorEmpty defines a constructor function with no arguments that returns a value of type T and an error.

func (ConstructorEmpty[T]) Binder added in v1.8.2

func (cons ConstructorEmpty[T]) Binder(types.DependencyRetriever) (T, error)

Binder calls the constructor function for ConstructorEmpty and returns the constructed value and any error encountered.

type DependencyRetriever

type DependencyRetriever = types.DependencyRetriever

type Injector

type Injector = types.Injector

func NewInjector

func NewInjector(configs ...Config) Injector

type InstancePairAny added in v1.1.0

type InstancePairAny = types.InstancePair[any]

type ReflectionOptions added in v1.5.0

type ReflectionOptions = types.ReflectionOptions

ReflectionOptions All options internally used to know how and when to use the `reflect` package

Directories

Path Synopsis
internal
pkg
test

Jump to

Keyboard shortcuts

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