diff --git a/di/erasure/injector.go b/di/erasure/injector.go index d9d8069..cc1b809 100644 --- a/di/erasure/injector.go +++ b/di/erasure/injector.go @@ -27,7 +27,6 @@ import ( IOE "github.com/IBM/fp-go/ioeither" L "github.com/IBM/fp-go/lazy" O "github.com/IBM/fp-go/option" - RIOE "github.com/IBM/fp-go/readerioeither" R "github.com/IBM/fp-go/record" T "github.com/IBM/fp-go/tuple" @@ -38,12 +37,21 @@ func providerToEntry(p Provider) T.Tuple2[string, ProviderFactory] { return T.MakeTuple2(p.Provides().Id(), p.Factory()) } -var missingProviderError = F.Flow3( +func itemProviderToMap(p Provider) map[string][]ProviderFactory { + return R.Singleton(p.Provides().Id(), A.Of(p.Factory())) +} + +var missingProviderError = F.Flow4( + Dependency.String, errors.OnSome[string]("no provider for dependency [%s]"), - RIOE.Left[InjectableFactory, any, error], - F.Constant[ProviderFactory], + IOE.Left[any, error], + F.Constant1[InjectableFactory, IOE.IOEither[error, any]], ) +var emptyMulti any = A.Empty[any]() + +var emptyMultiDependency = F.Constant1[Dependency](F.Constant1[InjectableFactory](IOE.Of[error](emptyMulti))) + func logEntryExit(name string, token Dependency) func() { log.Printf("Entry: [%s] -> [%s]:[%s]", name, token.Id(), token.String()) return func() { @@ -51,6 +59,55 @@ func logEntryExit(name string, token Dependency) func() { } } +// isMultiDependency tests if a dependency is a container dependency +func isMultiDependency(dep Dependency) bool { + return dep.Type() == Multi +} + +var handleMissingProvider = F.Flow2( + F.Ternary(isMultiDependency, emptyMultiDependency, missingProviderError), + F.Constant[ProviderFactory], +) + +// isItemProvider tests if a provivder provides a single item +func isItemProvider(provider Provider) bool { + return provider.Provides().Type() == Item +} + +// itemProviderFactory combines multiple factories into one, returning an array +func itemProviderFactory(fcts []ProviderFactory) ProviderFactory { + return func(inj InjectableFactory) IOE.IOEither[error, any] { + return F.Pipe2( + fcts, + IOE.TraverseArray(I.Flap[IOE.IOEither[error, any]](inj)), + IOE.Map[error](F.ToAny[[]any]), + ) + } +} + +var mergeItemProviders = R.UnionMonoid[string](A.Semigroup[ProviderFactory]()) + +// collectItemProviders create a provider map for item providers +var collectItemProviders = F.Flow2( + A.FoldMap[Provider](mergeItemProviders)(itemProviderToMap), + R.Map[string](itemProviderFactory), +) + +// collectProviders collects non-item providers +var collectProviders = F.Flow2( + A.Map(providerToEntry), + R.FromEntries[string, ProviderFactory], +) + +var mergeProviders = R.UnionLastMonoid[string, ProviderFactory]() + +// assembleProviders constructs the provider map for item and non-item providers +var assembleProviders = F.Flow3( + A.Partition(isItemProvider), + T.Map2(collectProviders, collectItemProviders), + T.Tupled2(mergeProviders.Concat), +) + func MakeInjector(providers []Provider) InjectableFactory { type Result = IOE.IOEither[error, any] @@ -61,11 +118,7 @@ func MakeInjector(providers []Provider) InjectableFactory { var resolved sync.Map // provide a mapping for all providers - factoryById := F.Pipe2( - providers, - A.Map(providerToEntry), - R.FromEntries[string, ProviderFactory], - ) + factoryById := assembleProviders(providers) // the actual factory, we need lazy initialization var injFct InjectableFactory @@ -91,10 +144,7 @@ func MakeInjector(providers []Provider) InjectableFactory { Dependency.Id, R.Lookup[ProviderFactory, string], I.Ap[O.Option[ProviderFactory]](factoryById), - ), F.Flow2( - Dependency.String, - missingProviderError, - )), + ), handleMissingProvider), T.Tupled2(O.MonadGetOrElse[ProviderFactory]), IG.Ap[ProviderFactory](injFct), IOE.Memoize[error, any], diff --git a/di/erasure/provider.go b/di/erasure/provider.go index 3cc6956..f814d34 100644 --- a/di/erasure/provider.go +++ b/di/erasure/provider.go @@ -27,12 +27,12 @@ import ( IOO "github.com/IBM/fp-go/iooption" Int "github.com/IBM/fp-go/number/integer" O "github.com/IBM/fp-go/option" - RIOE "github.com/IBM/fp-go/readerioeither" R "github.com/IBM/fp-go/record" ) -type InjectableFactory = RIOE.ReaderIOEither[Dependency, error, any] -type ProviderFactory = RIOE.ReaderIOEither[InjectableFactory, error, any] +type InjectableFactory = func(Dependency) IOE.IOEither[error, any] +type ProviderFactory = func(InjectableFactory) IOE.IOEither[error, any] + type paramIndex = map[int]int type Provider interface { diff --git a/di/erasure/token.go b/di/erasure/token.go index de86d20..3782f5f 100644 --- a/di/erasure/token.go +++ b/di/erasure/token.go @@ -20,12 +20,17 @@ import "fmt" type TokenType int const ( - Identity TokenType = iota - Option - IOEither - IOOption + Identity TokenType = iota // required dependency + Option // optional dependency + IOEither // lazy and required + IOOption // lazy and optional + Multi // array of implementations + Item // item of a multi token + IOMulti // lazy and multi + Unknown ) +// Dependency describes the relationship to a service type Dependency interface { fmt.Stringer // Id returns a unique identifier for a token that can be used as a cache key diff --git a/di/injector.go b/di/injector.go index d65f8c5..a9ebdb3 100644 --- a/di/injector.go +++ b/di/injector.go @@ -28,6 +28,6 @@ import ( func Resolve[T any](token InjectionToken[T]) RIOE.ReaderIOEither[DIE.InjectableFactory, error, T] { return F.Flow2( IG.Ap[DIE.InjectableFactory](DIE.AsDependency(token)), - IOE.ChainEitherK(toType[T]()), + IOE.ChainEitherK(token.Unerase), ) } diff --git a/di/provider.go b/di/provider.go index bc889eb..8158311 100644 --- a/di/provider.go +++ b/di/provider.go @@ -116,3 +116,8 @@ func MakeProvider2[T1, T2, R any]( ), ) } + +// ConstProvider simple implementation for a provider with a constant value +func ConstProvider[R any](token InjectionToken[R], value R) DIE.Provider { + return MakeProvider0[R](token, F.Constant(IOE.Of[error](value))) +} diff --git a/di/provider_test.go b/di/provider_test.go index 016f889..15c47f2 100644 --- a/di/provider_test.go +++ b/di/provider_test.go @@ -231,3 +231,48 @@ func TestEagerAndLazyProvider(t *testing.T) { assert.Equal(t, 1, dynamicCount) assert.Equal(t, 1, lazyEagerCount) } + +func TestItemProvider(t *testing.T) { + // define a multi token + injMulti := MakeMultiToken[string]("configs") + + // provide some values + v1 := ConstProvider(injMulti.Item(), "Value1") + v2 := ConstProvider(injMulti.Item(), "Value2") + // mix in non-multi values + p1 := ConstProvider(INJ_KEY1, "Value3") + p2 := ConstProvider(INJ_KEY2, "Value4") + + // populate the injector + inj := DIE.MakeInjector(A.From(p1, v1, p2, v2)) + + // access the multi value + multi := Resolve(injMulti.Container()) + + multiInj := multi(inj) + + value := multiInj() + + assert.Equal(t, E.Of[error](A.From("Value1", "Value2")), value) +} + +func TestEmptyItemProvider(t *testing.T) { + // define a multi token + injMulti := MakeMultiToken[string]("configs") + + // mix in non-multi values + p1 := ConstProvider(INJ_KEY1, "Value3") + p2 := ConstProvider(INJ_KEY2, "Value4") + + // populate the injector + inj := DIE.MakeInjector(A.From(p1, p2)) + + // access the multi value + multi := Resolve(injMulti.Container()) + + multiInj := multi(inj) + + value := multiInj() + + assert.Equal(t, E.Of[error](A.Empty[string]()), value) +} diff --git a/di/token.go b/di/token.go index 96e44d2..0b371ae 100644 --- a/di/token.go +++ b/di/token.go @@ -55,6 +55,14 @@ type InjectionToken[T any] interface { IOOption() Dependency[IOO.IOOption[T]] } +// MultiInjectionToken uniquely identifies a dependency by giving it an Id, Type and name +type MultiInjectionToken[T any] interface { + // Container returns the injection token used to request an array of all provided items + Container() InjectionToken[[]T] + // Item returns the injection token used to provide an item + Item() InjectionToken[T] +} + // makeID creates a generator of unique string IDs func makeId() IO.IO[string] { var count atomic.Int64 @@ -100,6 +108,11 @@ type injectionToken[T any] struct { iooption Dependency[IOO.IOOption[T]] } +type multiInjectionToken[T any] struct { + container *injectionToken[[]T] + item *injectionToken[T] +} + func (i *injectionToken[T]) Identity() Dependency[T] { return i } @@ -116,13 +129,46 @@ func (i *injectionToken[T]) IOOption() Dependency[IOO.IOOption[T]] { return i.iooption } +func (m *multiInjectionToken[T]) Container() InjectionToken[[]T] { + return m.container +} + +func (m *multiInjectionToken[T]) Item() InjectionToken[T] { + return m.item +} + // MakeToken create a unique `InjectionToken` for a specific type func MakeToken[T any](name string) InjectionToken[T] { id := genId() + toIdentity := toType[T]() return &injectionToken[T]{ - token[T]{name, id, DIE.Identity, toType[T]()}, - makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", name), id, DIE.Option, toOptionType[T]()), - makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEither, toIOEitherType[T]()), - makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOption, toIOOptionType[T]()), + token[T]{name, id, DIE.Identity, toIdentity}, + makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", name), id, DIE.Option, toOptionType(toIdentity)), + makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEither, toIOEitherType(toIdentity)), + makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOption, toIOOptionType(toIdentity)), } } + +func MakeMultiToken[T any](name string) MultiInjectionToken[T] { + id := genId() + toItem := toType[T]() + toContainer := toArrayType(toItem) + containerName := fmt.Sprintf("Container[%s]", name) + itemName := fmt.Sprintf("Item[%s]", name) + // container + container := &injectionToken[[]T]{ + token[[]T]{containerName, id, DIE.Multi, toContainer}, + makeToken[O.Option[[]T]](fmt.Sprintf("Option[%s]", containerName), id, DIE.Unknown, toOptionType(toContainer)), + makeToken[IOE.IOEither[error, []T]](fmt.Sprintf("IOEither[%s]", containerName), id, DIE.IOMulti, toIOEitherType(toContainer)), + makeToken[IOO.IOOption[[]T]](fmt.Sprintf("IOOption[%s]", containerName), id, DIE.Unknown, toIOOptionType(toContainer)), + } + // item + item := &injectionToken[T]{ + token[T]{itemName, id, DIE.Item, toItem}, + makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", itemName), id, DIE.Unknown, toOptionType(toItem)), + makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", itemName), id, DIE.Unknown, toIOEitherType(toItem)), + makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", itemName), id, DIE.Unknown, toIOOptionType(toItem)), + } + // returns the token + return &multiInjectionToken[T]{container, item} +} diff --git a/di/utils.go b/di/utils.go index 47f88d8..e2f7ec3 100644 --- a/di/utils.go +++ b/di/utils.go @@ -23,19 +23,19 @@ import ( O "github.com/IBM/fp-go/option" ) -// toType converts and any to a T +// toType converts an any to a T func toType[T any]() func(t any) E.Either[error, T] { return E.ToType[T](errors.OnSome[any]("Value of type [%T] cannot be converted.")) } // toOptionType converts an any to an Option[any] and then to an Option[T] -func toOptionType[T any]() func(t any) E.Either[error, O.Option[T]] { +func toOptionType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, O.Option[T]] { return F.Flow2( toType[O.Option[any]](), E.Chain(O.Fold( F.Nullary2(O.None[T], E.Of[error, O.Option[T]]), F.Flow2( - toType[T](), + item, E.Map[error](O.Of[T]), ), )), @@ -43,20 +43,28 @@ func toOptionType[T any]() func(t any) E.Either[error, O.Option[T]] { } // toIOEitherType converts an any to an IOEither[error, any] and then to an IOEither[error, T] -func toIOEitherType[T any]() func(t any) E.Either[error, IOE.IOEither[error, T]] { +func toIOEitherType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, IOE.IOEither[error, T]] { return F.Flow2( toType[IOE.IOEither[error, any]](), - E.Map[error](IOE.ChainEitherK(toType[T]())), + E.Map[error](IOE.ChainEitherK(item)), ) } // toIOOptionType converts an any to an IOOption[any] and then to an IOOption[T] -func toIOOptionType[T any]() func(t any) E.Either[error, IOO.IOOption[T]] { +func toIOOptionType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, IOO.IOOption[T]] { return F.Flow2( toType[IOO.IOOption[any]](), E.Map[error](IOO.ChainOptionK(F.Flow2( - toType[T](), + item, E.ToOption[error, T], ))), ) } + +// toArrayType converts an any to a []T +func toArrayType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, []T] { + return F.Flow2( + toType[[]any](), + E.Chain(E.TraverseArray(item)), + ) +} diff --git a/di/utils_test.go b/di/utils_test.go index 5ed5694..e8cc0ea 100644 --- a/di/utils_test.go +++ b/di/utils_test.go @@ -17,6 +17,7 @@ package di import ( "testing" + A "github.com/IBM/fp-go/array" E "github.com/IBM/fp-go/either" F "github.com/IBM/fp-go/function" IOE "github.com/IBM/fp-go/ioeither" @@ -24,24 +25,32 @@ import ( "github.com/stretchr/testify/assert" ) +var ( + toInt = toType[int]() + toString = toType[string]() +) + func TestToType(t *testing.T) { // good cases - assert.Equal(t, E.Of[error](10), toType[int]()(any(10))) - assert.Equal(t, E.Of[error]("Carsten"), toType[string]()(any("Carsten"))) + assert.Equal(t, E.Of[error](10), toInt(any(10))) + assert.Equal(t, E.Of[error]("Carsten"), toString(any("Carsten"))) assert.Equal(t, E.Of[error](O.Of("Carsten")), toType[O.Option[string]]()(any(O.Of("Carsten")))) assert.Equal(t, E.Of[error](O.Of(any("Carsten"))), toType[O.Option[any]]()(any(O.Of(any("Carsten"))))) // failure - assert.False(t, E.IsRight(toType[int]()(any("Carsten")))) + assert.False(t, E.IsRight(toInt(any("Carsten")))) assert.False(t, E.IsRight(toType[O.Option[string]]()(O.Of(any("Carsten"))))) } func TestToOptionType(t *testing.T) { + // shortcuts + toOptInt := toOptionType(toInt) + toOptString := toOptionType(toString) // good cases - assert.Equal(t, E.Of[error](O.Of(10)), toOptionType[int]()(any(O.Of(any(10))))) - assert.Equal(t, E.Of[error](O.Of("Carsten")), toOptionType[string]()(any(O.Of(any("Carsten"))))) + assert.Equal(t, E.Of[error](O.Of(10)), toOptInt(any(O.Of(any(10))))) + assert.Equal(t, E.Of[error](O.Of("Carsten")), toOptString(any(O.Of(any("Carsten"))))) // bad cases - assert.False(t, E.IsRight(toOptionType[int]()(any(10)))) - assert.False(t, E.IsRight(toOptionType[int]()(any(O.Of(10))))) + assert.False(t, E.IsRight(toOptInt(any(10)))) + assert.False(t, E.IsRight(toOptInt(any(O.Of(10))))) } func invokeIOEither[T any](e E.Either[error, IOE.IOEither[error, T]]) E.Either[error, T] { @@ -54,11 +63,21 @@ func invokeIOEither[T any](e E.Either[error, IOE.IOEither[error, T]]) E.Either[e } func TestToIOEitherType(t *testing.T) { + // shortcuts + toIOEitherInt := toIOEitherType(toInt) + toIOEitherString := toIOEitherType(toString) // good cases - assert.Equal(t, E.Of[error](10), invokeIOEither(toIOEitherType[int]()(any(IOE.Of[error](any(10)))))) - assert.Equal(t, E.Of[error]("Carsten"), invokeIOEither(toIOEitherType[string]()(any(IOE.Of[error](any("Carsten")))))) + assert.Equal(t, E.Of[error](10), invokeIOEither(toIOEitherInt(any(IOE.Of[error](any(10)))))) + assert.Equal(t, E.Of[error]("Carsten"), invokeIOEither(toIOEitherString(any(IOE.Of[error](any("Carsten")))))) // bad cases - assert.False(t, E.IsRight(invokeIOEither(toIOEitherType[string]()(any(IOE.Of[error](any(10))))))) - assert.False(t, E.IsRight(invokeIOEither(toIOEitherType[string]()(any(IOE.Of[error]("Carsten")))))) - assert.False(t, E.IsRight(invokeIOEither(toIOEitherType[string]()(any("Carsten"))))) + assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any(IOE.Of[error](any(10))))))) + assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any(IOE.Of[error]("Carsten")))))) + assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any("Carsten"))))) +} + +func TestToArrayType(t *testing.T) { + // shortcuts + toArrayString := toArrayType(toString) + // good cases + assert.Equal(t, E.Of[error](A.From("a", "b")), toArrayString(any(A.From(any("a"), any("b"))))) }