Skip to content

Commit

Permalink
Merge branch 'main' of github.com:firasdarwish/ore
Browse files Browse the repository at this point in the history
  • Loading branch information
Firas Darwish committed Nov 4, 2024
2 parents fdc7e5e + d477ee8 commit fdc46c2
Show file tree
Hide file tree
Showing 27 changed files with 1,188 additions and 280 deletions.
114 changes: 114 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ func main() {

```

#### Injecting Mocks in Tests

The last registered implementation takes precedence, so you can register a mock implementation in the test, which will override the real implementation.

<br />

### Keyed Services Retrieval Example
Expand Down Expand Up @@ -280,6 +284,116 @@ func main() {

```

### Alias: Register struct, get interface

```go
type IPerson interface{}
type Broker struct {
Name string
} //implements IPerson

type Trader struct {
Name string
} //implements IPerson

func TestGetInterfaceAlias(t *testing.T) {
ore.RegisterLazyFunc(ore.Scoped, func(ctx context.Context) (*Broker, context.Context) {
return &Broker{Name: "Peter"}, ctx
})
ore.RegisterLazyFunc(ore.Scoped, func(ctx context.Context) (*Broker, context.Context) {
return &Broker{Name: "John"}, ctx
})
ore.RegisterLazyFunc(ore.Scoped, func(ctx context.Context) (*Trader, context.Context) {
return &Trader{Name: "Mary"}, ctx
})

ore.RegisterAlias[IPerson, *Trader]() //link IPerson to *Trader
ore.RegisterAlias[IPerson, *Broker]() //link IPerson to *Broker

//no IPerson was registered to the container, but we can still `Get` it out of the container.
//(1) IPerson is alias to both *Broker and *Trader. *Broker takes precedence because it's the last one linked to IPerson.
//(2) multiple *Borker (Peter and John) are registered to the container, the last registered (John) takes precedence.
person, _ := ore.Get[IPerson](context.Background()) // will return the broker John

personList, _ := ore.GetList[IPerson](context.Background()) // will return all registered broker and trader
}
```

Alias is also scoped by key. When you "Get" an alias with keys for eg: `ore.Get[IPerson](ctx, "module1")` then Ore would return only Services registered under this key ("module1") and panic if no service found.

### Graceful application termination

On application termination, you want to call `Shutdown()` on all the "Singletons" objects which have been created during the application life time.

Here how Ore can help you:

```go
// Assuming that the Application provides certain instances with Singleton lifetime.
// Some of these singletons implement a custom `Shutdowner` interface (defined within the application)
type Shutdowner interface {
Shutdown()
}
ore.RegisterEagerSingleton(&Logger{}) //*Logger implements Shutdowner
ore.RegisterEagerSingleton(&SomeRepository{}) //*SomeRepository implements Shutdowner
ore.RegisterEagerSingleton(&SomeService{}, "some_module") //*SomeService implements Shutdowner

//On application termination, Ore can help to retreive all the singletons implementation of the `Shutdowner` interface.
//There might be other `Shutdowner`'s implementation which were lazily registered but have never been created (a.k.a invoked).
//Ore will ignore them, and return only the concrete instances which can be Shutdown()
shutdowables := ore.GetResolvedSingletons[Shutdowner]()

//Now we can Shutdown() them all and gracefully terminate our application.
//The most recently created instance will be Shutdown() first
for _, instance := range disposables {
instance.Shutdown()
}
```

In resume, the `ore.GetResolvedSingletons[TInterface]()` function returns a list of Singleton implementations of the `[TInterface]`.

- It returns only the instances which had been invoked (a.k.a resolved).
- All the implementations including "keyed" one will be returned.
- The returned instances are sorted by creation time (a.k.a the invocation order), the first one being the most recently created one.

### Graceful context termination

On context termination, you want to call `Dispose()` on all the "Scoped" objects which have been created during the context life time.

Here how Ore can help you:

```go
//Assuming that your Application provides certain instances with Scoped lifetime.
//Some of them implements a "Disposer" interface (defined winthin the application).
type Disposer interface {
Dispose()
}
ore.RegisterLazyCreator(ore.Scoped, &SomeDisposableService{}) //*SomeDisposableService implements Disposer

//a new request arrive
ctx, cancel := context.WithCancel(context.Background())

//start a go routine that will clean up resources when the context is canceled
go func() {
<-ctx.Done() // Wait for the context to be canceled
// Perform your cleanup tasks here
disposables := ore.GetResolvedScopedInstances[Disposer](ctx)
//The most recently created instance will be Dispose() first
for _, d := range disposables {
_ = d.Dispose(ctx)
}
}()
...
ore.Get[*SomeDisposableService](ctx) //invoke some scoped services
cancel() //cancel the ctx

```

The `ore.GetResolvedScopedInstances[TInterface](context)` function returns a list of implementations of the `[TInterface]` which are Scoped in the input context:

- It returns only the instances which had been invoked (a.k.a resolved) during the context life time.
- All the implementations including "keyed" one will be returned.
- The returned instances are sorted by creation time (a.k.a the invocation order), the first one being the most recently created one.

## More Complex Example

```go
Expand Down
189 changes: 189 additions & 0 deletions alias_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package ore

import (
"context"
"testing"

m "github.com/firasdarwish/ore/internal/models"
"github.com/stretchr/testify/assert"
)

func TestAliasResolverConflict(t *testing.T) {
clearAll()
RegisterLazyFunc(Singleton, func(ctx context.Context) (m.IPerson, context.Context) {
return &m.Trader{Name: "Peter Singleton"}, ctx
})
RegisterLazyFunc(Transient, func(ctx context.Context) (*m.Broker, context.Context) {
return &m.Broker{Name: "Mary Transient"}, ctx
})

RegisterAlias[m.IPerson, *m.Trader]()
RegisterAlias[m.IPerson, *m.Broker]()

ctx := context.Background()

//The last registered IPerson is "Mary Transient", it would normally takes precedence.
//However we registered a direct resolver for IPerson which is "Peter Singleton".
//So Ore won't treat IPerson as an alias and will resolve IPerson directly as "Peter Singleton"
person, ctx := Get[m.IPerson](ctx)
assert.Equal(t, person.(*m.Trader).Name, "Peter Singleton")

//GetlList will return all possible IPerson whatever alias or from direct resolver.
personList, _ := GetList[m.IPerson](ctx)
assert.Equal(t, len(personList), 2)
}

func TestAliasOfAliasIsNotAllow(t *testing.T) {
clearAll()
RegisterLazyFunc(Singleton, func(ctx context.Context) (*m.Trader, context.Context) {
return &m.Trader{Name: "Peter Singleton"}, ctx
})
RegisterLazyFunc(Transient, func(ctx context.Context) (*m.Broker, context.Context) {
return &m.Broker{Name: "Mary Transient"}, ctx
})

RegisterAlias[m.IPerson, *m.Trader]()
RegisterAlias[m.IPerson, *m.Broker]()
RegisterAlias[m.IHuman, m.IPerson]() //alias of alias

assert.Panics(t, func() {
_, _ = Get[m.IHuman](context.Background())
}, "implementation not found for type: IHuman")

humans, _ := GetList[m.IHuman](context.Background())
assert.Empty(t, humans)
}

func TestAliasWithDifferentScope(t *testing.T) {
clearAll()
module := "TestGetInterfaceAliasWithDifferentScope"
RegisterLazyFunc(Transient, func(ctx context.Context) (*m.Broker, context.Context) {
return &m.Broker{Name: "Transient"}, ctx
}, module)
RegisterLazyFunc(Singleton, func(ctx context.Context) (*m.Broker, context.Context) {
return &m.Broker{Name: "Singleton"}, ctx
}, module)
RegisterLazyFunc(Scoped, func(ctx context.Context) (*m.Broker, context.Context) {
return &m.Broker{Name: "Scoped"}, ctx
}, module)
RegisterAlias[m.IPerson, *m.Broker]() //link m.IPerson to *m.Broker

ctx := context.Background()

person, ctx := Get[m.IPerson](ctx, module)
assert.Equal(t, person.(*m.Broker).Name, "Scoped")

personList, _ := GetList[m.IPerson](ctx, module)
assert.Equal(t, len(personList), 3)
}

func TestAliasIsScopedByKeys(t *testing.T) {
clearAll()
RegisterLazyFunc(Scoped, func(ctx context.Context) (*m.Broker, context.Context) {
return &m.Broker{Name: "Peter1"}, ctx
}, "module1")
RegisterLazyFunc(Scoped, func(ctx context.Context) (*m.Broker, context.Context) {
return &m.Broker{Name: "John1"}, ctx
}, "module1")
RegisterLazyFunc(Scoped, func(ctx context.Context) (*m.Trader, context.Context) {
return &m.Trader{Name: "Mary1"}, ctx
}, "module1")

RegisterLazyFunc(Scoped, func(ctx context.Context) (*m.Broker, context.Context) {
return &m.Broker{Name: "John2"}, ctx
}, "module2")
RegisterLazyFunc(Scoped, func(ctx context.Context) (*m.Trader, context.Context) {
return &m.Trader{Name: "Mary2"}, ctx
}, "module2")

RegisterLazyFunc(Scoped, func(ctx context.Context) (*m.Trader, context.Context) {
return &m.Trader{Name: "Mary3"}, ctx
}, "module3")

RegisterAlias[m.IPerson, *m.Trader]() //link m.IPerson to *m.Trader
RegisterAlias[m.IPerson, *m.Broker]() //link m.IPerson to *m.Broker

ctx := context.Background()

person1, ctx := Get[m.IPerson](ctx, "module1") // will return the m.Broker John
assert.Equal(t, person1.(*m.Broker).Name, "John1")

personList1, ctx := GetList[m.IPerson](ctx, "module1") // will return all registered m.Broker and m.Trader
assert.Equal(t, len(personList1), 3)

person2, ctx := Get[m.IPerson](ctx, "module2") // will return the m.Broker John
assert.Equal(t, person2.(*m.Broker).Name, "John2")

personList2, ctx := GetList[m.IPerson](ctx, "module2") // will return all registered m.Broker and m.Trader
assert.Equal(t, len(personList2), 2)

person3, ctx := Get[m.IPerson](ctx, "module3") // will return the m.Trader Mary
assert.Equal(t, person3.(*m.Trader).Name, "Mary3")

personList3, ctx := GetList[m.IPerson](ctx, "module3") // will return all registered m.Broker and m.Trader
assert.Equal(t, len(personList3), 1)

personListNoModule, _ := GetList[m.IPerson](ctx) // will return all registered m.Broker and m.Trader without keys
assert.Empty(t, personListNoModule)
}

func TestInvalidAlias(t *testing.T) {
assert.Panics(t, func() {
RegisterAlias[error, *m.Broker]() //register a struct (Broker) that does not implement interface (error)
}, "Broker does not implements error")
}

func TestGetGenericAlias(t *testing.T) {
for _, registrationType := range types {
clearAll()

RegisterLazyFunc(registrationType, func(ctx context.Context) (*simpleCounterUint, context.Context) {
return &simpleCounterUint{}, ctx
})
RegisterAlias[someCounterGeneric[uint], *simpleCounterUint]()

c, _ := Get[someCounterGeneric[uint]](context.Background())

c.Add(1)
c.Add(1)

assert.Equal(t, uint(2), c.GetCount())
}
}

func TestGetListGenericAlias(t *testing.T) {
for _, registrationType := range types {
clearAll()

for i := 0; i < 3; i++ {
RegisterLazyFunc(registrationType, func(ctx context.Context) (*simpleCounterUint, context.Context) {
return &simpleCounterUint{}, ctx
})
}

RegisterAlias[someCounterGeneric[uint], *simpleCounterUint]()

counters, _ := GetList[someCounterGeneric[uint]](context.Background())
assert.Equal(t, len(counters), 3)

c := counters[1]
c.Add(1)
c.Add(1)

assert.Equal(t, uint(2), c.GetCount())
}
}

var _ someCounterGeneric[uint] = (*simpleCounterUint)(nil)

type simpleCounterUint struct {
counter uint
}

func (this *simpleCounterUint) Add(number uint) {
this.counter += number
}

func (this *simpleCounterUint) GetCount() uint {
return this.counter
}
22 changes: 11 additions & 11 deletions benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func BenchmarkRegisterLazyFunc(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
RegisterLazyFunc[Counter](Scoped, func(ctx context.Context) (Counter, context.Context) {
RegisterLazyFunc[someCounter](Scoped, func(ctx context.Context) (someCounter, context.Context) {
return &simpleCounter{}, ctx
})
}
Expand All @@ -21,7 +21,7 @@ func BenchmarkRegisterLazyCreator(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
RegisterLazyCreator[Counter](Scoped, &simpleCounter{})
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{})
}
}

Expand All @@ -30,44 +30,44 @@ func BenchmarkRegisterEagerSingleton(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
RegisterEagerSingleton[Counter](&simpleCounter{})
RegisterEagerSingleton[someCounter](&simpleCounter{})
}
}

func BenchmarkGet(b *testing.B) {
clearAll()

RegisterLazyFunc[Counter](Scoped, func(ctx context.Context) (Counter, context.Context) {
RegisterLazyFunc[someCounter](Scoped, func(ctx context.Context) (someCounter, context.Context) {
return &simpleCounter{}, ctx
})

RegisterEagerSingleton[Counter](&simpleCounter{})
RegisterEagerSingleton[someCounter](&simpleCounter{})

RegisterLazyCreator[Counter](Scoped, &simpleCounter{})
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{})

ctx := context.Background()

b.ResetTimer()
for i := 0; i < b.N; i++ {
Get[Counter](ctx)
Get[someCounter](ctx)
}
}

func BenchmarkGetList(b *testing.B) {
clearAll()

RegisterLazyFunc[Counter](Scoped, func(ctx context.Context) (Counter, context.Context) {
RegisterLazyFunc[someCounter](Scoped, func(ctx context.Context) (someCounter, context.Context) {
return &simpleCounter{}, ctx
})

RegisterEagerSingleton[Counter](&simpleCounter{})
RegisterEagerSingleton[someCounter](&simpleCounter{})

RegisterLazyCreator[Counter](Scoped, &simpleCounter{})
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{})

ctx := context.Background()

b.ResetTimer()
for i := 0; i < b.N; i++ {
GetList[Counter](ctx)
GetList[someCounter](ctx)
}
}
Loading

0 comments on commit fdc46c2

Please sign in to comment.