From 37430c06989b32ceeb20f5a80e3c9ca482d3827d Mon Sep 17 00:00:00 2001 From: Carsten Leue Date: Wed, 8 Nov 2023 20:21:38 +0100 Subject: [PATCH 1/7] fix: checkin Signed-off-by: Carsten Leue --- array/array.go | 9 +++ array/generic/array.go | 18 +++++ array/monoid.go | 5 ++ di/erasure/injector.go | 89 +++++++++++++++++++++ di/erasure/provider.go | 167 ++++++++++++++++++++++++++++++++++++++++ di/erasure/token.go | 33 ++++++++ di/provider.go | 85 ++++++++++++++++++++ di/provider_test.go | 38 +++++++++ di/token.go | 105 +++++++++++++++++++++++++ erasure/erasure.go | 11 +++ internal/array/array.go | 9 +++ io/generic/traverse.go | 6 +- io/traverse.go | 4 +- lazy/traverse.go | 4 +- 14 files changed, 576 insertions(+), 7 deletions(-) create mode 100644 di/erasure/injector.go create mode 100644 di/erasure/provider.go create mode 100644 di/erasure/token.go create mode 100644 di/provider.go create mode 100644 di/provider_test.go create mode 100644 di/token.go diff --git a/array/array.go b/array/array.go index 20320a7..6aba87a 100644 --- a/array/array.go +++ b/array/array.go @@ -52,6 +52,10 @@ func MonadMapRef[A, B any](as []A, f func(a *A) B) []B { return bs } +func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B { + return G.MapWithIndex[[]A, []B](f) +} + func Map[A, B any](f func(a A) B) func([]A) []B { return F.Bind2nd(MonadMap[A, B], f) } @@ -300,6 +304,11 @@ func FoldMap[A, B any](m M.Monoid[B]) func(func(A) B) func([]A) B { return G.FoldMap[[]A](m) } +// FoldMapWithIndex maps and folds an array. Map the Array passing each value to the iterating function. Then fold the results using the provided Monoid. +func FoldMapWithIndex[A, B any](m M.Monoid[B]) func(func(int, A) B) func([]A) B { + return G.FoldMapWithIndex[[]A](m) +} + // Fold folds the array using the provided Monoid. func Fold[A any](m M.Monoid[A]) func([]A) A { return G.Fold[[]A](m) diff --git a/array/generic/array.go b/array/generic/array.go index 3ff3bd0..bc4ef14 100644 --- a/array/generic/array.go +++ b/array/generic/array.go @@ -118,6 +118,14 @@ func Map[GA ~[]A, GB ~[]B, A, B any](f func(a A) B) func(GA) GB { return F.Bind2nd(MonadMap[GA, GB, A, B], f) } +func MonadMapWithIndex[GA ~[]A, GB ~[]B, A, B any](as GA, f func(int, A) B) GB { + return array.MonadMapWithIndex[GA, GB](as, f) +} + +func MapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) B) func(GA) GB { + return F.Bind2nd(MonadMapWithIndex[GA, GB, A, B], f) +} + func Size[GA ~[]A, A any](as GA) int { return len(as) } @@ -242,6 +250,16 @@ func FoldMap[AS ~[]A, A, B any](m M.Monoid[B]) func(func(A) B) func(AS) B { } } +func FoldMapWithIndex[AS ~[]A, A, B any](m M.Monoid[B]) func(func(int, A) B) func(AS) B { + return func(f func(int, A) B) func(AS) B { + return func(as AS) B { + return array.ReduceWithIndex(as, func(idx int, cur B, a A) B { + return m.Concat(cur, f(idx, a)) + }, m.Empty()) + } + } +} + func Fold[AS ~[]A, A any](m M.Monoid[A]) func(AS) A { return func(as AS) A { return array.Reduce(as, m.Concat, m.Empty()) diff --git a/array/monoid.go b/array/monoid.go index c312f2b..ef87f5e 100644 --- a/array/monoid.go +++ b/array/monoid.go @@ -18,6 +18,7 @@ package array import ( "github.com/IBM/fp-go/internal/array" M "github.com/IBM/fp-go/monoid" + S "github.com/IBM/fp-go/semigroup" ) func concat[T any](left, right []T) []T { @@ -40,6 +41,10 @@ func Monoid[T any]() M.Monoid[[]T] { return M.MakeMonoid(concat[T], Empty[T]()) } +func Semigroup[T any]() S.Semigroup[[]T] { + return S.MakeSemigroup(concat[T]) +} + func addLen[A any](count int, data []A) int { return count + len(data) } diff --git a/di/erasure/injector.go b/di/erasure/injector.go new file mode 100644 index 0000000..704b167 --- /dev/null +++ b/di/erasure/injector.go @@ -0,0 +1,89 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package erasure + +import ( + "fmt" + + A "github.com/IBM/fp-go/array" + F "github.com/IBM/fp-go/function" + I "github.com/IBM/fp-go/identity" + IOE "github.com/IBM/fp-go/ioeither" + O "github.com/IBM/fp-go/option" + R "github.com/IBM/fp-go/record" + T "github.com/IBM/fp-go/tuple" +) + +func providerToEntry(p Provider) T.Tuple2[string, ProviderFactory] { + return T.MakeTuple2(p.Provides().Id(), p.Factory()) +} + +func missingProviderError(name string) func() IOE.IOEither[error, any] { + return func() IOE.IOEither[error, any] { + return IOE.Left[any](fmt.Errorf("No provider for dependency [%s]", name)) + } +} + +func MakeInjector(providers []Provider) InjectableFactory { + + type Result = IOE.IOEither[error, any] + + // provide a mapping for all providers + factoryById := F.Pipe2( + providers, + A.Map(providerToEntry), + R.FromEntries[string, ProviderFactory], + ) + // the resolved map + var resolved = R.Empty[string, Result]() + // the callback + var injFct InjectableFactory + + // lazy initialization, so we can cross reference it + injFct = func(token Token) Result { + + hit := F.Pipe3( + token, + Token.Id, + R.Lookup[Result, string], + I.Ap[O.Option[Result]](resolved), + ) + + provFct := F.Pipe2( + token, + T.Replicate2[Token], + T.Map2(F.Flow3( + Token.Id, + R.Lookup[ProviderFactory, string], + I.Ap[O.Option[ProviderFactory]](factoryById), + ), F.Flow2( + Token.String, + missingProviderError, + )), + ) + + x := F.Pipe4( + token, + Token.Id, + R.Lookup[Result, string], + I.Ap[O.Option[Result]](resolved), + O.GetOrElse(F.Flow2()), + ) + } + + return injFct +} diff --git a/di/erasure/provider.go b/di/erasure/provider.go new file mode 100644 index 0000000..50c2ac8 --- /dev/null +++ b/di/erasure/provider.go @@ -0,0 +1,167 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package erasure + +import ( + "fmt" + + A "github.com/IBM/fp-go/array" + E "github.com/IBM/fp-go/either" + F "github.com/IBM/fp-go/function" + IO "github.com/IBM/fp-go/io" + IOG "github.com/IBM/fp-go/io/generic" + IOE "github.com/IBM/fp-go/ioeither" + 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" +) + +type InjectableFactory = RIOE.ReaderIOEither[Token, error, any] +type ProviderFactory = RIOE.ReaderIOEither[InjectableFactory, error, any] + +type Provider interface { + fmt.Stringer + Provides() Token + Factory() ProviderFactory +} + +type provider struct { + provides Token + factory ProviderFactory +} + +func (p *provider) Provides() Token { + return p.provides +} + +func (p *provider) Factory() ProviderFactory { + return p.factory +} + +func (p *provider) String() string { + return fmt.Sprintf("Provider for [%s]", p.provides) +} + +func MakeProvider(token Token, fct ProviderFactory) Provider { + return &provider{token, fct} +} + +func mapFromToken(idx int, token Token) map[TokenType]map[int]int { + return R.Singleton(token.Type(), R.Singleton(idx, idx)) +} + +var mergeTokenMaps = R.UnionMonoid[TokenType](R.UnionLastSemigroup[int, int]()) +var foldDeps = A.FoldMapWithIndex[Token](mergeTokenMaps)(mapFromToken) + +var lookupMandatory = R.Lookup[map[int]int](Mandatory) +var lookupOption = R.Lookup[map[int]int](Option) + +type Mapping = map[TokenType]map[int]int + +func getAt[T any](ar []T) func(idx int) T { + return func(idx int) T { + return ar[idx] + } +} + +func handleMandatory(mp Mapping) func(res []IOE.IOEither[error, any]) IOE.IOEither[error, map[int]any] { + + onNone := F.Nullary2(R.Empty[int, any], IOE.Of[error, map[int]any]) + + return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, map[int]any] { + return F.Pipe2( + mp, + lookupMandatory, + O.Fold( + onNone, + IOE.TraverseRecord[int](getAt(res)), + ), + ) + } +} + +func handleOption(mp Mapping) func(res []IOE.IOEither[error, any]) IO.IO[map[int]O.Option[any]] { + + onNone := F.Nullary2(R.Empty[int, O.Option[any]], IO.Of[map[int]O.Option[any]]) + + return func(res []IOE.IOEither[error, any]) IO.IO[map[int]O.Option[any]] { + + return F.Pipe2( + mp, + lookupOption, + O.Fold( + onNone, + F.Flow2( + IOG.TraverseRecord[IO.IO[map[int]E.Either[error, any]], map[int]int](getAt(res)), + IO.Map(R.Map[int](E.ToOption[error, any])), + ), + ), + ) + } +} + +func mergeArguments(count int) func( + mandatory IOE.IOEither[error, map[int]any], + optonal IO.IO[map[int]O.Option[any]], +) IOE.IOEither[error, []any] { + + optMapToAny := R.Map[int](F.ToAny[O.Option[any]]) + mergeMaps := R.UnionLastMonoid[int, any]() + + return func( + mandatory IOE.IOEither[error, map[int]any], + optional IO.IO[map[int]O.Option[any]], + ) IOE.IOEither[error, []any] { + + return F.Pipe1( + IOE.SequenceT2(mandatory, IOE.FromIO[error](optional)), + IOE.Map[error](T.Tupled2(func(mnd map[int]any, opt map[int]O.Option[any]) []any { + // merge all parameters + merged := mergeMaps.Concat(mnd, optMapToAny(opt)) + + return R.ReduceWithIndex(func(idx int, res []any, value any) []any { + res[idx] = value + return res + }, make([]any, count))(merged) + })), + ) + } +} + +func MakeProviderFactory( + deps []Token, + fct func(param ...any) IOE.IOEither[error, any]) ProviderFactory { + + mapping := foldDeps(deps) + + mandatory := handleMandatory(mapping) + optional := handleOption(mapping) + + merge := mergeArguments(A.Size(deps)) + + f := F.Unvariadic0(fct) + + return func(inj InjectableFactory) IOE.IOEither[error, any] { + // resolve all dependencies + resolved := A.MonadMap(deps, inj) + // resolve dependencies + return F.Pipe1( + merge(mandatory(resolved), optional(resolved)), + IOE.Chain(f), + ) + } +} diff --git a/di/erasure/token.go b/di/erasure/token.go new file mode 100644 index 0000000..8f0fe9f --- /dev/null +++ b/di/erasure/token.go @@ -0,0 +1,33 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package erasure + +import "fmt" + +type TokenType int + +const ( + Mandatory TokenType = iota + Option + IOEither + IOOption +) + +type Token interface { + fmt.Stringer + Id() string + Type() TokenType +} diff --git a/di/provider.go b/di/provider.go new file mode 100644 index 0000000..d513031 --- /dev/null +++ b/di/provider.go @@ -0,0 +1,85 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package di + +import ( + A "github.com/IBM/fp-go/array" + DIE "github.com/IBM/fp-go/di/erasure" + E "github.com/IBM/fp-go/either" + ER "github.com/IBM/fp-go/erasure" + "github.com/IBM/fp-go/errors" + F "github.com/IBM/fp-go/function" + IOE "github.com/IBM/fp-go/ioeither" + T "github.com/IBM/fp-go/tuple" +) + +func lookupAt[T any](idx int) func(params []any) E.Either[error, T] { + return F.Flow3( + A.Lookup[any](idx), + E.FromOption[any](errors.OnNone("No parameter at position %d", idx)), + E.Chain(ER.SafeUnerase[T]), + ) +} + +func eraseProviderFactory0[R any](f func() IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] { + return func(params ...any) IOE.IOEither[error, any] { + return F.Pipe1( + f(), + IOE.Map[error](ER.Erase[R]), + ) + } +} + +func eraseProviderFactory1[T1 any, R any]( + f func(T1) IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] { + ft := T.Tupled1(f) + t1 := lookupAt[T1](0) + return func(params ...any) IOE.IOEither[error, any] { + return F.Pipe3( + E.SequenceT1(t1(params)), + IOE.FromEither[error, T.Tuple1[T1]], + IOE.Chain(ft), + IOE.Map[error](ER.Erase[R]), + ) + } +} + +func MakeProvider0[R any]( + token InjectionToken[R], + fct func() IOE.IOEither[error, R], +) DIE.Provider { + return DIE.MakeProvider( + token, + DIE.MakeProviderFactory( + A.Empty[DIE.Token](), + eraseProviderFactory0(fct), + ), + ) +} + +func MakeProvider1[T1, R any]( + token InjectionToken[R], + d1 InjectionToken[T1], + fct func(T1) IOE.IOEither[error, R], +) DIE.Provider { + + return DIE.MakeProvider( + token, + DIE.MakeProviderFactory( + A.From[DIE.Token](d1), + eraseProviderFactory1(fct), + ), + ) +} diff --git a/di/provider_test.go b/di/provider_test.go new file mode 100644 index 0000000..bb9f53c --- /dev/null +++ b/di/provider_test.go @@ -0,0 +1,38 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package di + +import ( + "fmt" + "testing" + + F "github.com/IBM/fp-go/function" + IOE "github.com/IBM/fp-go/ioeither" +) + +func staticValue(value string) func() IOE.IOEither[error, string] { + return F.Constant(IOE.Of[error](value)) +} + +var ( + INJ_KEY1 = MakeToken[string]("INJ_KEY1") + INJ_KEY2 = MakeToken[string]("INJ_KEY2") +) + +func TestSimpleProvider(t *testing.T) { + p1 := MakeProvider0(INJ_KEY1, staticValue("Carsten")) + + fmt.Println(p1) +} diff --git a/di/token.go b/di/token.go new file mode 100644 index 0000000..1bbc346 --- /dev/null +++ b/di/token.go @@ -0,0 +1,105 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package di + +import ( + "fmt" + "strconv" + "sync/atomic" + + DIE "github.com/IBM/fp-go/di/erasure" + IO "github.com/IBM/fp-go/io" + IOE "github.com/IBM/fp-go/ioeither" + IOO "github.com/IBM/fp-go/iooption" + O "github.com/IBM/fp-go/option" +) + +type Token[T any] interface { + DIE.Token + ToType(any) O.Option[T] +} + +type InjectionToken[T any] interface { + Token[T] + Option() Token[O.Option[T]] + IOEither() Token[IOE.IOEither[error, T]] + IOOption() Token[IOO.IOOption[T]] +} + +func makeId() IO.IO[string] { + var count int64 + return func() string { + return strconv.FormatInt(atomic.AddInt64(&count, 1), 16) + } +} + +var genId = makeId() + +type token[T any] struct { + name string + id string + typ DIE.TokenType +} + +func (t *token[T]) Id() string { + return t.id +} + +func (t *token[T]) Type() DIE.TokenType { + return t.typ +} + +func (t *token[T]) ToType(value any) O.Option[T] { + return O.ToType[T](value) +} + +func (t *token[T]) String() string { + return t.name +} + +func makeToken[T any](name string, id string, typ DIE.TokenType) Token[T] { + return &token[T]{name, id, typ} +} + +type injectionToken[T any] struct { + token[T] + option Token[O.Option[T]] + ioeither Token[IOE.IOEither[error, T]] + iooption Token[IOO.IOOption[T]] +} + +func (i *injectionToken[T]) Option() Token[O.Option[T]] { + return i.option +} + +func (i *injectionToken[T]) IOEither() Token[IOE.IOEither[error, T]] { + return i.ioeither +} + +func (i *injectionToken[T]) IOOption() Token[IOO.IOOption[T]] { + return i.iooption +} + +// MakeToken create a unique injection token for a specific type +func MakeToken[T any](name string) InjectionToken[T] { + id := genId() + return &injectionToken[T]{ + token[T]{name, id, DIE.Mandatory}, + makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", name), id, DIE.Option), + makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEither), + makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOption), + } +} diff --git a/erasure/erasure.go b/erasure/erasure.go index c217a81..c939592 100644 --- a/erasure/erasure.go +++ b/erasure/erasure.go @@ -16,6 +16,8 @@ package erasure import ( + E "github.com/IBM/fp-go/either" + "github.com/IBM/fp-go/errors" F "github.com/IBM/fp-go/function" ) @@ -29,6 +31,15 @@ func Unerase[T any](t any) T { return *t.(*T) } +// SafeUnerase converts an erased variable back to its original value +func SafeUnerase[T any](t any) E.Either[error, T] { + return F.Pipe2( + t, + E.ToType[*T](errors.OnSome[any]("Value %T is not unerased")), + E.Map[error](F.Deref[T]), + ) +} + // Erase0 converts a type safe function into an erased function func Erase0[T1 any](f func() T1) func() any { return F.Nullary2(f, Erase[T1]) diff --git a/internal/array/array.go b/internal/array/array.go index 035e618..015001b 100644 --- a/internal/array/array.go +++ b/internal/array/array.go @@ -88,6 +88,15 @@ func MonadMap[GA ~[]A, GB ~[]B, A, B any](as GA, f func(a A) B) GB { return bs } +func MonadMapWithIndex[GA ~[]A, GB ~[]B, A, B any](as GA, f func(idx int, a A) B) GB { + count := len(as) + bs := make(GB, count) + for i := count - 1; i >= 0; i-- { + bs[i] = f(i, as[i]) + } + return bs +} + func ConstNil[GA ~[]A, A any]() GA { return (GA)(nil) } diff --git a/io/generic/traverse.go b/io/generic/traverse.go index 71e256c..613c882 100644 --- a/io/generic/traverse.go +++ b/io/generic/traverse.go @@ -57,7 +57,7 @@ func SequenceArray[GA ~func() A, GAS ~func() AAS, AAS ~[]A, GAAS ~[]GA, A any](t } // MonadTraverseRecord transforms a record using an IO transform an IO of a record -func MonadTraverseRecord[GB ~func() B, GBS ~func() MB, MA ~map[K]A, MB ~map[K]B, K comparable, A, B any](ma MA, f func(A) GB) GBS { +func MonadTraverseRecord[GBS ~func() MB, MA ~map[K]A, GB ~func() B, MB ~map[K]B, K comparable, A, B any](ma MA, f func(A) GB) GBS { return RR.MonadTraverse[MA]( Of[GBS, MB], Map[GBS, func() func(B) MB, MB, func(B) MB], @@ -67,7 +67,7 @@ func MonadTraverseRecord[GB ~func() B, GBS ~func() MB, MA ~map[K]A, MB ~map[K]B, } // TraverseRecord transforms a record using an IO transform an IO of a record -func TraverseRecord[GB ~func() B, GBS ~func() MB, MA ~map[K]A, MB ~map[K]B, K comparable, A, B any](f func(A) GB) func(MA) GBS { +func TraverseRecord[GBS ~func() MB, MA ~map[K]A, GB ~func() B, MB ~map[K]B, K comparable, A, B any](f func(A) GB) func(MA) GBS { return RR.Traverse[MA]( Of[GBS, MB], Map[GBS, func() func(B) MB, MB, func(B) MB], @@ -87,5 +87,5 @@ func TraverseRecordWithIndex[GB ~func() B, GBS ~func() MB, MA ~map[K]A, MB ~map[ } func SequenceRecord[GA ~func() A, GAS ~func() AAS, AAS ~map[K]A, GAAS ~map[K]GA, K comparable, A any](tas GAAS) GAS { - return MonadTraverseRecord[GA, GAS](tas, F.Identity[GA]) + return MonadTraverseRecord[GAS](tas, F.Identity[GA]) } diff --git a/io/traverse.go b/io/traverse.go index 4065afa..6fdf192 100644 --- a/io/traverse.go +++ b/io/traverse.go @@ -41,13 +41,13 @@ func SequenceArray[A any](tas []IO[A]) IO[[]A] { } func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) IO[B]) IO[map[K]B] { - return G.MonadTraverseRecord[IO[B], IO[map[K]B]](tas, f) + return G.MonadTraverseRecord[IO[map[K]B]](tas, f) } // TraverseRecord applies a function returning an [IO] to all elements in a record and the // transforms this into an [IO] of that record func TraverseRecord[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO[map[K]B] { - return G.TraverseRecord[IO[B], IO[map[K]B], map[K]A](f) + return G.TraverseRecord[IO[map[K]B], map[K]A, IO[B]](f) } // TraverseRecordWithIndex applies a function returning an [IO] to all elements in a record and the diff --git a/lazy/traverse.go b/lazy/traverse.go index 3498569..0d75267 100644 --- a/lazy/traverse.go +++ b/lazy/traverse.go @@ -41,13 +41,13 @@ func SequenceArray[A any](tas []Lazy[A]) Lazy[[]A] { } func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) Lazy[B]) Lazy[map[K]B] { - return G.MonadTraverseRecord[Lazy[B], Lazy[map[K]B]](tas, f) + return G.MonadTraverseRecord[Lazy[map[K]B]](tas, f) } // TraverseRecord applies a function returning an [IO] to all elements in a record and the // transforms this into an [IO] of that record func TraverseRecord[K comparable, A, B any](f func(A) Lazy[B]) func(map[K]A) Lazy[map[K]B] { - return G.TraverseRecord[Lazy[B], Lazy[map[K]B], map[K]A](f) + return G.TraverseRecord[Lazy[map[K]B], map[K]A, Lazy[B]](f) } // TraverseRecord applies a function returning an [IO] to all elements in a record and the From 16f708d69fd7cad5ea2e8d8bbe86000ceace83e7 Mon Sep 17 00:00:00 2001 From: Carsten Leue Date: Thu, 9 Nov 2023 19:44:11 +0100 Subject: [PATCH 2/7] fix: add initial DI implementation Signed-off-by: Carsten Leue --- di/erasure/injector.go | 97 +++++++++++------- di/erasure/provider.go | 158 ++++++++++++++++++++++-------- di/erasure/token.go | 10 +- di/injector.go | 33 +++++++ di/provider.go | 53 ++++++++-- di/provider_test.go | 207 +++++++++++++++++++++++++++++++++++++-- di/token.go | 85 ++++++++++------ di/utils.go | 62 ++++++++++++ di/utils_test.go | 64 ++++++++++++ erasure/erasure.go | 2 +- option/option.go | 4 + record/generic/record.go | 8 ++ record/record.go | 5 + 13 files changed, 659 insertions(+), 129 deletions(-) create mode 100644 di/injector.go create mode 100644 di/utils.go create mode 100644 di/utils_test.go diff --git a/di/erasure/injector.go b/di/erasure/injector.go index 704b167..d9d8069 100644 --- a/di/erasure/injector.go +++ b/di/erasure/injector.go @@ -17,30 +17,48 @@ package erasure import ( - "fmt" + "log" A "github.com/IBM/fp-go/array" + "github.com/IBM/fp-go/errors" F "github.com/IBM/fp-go/function" I "github.com/IBM/fp-go/identity" + IG "github.com/IBM/fp-go/identity/generic" 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" + + "sync" ) func providerToEntry(p Provider) T.Tuple2[string, ProviderFactory] { return T.MakeTuple2(p.Provides().Id(), p.Factory()) } -func missingProviderError(name string) func() IOE.IOEither[error, any] { - return func() IOE.IOEither[error, any] { - return IOE.Left[any](fmt.Errorf("No provider for dependency [%s]", name)) +var missingProviderError = F.Flow3( + errors.OnSome[string]("no provider for dependency [%s]"), + RIOE.Left[InjectableFactory, any, error], + F.Constant[ProviderFactory], +) + +func logEntryExit(name string, token Dependency) func() { + log.Printf("Entry: [%s] -> [%s]:[%s]", name, token.Id(), token.String()) + return func() { + log.Printf("Exit: [%s] -> [%s]:[%s]", name, token.Id(), token.String()) } } func MakeInjector(providers []Provider) InjectableFactory { type Result = IOE.IOEither[error, any] + type LazyResult = L.Lazy[Result] + + // resolved stores the values resolved so far, key is the string ID + // of the token, value is a lazy result + var resolved sync.Map // provide a mapping for all providers factoryById := F.Pipe2( @@ -48,41 +66,48 @@ func MakeInjector(providers []Provider) InjectableFactory { A.Map(providerToEntry), R.FromEntries[string, ProviderFactory], ) - // the resolved map - var resolved = R.Empty[string, Result]() - // the callback + + // the actual factory, we need lazy initialization var injFct InjectableFactory // lazy initialization, so we can cross reference it - injFct = func(token Token) Result { - - hit := F.Pipe3( - token, - Token.Id, - R.Lookup[Result, string], - I.Ap[O.Option[Result]](resolved), - ) - - provFct := F.Pipe2( - token, - T.Replicate2[Token], - T.Map2(F.Flow3( - Token.Id, - R.Lookup[ProviderFactory, string], - I.Ap[O.Option[ProviderFactory]](factoryById), - ), F.Flow2( - Token.String, - missingProviderError, - )), - ) - - x := F.Pipe4( - token, - Token.Id, - R.Lookup[Result, string], - I.Ap[O.Option[Result]](resolved), - O.GetOrElse(F.Flow2()), - ) + injFct = func(token Dependency) Result { + + defer logEntryExit("inj", token)() + + key := token.Id() + + // according to https://github.com/golang/go/issues/44159 this + // is the best way to use the sync map + actual, loaded := resolved.Load(key) + if !loaded { + + computeResult := func() Result { + defer logEntryExit("computeResult", token)() + return F.Pipe5( + token, + T.Replicate2[Dependency], + T.Map2(F.Flow3( + Dependency.Id, + R.Lookup[ProviderFactory, string], + I.Ap[O.Option[ProviderFactory]](factoryById), + ), F.Flow2( + Dependency.String, + missingProviderError, + )), + T.Tupled2(O.MonadGetOrElse[ProviderFactory]), + IG.Ap[ProviderFactory](injFct), + IOE.Memoize[error, any], + ) + } + + actual, _ = resolved.LoadOrStore(key, F.Pipe1( + computeResult, + L.Memoize[Result], + )) + } + + return actual.(LazyResult)() } return injFct diff --git a/di/erasure/provider.go b/di/erasure/provider.go index 50c2ac8..3cc6956 100644 --- a/di/erasure/provider.go +++ b/di/erasure/provider.go @@ -24,27 +24,29 @@ import ( IO "github.com/IBM/fp-go/io" IOG "github.com/IBM/fp-go/io/generic" IOE "github.com/IBM/fp-go/ioeither" + 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" - T "github.com/IBM/fp-go/tuple" ) -type InjectableFactory = RIOE.ReaderIOEither[Token, error, any] +type InjectableFactory = RIOE.ReaderIOEither[Dependency, error, any] type ProviderFactory = RIOE.ReaderIOEither[InjectableFactory, error, any] +type paramIndex = map[int]int type Provider interface { fmt.Stringer - Provides() Token + Provides() Dependency Factory() ProviderFactory } type provider struct { - provides Token + provides Dependency factory ProviderFactory } -func (p *provider) Provides() Token { +func (p *provider) Provides() Dependency { return p.provides } @@ -56,21 +58,23 @@ func (p *provider) String() string { return fmt.Sprintf("Provider for [%s]", p.provides) } -func MakeProvider(token Token, fct ProviderFactory) Provider { +func MakeProvider(token Dependency, fct ProviderFactory) Provider { return &provider{token, fct} } -func mapFromToken(idx int, token Token) map[TokenType]map[int]int { +func mapFromToken(idx int, token Dependency) map[TokenType]paramIndex { return R.Singleton(token.Type(), R.Singleton(idx, idx)) } var mergeTokenMaps = R.UnionMonoid[TokenType](R.UnionLastSemigroup[int, int]()) -var foldDeps = A.FoldMapWithIndex[Token](mergeTokenMaps)(mapFromToken) +var foldDeps = A.FoldMapWithIndex[Dependency](mergeTokenMaps)(mapFromToken) -var lookupMandatory = R.Lookup[map[int]int](Mandatory) -var lookupOption = R.Lookup[map[int]int](Option) +var lookupIdentity = R.Lookup[paramIndex](Identity) +var lookupOption = R.Lookup[paramIndex](Option) +var lookupIOEither = R.Lookup[paramIndex](IOEither) +var lookupIOOption = R.Lookup[paramIndex](IOOption) -type Mapping = map[TokenType]map[int]int +type Mapping = map[TokenType]paramIndex func getAt[T any](ar []T) func(idx int) T { return func(idx int) T { @@ -78,14 +82,16 @@ func getAt[T any](ar []T) func(idx int) T { } } -func handleMandatory(mp Mapping) func(res []IOE.IOEither[error, any]) IOE.IOEither[error, map[int]any] { +type identityResult = IOE.IOEither[error, map[int]any] + +func handleIdentity(mp Mapping) func(res []IOE.IOEither[error, any]) identityResult { onNone := F.Nullary2(R.Empty[int, any], IOE.Of[error, map[int]any]) - return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, map[int]any] { + return func(res []IOE.IOEither[error, any]) identityResult { return F.Pipe2( mp, - lookupMandatory, + lookupIdentity, O.Fold( onNone, IOE.TraverseRecord[int](getAt(res)), @@ -94,11 +100,13 @@ func handleMandatory(mp Mapping) func(res []IOE.IOEither[error, any]) IOE.IOEith } } -func handleOption(mp Mapping) func(res []IOE.IOEither[error, any]) IO.IO[map[int]O.Option[any]] { +type optionResult = IO.IO[map[int]O.Option[any]] + +func handleOption(mp Mapping) func(res []IOE.IOEither[error, any]) optionResult { onNone := F.Nullary2(R.Empty[int, O.Option[any]], IO.Of[map[int]O.Option[any]]) - return func(res []IOE.IOEither[error, any]) IO.IO[map[int]O.Option[any]] { + return func(res []IOE.IOEither[error, any]) optionResult { return F.Pipe2( mp, @@ -106,7 +114,7 @@ func handleOption(mp Mapping) func(res []IOE.IOEither[error, any]) IO.IO[map[int O.Fold( onNone, F.Flow2( - IOG.TraverseRecord[IO.IO[map[int]E.Either[error, any]], map[int]int](getAt(res)), + IOG.TraverseRecord[IO.IO[map[int]E.Either[error, any]], paramIndex](getAt(res)), IO.Map(R.Map[int](E.ToOption[error, any])), ), ), @@ -114,44 +122,103 @@ func handleOption(mp Mapping) func(res []IOE.IOEither[error, any]) IO.IO[map[int } } -func mergeArguments(count int) func( - mandatory IOE.IOEither[error, map[int]any], - optonal IO.IO[map[int]O.Option[any]], -) IOE.IOEither[error, []any] { +type ioeitherResult = IO.IO[map[int]IOE.IOEither[error, any]] - optMapToAny := R.Map[int](F.ToAny[O.Option[any]]) - mergeMaps := R.UnionLastMonoid[int, any]() +func handleIOEither(mp Mapping) func(res []IOE.IOEither[error, any]) ioeitherResult { - return func( - mandatory IOE.IOEither[error, map[int]any], - optional IO.IO[map[int]O.Option[any]], - ) IOE.IOEither[error, []any] { + onNone := F.Nullary2(R.Empty[int, IOE.IOEither[error, any]], IO.Of[map[int]IOE.IOEither[error, any]]) - return F.Pipe1( - IOE.SequenceT2(mandatory, IOE.FromIO[error](optional)), - IOE.Map[error](T.Tupled2(func(mnd map[int]any, opt map[int]O.Option[any]) []any { - // merge all parameters - merged := mergeMaps.Concat(mnd, optMapToAny(opt)) - - return R.ReduceWithIndex(func(idx int, res []any, value any) []any { - res[idx] = value - return res - }, make([]any, count))(merged) - })), + return func(res []IOE.IOEither[error, any]) ioeitherResult { + + return F.Pipe2( + mp, + lookupIOEither, + O.Fold( + onNone, + F.Flow2( + R.Map[int](getAt(res)), + IO.Of[map[int]IOE.IOEither[error, any]], + ), + ), + ) + } +} + +type iooptionResult = IO.IO[map[int]IOO.IOOption[any]] + +func handleIOOption(mp Mapping) func(res []IOE.IOEither[error, any]) iooptionResult { + + onNone := F.Nullary2(R.Empty[int, IOO.IOOption[any]], IO.Of[map[int]IOO.IOOption[any]]) + + return func(res []IOE.IOEither[error, any]) iooptionResult { + + return F.Pipe2( + mp, + lookupIOOption, + O.Fold( + onNone, + F.Flow2( + R.Map[int](F.Flow2( + getAt(res), + IOO.FromIOEither[error, any], + )), + IO.Of[map[int]IOO.IOOption[any]], + ), + ), ) } } +var optionMapToAny = R.Map[int](F.ToAny[O.Option[any]]) +var ioeitherMapToAny = R.Map[int](F.ToAny[IOE.IOEither[error, any]]) +var iooptionMapToAny = R.Map[int](F.ToAny[IOO.IOOption[any]]) +var mergeMaps = R.UnionLastMonoid[int, any]() +var collectParams = R.CollectOrd[any, any](Int.Ord)(F.SK[int, any]) + +func mergeArguments( + identity identityResult, + option optionResult, + ioeither ioeitherResult, + iooption iooptionResult, +) IOE.IOEither[error, []any] { + + return F.Pipe2( + A.From( + identity, + F.Pipe2( + option, + IO.Map(optionMapToAny), + IOE.FromIO[error, map[int]any], + ), + F.Pipe2( + ioeither, + IO.Map(ioeitherMapToAny), + IOE.FromIO[error, map[int]any], + ), + F.Pipe2( + iooption, + IO.Map(iooptionMapToAny), + IOE.FromIO[error, map[int]any], + ), + ), + IOE.SequenceArray[error, map[int]any], + IOE.Map[error](F.Flow2( + A.Fold(mergeMaps), + collectParams, + )), + ) +} + func MakeProviderFactory( - deps []Token, + deps []Dependency, fct func(param ...any) IOE.IOEither[error, any]) ProviderFactory { mapping := foldDeps(deps) - mandatory := handleMandatory(mapping) + identity := handleIdentity(mapping) optional := handleOption(mapping) - - merge := mergeArguments(A.Size(deps)) + ioeither := handleIOEither(mapping) + iooption := handleIOOption(mapping) f := F.Unvariadic0(fct) @@ -160,7 +227,12 @@ func MakeProviderFactory( resolved := A.MonadMap(deps, inj) // resolve dependencies return F.Pipe1( - merge(mandatory(resolved), optional(resolved)), + mergeArguments( + identity(resolved), + optional(resolved), + ioeither(resolved), + iooption(resolved), + ), IOE.Chain(f), ) } diff --git a/di/erasure/token.go b/di/erasure/token.go index 8f0fe9f..de86d20 100644 --- a/di/erasure/token.go +++ b/di/erasure/token.go @@ -20,14 +20,20 @@ import "fmt" type TokenType int const ( - Mandatory TokenType = iota + Identity TokenType = iota Option IOEither IOOption ) -type Token interface { +type Dependency interface { fmt.Stringer + // Id returns a unique identifier for a token that can be used as a cache key Id() string + // Type returns a tag that identifies the behaviour of the dependency Type() TokenType } + +func AsDependency[T Dependency](t T) Dependency { + return t +} diff --git a/di/injector.go b/di/injector.go new file mode 100644 index 0000000..d65f8c5 --- /dev/null +++ b/di/injector.go @@ -0,0 +1,33 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package di + +import ( + DIE "github.com/IBM/fp-go/di/erasure" + F "github.com/IBM/fp-go/function" + IG "github.com/IBM/fp-go/identity/generic" + IOE "github.com/IBM/fp-go/ioeither" + RIOE "github.com/IBM/fp-go/readerioeither" +) + +// Resolve performs a type safe resolution of a dependency +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]()), + ) +} diff --git a/di/provider.go b/di/provider.go index d513031..bc889eb 100644 --- a/di/provider.go +++ b/di/provider.go @@ -18,18 +18,17 @@ import ( A "github.com/IBM/fp-go/array" DIE "github.com/IBM/fp-go/di/erasure" E "github.com/IBM/fp-go/either" - ER "github.com/IBM/fp-go/erasure" "github.com/IBM/fp-go/errors" F "github.com/IBM/fp-go/function" IOE "github.com/IBM/fp-go/ioeither" T "github.com/IBM/fp-go/tuple" ) -func lookupAt[T any](idx int) func(params []any) E.Either[error, T] { +func lookupAt[T any](idx int, token Dependency[T]) func(params []any) E.Either[error, T] { return F.Flow3( A.Lookup[any](idx), E.FromOption[any](errors.OnNone("No parameter at position %d", idx)), - E.Chain(ER.SafeUnerase[T]), + E.Chain(token.Unerase), ) } @@ -37,21 +36,39 @@ func eraseProviderFactory0[R any](f func() IOE.IOEither[error, R]) func(params . return func(params ...any) IOE.IOEither[error, any] { return F.Pipe1( f(), - IOE.Map[error](ER.Erase[R]), + IOE.Map[error](F.ToAny[R]), ) } } func eraseProviderFactory1[T1 any, R any]( + d1 Dependency[T1], f func(T1) IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] { ft := T.Tupled1(f) - t1 := lookupAt[T1](0) + t1 := lookupAt[T1](0, d1) return func(params ...any) IOE.IOEither[error, any] { return F.Pipe3( E.SequenceT1(t1(params)), IOE.FromEither[error, T.Tuple1[T1]], IOE.Chain(ft), - IOE.Map[error](ER.Erase[R]), + IOE.Map[error](F.ToAny[R]), + ) + } +} + +func eraseProviderFactory2[T1, T2 any, R any]( + d1 Dependency[T1], + d2 Dependency[T2], + f func(T1, T2) IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] { + ft := T.Tupled2(f) + t1 := lookupAt[T1](0, d1) + t2 := lookupAt[T2](1, d2) + return func(params ...any) IOE.IOEither[error, any] { + return F.Pipe3( + E.SequenceT2(t1(params), t2(params)), + IOE.FromEither[error, T.Tuple2[T1, T2]], + IOE.Chain(ft), + IOE.Map[error](F.ToAny[R]), ) } } @@ -63,7 +80,7 @@ func MakeProvider0[R any]( return DIE.MakeProvider( token, DIE.MakeProviderFactory( - A.Empty[DIE.Token](), + A.Empty[DIE.Dependency](), eraseProviderFactory0(fct), ), ) @@ -71,15 +88,31 @@ func MakeProvider0[R any]( func MakeProvider1[T1, R any]( token InjectionToken[R], - d1 InjectionToken[T1], + d1 Dependency[T1], fct func(T1) IOE.IOEither[error, R], ) DIE.Provider { return DIE.MakeProvider( token, DIE.MakeProviderFactory( - A.From[DIE.Token](d1), - eraseProviderFactory1(fct), + A.From[DIE.Dependency](d1), + eraseProviderFactory1(d1, fct), + ), + ) +} + +func MakeProvider2[T1, T2, R any]( + token InjectionToken[R], + d1 Dependency[T1], + d2 Dependency[T2], + fct func(T1, T2) IOE.IOEither[error, R], +) DIE.Provider { + + return DIE.MakeProvider( + token, + DIE.MakeProviderFactory( + A.From[DIE.Dependency](d1, d2), + eraseProviderFactory2(d1, d2, fct), ), ) } diff --git a/di/provider_test.go b/di/provider_test.go index bb9f53c..016f889 100644 --- a/di/provider_test.go +++ b/di/provider_test.go @@ -17,22 +17,217 @@ package di import ( "fmt" "testing" + "time" + A "github.com/IBM/fp-go/array" + DIE "github.com/IBM/fp-go/di/erasure" + E "github.com/IBM/fp-go/either" F "github.com/IBM/fp-go/function" IOE "github.com/IBM/fp-go/ioeither" + O "github.com/IBM/fp-go/option" + "github.com/stretchr/testify/assert" ) -func staticValue(value string) func() IOE.IOEither[error, string] { - return F.Constant(IOE.Of[error](value)) -} - var ( - INJ_KEY1 = MakeToken[string]("INJ_KEY1") INJ_KEY2 = MakeToken[string]("INJ_KEY2") + INJ_KEY1 = MakeToken[string]("INJ_KEY1") + INJ_KEY3 = MakeToken[string]("INJ_KEY3") ) func TestSimpleProvider(t *testing.T) { + + var staticCount int + + staticValue := func(value string) func() IOE.IOEither[error, string] { + return func() IOE.IOEither[error, string] { + return func() E.Either[error, string] { + staticCount++ + return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now())) + } + } + } + + var dynamicCount int + + dynamicValue := func(value string) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + dynamicCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) + } + } + p1 := MakeProvider0(INJ_KEY1, staticValue("Carsten")) + p2 := MakeProvider1(INJ_KEY2, INJ_KEY1.Identity(), dynamicValue) + + inj := DIE.MakeInjector(A.From(p1, p2)) + + i1 := Resolve(INJ_KEY1) + i2 := Resolve(INJ_KEY2) + + res := IOE.SequenceT4( + i2(inj), + i1(inj), + i2(inj), + i1(inj), + ) + + r := res() + + assert.True(t, E.IsRight(r)) + assert.Equal(t, 1, staticCount) + assert.Equal(t, 1, dynamicCount) +} + +func TestOptionalProvider(t *testing.T) { + + var staticCount int + + staticValue := func(value string) func() IOE.IOEither[error, string] { + return func() IOE.IOEither[error, string] { + return func() E.Either[error, string] { + staticCount++ + return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now())) + } + } + } + + var dynamicCount int + + dynamicValue := func(value O.Option[string]) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + dynamicCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) + } + } + + p1 := MakeProvider0(INJ_KEY1, staticValue("Carsten")) + p2 := MakeProvider1(INJ_KEY2, INJ_KEY1.Option(), dynamicValue) + + inj := DIE.MakeInjector(A.From(p1, p2)) + + i1 := Resolve(INJ_KEY1) + i2 := Resolve(INJ_KEY2) + + res := IOE.SequenceT4( + i2(inj), + i1(inj), + i2(inj), + i1(inj), + ) + + r := res() + + assert.True(t, E.IsRight(r)) + assert.Equal(t, 1, staticCount) + assert.Equal(t, 1, dynamicCount) +} + +func TestOptionalProviderMissingDependency(t *testing.T) { + + var dynamicCount int + + dynamicValue := func(value O.Option[string]) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + dynamicCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) + } + } + + p2 := MakeProvider1(INJ_KEY2, INJ_KEY1.Option(), dynamicValue) + + inj := DIE.MakeInjector(A.From(p2)) + + i2 := Resolve(INJ_KEY2) + + res := IOE.SequenceT2( + i2(inj), + i2(inj), + ) + + r := res() + + assert.True(t, E.IsRight(r)) + assert.Equal(t, 1, dynamicCount) +} + +func TestProviderMissingDependency(t *testing.T) { + + var dynamicCount int + + dynamicValue := func(value string) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + dynamicCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) + } + } + + p2 := MakeProvider1(INJ_KEY2, INJ_KEY1.Identity(), dynamicValue) + + inj := DIE.MakeInjector(A.From(p2)) + + i2 := Resolve(INJ_KEY2) + + res := IOE.SequenceT2( + i2(inj), + i2(inj), + ) + + r := res() + + assert.True(t, E.IsLeft(r)) + assert.Equal(t, 0, dynamicCount) +} + +func TestEagerAndLazyProvider(t *testing.T) { + + var staticCount int + + staticValue := func(value string) func() IOE.IOEither[error, string] { + return func() IOE.IOEither[error, string] { + return func() E.Either[error, string] { + staticCount++ + return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now())) + } + } + } + + var dynamicCount int + + dynamicValue := func(value string) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + dynamicCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) + } + } + + var lazyEagerCount int + + lazyEager := func(laz IOE.IOEither[error, string], eager string) IOE.IOEither[error, string] { + return F.Pipe1( + laz, + IOE.Chain(func(lazValue string) IOE.IOEither[error, string] { + return func() E.Either[error, string] { + lazyEagerCount++ + return E.Of[error](fmt.Sprintf("Dynamic based on [%s], [%s] at [%s]", lazValue, eager, time.Now())) + } + }), + ) + } + + p1 := MakeProvider0(INJ_KEY1, staticValue("Carsten")) + p2 := MakeProvider1(INJ_KEY2, INJ_KEY1.Identity(), dynamicValue) + p3 := MakeProvider2(INJ_KEY3, INJ_KEY2.IOEither(), INJ_KEY1.Identity(), lazyEager) + + inj := DIE.MakeInjector(A.From(p1, p2, p3)) + + i3 := Resolve(INJ_KEY3) + + r := i3(inj)() + + fmt.Println(r) - fmt.Println(p1) + assert.True(t, E.IsRight(r)) + assert.Equal(t, 1, staticCount) + assert.Equal(t, 1, dynamicCount) + assert.Equal(t, 1, lazyEagerCount) } diff --git a/di/token.go b/di/token.go index 1bbc346..96e44d2 100644 --- a/di/token.go +++ b/di/token.go @@ -21,37 +21,56 @@ import ( "sync/atomic" DIE "github.com/IBM/fp-go/di/erasure" + E "github.com/IBM/fp-go/either" IO "github.com/IBM/fp-go/io" IOE "github.com/IBM/fp-go/ioeither" IOO "github.com/IBM/fp-go/iooption" O "github.com/IBM/fp-go/option" ) -type Token[T any] interface { - DIE.Token - ToType(any) O.Option[T] +// Dependency describes the relationship to a service, that has a type and +// a behaviour such as required, option or lazy +type Dependency[T any] interface { + DIE.Dependency + // Unerase converts a value with erased type signature into a strongly typed value + Unerase(val any) E.Either[error, T] } +// InjectionToken uniquely identifies a dependency by giving it an Id, Type and name type InjectionToken[T any] interface { - Token[T] - Option() Token[O.Option[T]] - IOEither() Token[IOE.IOEither[error, T]] - IOOption() Token[IOO.IOOption[T]] + Dependency[T] + // Identity idenifies this dependency as a mandatory, required dependency, it will be resolved eagerly and injected as `T`. + // If the dependency cannot be resolved, the resolution process fails + Identity() Dependency[T] + // Option identifies this dependency as optional, it will be resolved eagerly and injected as `O.Option[T]`. + // If the dependency cannot be resolved, the resolution process continues and the dependency is represented as `O.None[T]` + Option() Dependency[O.Option[T]] + // IOEither identifies this dependency as mandatory but it will be resolved lazily as a `IOE.IOEither[error, T]`. This + // value is memoized to make sure the dependency is a singleton. + // If the dependency cannot be resolved, the resolution process fails + IOEither() Dependency[IOE.IOEither[error, T]] + // IOOption identifies this dependency as optional but it will be resolved lazily as a `IOO.IOOption[T]`. This + // value is memoized to make sure the dependency is a singleton. + // If the dependency cannot be resolved, the resolution process continues and the dependency is represented as the none value. + IOOption() Dependency[IOO.IOOption[T]] } +// makeID creates a generator of unique string IDs func makeId() IO.IO[string] { - var count int64 - return func() string { - return strconv.FormatInt(atomic.AddInt64(&count, 1), 16) - } + var count atomic.Int64 + return IO.MakeIO(func() string { + return strconv.FormatInt(count.Add(1), 16) + }) } +// genId is the common generator of unique string IDs var genId = makeId() type token[T any] struct { - name string - id string - typ DIE.TokenType + name string + id string + typ DIE.TokenType + toType func(val any) E.Either[error, T] } func (t *token[T]) Id() string { @@ -62,44 +81,48 @@ func (t *token[T]) Type() DIE.TokenType { return t.typ } -func (t *token[T]) ToType(value any) O.Option[T] { - return O.ToType[T](value) -} - func (t *token[T]) String() string { return t.name } -func makeToken[T any](name string, id string, typ DIE.TokenType) Token[T] { - return &token[T]{name, id, typ} +func (t *token[T]) Unerase(val any) E.Either[error, T] { + return t.toType(val) +} + +func makeToken[T any](name string, id string, typ DIE.TokenType, unerase func(val any) E.Either[error, T]) Dependency[T] { + return &token[T]{name, id, typ, unerase} } type injectionToken[T any] struct { token[T] - option Token[O.Option[T]] - ioeither Token[IOE.IOEither[error, T]] - iooption Token[IOO.IOOption[T]] + option Dependency[O.Option[T]] + ioeither Dependency[IOE.IOEither[error, T]] + iooption Dependency[IOO.IOOption[T]] +} + +func (i *injectionToken[T]) Identity() Dependency[T] { + return i } -func (i *injectionToken[T]) Option() Token[O.Option[T]] { +func (i *injectionToken[T]) Option() Dependency[O.Option[T]] { return i.option } -func (i *injectionToken[T]) IOEither() Token[IOE.IOEither[error, T]] { +func (i *injectionToken[T]) IOEither() Dependency[IOE.IOEither[error, T]] { return i.ioeither } -func (i *injectionToken[T]) IOOption() Token[IOO.IOOption[T]] { +func (i *injectionToken[T]) IOOption() Dependency[IOO.IOOption[T]] { return i.iooption } -// MakeToken create a unique injection token for a specific type +// MakeToken create a unique `InjectionToken` for a specific type func MakeToken[T any](name string) InjectionToken[T] { id := genId() return &injectionToken[T]{ - token[T]{name, id, DIE.Mandatory}, - makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", name), id, DIE.Option), - makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEither), - makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOption), + 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]()), } } diff --git a/di/utils.go b/di/utils.go new file mode 100644 index 0000000..47f88d8 --- /dev/null +++ b/di/utils.go @@ -0,0 +1,62 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package di + +import ( + E "github.com/IBM/fp-go/either" + "github.com/IBM/fp-go/errors" + F "github.com/IBM/fp-go/function" + IOE "github.com/IBM/fp-go/ioeither" + IOO "github.com/IBM/fp-go/iooption" + O "github.com/IBM/fp-go/option" +) + +// toType converts and 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]] { + 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](), + E.Map[error](O.Of[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]] { + return F.Flow2( + toType[IOE.IOEither[error, any]](), + E.Map[error](IOE.ChainEitherK(toType[T]())), + ) +} + +// 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]] { + return F.Flow2( + toType[IOO.IOOption[any]](), + E.Map[error](IOO.ChainOptionK(F.Flow2( + toType[T](), + E.ToOption[error, T], + ))), + ) +} diff --git a/di/utils_test.go b/di/utils_test.go new file mode 100644 index 0000000..5ed5694 --- /dev/null +++ b/di/utils_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package di + +import ( + "testing" + + E "github.com/IBM/fp-go/either" + F "github.com/IBM/fp-go/function" + IOE "github.com/IBM/fp-go/ioeither" + O "github.com/IBM/fp-go/option" + "github.com/stretchr/testify/assert" +) + +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](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(toType[O.Option[string]]()(O.Of(any("Carsten"))))) +} + +func TestToOptionType(t *testing.T) { + // 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"))))) + // bad cases + assert.False(t, E.IsRight(toOptionType[int]()(any(10)))) + assert.False(t, E.IsRight(toOptionType[int]()(any(O.Of(10))))) +} + +func invokeIOEither[T any](e E.Either[error, IOE.IOEither[error, T]]) E.Either[error, T] { + return F.Pipe1( + e, + E.Chain(func(ioe IOE.IOEither[error, T]) E.Either[error, T] { + return ioe() + }), + ) +} + +func TestToIOEitherType(t *testing.T) { + // 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")))))) + // 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"))))) +} diff --git a/erasure/erasure.go b/erasure/erasure.go index c939592..1ea8383 100644 --- a/erasure/erasure.go +++ b/erasure/erasure.go @@ -35,7 +35,7 @@ func Unerase[T any](t any) T { func SafeUnerase[T any](t any) E.Either[error, T] { return F.Pipe2( t, - E.ToType[*T](errors.OnSome[any]("Value %T is not unerased")), + E.ToType[*T](errors.OnSome[any]("Value of type [%T] is not erased")), E.Map[error](F.Deref[T]), ) } diff --git a/option/option.go b/option/option.go index dc43b57..d451898 100644 --- a/option/option.go +++ b/option/option.go @@ -82,6 +82,10 @@ func Fold[A, B any](onNone func() B, onSome func(a A) B) func(ma Option[A]) B { } } +func MonadGetOrElse[A any](fa Option[A], onNone func() A) A { + return MonadFold(fa, onNone, F.Identity[A]) +} + func GetOrElse[A any](onNone func() A) func(Option[A]) A { return Fold(onNone, F.Identity[A]) } diff --git a/record/generic/record.go b/record/generic/record.go index 1ff59ea..a0d2b2f 100644 --- a/record/generic/record.go +++ b/record/generic/record.go @@ -99,6 +99,14 @@ func Collect[M ~map[K]V, GR ~[]R, K comparable, V, R any](f func(K, V) R) func(M return F.Bind2nd(collect[M, GR, K, V, R], f) } +func CollectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K]) func(f func(K, V) R) func(M) GR { + return func(f func(K, V) R) func(M) GR { + return func(r M) GR { + return collectOrd[M, GR](o, r, f) + } + } +} + func Reduce[M ~map[K]V, K comparable, V, R any](f func(R, V) R, initial R) func(M) R { return func(r M) R { return G.Reduce(r, f, initial) diff --git a/record/record.go b/record/record.go index df4671c..c0c62f0 100644 --- a/record/record.go +++ b/record/record.go @@ -49,6 +49,11 @@ func Collect[K comparable, V, R any](f func(K, V) R) func(map[K]V) []R { return G.Collect[map[K]V, []R](f) } +// CollectOrd applies a collector function to the key value pairs in a map and returns the result as an array +func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(map[K]V) []R { + return G.CollectOrd[map[K]V, []R](o) +} + func Reduce[K comparable, V, R any](f func(R, V) R, initial R) func(map[K]V) R { return G.Reduce[map[K]V](f, initial) } From cc2f8e54e5bb491a088bfc5637c65da698b8d29c Mon Sep 17 00:00:00 2001 From: Carsten Leue Date: Thu, 9 Nov 2023 21:53:55 +0100 Subject: [PATCH 3/7] fix: add multi provider Signed-off-by: Carsten Leue --- di/erasure/injector.go | 76 ++++++++++++++++++++++++++++++++++-------- di/erasure/provider.go | 6 ++-- di/erasure/token.go | 13 +++++--- di/injector.go | 2 +- di/provider.go | 5 +++ di/provider_test.go | 45 +++++++++++++++++++++++++ di/token.go | 54 +++++++++++++++++++++++++++--- di/utils.go | 22 ++++++++---- di/utils_test.go | 43 +++++++++++++++++------- 9 files changed, 222 insertions(+), 44 deletions(-) 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"))))) } From 677392dfceae2a84517ba016da6a475a6a2b45df Mon Sep 17 00:00:00 2001 From: Carsten Leue Date: Fri, 10 Nov 2023 21:44:24 +0100 Subject: [PATCH 4/7] fix: simplify DI implementation Signed-off-by: Carsten Leue --- di/erasure/injector.go | 103 +++++++++-------- di/erasure/provider.go | 244 +++++++++++++++-------------------------- di/erasure/token.go | 23 ++-- di/provider_test.go | 26 +++++ di/token.go | 24 ++-- 5 files changed, 189 insertions(+), 231 deletions(-) diff --git a/di/erasure/injector.go b/di/erasure/injector.go index cc1b809..76626c4 100644 --- a/di/erasure/injector.go +++ b/di/erasure/injector.go @@ -17,8 +17,6 @@ package erasure import ( - "log" - A "github.com/IBM/fp-go/array" "github.com/IBM/fp-go/errors" F "github.com/IBM/fp-go/function" @@ -41,37 +39,61 @@ 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]"), - IOE.Left[any, error], - F.Constant1[InjectableFactory, IOE.IOEither[error, any]], +var ( + // missingProviderError returns a ProviderFactory that fails due to a missing dependency + missingProviderError = F.Flow4( + Dependency.String, + errors.OnSome[string]("no provider for dependency [%s]"), + IOE.Left[any, error], + F.Constant1[InjectableFactory, IOE.IOEither[error, any]], + ) + + emptyMulti any = A.Empty[any]() + + // emptyMultiDependency returns a ProviderFactory for an empty, multi dependency + emptyMultiDependency = F.Constant1[Dependency](F.Constant1[InjectableFactory](IOE.Of[error](emptyMulti))) + + // handleMissingProvider covers the case of a missing provider. It either + // returns an error or an empty multi value provider + handleMissingProvider = F.Flow2( + F.Ternary(isMultiDependency, emptyMultiDependency, missingProviderError), + F.Constant[ProviderFactory], + ) + + // mergeItemProviders is a monoid for item provider factories + mergeItemProviders = R.UnionMonoid[string](A.Semigroup[ProviderFactory]()) + + // mergeProviders is a monoid for provider factories + mergeProviders = R.UnionLastMonoid[string, ProviderFactory]() + + // collectItemProviders create a provider map for item providers + collectItemProviders = F.Flow2( + A.FoldMap[Provider](mergeItemProviders)(itemProviderToMap), + R.Map[string](itemProviderFactory), + ) + + // collectProviders collects non-item providers + collectProviders = F.Flow2( + A.Map(providerToEntry), + R.FromEntries[string, ProviderFactory], + ) + + // assembleProviders constructs the provider map for item and non-item providers + assembleProviders = F.Flow3( + A.Partition(isItemProvider), + T.Map2(collectProviders, collectItemProviders), + T.Tupled2(mergeProviders.Concat), + ) ) -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() { - log.Printf("Exit: [%s] -> [%s]:[%s]", name, token.Id(), token.String()) - } -} - // isMultiDependency tests if a dependency is a container dependency func isMultiDependency(dep Dependency) bool { - return dep.Type() == Multi + return dep.Flag()&Multi == 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 + return provider.Provides().Flag()&Item == Item } // itemProviderFactory combines multiple factories into one, returning an array @@ -85,29 +107,7 @@ func itemProviderFactory(fcts []ProviderFactory) ProviderFactory { } } -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), -) - +// MakeInjector creates an [InjectableFactory] based on a set of [Provider]s func MakeInjector(providers []Provider) InjectableFactory { type Result = IOE.IOEither[error, any] @@ -126,8 +126,6 @@ func MakeInjector(providers []Provider) InjectableFactory { // lazy initialization, so we can cross reference it injFct = func(token Dependency) Result { - defer logEntryExit("inj", token)() - key := token.Id() // according to https://github.com/golang/go/issues/44159 this @@ -135,8 +133,7 @@ func MakeInjector(providers []Provider) InjectableFactory { actual, loaded := resolved.Load(key) if !loaded { - computeResult := func() Result { - defer logEntryExit("computeResult", token)() + computeResult := L.MakeLazy(func() Result { return F.Pipe5( token, T.Replicate2[Dependency], @@ -149,7 +146,7 @@ func MakeInjector(providers []Provider) InjectableFactory { IG.Ap[ProviderFactory](injFct), IOE.Memoize[error, any], ) - } + }) actual, _ = resolved.LoadOrStore(key, F.Pipe1( computeResult, diff --git a/di/erasure/provider.go b/di/erasure/provider.go index f814d34..57847e5 100644 --- a/di/erasure/provider.go +++ b/di/erasure/provider.go @@ -21,6 +21,7 @@ import ( A "github.com/IBM/fp-go/array" E "github.com/IBM/fp-go/either" F "github.com/IBM/fp-go/function" + I "github.com/IBM/fp-go/identity" IO "github.com/IBM/fp-go/io" IOG "github.com/IBM/fp-go/io/generic" IOE "github.com/IBM/fp-go/ioeither" @@ -34,10 +35,14 @@ type InjectableFactory = func(Dependency) IOE.IOEither[error, any] type ProviderFactory = func(InjectableFactory) IOE.IOEither[error, any] type paramIndex = map[int]int +type paramValue = map[int]any +type handler = func(paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] type Provider interface { fmt.Stringer + // Provides returns the [Dependency] implemented by this provider Provides() Dependency + // Factory returns s function that can create an instance of the dependency based on an [InjectableFactory] Factory() ProviderFactory } @@ -62,19 +67,67 @@ func MakeProvider(token Dependency, fct ProviderFactory) Provider { return &provider{token, fct} } -func mapFromToken(idx int, token Dependency) map[TokenType]paramIndex { - return R.Singleton(token.Type(), R.Singleton(idx, idx)) +func mapFromToken(idx int, token Dependency) map[int]paramIndex { + return R.Singleton(token.Flag()&BehaviourMask, R.Singleton(idx, idx)) } -var mergeTokenMaps = R.UnionMonoid[TokenType](R.UnionLastSemigroup[int, int]()) -var foldDeps = A.FoldMapWithIndex[Dependency](mergeTokenMaps)(mapFromToken) - -var lookupIdentity = R.Lookup[paramIndex](Identity) -var lookupOption = R.Lookup[paramIndex](Option) -var lookupIOEither = R.Lookup[paramIndex](IOEither) -var lookupIOOption = R.Lookup[paramIndex](IOOption) +var ( + mergeTokenMaps = R.UnionMonoid[int](R.UnionLastSemigroup[int, int]()) + foldDeps = A.FoldMapWithIndex[Dependency](mergeTokenMaps)(mapFromToken) + mergeMaps = R.UnionLastMonoid[int, any]() + collectParams = R.CollectOrd[any, any](Int.Ord)(F.SK[int, any]) + + handlers = map[int]handler{ + Identity: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { + return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { + return F.Pipe1( + mp, + IOE.TraverseRecord[int](getAt(res)), + ) + } + }, + Option: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { + return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { + return F.Pipe3( + mp, + IOG.TraverseRecord[IO.IO[map[int]E.Either[error, any]], paramIndex](getAt(res)), + IO.Map(R.Map[int](F.Flow2( + E.ToOption[error, any], + F.ToAny[O.Option[any]], + ))), + IOE.FromIO[error, paramValue], + ) + } + }, + IOEither: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { + return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { + return F.Pipe2( + mp, + R.Map[int](F.Flow2( + getAt(res), + F.ToAny[IOE.IOEither[error, any]], + )), + IOE.Of[error, paramValue], + ) + } + }, + IOOption: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { + return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { + return F.Pipe2( + mp, + R.Map[int](F.Flow3( + getAt(res), + IOO.FromIOEither[error, any], + F.ToAny[IOO.IOOption[any]], + )), + IOE.Of[error, paramValue], + ) + } + }, + } +) -type Mapping = map[TokenType]paramIndex +type Mapping = map[int]paramIndex func getAt[T any](ar []T) func(idx int) T { return func(idx int) T { @@ -82,158 +135,41 @@ func getAt[T any](ar []T) func(idx int) T { } } -type identityResult = IOE.IOEither[error, map[int]any] - -func handleIdentity(mp Mapping) func(res []IOE.IOEither[error, any]) identityResult { - - onNone := F.Nullary2(R.Empty[int, any], IOE.Of[error, map[int]any]) - - return func(res []IOE.IOEither[error, any]) identityResult { - return F.Pipe2( - mp, - lookupIdentity, - O.Fold( - onNone, - IOE.TraverseRecord[int](getAt(res)), - ), - ) - } -} - -type optionResult = IO.IO[map[int]O.Option[any]] - -func handleOption(mp Mapping) func(res []IOE.IOEither[error, any]) optionResult { - - onNone := F.Nullary2(R.Empty[int, O.Option[any]], IO.Of[map[int]O.Option[any]]) - - return func(res []IOE.IOEither[error, any]) optionResult { - - return F.Pipe2( - mp, - lookupOption, - O.Fold( - onNone, - F.Flow2( - IOG.TraverseRecord[IO.IO[map[int]E.Either[error, any]], paramIndex](getAt(res)), - IO.Map(R.Map[int](E.ToOption[error, any])), - ), - ), - ) - } -} - -type ioeitherResult = IO.IO[map[int]IOE.IOEither[error, any]] - -func handleIOEither(mp Mapping) func(res []IOE.IOEither[error, any]) ioeitherResult { - - onNone := F.Nullary2(R.Empty[int, IOE.IOEither[error, any]], IO.Of[map[int]IOE.IOEither[error, any]]) - - return func(res []IOE.IOEither[error, any]) ioeitherResult { - - return F.Pipe2( - mp, - lookupIOEither, - O.Fold( - onNone, - F.Flow2( - R.Map[int](getAt(res)), - IO.Of[map[int]IOE.IOEither[error, any]], - ), - ), - ) - } -} - -type iooptionResult = IO.IO[map[int]IOO.IOOption[any]] - -func handleIOOption(mp Mapping) func(res []IOE.IOEither[error, any]) iooptionResult { - - onNone := F.Nullary2(R.Empty[int, IOO.IOOption[any]], IO.Of[map[int]IOO.IOOption[any]]) - - return func(res []IOE.IOEither[error, any]) iooptionResult { +func handleMapping(mp Mapping) func(res []IOE.IOEither[error, any]) IOE.IOEither[error, []any] { + preFct := F.Pipe2( + mp, + R.MapWithIndex(func(idx int, p paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { + return handlers[idx](p) + }), + R.Collect[int](F.SK[int, func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue]]), + ) + doFct := F.Flow2( + I.Flap[IOE.IOEither[error, paramValue], []IOE.IOEither[error, any]], + IOE.TraverseArray[error, func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue], paramValue], + ) + postFct := IOE.Map[error](F.Flow2( + A.Fold(mergeMaps), + collectParams, + )) + return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, []any] { return F.Pipe2( - mp, - lookupIOOption, - O.Fold( - onNone, - F.Flow2( - R.Map[int](F.Flow2( - getAt(res), - IOO.FromIOEither[error, any], - )), - IO.Of[map[int]IOO.IOOption[any]], - ), - ), + preFct, + doFct(res), + postFct, ) } } -var optionMapToAny = R.Map[int](F.ToAny[O.Option[any]]) -var ioeitherMapToAny = R.Map[int](F.ToAny[IOE.IOEither[error, any]]) -var iooptionMapToAny = R.Map[int](F.ToAny[IOO.IOOption[any]]) -var mergeMaps = R.UnionLastMonoid[int, any]() -var collectParams = R.CollectOrd[any, any](Int.Ord)(F.SK[int, any]) - -func mergeArguments( - identity identityResult, - option optionResult, - ioeither ioeitherResult, - iooption iooptionResult, -) IOE.IOEither[error, []any] { - - return F.Pipe2( - A.From( - identity, - F.Pipe2( - option, - IO.Map(optionMapToAny), - IOE.FromIO[error, map[int]any], - ), - F.Pipe2( - ioeither, - IO.Map(ioeitherMapToAny), - IOE.FromIO[error, map[int]any], - ), - F.Pipe2( - iooption, - IO.Map(iooptionMapToAny), - IOE.FromIO[error, map[int]any], - ), - ), - IOE.SequenceArray[error, map[int]any], - IOE.Map[error](F.Flow2( - A.Fold(mergeMaps), - collectParams, - )), - ) -} - +// MakeProviderFactory constructs a [ProviderFactory] based on a set of [Dependency]s and +// a function that accepts the resolved dependencies to return a result func MakeProviderFactory( deps []Dependency, fct func(param ...any) IOE.IOEither[error, any]) ProviderFactory { - mapping := foldDeps(deps) - - identity := handleIdentity(mapping) - optional := handleOption(mapping) - ioeither := handleIOEither(mapping) - iooption := handleIOOption(mapping) - - f := F.Unvariadic0(fct) - - return func(inj InjectableFactory) IOE.IOEither[error, any] { - // resolve all dependencies - resolved := A.MonadMap(deps, inj) - // resolve dependencies - return F.Pipe1( - mergeArguments( - identity(resolved), - optional(resolved), - ioeither(resolved), - iooption(resolved), - ), - IOE.Chain(f), - ) - } + return F.Flow3( + F.Curry2(A.MonadMap[Dependency, IOE.IOEither[error, any]])(deps), + handleMapping(foldDeps(deps)), + IOE.Chain(F.Unvariadic0(fct)), + ) } diff --git a/di/erasure/token.go b/di/erasure/token.go index 3782f5f..750b0a7 100644 --- a/di/erasure/token.go +++ b/di/erasure/token.go @@ -17,17 +17,16 @@ package erasure import "fmt" -type TokenType int - const ( - 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 + BehaviourMask = 0x0f + Identity = 0 // required dependency + Option = 1 // optional dependency + IOEither = 2 // lazy and required + IOOption = 3 // lazy and optional + + TypeMask = 0xf0 + Multi = 1 << 4 // array of implementations + Item = 2 << 4 // item of a multi token ) // Dependency describes the relationship to a service @@ -35,8 +34,8 @@ type Dependency interface { fmt.Stringer // Id returns a unique identifier for a token that can be used as a cache key Id() string - // Type returns a tag that identifies the behaviour of the dependency - Type() TokenType + // Flag returns a tag that identifies the behaviour of the dependency + Flag() int } func AsDependency[T Dependency](t T) Dependency { diff --git a/di/provider_test.go b/di/provider_test.go index 15c47f2..f163194 100644 --- a/di/provider_test.go +++ b/di/provider_test.go @@ -276,3 +276,29 @@ func TestEmptyItemProvider(t *testing.T) { assert.Equal(t, E.Of[error](A.Empty[string]()), value) } + +func TestDependencyOnMultiProvider(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") + + fromMulti := func(val string, multi []string) IOE.IOEither[error, string] { + return IOE.Of[error](fmt.Sprintf("Val: %s, Multi: %s", val, multi)) + } + p3 := MakeProvider2(INJ_KEY3, INJ_KEY1.Identity(), injMulti.Container().Identity(), fromMulti) + + // populate the injector + inj := DIE.MakeInjector(A.From(p1, p2, v1, v2, p3)) + + r3 := Resolve(INJ_KEY3) + + v := r3(inj)() + + assert.Equal(t, E.Of[error]("Val: Value3, Multi: [Value1 Value2]"), v) +} diff --git a/di/token.go b/di/token.go index 0b371ae..57e7095 100644 --- a/di/token.go +++ b/di/token.go @@ -77,7 +77,7 @@ var genId = makeId() type token[T any] struct { name string id string - typ DIE.TokenType + flag int toType func(val any) E.Either[error, T] } @@ -85,8 +85,8 @@ func (t *token[T]) Id() string { return t.id } -func (t *token[T]) Type() DIE.TokenType { - return t.typ +func (t *token[T]) Flag() int { + return t.flag } func (t *token[T]) String() string { @@ -97,7 +97,7 @@ func (t *token[T]) Unerase(val any) E.Either[error, T] { return t.toType(val) } -func makeToken[T any](name string, id string, typ DIE.TokenType, unerase func(val any) E.Either[error, T]) Dependency[T] { +func makeToken[T any](name string, id string, typ int, unerase func(val any) E.Either[error, T]) Dependency[T] { return &token[T]{name, id, typ, unerase} } @@ -157,17 +157,17 @@ func MakeMultiToken[T any](name string) MultiInjectionToken[T] { 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)), + token[[]T]{containerName, id, DIE.Multi | DIE.Identity, toContainer}, + makeToken[O.Option[[]T]](fmt.Sprintf("Option[%s]", containerName), id, DIE.Multi|DIE.Option, toOptionType(toContainer)), + makeToken[IOE.IOEither[error, []T]](fmt.Sprintf("IOEither[%s]", containerName), id, DIE.Multi|DIE.IOEither, toIOEitherType(toContainer)), + makeToken[IOO.IOOption[[]T]](fmt.Sprintf("IOOption[%s]", containerName), id, DIE.Multi|DIE.IOOption, 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)), + token[T]{itemName, id, DIE.Item | DIE.Identity, toItem}, + makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", itemName), id, DIE.Item|DIE.Option, toOptionType(toItem)), + makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", itemName), id, DIE.Item|DIE.IOEither, toIOEitherType(toItem)), + makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", itemName), id, DIE.Item|DIE.IOOption, toIOOptionType(toItem)), } // returns the token return &multiInjectionToken[T]{container, item} From 77e1efe6be289b798ebd9f26a5440649ad4a28eb Mon Sep 17 00:00:00 2001 From: Carsten Leue Date: Fri, 10 Nov 2023 22:13:14 +0100 Subject: [PATCH 5/7] fix: simplify provider Signed-off-by: Carsten Leue --- di/erasure/provider.go | 5 ++--- identity/generic/identity.go | 2 +- record/generic/switch.go | 33 +++++++++++++++++++++++++++++++++ record/switch.go | 25 +++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 record/generic/switch.go create mode 100644 record/switch.go diff --git a/di/erasure/provider.go b/di/erasure/provider.go index 57847e5..6c646f6 100644 --- a/di/erasure/provider.go +++ b/di/erasure/provider.go @@ -136,12 +136,11 @@ func getAt[T any](ar []T) func(idx int) T { } func handleMapping(mp Mapping) func(res []IOE.IOEither[error, any]) IOE.IOEither[error, []any] { - preFct := F.Pipe2( + preFct := F.Pipe1( mp, - R.MapWithIndex(func(idx int, p paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { + R.Collect(func(idx int, p paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { return handlers[idx](p) }), - R.Collect[int](F.SK[int, func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue]]), ) doFct := F.Flow2( I.Flap[IOE.IOEither[error, paramValue], []IOE.IOEither[error, any]], diff --git a/identity/generic/identity.go b/identity/generic/identity.go index 83dba8a..6eb56e2 100644 --- a/identity/generic/identity.go +++ b/identity/generic/identity.go @@ -57,6 +57,6 @@ func MonadFlap[GAB ~func(A) B, A, B any](fab GAB, a A) B { return FC.MonadFlap(MonadMap[func(GAB) B, GAB, B], fab, a) } -func Flap[GAB ~func(A) B, A, B any](a A) func(GAB) B { +func Flap[GAB ~func(A) B, B, A any](a A) func(GAB) B { return F.Bind2nd(MonadFlap[GAB, A, B], a) } diff --git a/record/generic/switch.go b/record/generic/switch.go new file mode 100644 index 0000000..d3966d5 --- /dev/null +++ b/record/generic/switch.go @@ -0,0 +1,33 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package generic + +import ( + F "github.com/IBM/fp-go/function" + IG "github.com/IBM/fp-go/identity/generic" + O "github.com/IBM/fp-go/option" +) + +func Switch[M1 ~map[K]V, M2 ~map[K]R, N ~map[K]FCT, FCT ~func(V) R, K comparable, V, R any](n N, d FCT) func(M1) M2 { + return MapWithIndex[M1, M2](func(idx K, val V) R { + return F.Pipe3( + n, + Lookup[N](idx), + O.GetOrElse(F.Constant(d)), + IG.Flap[FCT, R](val), + ) + }) +} diff --git a/record/switch.go b/record/switch.go new file mode 100644 index 0000000..d43aa92 --- /dev/null +++ b/record/switch.go @@ -0,0 +1,25 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package record + +import ( + G "github.com/IBM/fp-go/record/generic" +) + +// Switch maps every value based on a lookup table of transformation functions +func Switch[K comparable, V, R any](n map[K]func(V) R, d func(V) R) func(map[K]V) map[K]R { + return G.Switch[map[K]V, map[K]R](n, d) +} From 796f627d208e8719171535680665dc24c793bbf1 Mon Sep 17 00:00:00 2001 From: Carsten Leue Date: Sat, 11 Nov 2023 14:00:41 +0100 Subject: [PATCH 6/7] fix: add Switch to function package Signed-off-by: Carsten Leue --- {record => function}/generic/switch.go | 25 ++++++++++--------------- {record => function}/switch.go | 11 ++++++----- 2 files changed, 16 insertions(+), 20 deletions(-) rename {record => function}/generic/switch.go (61%) rename {record => function}/switch.go (66%) diff --git a/record/generic/switch.go b/function/generic/switch.go similarity index 61% rename from record/generic/switch.go rename to function/generic/switch.go index d3966d5..ea2bbbb 100644 --- a/record/generic/switch.go +++ b/function/generic/switch.go @@ -15,19 +15,14 @@ package generic -import ( - F "github.com/IBM/fp-go/function" - IG "github.com/IBM/fp-go/identity/generic" - O "github.com/IBM/fp-go/option" -) - -func Switch[M1 ~map[K]V, M2 ~map[K]R, N ~map[K]FCT, FCT ~func(V) R, K comparable, V, R any](n N, d FCT) func(M1) M2 { - return MapWithIndex[M1, M2](func(idx K, val V) R { - return F.Pipe3( - n, - Lookup[N](idx), - O.GetOrElse(F.Constant(d)), - IG.Flap[FCT, R](val), - ) - }) +// Switch applies a handler to different cases. The handers are stored in a map. A key function +// extracts the case from a value. +func Switch[HF ~func(T) R, N ~map[K]HF, KF ~func(T) K, K comparable, T, R any](kf KF, n N, d HF) HF { + return func(t T) R { + f, ok := n[kf(t)] + if ok { + return f(t) + } + return d(t) + } } diff --git a/record/switch.go b/function/switch.go similarity index 66% rename from record/switch.go rename to function/switch.go index d43aa92..9d68a2a 100644 --- a/record/switch.go +++ b/function/switch.go @@ -13,13 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package record +package function import ( - G "github.com/IBM/fp-go/record/generic" + G "github.com/IBM/fp-go/function/generic" ) -// Switch maps every value based on a lookup table of transformation functions -func Switch[K comparable, V, R any](n map[K]func(V) R, d func(V) R) func(map[K]V) map[K]R { - return G.Switch[map[K]V, map[K]R](n, d) +// Switch applies a handler to different cases. The handers are stored in a map. A key function +// extracts the case from a value. +func Switch[K comparable, T, R any](kf func(T) K, n map[K]func(T) R, d func(T) R) func(T) R { + return G.Switch(kf, n, d) } From 5675893896912160fa51001675182aa7209d33f0 Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Sat, 25 Nov 2023 16:55:37 +0100 Subject: [PATCH 7/7] fix: add DI Signed-off-by: Dr. Carsten Leue --- di/erasure/injector.go | 13 ++++++-- di/erasure/token.go | 12 ++++---- di/injector.go | 2 +- di/provider.go | 67 ++++++++++++++++++++++++++++++++++-------- di/provider_test.go | 47 +++++++++++++++++++++++++++++ di/token.go | 64 +++++++++++++++++++++++++++------------- di/utils.go | 6 ++++ 7 files changed, 169 insertions(+), 42 deletions(-) diff --git a/di/erasure/injector.go b/di/erasure/injector.go index 76626c4..ed7e273 100644 --- a/di/erasure/injector.go +++ b/di/erasure/injector.go @@ -40,7 +40,7 @@ func itemProviderToMap(p Provider) map[string][]ProviderFactory { } var ( - // missingProviderError returns a ProviderFactory that fails due to a missing dependency + // missingProviderError returns a [ProviderFactory] that fails due to a missing dependency missingProviderError = F.Flow4( Dependency.String, errors.OnSome[string]("no provider for dependency [%s]"), @@ -48,15 +48,22 @@ var ( F.Constant1[InjectableFactory, IOE.IOEither[error, any]], ) + // missingProviderErrorOrDefault returns the default [ProviderFactory] or an error + missingProviderErrorOrDefault = F.Flow3( + T.Replicate2[Dependency], + T.Map2(Dependency.ProviderFactory, F.Flow2(missingProviderError, F.Constant[ProviderFactory])), + T.Tupled2(O.MonadGetOrElse[ProviderFactory]), + ) + emptyMulti any = A.Empty[any]() - // emptyMultiDependency returns a ProviderFactory for an empty, multi dependency + // emptyMultiDependency returns a [ProviderFactory] for an empty, multi dependency emptyMultiDependency = F.Constant1[Dependency](F.Constant1[InjectableFactory](IOE.Of[error](emptyMulti))) // handleMissingProvider covers the case of a missing provider. It either // returns an error or an empty multi value provider handleMissingProvider = F.Flow2( - F.Ternary(isMultiDependency, emptyMultiDependency, missingProviderError), + F.Ternary(isMultiDependency, emptyMultiDependency, missingProviderErrorOrDefault), F.Constant[ProviderFactory], ) diff --git a/di/erasure/token.go b/di/erasure/token.go index 750b0a7..d9824cc 100644 --- a/di/erasure/token.go +++ b/di/erasure/token.go @@ -15,7 +15,11 @@ package erasure -import "fmt" +import ( + "fmt" + + O "github.com/IBM/fp-go/option" +) const ( BehaviourMask = 0x0f @@ -36,8 +40,6 @@ type Dependency interface { Id() string // Flag returns a tag that identifies the behaviour of the dependency Flag() int -} - -func AsDependency[T Dependency](t T) Dependency { - return t + // ProviderFactory optionally returns an attached [ProviderFactory] that represents the default for this dependency + ProviderFactory() O.Option[ProviderFactory] } diff --git a/di/injector.go b/di/injector.go index a9ebdb3..4110cf5 100644 --- a/di/injector.go +++ b/di/injector.go @@ -27,7 +27,7 @@ import ( // Resolve performs a type safe resolution of a dependency func Resolve[T any](token InjectionToken[T]) RIOE.ReaderIOEither[DIE.InjectableFactory, error, T] { return F.Flow2( - IG.Ap[DIE.InjectableFactory](DIE.AsDependency(token)), + IG.Ap[DIE.InjectableFactory](asDependency(token)), IOE.ChainEitherK(token.Unerase), ) } diff --git a/di/provider.go b/di/provider.go index 8158311..55104df 100644 --- a/di/provider.go +++ b/di/provider.go @@ -73,19 +73,48 @@ func eraseProviderFactory2[T1, T2 any, R any]( } } +func MakeProviderFactory0[R any]( + fct func() IOE.IOEither[error, R], +) DIE.ProviderFactory { + return DIE.MakeProviderFactory( + A.Empty[DIE.Dependency](), + eraseProviderFactory0(fct), + ) +} + +// MakeTokenWithDefault0 create a unique `InjectionToken` for a specific type with an attached default provider +func MakeTokenWithDefault0[R any](name string, fct func() IOE.IOEither[error, R]) InjectionToken[R] { + return MakeTokenWithDefault[R](name, MakeProviderFactory0(fct)) +} + func MakeProvider0[R any]( token InjectionToken[R], fct func() IOE.IOEither[error, R], ) DIE.Provider { return DIE.MakeProvider( token, - DIE.MakeProviderFactory( - A.Empty[DIE.Dependency](), - eraseProviderFactory0(fct), - ), + MakeProviderFactory0(fct), ) } +func MakeProviderFactory1[T1, R any]( + d1 Dependency[T1], + fct func(T1) IOE.IOEither[error, R], +) DIE.ProviderFactory { + + return DIE.MakeProviderFactory( + A.From[DIE.Dependency](d1), + eraseProviderFactory1(d1, fct), + ) +} + +// MakeTokenWithDefault1 create a unique `InjectionToken` for a specific type with an attached default provider +func MakeTokenWithDefault1[T1, R any](name string, + d1 Dependency[T1], + fct func(T1) IOE.IOEither[error, R]) InjectionToken[R] { + return MakeTokenWithDefault[R](name, MakeProviderFactory1(d1, fct)) +} + func MakeProvider1[T1, R any]( token InjectionToken[R], d1 Dependency[T1], @@ -94,13 +123,30 @@ func MakeProvider1[T1, R any]( return DIE.MakeProvider( token, - DIE.MakeProviderFactory( - A.From[DIE.Dependency](d1), - eraseProviderFactory1(d1, fct), - ), + MakeProviderFactory1(d1, fct), ) } +func MakeProviderFactory2[T1, T2, R any]( + d1 Dependency[T1], + d2 Dependency[T2], + fct func(T1, T2) IOE.IOEither[error, R], +) DIE.ProviderFactory { + + return DIE.MakeProviderFactory( + A.From[DIE.Dependency](d1, d2), + eraseProviderFactory2(d1, d2, fct), + ) +} + +// MakeTokenWithDefault2 create a unique `InjectionToken` for a specific type with an attached default provider +func MakeTokenWithDefault2[T1, T2, R any](name string, + d1 Dependency[T1], + d2 Dependency[T2], + fct func(T1, T2) IOE.IOEither[error, R]) InjectionToken[R] { + return MakeTokenWithDefault[R](name, MakeProviderFactory2(d1, d2, fct)) +} + func MakeProvider2[T1, T2, R any]( token InjectionToken[R], d1 Dependency[T1], @@ -110,10 +156,7 @@ func MakeProvider2[T1, T2, R any]( return DIE.MakeProvider( token, - DIE.MakeProviderFactory( - A.From[DIE.Dependency](d1, d2), - eraseProviderFactory2(d1, d2, fct), - ), + MakeProviderFactory2(d1, d2, fct), ) } diff --git a/di/provider_test.go b/di/provider_test.go index f163194..969237e 100644 --- a/di/provider_test.go +++ b/di/provider_test.go @@ -302,3 +302,50 @@ func TestDependencyOnMultiProvider(t *testing.T) { assert.Equal(t, E.Of[error]("Val: Value3, Multi: [Value1 Value2]"), v) } + +func TestTokenWithDefaultProvider(t *testing.T) { + // token without a default + injToken1 := MakeToken[string]("Token1") + // token with a default + injToken2 := MakeTokenWithDefault0("Token2", F.Constant(IOE.Of[error]("Carsten"))) + // dependency + injToken3 := MakeToken[string]("Token3") + + p3 := MakeProvider1(injToken3, injToken2.Identity(), func(data string) IOE.IOEither[error, string] { + return IOE.Of[error](fmt.Sprintf("Token: %s", data)) + }) + + // populate the injector + inj := DIE.MakeInjector(A.From(p3)) + + // resolving injToken3 should work and use the default provider for injToken2 + r1 := Resolve(injToken1) + r3 := Resolve(injToken3) + + // inj1 should not be available + assert.True(t, E.IsLeft(r1(inj)())) + // r3 should work + assert.Equal(t, E.Of[error]("Token: Carsten"), r3(inj)()) +} + +func TestTokenWithDefaultProviderAndOverride(t *testing.T) { + // token with a default + injToken2 := MakeTokenWithDefault0("Token2", F.Constant(IOE.Of[error]("Carsten"))) + // dependency + injToken3 := MakeToken[string]("Token3") + + p2 := ConstProvider(injToken2, "Override") + + p3 := MakeProvider1(injToken3, injToken2.Identity(), func(data string) IOE.IOEither[error, string] { + return IOE.Of[error](fmt.Sprintf("Token: %s", data)) + }) + + // populate the injector + inj := DIE.MakeInjector(A.From(p2, p3)) + + // resolving injToken3 should work and use the default provider for injToken2 + r3 := Resolve(injToken3) + + // r3 should work + assert.Equal(t, E.Of[error]("Token: Override"), r3(inj)()) +} diff --git a/di/token.go b/di/token.go index 57e7095..2208e4e 100644 --- a/di/token.go +++ b/di/token.go @@ -55,7 +55,7 @@ type InjectionToken[T any] interface { IOOption() Dependency[IOO.IOOption[T]] } -// MultiInjectionToken uniquely identifies a dependency by giving it an Id, Type and name +// 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] @@ -75,10 +75,11 @@ func makeId() IO.IO[string] { var genId = makeId() type token[T any] struct { - name string - id string - flag int - toType func(val any) E.Either[error, T] + name string + id string + flag int + toType func(val any) E.Either[error, T] + providerFactory O.Option[DIE.ProviderFactory] } func (t *token[T]) Id() string { @@ -97,8 +98,12 @@ func (t *token[T]) Unerase(val any) E.Either[error, T] { return t.toType(val) } -func makeToken[T any](name string, id string, typ int, unerase func(val any) E.Either[error, T]) Dependency[T] { - return &token[T]{name, id, typ, unerase} +func (t *token[T]) ProviderFactory() O.Option[DIE.ProviderFactory] { + return t.providerFactory +} + +func makeToken[T any](name string, id string, typ int, unerase func(val any) E.Either[error, T], providerFactory O.Option[DIE.ProviderFactory]) Dependency[T] { + return &token[T]{name, id, typ, unerase, providerFactory} } type injectionToken[T any] struct { @@ -129,6 +134,10 @@ func (i *injectionToken[T]) IOOption() Dependency[IOO.IOOption[T]] { return i.iooption } +func (i *injectionToken[T]) ProviderFactory() O.Option[DIE.ProviderFactory] { + return i.providerFactory +} + func (m *multiInjectionToken[T]) Container() InjectionToken[[]T] { return m.container } @@ -137,37 +146,50 @@ 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] { +// makeToken create a unique `InjectionToken` for a specific type +func makeInjectionToken[T any](name string, providerFactory O.Option[DIE.ProviderFactory]) InjectionToken[T] { id := genId() toIdentity := toType[T]() return &injectionToken[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)), + token[T]{name, id, DIE.Identity, toIdentity, providerFactory}, + makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", name), id, DIE.Option, toOptionType(toIdentity), providerFactory), + makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEither, toIOEitherType(toIdentity), providerFactory), + makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOption, toIOOptionType(toIdentity), providerFactory), } } +// MakeToken create a unique `InjectionToken` for a specific type +func MakeToken[T any](name string) InjectionToken[T] { + return makeInjectionToken[T](name, O.None[DIE.ProviderFactory]()) +} + +// MakeToken create a unique `InjectionToken` for a specific type +func MakeTokenWithDefault[T any](name string, providerFactory DIE.ProviderFactory) InjectionToken[T] { + return makeInjectionToken[T](name, O.Of(providerFactory)) +} + +// MakeMultiToken creates a [MultiInjectionToken] 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) + // empty factory + providerFactory := O.None[DIE.ProviderFactory]() // container container := &injectionToken[[]T]{ - token[[]T]{containerName, id, DIE.Multi | DIE.Identity, toContainer}, - makeToken[O.Option[[]T]](fmt.Sprintf("Option[%s]", containerName), id, DIE.Multi|DIE.Option, toOptionType(toContainer)), - makeToken[IOE.IOEither[error, []T]](fmt.Sprintf("IOEither[%s]", containerName), id, DIE.Multi|DIE.IOEither, toIOEitherType(toContainer)), - makeToken[IOO.IOOption[[]T]](fmt.Sprintf("IOOption[%s]", containerName), id, DIE.Multi|DIE.IOOption, toIOOptionType(toContainer)), + token[[]T]{containerName, id, DIE.Multi | DIE.Identity, toContainer, providerFactory}, + makeToken[O.Option[[]T]](fmt.Sprintf("Option[%s]", containerName), id, DIE.Multi|DIE.Option, toOptionType(toContainer), providerFactory), + makeToken[IOE.IOEither[error, []T]](fmt.Sprintf("IOEither[%s]", containerName), id, DIE.Multi|DIE.IOEither, toIOEitherType(toContainer), providerFactory), + makeToken[IOO.IOOption[[]T]](fmt.Sprintf("IOOption[%s]", containerName), id, DIE.Multi|DIE.IOOption, toIOOptionType(toContainer), providerFactory), } // item item := &injectionToken[T]{ - token[T]{itemName, id, DIE.Item | DIE.Identity, toItem}, - makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", itemName), id, DIE.Item|DIE.Option, toOptionType(toItem)), - makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", itemName), id, DIE.Item|DIE.IOEither, toIOEitherType(toItem)), - makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", itemName), id, DIE.Item|DIE.IOOption, toIOOptionType(toItem)), + token[T]{itemName, id, DIE.Item | DIE.Identity, toItem, providerFactory}, + makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", itemName), id, DIE.Item|DIE.Option, toOptionType(toItem), providerFactory), + makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", itemName), id, DIE.Item|DIE.IOEither, toIOEitherType(toItem), providerFactory), + makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", itemName), id, DIE.Item|DIE.IOOption, toIOOptionType(toItem), providerFactory), } // returns the token return &multiInjectionToken[T]{container, item} diff --git a/di/utils.go b/di/utils.go index e2f7ec3..4d4b73c 100644 --- a/di/utils.go +++ b/di/utils.go @@ -15,6 +15,7 @@ package di import ( + DIE "github.com/IBM/fp-go/di/erasure" E "github.com/IBM/fp-go/either" "github.com/IBM/fp-go/errors" F "github.com/IBM/fp-go/function" @@ -23,6 +24,11 @@ import ( O "github.com/IBM/fp-go/option" ) +// asDependency converts a generic type to a [DIE.Dependency] +func asDependency[T DIE.Dependency](t T) DIE.Dependency { + return 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."))