Skip to content

Commit

Permalink
Module feature and Place holder feature (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
firasdarwish authored Nov 12, 2024
2 parents 5185de5 + 0d481d2 commit aeb9fe1
Show file tree
Hide file tree
Showing 29 changed files with 947 additions and 443 deletions.
110 changes: 102 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ the management of object lifetimes and the inversion of control in your applicat

<br />

# Features
## Features

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

<br />

# Installation
## Installation

```bash
go get -u github.com/firasdarwish/ore
```

<br />

# Usage
## Usage

### Import

Expand Down Expand Up @@ -433,12 +433,106 @@ 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 lifetime.
- All the implementations including "keyed" one will be returned.
- All the implementations (of all modules) including "keyed" one will be returned.
- The returned instances are sorted by invocation order, the first one being the latest invoked one.
- 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".

<br />

### Multiple Containers (a.k.a Modules)

| DefaultContainer | Custom container |
|------------------|------------------|
| Get | GetFromContainer |
| GetList | GetListFromContainer |
| GetResolvedSingletons | GetResolvedSingletonsFromContainer |
| RegisterAlias | RegisterAliasToContainer |
| RegisterEagerSingleton | RegisterEagerSingletonToContainer |
| RegisterLazyCreator | RegisterLazyCreatorToContainer |
| RegisterLazyFunc | RegisterLazyFuncToContainer |
| RegisterPlaceHolder | RegisterPlaceHolderToContainer |
| ProvideScopedValue | ProvideScopedValueToContainer |

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:

```go
//broker module
brokerContainer := ore.NewContainer()
ore.RegisterLazyFuncToContainer(brokerContainer, ore.Singleton, func(ctx context.Context) (*Broker, context.Context) {
brs, ctx = ore.GetFromContainer[*BrokerageSystem](brokerContainer, ctx)
return &Broker{brs}, ctx
})
// brokerContainer.Build() //prevent further registration
// brokerContainer.Validate() //check the dependency graph
// brokerContainer.DisableValidation = true //disable check when resolve new object
broker, _ := ore.GetFromContainer[*Broker](brokerContainer, context.Background())

//trader module
traderContainer := ore.NewContainer()
ore.RegisterLazyFuncToContainer(traderContainer, ore.Singleton, func(ctx context.Context) (*Trader, context.Context) {
mkp, ctx = ore.GetFromContainer[*MarketPlace](traderContainer, ctx)
return &Trader{mkp}, ctx
})
trader, _ := ore.GetFromContainer[*Trader](traderContainer, context.Background())
```

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

<br />

### Injecting value at Runtime

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".

```go
//register SomeService which depends on "someConfig"
ore.RegisterLazyFunc[*SomeService](ore.Scoped, func(ctx context.Context) (*SomeService, context.Context) {
someConfig, ctx := ore.Get[string](ctx, "someConfig")
return &SomeService{someConfig}, ctx
})

//someConfig is unknow at registration time because
//this value depends on the future user's request
ore.RegisterPlaceHolder[string]("someConfig")

//a new request arrive
ctx := context.Background()
//suppose that the request is sent by "admin"
ctx = context.WithValue(ctx, "role", "admin")

//inject a different somConfig value depending on the request's content
userRole := ctx.Value("role").(string)
if userRole == "admin" {
ctx = ore.ProvideScopedValue(ctx, "Admin config", "someConfig")
} else if userRole == "supervisor" {
ctx = ore.ProvideScopedValue(ctx, "Supervisor config", "someConfig")
} else if userRole == "user" {
if (isAuthenticatedUser) {
ctx = ore.ProvideScopedValue(ctx, "Public user config", "someConfig")
} else {
ctx = ore.ProvideScopedValue(ctx, "Private user config", "someConfig")
}
}

//Get the service to handle this request
service, ctx := ore.Get[*SomeService](ctx)
fmt.Println(service.someConfig) //"Admin config"
```

([See full codes here](./examples/placeholderdemo/main.go))

- `ore.RegisterPlaceHolder[T](key...)` registers a future value with Scoped lifetime.
- This value will be injected in runtime using the `ProvideScopedValue` function.
- Resolving objects which depend on this value will panic if the value has not been provided.

- `ore.ProvideScopedValue[T](context, value T, key...)` injects a concrete value into the given context
- `ore` can access (`Get()` or `GetList()`) to this value only if the corresponding place holder (which matches the type and keys) is registered.

- 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.

<br/>

## More Complex Example

```go
Expand Down Expand Up @@ -488,7 +582,7 @@ func main() {

<br />

# Benchmarks
## Benchmarks

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

<br />

# 👤 Contributors
## 👤 Contributors

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


# Contributing
## Contributing

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

# License
## License

This project is licensed under the MIT License - see the LICENSE file for details.
16 changes: 8 additions & 8 deletions alias_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,14 @@ func TestInvalidAlias(t *testing.T) {

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

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

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

c.Add(1)
c.Add(1)
Expand All @@ -153,17 +153,17 @@ func TestGetGenericAlias(t *testing.T) {

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

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

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

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

c := counters[1]
Expand Down
100 changes: 100 additions & 0 deletions container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package ore

import (
"context"
"sync"
"sync/atomic"
)

type Container struct {
//DisableValidation is false by default, Set to true to skip validation.
// Use case: you called the [Validate] function (either in the test pipeline or on application startup).
// So you are confident that your registrations are good:
//
// - no missing dependencies
// - no circular dependencies
// - no lifetime misalignment (a longer lifetime service depends on a shorter one).
//
// You don't need Ore to validate over and over again each time it creates a new concrete.
// It's a waste of resource especially when you will need Ore to create milion of transient concretes
// and any "pico" seconds or memory allocation matter for you.
//
// In this case, you can set DisableValidation = true.
//
// This config would impact also the the [GetResolvedSingletons] and the [GetResolvedScopedInstances] functions,
// the returning order would be no longer guaranteed.
DisableValidation bool
containerID int32
lock *sync.RWMutex
isBuilt bool
resolvers map[typeID][]serviceResolver

//isSealed will be set to `true` when `Validate()` is called AFTER `Build()` is called
//it prevents any further validations thus enhancing performance
isSealed bool

//map interface type to the implementations type
aliases map[pointerTypeName][]pointerTypeName
}

var lastContainerID atomic.Int32

func NewContainer() *Container {
return &Container{
containerID: lastContainerID.Add(1),
lock: &sync.RWMutex{},
isBuilt: false,
resolvers: map[typeID][]serviceResolver{},
aliases: map[pointerTypeName][]pointerTypeName{},
}
}

// Validate invokes all registered resolvers. It panics if any of them fails.
// It is recommended to call this function on application start, or in the CI/CD test pipeline
// The objectif is to panic early when the container is bad configured. For eg:
//
// - (1) Missing depedency (forget to register certain resolvers)
// - (2) cyclic dependency
// - (3) lifetime misalignment (a longer lifetime service depends on a shorter one).
func (this *Container) Validate() {
if this.DisableValidation {
panic("Validation is disabled")
}
ctx := context.Background()

//provide default value for all placeHolders
for _, resolvers := range this.resolvers {
for _, resolver := range resolvers {
if resolver.isPlaceHolder() {
ctx = resolver.providePlaceHolderDefaultValue(this, ctx)
}
}
}

//invoke all resolver to detect potential registration problem
for _, resolvers := range this.resolvers {
for _, resolver := range resolvers {
_, ctx = resolver.resolveService(this, ctx)
}
}

this.lock.Lock()
defer this.lock.Unlock()
if this.isBuilt && this.isSealed == false {
this.isSealed = true
}
}

func (this *Container) Build() {
this.lock.Lock()
defer this.lock.Unlock()
if this.isBuilt {
panic(alreadyBuilt)
}

this.isBuilt = true
}

func (this *Container) IsBuilt() bool {
return this.isBuilt
}
28 changes: 17 additions & 11 deletions creator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package ore
import (
"context"
"testing"

"github.com/stretchr/testify/assert"
)

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

func TestRegisterLazyCreatorNilFuncTransient(t *testing.T) {
clearAll()
defer mustHavePanicked(t)
RegisterLazyCreator[someCounter](Transient, nil)
assert.Panics(t, func() {
RegisterLazyCreator[someCounter](Transient, nil)
})
}

func TestRegisterLazyCreatorNilFuncScoped(t *testing.T) {
clearAll()
defer mustHavePanicked(t)
RegisterLazyCreator[someCounter](Scoped, nil)
assert.Panics(t, func() {
RegisterLazyCreator[someCounter](Scoped, nil)
})
}

func TestRegisterLazyCreatorNilFuncSingleton(t *testing.T) {
clearAll()
defer mustHavePanicked(t)
RegisterLazyCreator[someCounter](Singleton, nil)
assert.Panics(t, func() {
RegisterLazyCreator[someCounter](Singleton, nil)
})
}

func TestRegisterLazyCreatorMultipleImplementations(t *testing.T) {
Expand Down Expand Up @@ -154,16 +159,17 @@ func TestRegisterLazyCreatorTransientState(t *testing.T) {

func TestRegisterLazyCreatorNilKeyOnRegistering(t *testing.T) {
clearAll()
defer mustHavePanicked(t)
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{}, nil)
assert.Panics(t, func() {
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{}, "", nil)
})
}

func TestRegisterLazyCreatorNilKeyOnGetting(t *testing.T) {
clearAll()
defer mustHavePanicked(t)
RegisterLazyCreator[someCounter](Scoped, &simpleCounter{}, "firas")

Get[someCounter](context.Background(), nil)
assert.Panics(t, func() {
Get[someCounter](context.Background(), nil)
})
}

func TestRegisterLazyCreatorGeneric(t *testing.T) {
Expand Down
Loading

0 comments on commit aeb9fe1

Please sign in to comment.