Skip to content

Commit aeb9fe1

Browse files
authored
Module feature and Place holder feature (#9)
2 parents 5185de5 + 0d481d2 commit aeb9fe1

29 files changed

+947
-443
lines changed

README.md

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ the management of object lifetimes and the inversion of control in your applicat
1818

1919
<br />
2020

21-
# Features
21+
## Features
2222

2323
- **Singletons**: Register components as singletons, ensuring that there's only one instance throughout the entire
2424
application.
@@ -46,15 +46,15 @@ the management of object lifetimes and the inversion of control in your applicat
4646

4747
<br />
4848

49-
# Installation
49+
## Installation
5050

5151
```bash
5252
go get -u github.com/firasdarwish/ore
5353
```
5454

5555
<br />
5656

57-
# Usage
57+
## Usage
5858

5959
### Import
6060

@@ -433,12 +433,106 @@ cancel() //cancel the ctx
433433
The `ore.GetResolvedScopedInstances[TInterface](context)` function returns a list of implementations of the `[TInterface]` which are Scoped in the input context:
434434

435435
- It returns only the instances which had been invoked (a.k.a resolved) during the context lifetime.
436-
- All the implementations including "keyed" one will be returned.
436+
- All the implementations (of all modules) including "keyed" one will be returned.
437437
- The returned instances are sorted by invocation order, the first one being the latest invoked one.
438438
- if "A" depends on "B", "C", Ore will make sure to return "B" and "C" first in the list so that they would be Disposed before "A".
439439

440440
<br />
441441

442+
### Multiple Containers (a.k.a Modules)
443+
444+
| DefaultContainer | Custom container |
445+
|------------------|------------------|
446+
| Get | GetFromContainer |
447+
| GetList | GetListFromContainer |
448+
| GetResolvedSingletons | GetResolvedSingletonsFromContainer |
449+
| RegisterAlias | RegisterAliasToContainer |
450+
| RegisterEagerSingleton | RegisterEagerSingletonToContainer |
451+
| RegisterLazyCreator | RegisterLazyCreatorToContainer |
452+
| RegisterLazyFunc | RegisterLazyFuncToContainer |
453+
| RegisterPlaceHolder | RegisterPlaceHolderToContainer |
454+
| ProvideScopedValue | ProvideScopedValueToContainer |
455+
456+
Most of time you only need the Default Container. In rare use case such as the Modular Monolith Architecture, you might want to use multiple containers, one per module. Ore provides minimum support for "module" in this case:
457+
458+
```go
459+
//broker module
460+
brokerContainer := ore.NewContainer()
461+
ore.RegisterLazyFuncToContainer(brokerContainer, ore.Singleton, func(ctx context.Context) (*Broker, context.Context) {
462+
brs, ctx = ore.GetFromContainer[*BrokerageSystem](brokerContainer, ctx)
463+
return &Broker{brs}, ctx
464+
})
465+
// brokerContainer.Build() //prevent further registration
466+
// brokerContainer.Validate() //check the dependency graph
467+
// brokerContainer.DisableValidation = true //disable check when resolve new object
468+
broker, _ := ore.GetFromContainer[*Broker](brokerContainer, context.Background())
469+
470+
//trader module
471+
traderContainer := ore.NewContainer()
472+
ore.RegisterLazyFuncToContainer(traderContainer, ore.Singleton, func(ctx context.Context) (*Trader, context.Context) {
473+
mkp, ctx = ore.GetFromContainer[*MarketPlace](traderContainer, ctx)
474+
return &Trader{mkp}, ctx
475+
})
476+
trader, _ := ore.GetFromContainer[*Trader](traderContainer, context.Background())
477+
```
478+
479+
Important: You will have to prevent cross modules access to the containers by yourself. For eg, don't let your "Broker
480+
module" to have access to the `traderContainer` of the "Trader module".
481+
482+
<br />
483+
484+
### Injecting value at Runtime
485+
486+
A common scenario is that your "Service" depends on something which you couldn't provide on registration time. You can provide this dependency only when certain requests or events arrive later. Ore allows you to build an "incomplete" dependency graph using the "place holder".
487+
488+
```go
489+
//register SomeService which depends on "someConfig"
490+
ore.RegisterLazyFunc[*SomeService](ore.Scoped, func(ctx context.Context) (*SomeService, context.Context) {
491+
someConfig, ctx := ore.Get[string](ctx, "someConfig")
492+
return &SomeService{someConfig}, ctx
493+
})
494+
495+
//someConfig is unknow at registration time because
496+
//this value depends on the future user's request
497+
ore.RegisterPlaceHolder[string]("someConfig")
498+
499+
//a new request arrive
500+
ctx := context.Background()
501+
//suppose that the request is sent by "admin"
502+
ctx = context.WithValue(ctx, "role", "admin")
503+
504+
//inject a different somConfig value depending on the request's content
505+
userRole := ctx.Value("role").(string)
506+
if userRole == "admin" {
507+
ctx = ore.ProvideScopedValue(ctx, "Admin config", "someConfig")
508+
} else if userRole == "supervisor" {
509+
ctx = ore.ProvideScopedValue(ctx, "Supervisor config", "someConfig")
510+
} else if userRole == "user" {
511+
if (isAuthenticatedUser) {
512+
ctx = ore.ProvideScopedValue(ctx, "Public user config", "someConfig")
513+
} else {
514+
ctx = ore.ProvideScopedValue(ctx, "Private user config", "someConfig")
515+
}
516+
}
517+
518+
//Get the service to handle this request
519+
service, ctx := ore.Get[*SomeService](ctx)
520+
fmt.Println(service.someConfig) //"Admin config"
521+
```
522+
523+
([See full codes here](./examples/placeholderdemo/main.go))
524+
525+
- `ore.RegisterPlaceHolder[T](key...)` registers a future value with Scoped lifetime.
526+
- This value will be injected in runtime using the `ProvideScopedValue` function.
527+
- Resolving objects which depend on this value will panic if the value has not been provided.
528+
529+
- `ore.ProvideScopedValue[T](context, value T, key...)` injects a concrete value into the given context
530+
- `ore` can access (`Get()` or `GetList()`) to this value only if the corresponding place holder (which matches the type and keys) is registered.
531+
532+
- A value provided to a place holder would never replace value returned by other resolvers. It's the opposite, if a type (and key) could be resolved by a real resolver (such as `RegisterLazyFunc`, `RegisterLazyCreator`...), then the later would take precedent.
533+
534+
<br/>
535+
442536
## More Complex Example
443537

444538
```go
@@ -488,7 +582,7 @@ func main() {
488582

489583
<br />
490584

491-
# Benchmarks
585+
## Benchmarks
492586

493587
```bash
494588
goos: windows
@@ -510,16 +604,16 @@ Checkout also [examples/benchperf/README.md](examples/benchperf/README.md)
510604

511605
<br />
512606

513-
# 👤 Contributors
607+
## 👤 Contributors
514608

515609
![Contributors](https://contrib.rocks/image?repo=firasdarwish/ore)
516610

517611

518-
# Contributing
612+
## Contributing
519613

520614
Feel free to contribute by opening issues, suggesting features, or submitting pull requests. We welcome your feedback
521615
and contributions.
522616

523-
# License
617+
## License
524618

525619
This project is licensed under the MIT License - see the LICENSE file for details.

alias_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,14 @@ func TestInvalidAlias(t *testing.T) {
135135

136136
func TestGetGenericAlias(t *testing.T) {
137137
for _, registrationType := range types {
138-
clearAll()
138+
container := NewContainer()
139139

140-
RegisterLazyFunc(registrationType, func(ctx context.Context) (*simpleCounterUint, context.Context) {
140+
RegisterLazyFuncToContainer(container, registrationType, func(ctx context.Context) (*simpleCounterUint, context.Context) {
141141
return &simpleCounterUint{}, ctx
142142
})
143-
RegisterAlias[someCounterGeneric[uint], *simpleCounterUint]()
143+
RegisterAliasToContainer[someCounterGeneric[uint], *simpleCounterUint](container)
144144

145-
c, _ := Get[someCounterGeneric[uint]](context.Background())
145+
c, _ := GetFromContainer[someCounterGeneric[uint]](container, context.Background())
146146

147147
c.Add(1)
148148
c.Add(1)
@@ -153,17 +153,17 @@ func TestGetGenericAlias(t *testing.T) {
153153

154154
func TestGetListGenericAlias(t *testing.T) {
155155
for _, registrationType := range types {
156-
clearAll()
156+
container := NewContainer()
157157

158158
for i := 0; i < 3; i++ {
159-
RegisterLazyFunc(registrationType, func(ctx context.Context) (*simpleCounterUint, context.Context) {
159+
RegisterLazyFuncToContainer(container, registrationType, func(ctx context.Context) (*simpleCounterUint, context.Context) {
160160
return &simpleCounterUint{}, ctx
161161
})
162162
}
163163

164-
RegisterAlias[someCounterGeneric[uint], *simpleCounterUint]()
164+
RegisterAliasToContainer[someCounterGeneric[uint], *simpleCounterUint](container)
165165

166-
counters, _ := GetList[someCounterGeneric[uint]](context.Background())
166+
counters, _ := GetListFromContainer[someCounterGeneric[uint]](container, context.Background())
167167
assert.Equal(t, len(counters), 3)
168168

169169
c := counters[1]

container.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package ore
2+
3+
import (
4+
"context"
5+
"sync"
6+
"sync/atomic"
7+
)
8+
9+
type Container struct {
10+
//DisableValidation is false by default, Set to true to skip validation.
11+
// Use case: you called the [Validate] function (either in the test pipeline or on application startup).
12+
// So you are confident that your registrations are good:
13+
//
14+
// - no missing dependencies
15+
// - no circular dependencies
16+
// - no lifetime misalignment (a longer lifetime service depends on a shorter one).
17+
//
18+
// You don't need Ore to validate over and over again each time it creates a new concrete.
19+
// It's a waste of resource especially when you will need Ore to create milion of transient concretes
20+
// and any "pico" seconds or memory allocation matter for you.
21+
//
22+
// In this case, you can set DisableValidation = true.
23+
//
24+
// This config would impact also the the [GetResolvedSingletons] and the [GetResolvedScopedInstances] functions,
25+
// the returning order would be no longer guaranteed.
26+
DisableValidation bool
27+
containerID int32
28+
lock *sync.RWMutex
29+
isBuilt bool
30+
resolvers map[typeID][]serviceResolver
31+
32+
//isSealed will be set to `true` when `Validate()` is called AFTER `Build()` is called
33+
//it prevents any further validations thus enhancing performance
34+
isSealed bool
35+
36+
//map interface type to the implementations type
37+
aliases map[pointerTypeName][]pointerTypeName
38+
}
39+
40+
var lastContainerID atomic.Int32
41+
42+
func NewContainer() *Container {
43+
return &Container{
44+
containerID: lastContainerID.Add(1),
45+
lock: &sync.RWMutex{},
46+
isBuilt: false,
47+
resolvers: map[typeID][]serviceResolver{},
48+
aliases: map[pointerTypeName][]pointerTypeName{},
49+
}
50+
}
51+
52+
// Validate invokes all registered resolvers. It panics if any of them fails.
53+
// It is recommended to call this function on application start, or in the CI/CD test pipeline
54+
// The objectif is to panic early when the container is bad configured. For eg:
55+
//
56+
// - (1) Missing depedency (forget to register certain resolvers)
57+
// - (2) cyclic dependency
58+
// - (3) lifetime misalignment (a longer lifetime service depends on a shorter one).
59+
func (this *Container) Validate() {
60+
if this.DisableValidation {
61+
panic("Validation is disabled")
62+
}
63+
ctx := context.Background()
64+
65+
//provide default value for all placeHolders
66+
for _, resolvers := range this.resolvers {
67+
for _, resolver := range resolvers {
68+
if resolver.isPlaceHolder() {
69+
ctx = resolver.providePlaceHolderDefaultValue(this, ctx)
70+
}
71+
}
72+
}
73+
74+
//invoke all resolver to detect potential registration problem
75+
for _, resolvers := range this.resolvers {
76+
for _, resolver := range resolvers {
77+
_, ctx = resolver.resolveService(this, ctx)
78+
}
79+
}
80+
81+
this.lock.Lock()
82+
defer this.lock.Unlock()
83+
if this.isBuilt && this.isSealed == false {
84+
this.isSealed = true
85+
}
86+
}
87+
88+
func (this *Container) Build() {
89+
this.lock.Lock()
90+
defer this.lock.Unlock()
91+
if this.isBuilt {
92+
panic(alreadyBuilt)
93+
}
94+
95+
this.isBuilt = true
96+
}
97+
98+
func (this *Container) IsBuilt() bool {
99+
return this.isBuilt
100+
}

creator_test.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package ore
33
import (
44
"context"
55
"testing"
6+
7+
"github.com/stretchr/testify/assert"
68
)
79

810
func TestRegisterLazyCreator(t *testing.T) {
@@ -24,20 +26,23 @@ func TestRegisterLazyCreator(t *testing.T) {
2426

2527
func TestRegisterLazyCreatorNilFuncTransient(t *testing.T) {
2628
clearAll()
27-
defer mustHavePanicked(t)
28-
RegisterLazyCreator[someCounter](Transient, nil)
29+
assert.Panics(t, func() {
30+
RegisterLazyCreator[someCounter](Transient, nil)
31+
})
2932
}
3033

3134
func TestRegisterLazyCreatorNilFuncScoped(t *testing.T) {
3235
clearAll()
33-
defer mustHavePanicked(t)
34-
RegisterLazyCreator[someCounter](Scoped, nil)
36+
assert.Panics(t, func() {
37+
RegisterLazyCreator[someCounter](Scoped, nil)
38+
})
3539
}
3640

3741
func TestRegisterLazyCreatorNilFuncSingleton(t *testing.T) {
3842
clearAll()
39-
defer mustHavePanicked(t)
40-
RegisterLazyCreator[someCounter](Singleton, nil)
43+
assert.Panics(t, func() {
44+
RegisterLazyCreator[someCounter](Singleton, nil)
45+
})
4146
}
4247

4348
func TestRegisterLazyCreatorMultipleImplementations(t *testing.T) {
@@ -154,16 +159,17 @@ func TestRegisterLazyCreatorTransientState(t *testing.T) {
154159

155160
func TestRegisterLazyCreatorNilKeyOnRegistering(t *testing.T) {
156161
clearAll()
157-
defer mustHavePanicked(t)
158-
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{}, nil)
162+
assert.Panics(t, func() {
163+
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{}, "", nil)
164+
})
159165
}
160166

161167
func TestRegisterLazyCreatorNilKeyOnGetting(t *testing.T) {
162168
clearAll()
163-
defer mustHavePanicked(t)
164169
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{}, "firas")
165-
166-
Get[someCounter](context.Background(), nil)
170+
assert.Panics(t, func() {
171+
Get[someCounter](context.Background(), nil)
172+
})
167173
}
168174

169175
func TestRegisterLazyCreatorGeneric(t *testing.T) {

0 commit comments

Comments
 (0)