Skip to content

Commit

Permalink
R/0.5.1 (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
firasdarwish authored Nov 13, 2024
2 parents aeb9fe1 + 823283e commit ee9933b
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 129 deletions.
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 @@ type Container struct {
// - 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 @@ func NewContainer() *Container {
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")
}

if this.name == name {
return this
}

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 @@ func (this *Container) Validate() {
_, 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 @@ func oreKey(key ...KeyStringer) string {
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)
case int16:
return strconv.FormatInt(int64(key.(int16)), 36)
return "i16", strconv.FormatInt(int64(key.(int16)), 36)
case int32:
return strconv.FormatInt(int64(key.(int32)), 36)
return "i32", strconv.FormatInt(int64(key.(int32)), 36)
case int64:
return strconv.FormatInt(key.(int64), 36)
return "i64", strconv.FormatInt(key.(int64), 36)

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)
case uint16:
return strconv.FormatUint(uint64(key.(uint16)), 36)
return "ui16", strconv.FormatUint(uint64(key.(uint16)), 36)
case uint32:
return strconv.FormatUint(uint64(key.(uint32)), 36)
return "ui32", strconv.FormatUint(uint64(key.(uint32)), 36)
case uint64:
return strconv.FormatUint(key.(uint64), 36)
return "ui64", strconv.FormatUint(key.(uint64), 36)
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

0 comments on commit ee9933b

Please sign in to comment.