Compare commits

..

3 Commits
v1.3.0 ... main

Author SHA1 Message Date
4f04dc57d8 add delete match function 2025-08-30 21:43:36 +02:00
d5fcd40f50 refactor into container struct and add replace instance 2025-08-29 16:31:49 +02:00
24e307bd32 add Replace di Instance 2025-06-23 20:39:18 +02:00
5 changed files with 335 additions and 47 deletions

124
README.md
View File

@ -4,6 +4,8 @@ Go Dependency Injection with Generics
## Example ## Example
### Struct
configuration.go configuration.go
```go ```go
@ -97,3 +99,125 @@ func main() {
println(msgService.Welcome()) println(msgService.Welcome())
} }
``` ```
### Interface
message_service.go
```go
package main
import "git.apihub24.de/admin/generic-di"
func init() {
di.Injectable(newMessageService)
}
type IMessageService interface {
Welcome() string
}
type messageService struct {
greeter *Greeter
}
func NewMessageService() IMessageService {
return &messageService{
// here was the Greeter from greeter.go injected
greeter: di.Inject[*Greeter](),
}
}
func (ctx *messageService) Welcome() string {
return ctx.greeter.Greet()
}
```
main.go
```go
package main
import di "git.apihub24.de/admin/generic-di"
func main() {
msgService := di.Inject[IMessageService]()
// prints the message "Hello, Markus"
println(msgService.Welcome())
}
```
## Replace Instance
services/message_service.go
```go
package services
import "git.apihub24.de/admin/generic-di"
func init() {
di.Injectable(newMessageService)
}
type IMessageService interface {
Welcome() string
}
type messageService struct {
greeter *Greeter
}
func NewMessageService() IMessageService {
return &messageService{
// here was the Greeter from greeter.go injected
greeter: di.Inject[*Greeter](),
}
}
func (ctx *messageService) Welcome() string {
return ctx.greeter.Greet()
}
```
main.go
```go
package main
import di "git.apihub24.de/admin/generic-di"
func main() {
msgService := di.Inject[services.IMessageService]()
// prints the message "Hello, Markus"
println(msgService.Welcome())
}
```
message_service_test.go
```go
package services_test
func init() {
// replace the instance of IMessageService with the Mock
di.Replace(newMessageServiceMock)
}
type messageServiceMock struct {}
func newMessageServiceMock() services.IMessageService {
return &messageServiceMock{}
}
func (svc *messageServiceMock) Welcome() string {
return "Hello, Mock"
}
func TestMessageServiceMocking(t *testing.T) {
service := di.Inject[services.IMessageService]()
if service.Welcome() != "Hello, Mock" {
t.Errorf("expect Hello, Mock but get %s", service.Welcome())
}
}
```

103
container.go Normal file
View File

@ -0,0 +1,103 @@
package di
import (
"fmt"
"reflect"
"strings"
"sync"
)
type container struct {
mu sync.RWMutex
creators map[string]func() any
instances map[string]any
}
var globalContainer *container
var once sync.Once
func getContainer() *container {
once.Do(func() {
globalContainer = &container{
mu: sync.RWMutex{},
creators: make(map[string]func() any),
instances: make(map[string]any),
}
})
return globalContainer
}
func (c *container) injectable(typ reflect.Type, creator func() any) {
c.mu.Lock()
defer c.mu.Unlock()
selector := c.getSelector(typ)
c.creators[selector] = creator
}
func (c *container) replace(typ reflect.Type, creator func() any, identifier ...string) {
c.mu.Lock()
defer c.mu.Unlock()
selector := c.getSelector(typ)
instanceSelector := c.getSelector(typ, identifier...)
c.creators[selector] = creator
createdInstance := creator()
c.instances[instanceSelector] = createdInstance
}
func (c *container) inject(typ reflect.Type, identifier ...string) (any, bool) {
instanceSelector := c.getSelector(typ, identifier...)
c.mu.RLock()
if instance, ok := c.instances[instanceSelector]; ok {
c.mu.RUnlock()
return instance, true
}
c.mu.RUnlock()
c.mu.RLock()
if instance, ok := c.instances[instanceSelector]; ok {
c.mu.RUnlock()
return instance, true
}
c.mu.RUnlock()
selector := c.getSelector(typ)
creator, creatorExists := c.creators[selector]
if !creatorExists {
return nil, false
}
createdInstance := creator()
c.mu.Lock()
c.instances[instanceSelector] = createdInstance
c.mu.Unlock()
return createdInstance, true
}
func (c *container) destroy(typ reflect.Type, identifier ...string) {
c.mu.Lock()
defer c.mu.Unlock()
instanceSelector := c.getSelector(typ, identifier...)
delete(c.instances, instanceSelector)
}
func (c *container) destroyAllMatching(match func(string) bool) {
c.mu.Lock()
defer c.mu.Unlock()
for key := range c.instances {
if match(key) {
delete(c.instances, key)
}
}
println("")
}
func (c *container) getSelector(typ reflect.Type, identifier ...string) string {
typeName := typ.String()
additionalKey := strings.Join(identifier, "_")
return fmt.Sprintf("%s_%s", additionalKey, typeName)
}

