Skip to content

Commit

Permalink
feat: add dependency injection container
Browse files Browse the repository at this point in the history
  • Loading branch information
lvlcn-t committed Aug 13, 2024
1 parent 4a091bc commit a40b0b4
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 0 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/dependency.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Test Dependency Module

on:
push:
branches:
- main
pull_request:
paths:
- dependency/**
- .github/workflows/dependency.yml

permissions:
# Needed to read the content of the repository
contents: read
# Needed for the static analysis result upload
security-events: write
# Needed for golangci-lint for the only-new-issues flag
pull-requests: read
# Needed to annotate the code with the scan results > https://github.blog/2018-12-14-introducing-check-runs-and-annotations/
checks: write

defaults:
run:
shell: bash
working-directory: ./dependency/...

jobs:
test:
uses: lvlcn-t/meta/.github/workflows/test.yml@main
with:
go_version_file: ./dependency/go.mod
golangci_config: skip
private_go_server: ""
before_tests: ""
additional_args: -v
package: ./dependency/...
secrets:
PRIVATE_GO_SERVER_TOKEN: ${{ secrets.PRIVATE_GO_SERVER_TOKEN }}
1 change: 1 addition & 0 deletions dependency/.golangci.yaml
3 changes: 3 additions & 0 deletions dependency/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/lvlcn-t/go-kit/dependency

go 1.22
270 changes: 270 additions & 0 deletions dependency/injector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package dependency

import (
"errors"
"fmt"
"reflect"
"sync"
)

// Injector is a dependency injector that can store and provide dependencies.
type Injector interface {
// Provide registers a dependency with the DI container under a specified interface type.
//
// Parameters:
// - dep: The concrete implementation of the dependency you want to register.
// - iface: The [reflect.Type] of the interface under which the dependency should be registered. Defaults to the type of the provided dependency.
// - singleton: If true, the DI container will ensure that only one instance of the dependency is created and shared.
// - factory: An optional function that creates a new instance of the dependency. If provided, this function is used to create instances instead of the passed 'dep' value.
// - overwrite: An optional boolean flag that, if set to true, allows overwriting an existing dependency of the same interface type.
//
// Usage:
//
// err := container.Provide(&MyConcreteType{}, reflect.TypeOf((*MyInterface)(nil)).Elem(), nil)
// if err != nil {
// log.Fatal(err)
// }
Provide(dep any, iface reflect.Type, singleton bool, factory func() any, overwrite ...bool) error
// Resolve returns either the first dependency of the given type or the given index.
// Returns an error if the type is nil or if the dependency is not found.
Resolve(t reflect.Type, i ...int) (any, error)
// ResolveAll returns all dependencies of the given interface type.
// Returns an error if the type is nil or if no dependencies are found.
ResolveAll(t reflect.Type) ([]any, error)
// Delete removes a dependency from the DI container.
Delete(t reflect.Type)
}

// container is the default DI container
var container Injector = NewDIContainer()

// Provide registers a dependency with the DI container under a specified interface type.
//
// Parameters:
// - dep: The dependency you want to register.
// - singleton: If true, the DI container will ensure that only one instance of the dependency is created and shared.
// - factory: An optional function that creates a new instance of the dependency. If provided, this function is used to create instances instead of the passed 'dep' value.
// - overwrite: An optional boolean flag that, if set to true, allows overwriting an existing dependency of the same interface type.
//
// Usage:
//
// // Initialize a new instance of an implementation of MyInterface that should be registered as the ladder.
// var instance MyInterface = &MyConcreteType{}
// err := Provide(instance, false, nil)
// if err != nil {
// log.Fatal(err)
// }
func Provide[T any](dep T, singleton bool, factory func() T, overwrite ...bool) error {
fun := (func() any)(nil)
if factory != nil {
fun = func() any {
return factory()
}
}
return container.Provide(dep, reflect.TypeOf((*T)(nil)).Elem(), singleton, fun, overwrite...)
}

// Resolve returns either the first dependency of the given type or the given index.
// Returns an error if the type is nil or if the dependency is not found.
func Resolve[T any](i ...int) (T, error) {
var empty T
v, err := container.Resolve(reflect.TypeOf((*T)(nil)).Elem(), i...)
if err != nil {
return empty, err
}
return v.(T), nil
}

// ResolveAll returns all dependencies of the given interface type.
// Returns an error if the type is nil or if no dependencies are found.
func ResolveAll[T any]() ([]T, error) {
v, err := container.ResolveAll(reflect.TypeOf((*T)(nil)).Elem())
if err != nil {
return nil, err
}

var results []T
for _, dep := range v {
results = append(results, dep.(T))
}
return results, nil
}

// Delete removes a dependency from the DI container.
func Delete[T any]() {
container.Delete(reflect.TypeOf((*T)(nil)).Elem())
}

// injector is a dependency injector that can store and provide dependencies.
// It is the default implementation of the Injector interface.
type injector struct {
// mu is a mutex that protects the dependencies map
mu sync.RWMutex
// dependencies is a map of types to their respective list of dependencies
dependencies map[reflect.Type][]dependency
}

// dependency is a dependency that can be stored in the injector.
type dependency struct {
// value is the value of the dependency
value reflect.Value
// factory is a function that creates a new instance of the dependency
factory func() any
// singleton is a flag that indicates if the dependency is a singleton
singleton bool
// once ensures the singleton is initialized only once
once sync.Once
}

// NewDIContainer creates a new DI container.
func NewDIContainer() Injector {
return &injector{
mu: sync.RWMutex{},
dependencies: map[reflect.Type][]dependency{},
}
}

// Provide registers a dependency with the DI container under a specified interface type.
//
// Parameters:
// - dep: The concrete implementation of the dependency you want to register.
// - iface: The [reflect.Type] of the interface under which the dependency should be registered. Defaults to the type of the provided dependency.
// - singleton: If true, the DI container will ensure that only one instance of the dependency is created and shared.
// - factory: An optional function that creates a new instance of the dependency. If provided, this function is used to create instances instead of the passed 'dep' value.
// - overwrite: An optional boolean flag that, if set to true, allows overwriting an existing dependency of the same interface type.
//
// Usage:
//
// err := container.Provide(&MyConcreteType{}, reflect.TypeOf((*MyInterface)(nil)).Elem(), nil)
// if err != nil {
// log.Fatal(err)
// }
func (c *injector) Provide(dep any, iface reflect.Type, singleton bool, factory func() any, overwrite ...bool) error {
if dep == nil && factory == nil {
return errors.New("dependency and factory are both nil")
}

if iface == nil {
iface = reflect.TypeOf(dep)
if iface.Kind() == reflect.Pointer {
iface = iface.Elem()
}
}

c.mu.Lock()
defer c.mu.Unlock()
if _, ok := c.dependencies[iface]; ok && (len(overwrite) == 0 || !overwrite[0]) {
return fmt.Errorf("dependency of type %T already exists", dep)
}

c.dependencies[iface] = append(c.dependencies[iface], dependency{
value: reflect.ValueOf(dep),
factory: factory,
singleton: singleton,
})
return nil
}

// Resolve returns either the first dependency of the given type or the given index.
// Returns an error if the type is nil or if the dependency is not found.
func (c *injector) Resolve(t reflect.Type, i ...int) (any, error) {
if t == nil {
return nil, errors.New("type is nil")
}

c.mu.RLock()
defer c.mu.RUnlock()
deps, ok := c.dependencies[t]
if !ok || len(deps) == 0 {
return nil, &errDependencyNotFound{}
}

index := 0
if len(i) > 0 {
index = i[0]
}
if index < -1 || index >= len(deps) {
return nil, errors.New("index out of range")
}
if index == -1 {
index = len(deps) - 1
}

dep := &deps[index]
if dep.singleton {
dep.once.Do(func() {
if dep.factory != nil {
instance := dep.factory()
dep.value = reflect.ValueOf(instance)
}
})
return dep.value.Interface(), nil
}

if dep.factory != nil {
return dep.factory(), nil
}
return reflect.New(dep.value.Type()).Elem().Interface(), nil
}

// ResolveAll returns all dependencies of the given interface type.
// Returns an error if the type is nil or if no dependencies are found.
func (c *injector) ResolveAll(t reflect.Type) ([]any, error) {
if t == nil {
return nil, errors.New("type is nil")
}

c.mu.RLock()
defer c.mu.RUnlock()
deps, exists := c.dependencies[t]
if !exists || len(deps) == 0 {
return nil, &errDependencyNotFound{}
}

var results []any
for i := range deps {
dep := &deps[i]
if dep.singleton {
dep.once.Do(func() {
if dep.factory != nil {
instance := dep.factory()
dep.value = reflect.ValueOf(instance)
}
})
results = append(results, dep.value.Interface())
continue
}

if dep.factory != nil {
results = append(results, dep.factory())
continue
}

results = append(results, dep.value.Interface())
}

return results, nil
}

// Delete removes a dependency from the DI container.
func (c *injector) Delete(t reflect.Type) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.dependencies, t)
}

var _ error = (*errDependencyNotFound)(nil)

type errDependencyNotFound struct{}

func (e errDependencyNotFound) Error() string {
return "dependency not found"
}

func (e *errDependencyNotFound) Is(target error) bool {
_, ok := target.(*errDependencyNotFound)
if !ok {
_, ok = target.(errDependencyNotFound)
}
return ok
}

0 comments on commit a40b0b4

Please sign in to comment.