Skip to content

Commit

Permalink
Add advanced dependency managment
Browse files Browse the repository at this point in the history
  • Loading branch information
olbrichattila committed Jul 14, 2024
1 parent d055069 commit 220fb51
Show file tree
Hide file tree
Showing 22 changed files with 445 additions and 140 deletions.
48 changes: 44 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Where "TestInterface" is your interface name, "NewTest2()" returns an interface

- If it is in defined in your home folder, then use only the interface name
- If it is in a sub folder, provide full path, exampe: ```examplemodule-1.mod.ExampleInterface``` where your interface defined in folder ```./examplemodule-1/mod/```
- If you initiated your project with a domain. (for example this module) ```github.com/olbrichattila/godi```. use the path from your module as well. Example: ```olbrichattila.godi.internal.container-test.noParamConstructorInterface```
- If you initiated your project with a domain. (for example this module) ```github.com/olbrichattila/godi```. use the path from your module as well. Example: ```olbrichattila.godi.internal.test.container.noParamConstructorInterface```

Note: Look at the test folder for examples, and see the following example as well:

Expand Down Expand Up @@ -63,6 +63,49 @@ type exampleMultipleImpl struct {

The dependencies will be auto wired, if you initiate your struct with the DI container as above:

## Managing dependencies
One way of managing dependencies was already introduced above:

### Add single dependency
```
container := godi.New()
container.Set("TestInterface", NewTest2())
```
### Retrieve single dependency

```
dependency, err := container.GetDependency("dependencyInterfaceName2")
```

### Add multiple dependencies at the same time
```
container := godi.New()
dependencies := map[string]interface{}{
"dependencyInterfaceName1": NewDependency1(),
"dependencyInterfaceName2": NewDependency1(),
"dependencyInterfaceName3": NewDependency1(),
}
container.Build(dependencies)
```

### Flush all dependencies
```
container.Flush()
```

### Delete one dependency
```
container.Delete("dependencyInterfaceName2")
```

### Get number or existing dependencies
```
count := container.Count()
fmt.Println(count)
```


Example usage:
Expand Down Expand Up @@ -170,6 +213,3 @@ func (t *example) Construct() {
}
```

## Please see tests folder for mocks to see more variations of usage


165 changes: 44 additions & 121 deletions godi.go
Original file line number Diff line number Diff line change
@@ -1,143 +1,66 @@
// Package godi is a golang dependency injector container
package godi

import (
"errors"
"fmt"
"reflect"
"strings"
)

var (
// ErrCannotBeResolved is returned when the container is not able to resolve the dependency.
ErrCannotBeResolved = errors.New("the DI parameter cannot be resolved")
// ErrCannotBeResolvedPossibleNeedExport is returned when the container is not able to resolve the dependency, possibly due to an unexported field.
ErrCannotBeResolvedPossibleNeedExport = errors.New("the DI parameter cannot be resolved, possible unexported field for autowire notation")
// ErrCircularReference is returned when there is a circular dependency reference.
ErrCircularReference = errors.New("circular reference")
)
// Package gody is the wrapper around dependency injection container
package gody

import "github.com/olbrichattila/godi/internal/container"

// Container is an just a wrapper around the internal container to be able to modify the implementation without effecting end users if they would think of referencing the sub package
type Container interface {
Set(string, interface{})
Get(interface{}) (interface{}, error)
Build(map[string]interface{})
GetDependency(string) (interface{}, error)
GetDependencies() map[string]interface{}
Flush()
Delete(paramName string)
Count() int
}

// dependencyMap stores the dependency.
type dependencyMap struct {
dependency interface{}
// Cont is the container structure which follows Container interface
type Cont struct {
c *container.Cont
}

// New creates a new dependency injector container.
func New() *Cont {
return &Cont{
dependencies: make(map[string]*dependencyMap),
}
// New creates a new container
func New() Container {
return &Cont{c: container.New()}
}

// Cont is the container returned by New.
type Cont struct {
callStack map[string]bool
dependencies map[string]*dependencyMap
// Build entire dependency tree
func (t *Cont) Build(dependencies map[string]interface{}) {
t.c.Build(dependencies)
}

// Set registers a new dependency. Provide a "packagePath.InterfaceName" as a string and your dependency, which should be an interface or struct.
// Set Set dependencies to the container
func (t *Cont) Set(paramName string, dependency interface{}) {
t.dependencies[paramName] = &dependencyMap{dependency: dependency}
t.c.Set(paramName, dependency)
}

// Get resolves dependencies. Use a construct function with your dependency interface type hints. They will be resolved recursively.
func (t *Cont) Get(obj interface{}) (interface{}, error) {
t.callStack = make(map[string]bool)
return t.getRecursive(obj)
return t.c.Get(obj)
}

// getRecursive resolves dependencies recursively, tracking call stack to detect circular references.
func (t *Cont) getRecursive(obj interface{}) (interface{}, error) {
v := reflect.ValueOf(obj)

if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct {
rt := v.Type()

if err := t.resolveConstructor(v, rt); err != nil {
return nil, err
}

if err := t.resolveAutoWire(v); err != nil {
return nil, err
}
}

return obj, nil
// GetDependency retrieve the dependency, or returns error
func (t *Cont) GetDependency(paramName string) (interface{}, error) {
return t.c.GetDependency(paramName)
}

// resolveConstructor resolves dependencies for the constructor method.
func (t *Cont) resolveConstructor(v reflect.Value, rt reflect.Type) error {
if method, found := rt.MethodByName("Construct"); found {
passParams := []reflect.Value{v}
methodType := method.Type
numParams := methodType.NumIn()

for i := 1; i < numParams; i++ {
paramType := methodType.In(i)
param, fullTypeName, err := t.resolveConstructorParam(paramType)
if err != nil {
return err
}
if t.callStack[fullTypeName] {
return fmt.Errorf("%w: circular call: %s", ErrCircularReference, fullTypeName)
}
t.callStack[fullTypeName] = true

if _, err := t.getRecursive(param); err != nil {
delete(t.callStack, fullTypeName)
return err
}
delete(t.callStack, fullTypeName)
passParams = append(passParams, reflect.ValueOf(param))
}

method.Func.Call(passParams)
}

return nil
// GetDependencies returns the entire dependency map
func (t *Cont) GetDependencies() map[string]interface{} {
return t.c.GetDependencies()
}

// resolveAutoWire resolves dependencies for struct fields with the "di" tag.
func (t *Cont) resolveAutoWire(v reflect.Value) error {
vTyp := v.Elem().Type()
for i := 0; i < v.Elem().NumField(); i++ {
field := vTyp.Field(i)
tag := field.Tag.Get("di")
if tag == "autowire" {
resolvedField := v.Elem().FieldByName(field.Name)

if !resolvedField.CanSet() {
return fmt.Errorf("%w: the field name: %s", ErrCannotBeResolvedPossibleNeedExport, field.Name)
}

value, _, err := t.resolveConstructorParam(field.Type)
if err != nil {
return err
}

if _, err := t.getRecursive(value); err != nil {
return err
}

fieldValue := reflect.ValueOf(value)
if field.Type.Kind() == reflect.Interface && !fieldValue.Type().Implements(field.Type) {
return fmt.Errorf("provided value does not implement the field's interface: %s", field.Name)
}

resolvedField.Set(fieldValue)
}
}
return nil
// Flush dependencies
func (t *Cont) Flush() {
t.c.Flush()
}

// resolveConstructorParam resolves a constructor parameter by its type.
func (t *Cont) resolveConstructorParam(paramType reflect.Type) (interface{}, string, error) {
pkgPath := paramType.PkgPath() + "/" + paramType.Name()
fullTypeName := strings.Join(strings.Split(pkgPath, "/")[1:], ".")
param, ok := t.dependencies[fullTypeName]
if !ok {
return nil, fullTypeName, fmt.Errorf("%w: dependency name: %s", ErrCannotBeResolved, fullTypeName)
}
// Delete one dependency
func (t *Cont) Delete(paramName string) {
t.c.Delete(paramName)
}

return param.dependency, fullTypeName, nil
// Count returns how any dependencies provided
func (t *Cont) Count() int {
return t.c.Count()
}
Loading

0 comments on commit 220fb51

Please sign in to comment.