Skip to content

Commit 0d481d2

Browse files
committed
add place holder feature
- fix validate lifetime bug which cause flaky tests - fix "simple" examples
1 parent 042fb72 commit 0d481d2

File tree

14 files changed

+522
-106
lines changed

14 files changed

+522
-106
lines changed

README.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,8 @@ The `ore.GetResolvedScopedInstances[TInterface](context)` function returns a lis
450450
| RegisterEagerSingleton | RegisterEagerSingletonToContainer |
451451
| RegisterLazyCreator | RegisterLazyCreatorToContainer |
452452
| RegisterLazyFunc | RegisterLazyFuncToContainer |
453+
| RegisterPlaceHolder | RegisterPlaceHolderToContainer |
454+
| ProvideScopedValue | ProvideScopedValueToContainer |
453455

454456
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:
455457

@@ -475,10 +477,62 @@ trader, _ := ore.GetFromContainer[*Trader](traderContainer, context.Background()
475477
```
476478

477479
Important: You will have to prevent cross modules access to the containers by yourself. For eg, don't let your "Broker
478-
module" to have access to the `tranderContainer` of the "Trader module".
480+
module" to have access to the `traderContainer` of the "Trader module".
479481

480482
<br />
481483

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+
482536
## More Complex Example
483537

484538
```go

container.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ func (this *Container) Validate() {
6161
panic("Validation is disabled")
6262
}
6363
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
6475
for _, resolvers := range this.resolvers {
6576
for _, resolver := range resolvers {
6677
_, ctx = resolver.resolveService(this, ctx)

errors.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ func cyclicDependency(resolver resolverMetadata) error {
2222
return fmt.Errorf("detect cyclic dependency where: %s depends on itself", resolver)
2323
}
2424

25+
func placeHolderValueNotProvided(resolver resolverMetadata) error {
26+
return fmt.Errorf("No value has been provided for this place holder: %s", resolver)
27+
}
28+
29+
func typeAlreadyRegistered(typeID typeID) error {
30+
return fmt.Errorf("The type '%s' has already been registered (as a Resolver or as a Place Holder). Cannot override it with other Place Holder", typeID)
31+
}
32+
2533
var alreadyBuilt = errors.New("services container is already built")
2634
var alreadyBuiltCannotAdd = errors.New("cannot appendToContainer, services container is already built")
2735
var nilKey = errors.New("cannot have nil keys")

examples/placeholderdemo/main.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/firasdarwish/ore"
8+
)
9+
10+
type UserRole struct {
11+
}
12+
type SomeService struct {
13+
someConfig string
14+
}
15+
16+
func main() {
17+
//register SomeService which depends on "someConfig"
18+
ore.RegisterLazyFunc[*SomeService](ore.Scoped, func(ctx context.Context) (*SomeService, context.Context) {
19+
someConfig, ctx := ore.Get[string](ctx, "someConfig")
20+
return &SomeService{someConfig}, ctx
21+
})
22+
23+
//someConfig is unknow at registration time
24+
//the value of "someConfig" depends on the future user's request
25+
ore.RegisterPlaceHolder[string]("someConfig")
26+
27+
//Seal registration, no further registration is allowed
28+
ore.Build()
29+
ore.Validate()
30+
31+
//a request arrive
32+
ctx := context.Background()
33+
//suppose that the request is sent by "admin"
34+
ctx = context.WithValue(ctx, "role", "admin")
35+
36+
//inject a different config depends on the request,
37+
userRole := ctx.Value("role").(string)
38+
if userRole == "admin" {
39+
ctx = ore.ProvideScopedValue(ctx, "Admin config", "someConfig")
40+
} else if userRole == "supervisor" {
41+
ctx = ore.ProvideScopedValue(ctx, "Supervisor config", "someConfig")
42+
} else if userRole == "user" {
43+
ctx = ore.ProvideScopedValue(ctx, "Public user config", "someConfig")
44+
}
45+
46+
service, _ := ore.Get[*SomeService](ctx)
47+
fmt.Println(service.someConfig) //"Admin config"
48+
}

examples/simple/main.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,19 @@ import (
88
)
99

1010
func main() {
11-
1211
ore.RegisterLazyFunc[Counter](ore.Singleton, func(ctx context.Context) (Counter, context.Context) {
1312
fmt.Println("NEWLY INITIALIZED FROM FUNC")
14-
return &counter{}, ctx
13+
return &mycounter{}, ctx
1514
}, "firas")
1615

1716
ore.RegisterLazyFunc[Counter](ore.Singleton, func(ctx context.Context) (Counter, context.Context) {
1817
fmt.Println("NEWLY INITIALIZED FROM FUNC")
19-
return &counter{}, ctx
18+
return &mycounter{}, ctx
2019
}, "darwish")
2120

22-
ore.RegisterLazyCreator[Counter](ore.Singleton, &counter{})
21+
ore.RegisterLazyCreator[Counter](ore.Singleton, &mycounter{})
2322

24-
var cc Counter
25-
cc = &counter{}
23+
cc := &mycounter{}
2624
ore.RegisterEagerSingleton[Counter](cc)
2725

2826
ctx := context.Background()
@@ -47,7 +45,7 @@ func main() {
4745
gc.Add(1)
4846
gc.Add(1)
4947

50-
gc, ctx = ore.Get[GenericCounter[uint]](ctx)
48+
gc, _ = ore.Get[GenericCounter[uint]](ctx)
5149
gc.Add(1)
5250

5351
fmt.Println(gc.Total())
@@ -62,3 +60,42 @@ type GenericCounter[T numeric] interface {
6260
Add(num T)
6361
Total() T
6462
}
63+
64+
type mycounter struct {
65+
count int
66+
}
67+
68+
var _ Counter = (*mycounter)(nil)
69+
70+
func (c *mycounter) AddOne() {
71+
c.count++
72+
}
73+
74+
func (c *mycounter) Total() int {
75+
return c.count
76+
}
77+
78+
func (*mycounter) New(ctx context.Context) (Counter, context.Context) {
79+
fmt.Println("NEWLY INITIALIZED")
80+
return &mycounter{}, ctx
81+
}
82+
83+
type numeric interface {
84+
uint
85+
}
86+
87+
type genCounter[T numeric] struct {
88+
count T
89+
}
90+
91+
func (t *genCounter[T]) Add(num T) {
92+
t.count += num
93+
}
94+
95+
func (t *genCounter[T]) Total() T {
96+
return t.count
97+
}
98+
99+
func (*genCounter[T]) New(ctx context.Context) (GenericCounter[T], context.Context) {
100+
return &genCounter[T]{}, ctx
101+
}

examples/simple/service.go

Lines changed: 0 additions & 43 deletions
This file was deleted.

getters.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ func GetListFromContainer[T any](con *Container, ctx context.Context, key ...Key
9797

9898
for index := 0; index < len(resolvers); index++ {
9999
resolver := resolvers[index]
100+
if resolver.isPlaceHolder() && !resolver.isScopedValueResolved(ctx) {
101+
//the resolver is a placeHolder and the placeHolder's value has not been provided
102+
//don't panic, just skip (don't add anything to the list)
103+
continue
104+
}
100105
con, newCtx := resolver.resolveService(con, ctx)
101106
servicesArray = append(servicesArray, con.value.(T))
102107
ctx = newCtx

internal/testtools/assert2/assertions.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package assert2
44
import (
55
"fmt"
66
"runtime/debug"
7+
"strings"
78

89
"github.com/stretchr/testify/assert"
910
)
@@ -37,19 +38,19 @@ func PanicsWithError(t assert.TestingT, errStringMatcher StringMatcher, f assert
3738

3839
func ErrorStartsWith(prefix string) StringMatcher {
3940
return func(s string) bool {
40-
return s != "" && s[:len(prefix)] == prefix
41+
return strings.HasPrefix(s, prefix)
4142
}
4243
}
4344

4445
func ErrorEndsWith(suffix string) StringMatcher {
4546
return func(s string) bool {
46-
return s != "" && s[len(s)-len(suffix):] == suffix
47+
return strings.HasSuffix(s, suffix)
4748
}
4849
}
4950

5051
func ErrorContains(substr string) StringMatcher {
5152
return func(s string) bool {
52-
return s != "" && s[:len(substr)] == substr
53+
return strings.Contains(s, substr)
5354
}
5455
}
5556

ore.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ var (
1111
contextKeysRepositoryID specialContextKey = "The context keys repository"
1212
//contextKeyResolversStack is a special context key. The value of this key is the [ResolversStack].
1313
contextKeyResolversStack specialContextKey = "Dependencies stack"
14+
15+
//placeHolderResolverID is a special resolverID of every "placeHolder". "placeHolder" is a special resolver
16+
//describing a "promise" for a concrete value, which will be provided in runtime.
17+
placeHolderResolverID = -1
1418
)
1519

1620
type contextKeysRepository = []contextKey
@@ -43,19 +47,28 @@ func addResolver[T any](this *Container, resolver serviceResolverImpl[T], key ..
4347
typeID := typeIdentifier[T](key...)
4448

4549
this.lock.Lock()
50+
defer this.lock.Unlock()
51+
52+
resolverID := len(this.resolvers[typeID])
53+
if resolver.isPlaceHolder() {
54+
if resolverID > 0 {
55+
panic(typeAlreadyRegistered(typeID))
56+
}
57+
resolverID = placeHolderResolverID
58+
}
59+
4660
resolver.id = contextKey{
4761
typeID: typeID,
4862
containerID: this.containerID,
49-
index: len(this.resolvers[typeID]),
63+
resolverID: resolverID,
5064
}
5165
this.resolvers[typeID] = append(this.resolvers[typeID], resolver)
52-
this.lock.Unlock()
5366
}
5467

5568
func replaceResolver[T any](this *Container, resolver serviceResolverImpl[T]) {
5669
this.lock.Lock()
57-
this.resolvers[resolver.id.typeID][resolver.id.index] = resolver
58-
this.lock.Unlock()
70+
defer this.lock.Unlock()
71+
this.resolvers[resolver.id.typeID][resolver.id.resolverID] = resolver
5972
}
6073

6174
func addAliases[TInterface, TImpl any](this *Container) {
@@ -65,13 +78,13 @@ func addAliases[TInterface, TImpl any](this *Container) {
6578
return
6679
}
6780
this.lock.Lock()
81+
defer this.lock.Unlock()
6882
for _, ot := range this.aliases[aliasType] {
6983
if ot == originalType {
7084
return //already registered
7185
}
7286
}
7387
this.aliases[aliasType] = append(this.aliases[aliasType], originalType)
74-
this.lock.Unlock()
7588
}
7689

7790
func Build() {

0 commit comments

Comments
 (0)