View File

@ -1,67 +1,41 @@
package di package di
import ( import (
"fmt"
"reflect" "reflect"
"strings"
"sync"
) )
var creatorMutex = sync.Mutex{}
var instanceMutex = sync.Mutex{}
var creators = make(map[string]func() any)
var instances = make(map[string]any)
// Injectable marks a constructor Function of a Struct for DI // Injectable marks a constructor Function of a Struct for DI
func Injectable[T any](creator func() T) { func Injectable[T any](creator func() T) {
creatorMutex.Lock() typ := reflect.TypeOf((*T)(nil)).Elem()
defer creatorMutex.Unlock() getContainer().injectable(typ, func() any { return creator() })
creators[getSelector[T]()] = func() any { }
return creator()
} func Replace[T any](creator func() T, identifier ...string) {
typ := reflect.TypeOf((*T)(nil)).Elem()
getContainer().replace(typ, func() any { return creator() })
}
func ReplaceInstance[T any](instance T, identifier ...string) {
Replace(func() T { return instance }, identifier...)
} }
// Inject gets or create a Instance of the Struct used the Injectable constructor Function // Inject gets or create a Instance of the Struct used the Injectable constructor Function
func Inject[T any](identifier ...string) T { func Inject[T any](identifier ...string) T {
var nilResult T var result T
selector := getSelector[T]() typ := reflect.TypeOf((*T)(nil)).Elem()
instanceSelector := getSelector[T](identifier...) if instance, ok := getContainer().inject(typ, identifier...); ok {
_, instanceExists := instances[instanceSelector].(T) if result, ok = instance.(T); ok {
if !instanceExists { return result
creator, creatorExists := creators[selector]
if !creatorExists {
return nilResult
}
createdInstance, instanceCreated := creator().(T)
if instanceCreated {
instanceMutex.Lock()
defer instanceMutex.Unlock()
instance, instanceExists := instances[instanceSelector].(T)
if instanceExists {
return instance
}
instances[instanceSelector] = createdInstance
} }
} }
return instances[instanceSelector].(T) return result
} }
func Destroy[T any](identifier ...string) { func Destroy[T any](identifier ...string) {
instanceMutex.Lock() typ := reflect.TypeOf((*T)(nil)).Elem()
defer instanceMutex.Unlock() getContainer().destroy(typ, identifier...)
instanceSelector := getSelector[T](identifier...)
delete(instances, instanceSelector)
} }
func getSelector[T any](identifier ...string) string { func DestroyAllMatching(match func(string) bool) {
var def T getContainer().destroyAllMatching(match)
typeName := ""
typeOf := reflect.TypeOf(def)
if typeOf != nil {
typeName = typeOf.String()
} else {
typeName = reflect.TypeOf((*T)(nil)).Elem().String()
}
additionalKey := strings.Join(identifier, "_")
return fmt.Sprintf("%s_%s", typeName, additionalKey)
} }

View File

