1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-11-25 22:21:49 +02:00

fix: add multi provider

Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
Carsten Leue
2023-11-09 21:53:55 +01:00
parent 16f708d69f
commit cc2f8e54e5
9 changed files with 222 additions and 44 deletions

View File

@@ -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],

View File

@@ -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 {

View File

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

View File

@@ -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),
)
}

View File

@@ -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)))
}

View File

@@ -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)
}

View File

@@ -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}
}

View File

@@ -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)),
)
}

View File

@@ -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")))))
}