Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

R/0.5.1 #13

Merged
merged 13 commits into from
Nov 13, 2024
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go?tab=readme-ov-file#dependency-injection)
[![Maintainability](https://api.codeclimate.com/v1/badges/3bd6f2fa4390af7c8faa/maintainability)](https://codeclimate.com/github/firasdarwish/ore/maintainability)
[![codecov](https://codecov.io/gh/firasdarwish/ore/graph/badge.svg?token=ISZVCCYGCR)](https://codecov.io/gh/firasdarwish/ore)

[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ffirasdarwish%2Fore.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Ffirasdarwish%2Fore?ref=badge_shield&issueType=license)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ffirasdarwish%2Fore.svg?type=shield&issueType=security)](https://app.fossa.com/projects/git%2Bgithub.com%2Ffirasdarwish%2Fore?ref=badge_shield&issueType=security)
![ore](https://github.com/firasdarwish/ore/assets/1930361/c1426ba1-777a-43f5-8a9a-7520caa45516)


Expand Down Expand Up @@ -326,7 +327,7 @@ Alias is also scoped by key. When you "Get" an alias with keys for eg: `ore.Get[

### Registration Validation

Once you're done with registering all the services, it is recommended to call `ore.Build()` AND `ore.Validate()`.
Once you're done with registering all the services, it is recommended to call `ore.Seal()`, then `ore.Validate()`, then finally `ore.DisableValidation=true`.

`ore.Validate()` invokes ALL your registered resolvers. The purpose is to panic early if your registrations were in bad shape:

Expand All @@ -345,9 +346,9 @@ Once you're done with registering all the services, it is recommended to call `o

Option 1 (run `ore.Validate` on test) is usually a better choice.

(2) It is recommended to build your container `ore.Build()` (which seals the container) on application start => Please don't call `ore.RegisterXX` all over the place.
(2) It is recommended to seal your container `ore.Seal()` (which seals the container) on application start => Please don't call `ore.RegisterXX` all over the place.

(3) A combination of `ore.Buile()` and then `ore.Validate()` ensures no more new resolvers will be registered AND all registered resolvers are validated, this will automatically
(3) A combination of `ore.Buile()` and then `ore.Validate()` and then `ore.DisabledValidation=true` ensures no more new resolvers will be registered AND all registered resolvers are validated, this will
prevent any further validation each time a resolver is invoked (`ore.Get`) which greatly enhances performance.

(4) Keep the object creation function (a.k.a resolvers) simple. Their only responsibility should be **object creation**.
Expand Down Expand Up @@ -462,7 +463,7 @@ ore.RegisterLazyFuncToContainer(brokerContainer, ore.Singleton, func(ctx context
brs, ctx = ore.GetFromContainer[*BrokerageSystem](brokerContainer, ctx)
return &Broker{brs}, ctx
})
// brokerContainer.Build() //prevent further registration
// brokerContainer.Seal() //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())
Expand All @@ -483,7 +484,7 @@ module" to have access to the `traderContainer` of the "Trader module".

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

```go
//register SomeService which depends on "someConfig"
Expand Down Expand Up @@ -527,9 +528,9 @@ fmt.Println(service.someConfig) //"Admin config"
- 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.
- `ore` can access (`Get()` or `GetList()`) to this value only if the corresponding placeholder (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.
- A value provided to a placeholder 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/>

Expand Down
8 changes: 4 additions & 4 deletions benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func BenchmarkInitialGet(b *testing.B) {

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

Build()
Seal()
Validate()

ctx := context.Background()
Expand All @@ -66,7 +66,7 @@ func BenchmarkGet(b *testing.B) {
RegisterEagerSingleton[someCounter](&simpleCounter{})

RegisterLazyCreator[someCounter](Scoped, &simpleCounter{})
Build()
Seal()
Validate()
ctx := context.Background()

Expand All @@ -86,7 +86,7 @@ func BenchmarkInitialGetList(b *testing.B) {
RegisterEagerSingleton[someCounter](&simpleCounter{})

RegisterLazyCreator[someCounter](Scoped, &simpleCounter{})
Build()
Seal()
Validate()

ctx := context.Background()
Expand All @@ -107,7 +107,7 @@ func BenchmarkGetList(b *testing.B) {
RegisterEagerSingleton[someCounter](&simpleCounter{})

RegisterLazyCreator[someCounter](Scoped, &simpleCounter{})
Build()
Seal()
Validate()
ctx := context.Background()

Expand Down
61 changes: 40 additions & 21 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,23 @@
// - 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
// It's a waste of resource especially when you will need Ore to create a million 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,
// This config would impact also the [GetResolvedSingletons] and the [GetResolvedScopedInstances] functions,
// the returning order would be no longer guaranteed.
DisableValidation bool
containerID int32
isSealed bool
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

name string
}

var lastContainerID atomic.Int32
Expand All @@ -43,17 +41,42 @@
return &Container{
containerID: lastContainerID.Add(1),
lock: &sync.RWMutex{},
isBuilt: false,
isSealed: false,
resolvers: map[typeID][]serviceResolver{},
aliases: map[pointerTypeName][]pointerTypeName{},
}
}

func (c *Container) ContainerID() int32 {
return c.containerID
}

func (c *Container) Name() string {
return c.name
}

func (this *Container) SetName(name string) *Container {
if name == "" {
panic("container name can not be empty")

Check warning on line 60 in container.go

View check run for this annotation

Codecov / codecov/patch

container.go#L60

Added line #L60 was not covered by tests
}

if this.name == name {
return this
}

Check warning on line 65 in container.go

View check run for this annotation

Codecov / codecov/patch

container.go#L64-L65

Added lines #L64 - L65 were not covered by tests

if this.name != "" {
panic("container name already set")
}

this.name = name
return this
}

// 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:
// The objective is to panic early when the container is bad configured. For eg:
//
// - (1) Missing depedency (forget to register certain resolvers)
// - (1) Missing dependency (forget to register certain resolvers)
// - (2) cyclic dependency
// - (3) lifetime misalignment (a longer lifetime service depends on a shorter one).
func (this *Container) Validate() {
Expand All @@ -77,24 +100,20 @@
_, 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() {
// Seal puts the container into read-only mode, preventing any further registrations.
func (this *Container) Seal() {
this.lock.Lock()
defer this.lock.Unlock()
if this.isBuilt {
if this.isSealed {
panic(alreadyBuilt)
}

this.isBuilt = true
this.isSealed = true
}

func (this *Container) IsBuilt() bool {
return this.isBuilt
// IsSealed checks whether the container is sealed (in readonly mode)
func (this *Container) IsSealed() bool {
return this.isSealed
}
8 changes: 6 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ func noValidImplementation[T any]() error {
return fmt.Errorf("implementation not found for type: %s", reflect.TypeFor[T]())
}

func invalidKeyType(t reflect.Type) error {
return fmt.Errorf("cannot use type: `%s` as a key", t)
}

func nilVal[T any]() error {
return fmt.Errorf("nil implementation for type: %s", reflect.TypeFor[T]())
}
Expand All @@ -23,11 +27,11 @@ func cyclicDependency(resolver resolverMetadata) error {
}

func placeHolderValueNotProvided(resolver resolverMetadata) error {
return fmt.Errorf("No value has been provided for this place holder: %s", resolver)
return fmt.Errorf("No value has been provided for this placeholder: %s", resolver)
}

func typeAlreadyRegistered(typeID typeID) error {
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)
return fmt.Errorf("The type '%s' has already been registered (as a Resolver or as a Placeholder). Cannot override it with other Placeholder", typeID)
}

var alreadyBuilt = errors.New("services container is already built")
Expand Down
2 changes: 1 addition & 1 deletion examples/placeholderdemo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func main() {
ore.RegisterPlaceHolder[string]("someConfig")

//Seal registration, no further registration is allowed
ore.Build()
ore.Seal()
ore.Validate()

//a request arrive
Expand Down
8 changes: 0 additions & 8 deletions get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,6 @@ func TestGetKeyed(t *testing.T) {
}
}

func TestGetKeyedUnhashable(t *testing.T) {
RegisterLazyCreator(Singleton, &simpleCounter{}, "a")
_, _ = Get[someCounter](context.Background(), "a")

RegisterLazyCreator(Singleton, &simpleCounter{}, []string{"a", "b"})
_, _ = Get[someCounter](context.Background(), []string{"a", "b"})
}

func TestGetResolvedSingletons(t *testing.T) {
t.Run("When multiple lifetimes and keys are registered", func(t *testing.T) {
//Arrange
Expand Down
54 changes: 22 additions & 32 deletions key.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
package ore

import (
"fmt"
"reflect"
"strconv"
"strings"
)

type KeyStringer any

type stringer interface {
String() string
}

func oreKey(key ...KeyStringer) string {
if key == nil {
return ""
Expand All @@ -20,59 +16,53 @@
l := len(key)

if l == 1 {
return stringifyOreKey(key[0])
keyT, kV := stringifyOreKey(key[0])
return keyT + kV
}

var sb strings.Builder

for _, s := range key {
sb.WriteString(stringifyOreKey(s))
keyT, keyV := stringifyOreKey(s)
sb.WriteString(keyT)
sb.WriteString(keyV)
}

return sb.String()
}

func stringifyOreKey(key KeyStringer) string {
func stringifyOreKey(key KeyStringer) (string, string) {
switch key.(type) {
case nil:
return ""
return "n", ""
case string:
return key.(string)
case bool:
return strconv.FormatBool(key.(bool))
return "s", key.(string)
case int:
return strconv.Itoa(key.(int))
return "i", strconv.Itoa(key.(int))
case int8:
return strconv.FormatInt(int64(key.(int8)), 36)
return "i8", strconv.FormatInt(int64(key.(int8)), 36)

Check warning on line 43 in key.go

View check run for this annotation

Codecov / codecov/patch

key.go#L43

Added line #L43 was not covered by tests
case int16:
return strconv.FormatInt(int64(key.(int16)), 36)
return "i16", strconv.FormatInt(int64(key.(int16)), 36)

Check warning on line 45 in key.go

View check run for this annotation

Codecov / codecov/patch

key.go#L45

Added line #L45 was not covered by tests
case int32:
return strconv.FormatInt(int64(key.(int32)), 36)
return "i32", strconv.FormatInt(int64(key.(int32)), 36)

Check warning on line 47 in key.go

View check run for this annotation

Codecov / codecov/patch

key.go#L47

Added line #L47 was not covered by tests
case int64:
return strconv.FormatInt(key.(int64), 36)
return "i64", strconv.FormatInt(key.(int64), 36)

Check warning on line 49 in key.go

View check run for this annotation

Codecov / codecov/patch

key.go#L49

Added line #L49 was not covered by tests

case uint:
return strconv.FormatUint(uint64(key.(uint)), 36)
return "ui", strconv.FormatUint(uint64(key.(uint)), 36)
case uint8:
return strconv.FormatUint(uint64(key.(uint8)), 36)
return "ui8", strconv.FormatUint(uint64(key.(uint8)), 36)

Check warning on line 54 in key.go

View check run for this annotation

Codecov / codecov/patch

key.go#L54

Added line #L54 was not covered by tests
case uint16:
return strconv.FormatUint(uint64(key.(uint16)), 36)
return "ui16", strconv.FormatUint(uint64(key.(uint16)), 36)

Check warning on line 56 in key.go

View check run for this annotation

Codecov / codecov/patch

key.go#L56

Added line #L56 was not covered by tests
case uint32:
return strconv.FormatUint(uint64(key.(uint32)), 36)
return "ui32", strconv.FormatUint(uint64(key.(uint32)), 36)

Check warning on line 58 in key.go

View check run for this annotation

Codecov / codecov/patch

key.go#L58

Added line #L58 was not covered by tests
case uint64:
return strconv.FormatUint(key.(uint64), 36)
return "ui64", strconv.FormatUint(key.(uint64), 36)

Check warning on line 60 in key.go

View check run for this annotation

Codecov / codecov/patch

key.go#L60

Added line #L60 was not covered by tests
case float32:
return strconv.FormatFloat(float64(key.(float32)), 'x', -1, 32)
return "f32", strconv.FormatFloat(float64(key.(float32)), 'x', -1, 32)
case float64:
return strconv.FormatFloat(key.(float64), 'x', -1, 64)
case stringer:
return key.(stringer).String()

return "f64", strconv.FormatFloat(key.(float64), 'x', -1, 64)
default:
return stringifyOreKeyUnknown(key)
panic(invalidKeyType(reflect.TypeOf(key)))
}
}

func stringifyOreKeyUnknown(key KeyStringer) string {
return fmt.Sprintf("%v", key)
}
Loading
Loading