@ -2,6 +2,7 @@ package di_test
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
di "git.apihub24.de/admin/generic-di" di "git.apihub24.de/admin/generic-di"
@ -13,6 +14,7 @@ func init() {
di.Injectable(newMessageService) di.Injectable(newMessageService)
di.Injectable(newConfiguration) di.Injectable(newConfiguration)
di.Injectable(newGreetingService) di.Injectable(newGreetingService)
di.Injectable(newBasicOverridableService)
} }
type ( type (
@ -30,8 +32,33 @@ type (
Greeting() string Greeting() string
TakeID() string TakeID() string
} }
overridableService interface {
GetInstanceID() string
GetValue() string
}
basicOverridableService struct {
id string
}
basicOverridableServiceMock struct {
id string
}
) )
func newBasicOverridableService() overridableService {
return &basicOverridableService{
id: uuid.NewString(),
}
}
func newBasicOverridableServiceMock() overridableService {
return &basicOverridableServiceMock{
id: uuid.NewString(),
}
}
func newConfiguration() *configuration { func newConfiguration() *configuration {
return &configuration{} return &configuration{}
} }
@ -53,6 +80,22 @@ func newMessageService() *messageService {
} }
} }
func (ctx *basicOverridableService) GetInstanceID() string {
return ctx.id
}
func (ctx *basicOverridableService) GetValue() string {
return "i am original"
}
func (ctx *basicOverridableServiceMock) GetInstanceID() string {
return ctx.id
}
func (ctx *basicOverridableServiceMock) GetValue() string {
return "i am mock"
}
func (ctx *configuration) GetUserName() string { func (ctx *configuration) GetUserName() string {
return "Markus" return "Markus"
} }
@ -74,6 +117,9 @@ func (ctx *messageService) GetTextServiceID() string {
} }
func TestInject(t *testing.T) { func TestInject(t *testing.T) {
// testMutex.Lock()
// defer testMutex.Unlock()
msg := newMessageService() msg := newMessageService()
println(msg.texts.Greeting()) println(msg.texts.Greeting())
if msg.texts.Greeting() != "Hello Markus" { if msg.texts.Greeting() != "Hello Markus" {
@ -82,6 +128,9 @@ func TestInject(t *testing.T) {
} }
func TestInject_Duplicate(t *testing.T) { func TestInject_Duplicate(t *testing.T) {
// testMutex.Lock()
// defer testMutex.Unlock()
msg1 := newMessageService() msg1 := newMessageService()
msg2 := newMessageService() msg2 := newMessageService()
println(msg1.texts.Greeting()) println(msg1.texts.Greeting())
@ -134,6 +183,42 @@ func TestTryInterface_MultipleInstances(t *testing.T) {
} }
} }
func TestOverwriteInjectable(t *testing.T) {
basic := di.Inject[overridableService]()
basicID := basic.GetInstanceID()
if basic.GetValue() != "i am original" {
t.Errorf("wrong service instance get")
}
di.Replace(newBasicOverridableServiceMock)
basic = di.Inject[overridableService]()
if basic.GetInstanceID() == basicID {
t.Errorf("basic and newOne are the same instance")
}
if basic.GetValue() != "i am mock" {
t.Errorf("service not overwritten")
}
}
func TestOverwriteInjectableInstance(t *testing.T) {
basic := di.Inject[overridableService]()
basicID := basic.GetInstanceID()
di.ReplaceInstance(newBasicOverridableServiceMock())
basic = di.Inject[overridableService]()
if basic.GetInstanceID() == basicID {
t.Errorf("basic and newOne are the same instance")
}
if basic.GetValue() != "i am mock" {
t.Errorf("service not overwritten")
}
}
func TestDestroyMatching(t *testing.T) {
_ = di.Inject[greetingService]("abc")
_ = di.Inject[greetingService]("def")
_ = di.Inject[greetingService]("abc_def")
di.DestroyAllMatching(func(key string) bool { return strings.HasPrefix(key, "abc") })
}
func TestDestroy(t *testing.T) { func TestDestroy(t *testing.T) {
_ = di.Inject[textService]("a") _ = di.Inject[textService]("a")
di.Destroy[textService]("a") di.Destroy[textService]("a")

2
makefile Normal file
View File

@ -0,0 +1,2 @@
test:
- go test ./...