mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-24 12:57:26 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b55cae265 | ||
|
|
1472fa5a50 | ||
|
|
49deb57d24 | ||
|
|
abb55ddbd0 | ||
|
|
f6b01dffdc | ||
|
|
43b666edbb | ||
|
|
e42d765852 | ||
|
|
d2da8a32b4 | ||
|
|
7484af664b |
@@ -460,8 +460,11 @@ func process() IOResult[string] {
|
||||
- **Either** - Type-safe error handling with left/right values
|
||||
- **Result** - Simplified Either with error as left type (recommended for error handling)
|
||||
- **IO** - Lazy evaluation and side effect management
|
||||
- **IOOption** - Combine IO with Option for optional values with side effects
|
||||
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
|
||||
- **Reader** - Dependency injection pattern
|
||||
- **ReaderOption** - Combine Reader with Option for optional values with dependency injection
|
||||
- **ReaderIOOption** - Combine Reader, IO, and Option for optional values with dependency injection and side effects
|
||||
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
|
||||
- **Array** - Functional array operations
|
||||
- **Record** - Functional record/map operations
|
||||
|
||||
@@ -68,7 +68,7 @@ func ApS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Effect[C, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApS[C](setter, fa)
|
||||
return readerreaderioresult.ApS(setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -76,7 +76,7 @@ func ApSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Effect[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApSL[C](lens, fa)
|
||||
return readerreaderioresult.ApSL(lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -84,7 +84,7 @@ func BindL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f func(T) Effect[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindL[C](lens, f)
|
||||
return readerreaderioresult.BindL(lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -132,7 +132,7 @@ func BindReaderK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f reader.Kleisli[C, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindReaderK[C](setter, f)
|
||||
return readerreaderioresult.BindReaderK(setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -140,7 +140,7 @@ func BindReaderIOK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f readerio.Kleisli[C, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindReaderIOK[C](setter, f)
|
||||
return readerreaderioresult.BindReaderIOK(setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -172,7 +172,7 @@ func BindReaderKL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f reader.Kleisli[C, T, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindReaderKL[C](lens, f)
|
||||
return readerreaderioresult.BindReaderKL(lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -180,7 +180,7 @@ func BindReaderIOKL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f readerio.Kleisli[C, T, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindReaderIOKL[C](lens, f)
|
||||
return readerreaderioresult.BindReaderIOKL(lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -204,7 +204,7 @@ func ApReaderS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Reader[C, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApReaderS[C](setter, fa)
|
||||
return readerreaderioresult.ApReaderS(setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -212,7 +212,7 @@ func ApReaderIOS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderIO[C, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApReaderIOS[C](setter, fa)
|
||||
return readerreaderioresult.ApReaderIOS(setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -244,7 +244,7 @@ func ApReaderSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Reader[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApReaderSL[C](lens, fa)
|
||||
return readerreaderioresult.ApReaderSL(lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -252,7 +252,7 @@ func ApReaderIOSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa ReaderIO[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApReaderIOSL[C](lens, fa)
|
||||
return readerreaderioresult.ApReaderIOSL(lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestBind(t *testing.T) {
|
||||
t.Run("binds effect result to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := Bind[TestContext](
|
||||
eff := Bind(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
@@ -69,7 +69,7 @@ func TestBind(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](30)
|
||||
return Of[TestContext](30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
@@ -83,7 +83,7 @@ func TestBind(t *testing.T) {
|
||||
t.Run("chains multiple binds", func(t *testing.T) {
|
||||
initial := BindState{}
|
||||
|
||||
eff := Bind[TestContext](
|
||||
eff := Bind(
|
||||
func(email string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Email = email
|
||||
@@ -91,9 +91,9 @@ func TestBind(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("alice@example.com")
|
||||
return Of[TestContext]("alice@example.com")
|
||||
},
|
||||
)(Bind[TestContext](
|
||||
)(Bind(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
@@ -101,9 +101,9 @@ func TestBind(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](30)
|
||||
return Of[TestContext](30)
|
||||
},
|
||||
)(Bind[TestContext](
|
||||
)(Bind(
|
||||
func(name string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Name = name
|
||||
@@ -111,7 +111,7 @@ func TestBind(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("Alice")
|
||||
return Of[TestContext]("Alice")
|
||||
},
|
||||
)(Do[TestContext](initial))))
|
||||
|
||||
@@ -127,7 +127,7 @@ func TestBind(t *testing.T) {
|
||||
expectedErr := errors.New("bind error")
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := Bind[TestContext](
|
||||
eff := Bind(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
@@ -182,7 +182,7 @@ func TestLet(t *testing.T) {
|
||||
func(s BindState) string {
|
||||
return s.Name + "@example.com"
|
||||
},
|
||||
)(Bind[TestContext](
|
||||
)(Bind(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
@@ -190,7 +190,7 @@ func TestLet(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](25)
|
||||
return Of[TestContext](25)
|
||||
},
|
||||
)(Do[TestContext](initial)))
|
||||
|
||||
@@ -270,7 +270,7 @@ func TestBindTo(t *testing.T) {
|
||||
|
||||
eff := BindTo[TestContext](func(v int) SimpleState {
|
||||
return SimpleState{Value: v}
|
||||
})(Of[TestContext, int](42))
|
||||
})(Of[TestContext](42))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
@@ -296,7 +296,7 @@ func TestBindTo(t *testing.T) {
|
||||
},
|
||||
)(BindTo[TestContext](func(x int) State {
|
||||
return State{X: x}
|
||||
})(Of[TestContext, int](10)))
|
||||
})(Of[TestContext](10)))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
@@ -309,9 +309,9 @@ func TestBindTo(t *testing.T) {
|
||||
func TestApS(t *testing.T) {
|
||||
t.Run("applies effect and binds result to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
ageEffect := Of[TestContext, int](30)
|
||||
ageEffect := Of[TestContext](30)
|
||||
|
||||
eff := ApS[TestContext](
|
||||
eff := ApS(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
@@ -333,7 +333,7 @@ func TestApS(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
ageEffect := Fail[TestContext, int](expectedErr)
|
||||
|
||||
eff := ApS[TestContext](
|
||||
eff := ApS(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
@@ -388,7 +388,7 @@ func TestBindIOEitherK(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s BindState) ioeither.IOEither[error, int] {
|
||||
return ioeither.Of[error, int](30)
|
||||
return ioeither.Of[error](30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
@@ -411,7 +411,7 @@ func TestBindIOEitherK(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s BindState) ioeither.IOEither[error, int] {
|
||||
return ioeither.Left[int, error](expectedErr)
|
||||
return ioeither.Left[int](expectedErr)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
@@ -434,7 +434,7 @@ func TestBindIOResultK(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s BindState) ioresult.IOResult[int] {
|
||||
return ioresult.Of[int](30)
|
||||
return ioresult.Of(30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
@@ -450,7 +450,7 @@ func TestBindReaderK(t *testing.T) {
|
||||
t.Run("binds Reader operation to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindReaderK[TestContext](
|
||||
eff := BindReaderK(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
@@ -476,7 +476,7 @@ func TestBindReaderIOK(t *testing.T) {
|
||||
t.Run("binds ReaderIO operation to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindReaderIOK[TestContext](
|
||||
eff := BindReaderIOK(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
@@ -512,7 +512,7 @@ func TestBindEitherK(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s BindState) either.Either[error, int] {
|
||||
return either.Of[error, int](30)
|
||||
return either.Of[error](30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
@@ -535,7 +535,7 @@ func TestBindEitherK(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s BindState) either.Either[error, int] {
|
||||
return either.Left[int, error](expectedErr)
|
||||
return either.Left[int](expectedErr)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
@@ -566,9 +566,9 @@ func TestLensOperations(t *testing.T) {
|
||||
|
||||
t.Run("ApSL applies effect using lens", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 25}
|
||||
ageEffect := Of[TestContext, int](30)
|
||||
ageEffect := Of[TestContext](30)
|
||||
|
||||
eff := ApSL[TestContext](ageLens, ageEffect)(Do[TestContext](initial))
|
||||
eff := ApSL(ageLens, ageEffect)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
@@ -580,10 +580,10 @@ func TestLensOperations(t *testing.T) {
|
||||
t.Run("BindL binds effect using lens", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 25}
|
||||
|
||||
eff := BindL[TestContext](
|
||||
eff := BindL(
|
||||
ageLens,
|
||||
func(age int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](age + 5)
|
||||
return Of[TestContext](age + 5)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
@@ -667,7 +667,7 @@ func TestApOperations(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
readerEffect := func(ctx TestContext) int { return 30 }
|
||||
|
||||
eff := ApReaderS[TestContext](
|
||||
eff := ApReaderS(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
@@ -685,7 +685,7 @@ func TestApOperations(t *testing.T) {
|
||||
|
||||
t.Run("ApEitherS applies Either effect", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
eitherEffect := either.Of[error, int](30)
|
||||
eitherEffect := either.Of[error](30)
|
||||
|
||||
eff := ApEitherS[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
@@ -742,7 +742,7 @@ func TestComplexBindChain(t *testing.T) {
|
||||
func(s ComplexState) string {
|
||||
return s.Name + "@example.com"
|
||||
},
|
||||
)(Bind[TestContext](
|
||||
)(Bind(
|
||||
func(age int) func(ComplexState) ComplexState {
|
||||
return func(s ComplexState) ComplexState {
|
||||
s.Age = age
|
||||
@@ -750,11 +750,11 @@ func TestComplexBindChain(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s ComplexState) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](25)
|
||||
return Of[TestContext](25)
|
||||
},
|
||||
)(BindTo[TestContext](func(name string) ComplexState {
|
||||
return ComplexState{Name: name}
|
||||
})(Of[TestContext, string]("Alice"))))))
|
||||
})(Of[TestContext]("Alice"))))))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ type InnerContext struct {
|
||||
func TestLocal(t *testing.T) {
|
||||
t.Run("transforms context for inner effect", func(t *testing.T) {
|
||||
// Create an effect that uses InnerContext
|
||||
innerEffect := Of[InnerContext, string]("result")
|
||||
innerEffect := Of[InnerContext]("result")
|
||||
|
||||
// Transform OuterContext to InnerContext
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
@@ -52,7 +52,7 @@ func TestLocal(t *testing.T) {
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -61,9 +61,9 @@ func TestLocal(t *testing.T) {
|
||||
|
||||
t.Run("allows accessing outer context fields", func(t *testing.T) {
|
||||
// Create an effect that reads from InnerContext
|
||||
innerEffect := Chain[InnerContext](func(_ string) Effect[InnerContext, string] {
|
||||
return Of[InnerContext, string]("inner value")
|
||||
})(Of[InnerContext, string]("start"))
|
||||
innerEffect := Chain(func(_ string) Effect[InnerContext, string] {
|
||||
return Of[InnerContext]("inner value")
|
||||
})(Of[InnerContext]("start"))
|
||||
|
||||
// Transform context
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
@@ -78,7 +78,7 @@ func TestLocal(t *testing.T) {
|
||||
Value: "original",
|
||||
Number: 100,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -100,7 +100,7 @@ func TestLocal(t *testing.T) {
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -119,7 +119,7 @@ func TestLocal(t *testing.T) {
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
level3Effect := Of[Level3, string]("deep result")
|
||||
level3Effect := Of[Level3]("deep result")
|
||||
|
||||
// Transform Level2 -> Level3
|
||||
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
|
||||
@@ -137,7 +137,7 @@ func TestLocal(t *testing.T) {
|
||||
|
||||
// Run with Level1 context
|
||||
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -158,7 +158,7 @@ func TestLocal(t *testing.T) {
|
||||
}
|
||||
|
||||
// Effect that needs only DatabaseConfig
|
||||
dbEffect := Of[DatabaseConfig, string]("connected")
|
||||
dbEffect := Of[DatabaseConfig]("connected")
|
||||
|
||||
// Extract DB config from AppConfig
|
||||
accessor := func(app AppConfig) DatabaseConfig {
|
||||
@@ -178,7 +178,7 @@ func TestLocal(t *testing.T) {
|
||||
APIKey: "secret",
|
||||
Timeout: 30,
|
||||
})(appEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -188,7 +188,7 @@ func TestLocal(t *testing.T) {
|
||||
|
||||
func TestContramap(t *testing.T) {
|
||||
t.Run("is equivalent to Local", func(t *testing.T) {
|
||||
innerEffect := Of[InnerContext, int](42)
|
||||
innerEffect := Of[InnerContext](42)
|
||||
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value}
|
||||
@@ -206,11 +206,11 @@ func TestContramap(t *testing.T) {
|
||||
|
||||
// Run both
|
||||
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
|
||||
localReader := RunSync[int](localIO)
|
||||
localReader := RunSync(localIO)
|
||||
localResult, localErr := localReader(context.Background())
|
||||
|
||||
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
|
||||
contramapReader := RunSync[int](contramapIO)
|
||||
contramapReader := RunSync(contramapIO)
|
||||
contramapResult, contramapErr := contramapReader(context.Background())
|
||||
|
||||
assert.NoError(t, localErr)
|
||||
@@ -219,7 +219,7 @@ func TestContramap(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("transforms context correctly", func(t *testing.T) {
|
||||
innerEffect := Of[InnerContext, string]("success")
|
||||
innerEffect := Of[InnerContext]("success")
|
||||
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value + " modified"}
|
||||
@@ -232,7 +232,7 @@ func TestContramap(t *testing.T) {
|
||||
Value: "original",
|
||||
Number: 50,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -254,7 +254,7 @@ func TestContramap(t *testing.T) {
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -275,7 +275,7 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
effect3 := Of[Config3, string]("result")
|
||||
effect3 := Of[Config3]("result")
|
||||
|
||||
// Use Local for first transformation
|
||||
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
|
||||
@@ -293,7 +293,7 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
|
||||
|
||||
// Run
|
||||
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -312,24 +312,24 @@ func TestLocalEffectK(t *testing.T) {
|
||||
}
|
||||
|
||||
// Effect that needs DatabaseConfig
|
||||
dbEffect := Of[DatabaseConfig, string]("query result")
|
||||
dbEffect := Of[DatabaseConfig]("query result")
|
||||
|
||||
// Transform AppConfig to DatabaseConfig effectfully
|
||||
loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
|
||||
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
|
||||
return Of[AppConfig](DatabaseConfig{
|
||||
ConnectionString: "loaded from " + app.ConfigPath,
|
||||
})
|
||||
}
|
||||
|
||||
// Apply the transformation
|
||||
transform := LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
|
||||
transform := LocalEffectK[string](loadConfig)
|
||||
appEffect := transform(dbEffect)
|
||||
|
||||
// Run with AppConfig
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
ConfigPath: "/etc/app.conf",
|
||||
})(appEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -345,7 +345,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
Path string
|
||||
}
|
||||
|
||||
innerEffect := Of[InnerCtx, string]("success")
|
||||
innerEffect := Of[InnerCtx]("success")
|
||||
|
||||
expectedErr := assert.AnError
|
||||
// Context transformation that fails
|
||||
@@ -353,11 +353,11 @@ func TestLocalEffectK(t *testing.T) {
|
||||
return Fail[OuterCtx, InnerCtx](expectedErr)
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string, InnerCtx, OuterCtx](failingTransform)
|
||||
transform := LocalEffectK[string](failingTransform)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -378,14 +378,14 @@ func TestLocalEffectK(t *testing.T) {
|
||||
|
||||
// Successful context transformation
|
||||
transform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
|
||||
return Of[OuterCtx, InnerCtx](InnerCtx{Value: outer.Path})
|
||||
return Of[OuterCtx](InnerCtx{Value: outer.Path})
|
||||
}
|
||||
|
||||
transformK := LocalEffectK[string, InnerCtx, OuterCtx](transform)
|
||||
transformK := LocalEffectK[string](transform)
|
||||
outerEffect := transformK(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -402,25 +402,25 @@ func TestLocalEffectK(t *testing.T) {
|
||||
}
|
||||
|
||||
// Effect that uses Config
|
||||
configEffect := Chain[Config](func(cfg Config) Effect[Config, string] {
|
||||
return Of[Config, string]("processed: " + cfg.Data)
|
||||
configEffect := Chain(func(cfg Config) Effect[Config, string] {
|
||||
return Of[Config]("processed: " + cfg.Data)
|
||||
})(readerreaderioresult.Ask[Config]())
|
||||
|
||||
// Effectful transformation that simulates loading config
|
||||
loadConfigEffect := func(app AppContext) Effect[AppContext, Config] {
|
||||
// Simulate IO operation (e.g., reading file)
|
||||
return Of[AppContext, Config](Config{
|
||||
return Of[AppContext](Config{
|
||||
Data: "loaded from " + app.ConfigFile,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string, Config, AppContext](loadConfigEffect)
|
||||
transform := LocalEffectK[string](loadConfigEffect)
|
||||
appEffect := transform(configEffect)
|
||||
|
||||
ioResult := Provide[AppContext, string](AppContext{
|
||||
ConfigFile: "config.json",
|
||||
})(appEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -439,16 +439,16 @@ func TestLocalEffectK(t *testing.T) {
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
level3Effect := Of[Level3, string]("deep result")
|
||||
level3Effect := Of[Level3]("deep result")
|
||||
|
||||
// Transform Level2 -> Level3 effectfully
|
||||
transform23 := LocalEffectK[string, Level3, Level2](func(l2 Level2) Effect[Level2, Level3] {
|
||||
return Of[Level2, Level3](Level3{C: l2.B + "-c"})
|
||||
transform23 := LocalEffectK[string](func(l2 Level2) Effect[Level2, Level3] {
|
||||
return Of[Level2](Level3{C: l2.B + "-c"})
|
||||
})
|
||||
|
||||
// Transform Level1 -> Level2 effectfully
|
||||
transform12 := LocalEffectK[string, Level2, Level1](func(l1 Level1) Effect[Level1, Level2] {
|
||||
return Of[Level1, Level2](Level2{B: l1.A + "-b"})
|
||||
transform12 := LocalEffectK[string](func(l1 Level1) Effect[Level1, Level2] {
|
||||
return Of[Level1](Level2{B: l1.A + "-b"})
|
||||
})
|
||||
|
||||
// Compose transformations
|
||||
@@ -457,7 +457,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
|
||||
// Run with Level1 context
|
||||
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -477,8 +477,8 @@ func TestLocalEffectK(t *testing.T) {
|
||||
}
|
||||
|
||||
// Effect that needs DatabaseConfig
|
||||
dbEffect := Chain[DatabaseConfig](func(cfg DatabaseConfig) Effect[DatabaseConfig, string] {
|
||||
return Of[DatabaseConfig, string](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
dbEffect := Chain(func(cfg DatabaseConfig) Effect[DatabaseConfig, string] {
|
||||
return Of[DatabaseConfig](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
})(readerreaderioresult.Ask[DatabaseConfig]())
|
||||
|
||||
// Transform using outer context
|
||||
@@ -488,13 +488,13 @@ func TestLocalEffectK(t *testing.T) {
|
||||
if app.Environment == "prod" {
|
||||
prefix = "prod-"
|
||||
}
|
||||
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
|
||||
return Of[AppConfig](DatabaseConfig{
|
||||
Host: prefix + app.DBHost,
|
||||
Port: app.DBPort,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string, DatabaseConfig, AppConfig](transformWithContext)
|
||||
transform := LocalEffectK[string](transformWithContext)
|
||||
appEffect := transform(dbEffect)
|
||||
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
@@ -502,7 +502,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
DBHost: "localhost",
|
||||
DBPort: 5432,
|
||||
})(appEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -518,31 +518,31 @@ func TestLocalEffectK(t *testing.T) {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
innerEffect := Of[ValidatedConfig, string]("success")
|
||||
innerEffect := Of[ValidatedConfig]("success")
|
||||
|
||||
// Validation that can fail
|
||||
validateConfig := func(raw RawConfig) Effect[RawConfig, ValidatedConfig] {
|
||||
if raw.APIKey == "" {
|
||||
return Fail[RawConfig, ValidatedConfig](assert.AnError)
|
||||
}
|
||||
return Of[RawConfig, ValidatedConfig](ValidatedConfig{
|
||||
return Of[RawConfig](ValidatedConfig{
|
||||
APIKey: raw.APIKey,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string, ValidatedConfig, RawConfig](validateConfig)
|
||||
transform := LocalEffectK[string](validateConfig)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
// Test with invalid config
|
||||
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test with valid config
|
||||
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
|
||||
readerResult2 := RunSync[string](ioResult2)
|
||||
readerResult2 := RunSync(ioResult2)
|
||||
result, err2 := readerResult2(context.Background())
|
||||
|
||||
assert.NoError(t, err2)
|
||||
@@ -561,11 +561,11 @@ func TestLocalEffectK(t *testing.T) {
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
effect3 := Of[Level3, string]("result")
|
||||
effect3 := Of[Level3]("result")
|
||||
|
||||
// Use LocalEffectK for first transformation (effectful)
|
||||
localEffectK23 := LocalEffectK[string, Level3, Level2](func(l2 Level2) Effect[Level2, Level3] {
|
||||
return Of[Level2, Level3](Level3{Info: l2.Data})
|
||||
localEffectK23 := LocalEffectK[string](func(l2 Level2) Effect[Level2, Level3] {
|
||||
return Of[Level2](Level3{Info: l2.Data})
|
||||
})
|
||||
|
||||
// Use Local for second transformation (pure)
|
||||
@@ -579,7 +579,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
|
||||
// Run
|
||||
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -596,22 +596,22 @@ func TestLocalEffectK(t *testing.T) {
|
||||
}
|
||||
|
||||
// Effect that uses InnerCtx
|
||||
innerEffect := Chain[InnerCtx](func(ctx InnerCtx) Effect[InnerCtx, int] {
|
||||
return Of[InnerCtx, int](ctx.Value * 2)
|
||||
innerEffect := Chain(func(ctx InnerCtx) Effect[InnerCtx, int] {
|
||||
return Of[InnerCtx](ctx.Value * 2)
|
||||
})(readerreaderioresult.Ask[InnerCtx]())
|
||||
|
||||
// Complex transformation with nested effects
|
||||
complexTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
|
||||
return Of[OuterCtx, InnerCtx](InnerCtx{
|
||||
return Of[OuterCtx](InnerCtx{
|
||||
Value: outer.Multiplier * 10,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[int, InnerCtx, OuterCtx](complexTransform)
|
||||
transform := LocalEffectK[int](complexTransform)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -31,13 +31,13 @@ type TestContext struct {
|
||||
|
||||
func runEffect[A any](eff Effect[TestContext, A], ctx TestContext) (A, error) {
|
||||
ioResult := Provide[TestContext, A](ctx)(eff)
|
||||
readerResult := RunSync[A](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
return readerResult(context.Background())
|
||||
}
|
||||
|
||||
func TestSucceed(t *testing.T) {
|
||||
t.Run("creates successful effect with value", func(t *testing.T) {
|
||||
eff := Succeed[TestContext, int](42)
|
||||
eff := Succeed[TestContext](42)
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -45,7 +45,7 @@ func TestSucceed(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("creates successful effect with string", func(t *testing.T) {
|
||||
eff := Succeed[TestContext, string]("hello")
|
||||
eff := Succeed[TestContext]("hello")
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -58,7 +58,7 @@ func TestSucceed(t *testing.T) {
|
||||
Age int
|
||||
}
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
eff := Succeed[TestContext, User](user)
|
||||
eff := Succeed[TestContext](user)
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -88,7 +88,7 @@ func TestFail(t *testing.T) {
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
t.Run("lifts value into effect", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](100)
|
||||
eff := Of[TestContext](100)
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -97,8 +97,8 @@ func TestOf(t *testing.T) {
|
||||
|
||||
t.Run("is equivalent to Succeed", func(t *testing.T) {
|
||||
value := "test value"
|
||||
eff1 := Of[TestContext, string](value)
|
||||
eff2 := Succeed[TestContext, string](value)
|
||||
eff1 := Of[TestContext](value)
|
||||
eff2 := Succeed[TestContext](value)
|
||||
|
||||
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
|
||||
result2, err2 := runEffect(eff2, TestContext{Value: "test"})
|
||||
@@ -111,7 +111,7 @@ func TestOf(t *testing.T) {
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("maps over successful effect", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](10)
|
||||
eff := Of[TestContext](10)
|
||||
mapped := Map[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})(eff)
|
||||
@@ -123,7 +123,7 @@ func TestMap(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("maps to different type", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](42)
|
||||
eff := Of[TestContext](42)
|
||||
mapped := Map[TestContext](func(x int) string {
|
||||
return fmt.Sprintf("value: %d", x)
|
||||
})(eff)
|
||||
@@ -148,7 +148,7 @@ func TestMap(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("chains multiple maps", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](5)
|
||||
eff := Of[TestContext](5)
|
||||
result := Map[TestContext](func(x int) int {
|
||||
return x + 1
|
||||
})(Map[TestContext](func(x int) int {
|
||||
@@ -164,9 +164,9 @@ func TestMap(t *testing.T) {
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
t.Run("chains successful effects", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](10)
|
||||
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
eff := Of[TestContext](10)
|
||||
chained := Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(chained, TestContext{Value: "test"})
|
||||
@@ -176,9 +176,9 @@ func TestChain(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("chains to different type", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](42)
|
||||
chained := Chain[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](fmt.Sprintf("number: %d", x))
|
||||
eff := Of[TestContext](42)
|
||||
chained := Chain(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](fmt.Sprintf("number: %d", x))
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(chained, TestContext{Value: "test"})
|
||||
@@ -190,8 +190,8 @@ func TestChain(t *testing.T) {
|
||||
t.Run("propagates first error", func(t *testing.T) {
|
||||
expectedErr := errors.New("first error")
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
chained := Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(chained, TestContext{Value: "test"})
|
||||
@@ -202,8 +202,8 @@ func TestChain(t *testing.T) {
|
||||
|
||||
t.Run("propagates second error", func(t *testing.T) {
|
||||
expectedErr := errors.New("second error")
|
||||
eff := Of[TestContext, int](10)
|
||||
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
eff := Of[TestContext](10)
|
||||
chained := Chain(func(x int) Effect[TestContext, int] {
|
||||
return Fail[TestContext, int](expectedErr)
|
||||
})(eff)
|
||||
|
||||
@@ -214,11 +214,11 @@ func TestChain(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("chains multiple operations", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](5)
|
||||
result := Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x + 10)
|
||||
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
eff := Of[TestContext](5)
|
||||
result := Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x + 10)
|
||||
})(Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
@@ -230,10 +230,10 @@ func TestChain(t *testing.T) {
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("applies function effect to value effect", func(t *testing.T) {
|
||||
fn := Of[TestContext, func(int) int](func(x int) int {
|
||||
fn := Of[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
value := Of[TestContext, int](21)
|
||||
value := Of[TestContext](21)
|
||||
|
||||
result := Ap[int](value)(fn)
|
||||
val, err := runEffect(result, TestContext{Value: "test"})
|
||||
@@ -243,10 +243,10 @@ func TestAp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("applies function to different type", func(t *testing.T) {
|
||||
fn := Of[TestContext, func(int) string](func(x int) string {
|
||||
fn := Of[TestContext](func(x int) string {
|
||||
return fmt.Sprintf("value: %d", x)
|
||||
})
|
||||
value := Of[TestContext, int](42)
|
||||
value := Of[TestContext](42)
|
||||
|
||||
result := Ap[string](value)(fn)
|
||||
val, err := runEffect(result, TestContext{Value: "test"})
|
||||
@@ -258,7 +258,7 @@ func TestAp(t *testing.T) {
|
||||
t.Run("propagates error from function effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("function error")
|
||||
fn := Fail[TestContext, func(int) int](expectedErr)
|
||||
value := Of[TestContext, int](42)
|
||||
value := Of[TestContext](42)
|
||||
|
||||
result := Ap[int](value)(fn)
|
||||
_, err := runEffect(result, TestContext{Value: "test"})
|
||||
@@ -269,7 +269,7 @@ func TestAp(t *testing.T) {
|
||||
|
||||
t.Run("propagates error from value effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("value error")
|
||||
fn := Of[TestContext, func(int) int](func(x int) int {
|
||||
fn := Of[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
value := Fail[TestContext, int](expectedErr)
|
||||
@@ -285,9 +285,9 @@ func TestAp(t *testing.T) {
|
||||
func TestSuspend(t *testing.T) {
|
||||
t.Run("suspends effect computation", func(t *testing.T) {
|
||||
callCount := 0
|
||||
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
|
||||
eff := Suspend(func() Effect[TestContext, int] {
|
||||
callCount++
|
||||
return Of[TestContext, int](42)
|
||||
return Of[TestContext](42)
|
||||
})
|
||||
|
||||
// Effect not executed yet
|
||||
@@ -302,7 +302,7 @@ func TestSuspend(t *testing.T) {
|
||||
|
||||
t.Run("suspends failing effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("suspended error")
|
||||
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
|
||||
eff := Suspend(func() Effect[TestContext, int] {
|
||||
return Fail[TestContext, int](expectedErr)
|
||||
})
|
||||
|
||||
@@ -314,8 +314,8 @@ func TestSuspend(t *testing.T) {
|
||||
|
||||
t.Run("allows lazy evaluation", func(t *testing.T) {
|
||||
var value int
|
||||
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
|
||||
return Of[TestContext, int](value)
|
||||
eff := Suspend(func() Effect[TestContext, int] {
|
||||
return Of[TestContext](value)
|
||||
})
|
||||
|
||||
value = 10
|
||||
@@ -334,8 +334,8 @@ func TestSuspend(t *testing.T) {
|
||||
func TestTap(t *testing.T) {
|
||||
t.Run("executes side effect without changing value", func(t *testing.T) {
|
||||
sideEffectValue := 0
|
||||
eff := Of[TestContext, int](42)
|
||||
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
eff := Of[TestContext](42)
|
||||
tapped := Tap(func(x int) Effect[TestContext, any] {
|
||||
sideEffectValue = x * 2
|
||||
return Of[TestContext, any](nil)
|
||||
})(eff)
|
||||
@@ -350,7 +350,7 @@ func TestTap(t *testing.T) {
|
||||
t.Run("propagates original error", func(t *testing.T) {
|
||||
expectedErr := errors.New("original error")
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
tapped := Tap(func(x int) Effect[TestContext, any] {
|
||||
return Of[TestContext, any](nil)
|
||||
})(eff)
|
||||
|
||||
@@ -362,8 +362,8 @@ func TestTap(t *testing.T) {
|
||||
|
||||
t.Run("propagates tap error", func(t *testing.T) {
|
||||
expectedErr := errors.New("tap error")
|
||||
eff := Of[TestContext, int](42)
|
||||
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
eff := Of[TestContext](42)
|
||||
tapped := Tap(func(x int) Effect[TestContext, any] {
|
||||
return Fail[TestContext, any](expectedErr)
|
||||
})(eff)
|
||||
|
||||
@@ -375,11 +375,11 @@ func TestTap(t *testing.T) {
|
||||
|
||||
t.Run("chains multiple taps", func(t *testing.T) {
|
||||
values := []int{}
|
||||
eff := Of[TestContext, int](10)
|
||||
result := Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
eff := Of[TestContext](10)
|
||||
result := Tap(func(x int) Effect[TestContext, any] {
|
||||
values = append(values, x+2)
|
||||
return Of[TestContext, any](nil)
|
||||
})(Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
})(Tap(func(x int) Effect[TestContext, any] {
|
||||
values = append(values, x+1)
|
||||
return Of[TestContext, any](nil)
|
||||
})(eff))
|
||||
@@ -394,13 +394,13 @@ func TestTap(t *testing.T) {
|
||||
|
||||
func TestTernary(t *testing.T) {
|
||||
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
|
||||
kleisli := Ternary[TestContext, int, string](
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("greater")
|
||||
return Of[TestContext]("greater")
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("less or equal")
|
||||
return Of[TestContext]("less or equal")
|
||||
},
|
||||
)
|
||||
|
||||
@@ -411,13 +411,13 @@ func TestTernary(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
|
||||
kleisli := Ternary[TestContext, int, string](
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("greater")
|
||||
return Of[TestContext]("greater")
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("less or equal")
|
||||
return Of[TestContext]("less or equal")
|
||||
},
|
||||
)
|
||||
|
||||
@@ -429,13 +429,13 @@ func TestTernary(t *testing.T) {
|
||||
|
||||
t.Run("handles errors in onTrue branch", func(t *testing.T) {
|
||||
expectedErr := errors.New("true branch error")
|
||||
kleisli := Ternary[TestContext, int, string](
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("less or equal")
|
||||
return Of[TestContext]("less or equal")
|
||||
},
|
||||
)
|
||||
|
||||
@@ -447,10 +447,10 @@ func TestTernary(t *testing.T) {
|
||||
|
||||
t.Run("handles errors in onFalse branch", func(t *testing.T) {
|
||||
expectedErr := errors.New("false branch error")
|
||||
kleisli := Ternary[TestContext, int, string](
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("greater")
|
||||
return Of[TestContext]("greater")
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
@@ -466,9 +466,9 @@ func TestTernary(t *testing.T) {
|
||||
|
||||
func TestEffectComposition(t *testing.T) {
|
||||
t.Run("composes Map and Chain", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](5)
|
||||
result := Chain[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](fmt.Sprintf("result: %d", x))
|
||||
eff := Of[TestContext](5)
|
||||
result := Chain(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](fmt.Sprintf("result: %d", x))
|
||||
})(Map[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})(eff))
|
||||
@@ -481,12 +481,12 @@ func TestEffectComposition(t *testing.T) {
|
||||
|
||||
t.Run("composes Chain and Tap", func(t *testing.T) {
|
||||
sideEffect := 0
|
||||
eff := Of[TestContext, int](10)
|
||||
result := Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
eff := Of[TestContext](10)
|
||||
result := Tap(func(x int) Effect[TestContext, any] {
|
||||
sideEffect = x
|
||||
return Of[TestContext, any](nil)
|
||||
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})(Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
@@ -499,7 +499,7 @@ func TestEffectComposition(t *testing.T) {
|
||||
|
||||
func TestEffectWithResult(t *testing.T) {
|
||||
t.Run("converts result to effect", func(t *testing.T) {
|
||||
res := result.Of[int](42)
|
||||
res := result.Of(42)
|
||||
// This demonstrates integration with result package
|
||||
assert.True(t, result.IsRight(res))
|
||||
})
|
||||
|
||||
@@ -30,11 +30,11 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
|
||||
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext, string]("Hello")
|
||||
eff2 := Of[TestContext, string](" ")
|
||||
eff3 := Of[TestContext, string]("World")
|
||||
eff1 := Of[TestContext]("Hello")
|
||||
eff2 := Of[TestContext](" ")
|
||||
eff3 := Of[TestContext]("World")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
@@ -49,11 +49,11 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
0,
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
|
||||
effectMonoid := ApplicativeMonoid[TestContext](intMonoid)
|
||||
|
||||
eff1 := Of[TestContext, int](10)
|
||||
eff2 := Of[TestContext, int](20)
|
||||
eff3 := Of[TestContext, int](30)
|
||||
eff1 := Of[TestContext](10)
|
||||
eff2 := Of[TestContext](20)
|
||||
eff3 := Of[TestContext](30)
|
||||
|
||||
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
@@ -68,7 +68,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
"empty",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
|
||||
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
|
||||
|
||||
@@ -83,10 +83,10 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
|
||||
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, string](expectedErr)
|
||||
eff2 := Of[TestContext, string]("World")
|
||||
eff2 := Of[TestContext]("World")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
_, err := runEffect(combined, TestContext{Value: "test"})
|
||||
@@ -102,9 +102,9 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
|
||||
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext, string]("Hello")
|
||||
eff1 := Of[TestContext]("Hello")
|
||||
eff2 := Fail[TestContext, string](expectedErr)
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
@@ -120,13 +120,13 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
1,
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
|
||||
effectMonoid := ApplicativeMonoid[TestContext](intMonoid)
|
||||
|
||||
effects := []Effect[TestContext, int]{
|
||||
Of[TestContext, int](2),
|
||||
Of[TestContext, int](3),
|
||||
Of[TestContext, int](4),
|
||||
Of[TestContext, int](5),
|
||||
Of[TestContext](2),
|
||||
Of[TestContext](3),
|
||||
Of[TestContext](4),
|
||||
Of[TestContext](5),
|
||||
}
|
||||
|
||||
combined := effectMonoid.Empty()
|
||||
@@ -152,11 +152,11 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
Counter{Count: 0},
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, Counter](counterMonoid)
|
||||
effectMonoid := ApplicativeMonoid[TestContext](counterMonoid)
|
||||
|
||||
eff1 := Of[TestContext, Counter](Counter{Count: 5})
|
||||
eff2 := Of[TestContext, Counter](Counter{Count: 10})
|
||||
eff3 := Of[TestContext, Counter](Counter{Count: 15})
|
||||
eff1 := Of[TestContext](Counter{Count: 5})
|
||||
eff2 := Of[TestContext](Counter{Count: 10})
|
||||
eff3 := Of[TestContext](Counter{Count: 15})
|
||||
|
||||
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
@@ -173,10 +173,10 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
|
||||
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext, string]("First")
|
||||
eff2 := Of[TestContext, string]("Second")
|
||||
eff1 := Of[TestContext]("First")
|
||||
eff2 := Of[TestContext]("Second")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
@@ -191,10 +191,10 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
|
||||
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, string](errors.New("first failed"))
|
||||
eff2 := Of[TestContext, string]("Second")
|
||||
eff2 := Of[TestContext]("Second")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
@@ -210,7 +210,7 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
|
||||
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, string](errors.New("first error"))
|
||||
eff2 := Fail[TestContext, string](expectedErr)
|
||||
@@ -228,7 +228,7 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
"default",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
|
||||
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
|
||||
|
||||
@@ -242,12 +242,12 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
0,
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, int](intMonoid)
|
||||
effectMonoid := AlternativeMonoid[TestContext](intMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, int](errors.New("error 1"))
|
||||
eff2 := Fail[TestContext, int](errors.New("error 2"))
|
||||
eff3 := Of[TestContext, int](42)
|
||||
eff4 := Of[TestContext, int](100)
|
||||
eff3 := Of[TestContext](42)
|
||||
eff4 := Of[TestContext](100)
|
||||
|
||||
combined := effectMonoid.Concat(
|
||||
effectMonoid.Concat(eff1, eff2),
|
||||
@@ -273,11 +273,11 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
Result{Value: "", Code: 0},
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, Result](resultMonoid)
|
||||
effectMonoid := AlternativeMonoid[TestContext](resultMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, Result](errors.New("failed"))
|
||||
eff2 := Of[TestContext, Result](Result{Value: "success", Code: 200})
|
||||
eff3 := Of[TestContext, Result](Result{Value: "backup", Code: 201})
|
||||
eff2 := Of[TestContext](Result{Value: "success", Code: 200})
|
||||
eff3 := Of[TestContext](Result{Value: "backup", Code: 201})
|
||||
|
||||
combined := effectMonoid.Concat(effectMonoid.Concat(eff1, eff2), eff3)
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
@@ -295,11 +295,11 @@ func TestMonoidComparison(t *testing.T) {
|
||||
"",
|
||||
)
|
||||
|
||||
applicativeMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
|
||||
alternativeMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
|
||||
applicativeMonoid := ApplicativeMonoid[TestContext](stringMonoid)
|
||||
alternativeMonoid := AlternativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext, string]("A")
|
||||
eff2 := Of[TestContext, string]("B")
|
||||
eff1 := Of[TestContext]("A")
|
||||
eff2 := Of[TestContext]("B")
|
||||
|
||||
// Applicative combines values
|
||||
applicativeResult, err1 := runEffect(
|
||||
@@ -325,11 +325,11 @@ func TestMonoidComparison(t *testing.T) {
|
||||
0,
|
||||
)
|
||||
|
||||
applicativeMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
|
||||
alternativeMonoid := AlternativeMonoid[TestContext, int](intMonoid)
|
||||
applicativeMonoid := ApplicativeMonoid[TestContext](intMonoid)
|
||||
alternativeMonoid := AlternativeMonoid[TestContext](intMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, int](errors.New("error 1"))
|
||||
eff2 := Of[TestContext, int](42)
|
||||
eff2 := Of[TestContext](42)
|
||||
|
||||
// Applicative fails on first error
|
||||
_, err1 := runEffect(
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestRetrying(t *testing.T) {
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
return Of[TestContext, string]("success")
|
||||
return Of[TestContext]("success")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
@@ -59,7 +59,7 @@ func TestRetrying(t *testing.T) {
|
||||
if attemptCount < 3 {
|
||||
return Fail[TestContext, string](errors.New("temporary error"))
|
||||
}
|
||||
return Of[TestContext, string]("success after retries")
|
||||
return Of[TestContext]("success after retries")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
@@ -78,7 +78,7 @@ func TestRetrying(t *testing.T) {
|
||||
maxRetries := uint(3)
|
||||
policy := retry.LimitRetries(maxRetries)
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
eff := Retrying(
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
@@ -103,7 +103,7 @@ func TestRetrying(t *testing.T) {
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, int] {
|
||||
attemptCount++
|
||||
return Of[TestContext, int](42)
|
||||
return Of[TestContext](42)
|
||||
},
|
||||
func(res Result[int]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
@@ -125,7 +125,7 @@ func TestRetrying(t *testing.T) {
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, int] {
|
||||
attemptCount++
|
||||
return Of[TestContext, int](attemptCount * 10)
|
||||
return Of[TestContext](attemptCount * 10)
|
||||
},
|
||||
func(res Result[int]) bool {
|
||||
// Retry if value is less than 30
|
||||
@@ -155,7 +155,7 @@ func TestRetrying(t *testing.T) {
|
||||
if len(statuses) < 3 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, string]("done")
|
||||
return Of[TestContext]("done")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
@@ -188,7 +188,7 @@ func TestRetrying(t *testing.T) {
|
||||
if attemptCount < 3 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, string]("success")
|
||||
return Of[TestContext]("success")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
@@ -218,7 +218,7 @@ func TestRetrying(t *testing.T) {
|
||||
if attemptCount < 2 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, string]("success")
|
||||
return Of[TestContext]("success")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
@@ -250,7 +250,7 @@ func TestRetrying(t *testing.T) {
|
||||
return Fail[TestContext, string](err)
|
||||
}
|
||||
attemptCount++
|
||||
return Of[TestContext, string]("finally succeeded")
|
||||
return Of[TestContext]("finally succeeded")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
@@ -268,7 +268,7 @@ func TestRetrying(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
eff := Retrying(
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
@@ -297,7 +297,7 @@ func TestRetrying(t *testing.T) {
|
||||
if attemptCount < 2 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, string]("success with context")
|
||||
return Of[TestContext]("success with context")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
@@ -335,7 +335,7 @@ func TestRetryingWithComplexScenarios(t *testing.T) {
|
||||
if status.IterNumber < 2 {
|
||||
return Fail[TestContext, State](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, State](state)
|
||||
return Of[TestContext](state)
|
||||
},
|
||||
func(res Result[State]) bool {
|
||||
return result.IsLeft(res)
|
||||
@@ -353,8 +353,8 @@ func TestRetryingWithComplexScenarios(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
eff := Chain[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("final: " + string(rune('0'+x)))
|
||||
eff := Chain(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext]("final: " + string(rune('0'+x)))
|
||||
})(Retrying[TestContext, int](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, int] {
|
||||
@@ -362,7 +362,7 @@ func TestRetryingWithComplexScenarios(t *testing.T) {
|
||||
if attemptCount < 2 {
|
||||
return Fail[TestContext, int](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, int](attemptCount)
|
||||
return Of[TestContext](attemptCount)
|
||||
},
|
||||
func(res Result[int]) bool {
|
||||
return result.IsLeft(res)
|
||||
|
||||
@@ -26,10 +26,10 @@ import (
|
||||
func TestProvide(t *testing.T) {
|
||||
t.Run("provides context to effect", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test-value"}
|
||||
eff := Of[TestContext, string]("result")
|
||||
eff := Of[TestContext]("result")
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -43,10 +43,10 @@ func TestProvide(t *testing.T) {
|
||||
}
|
||||
|
||||
cfg := Config{Host: "localhost", Port: 8080}
|
||||
eff := Of[Config, string]("connected")
|
||||
eff := Of[Config]("connected")
|
||||
|
||||
ioResult := Provide[Config, string](cfg)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -59,7 +59,7 @@ func TestProvide(t *testing.T) {
|
||||
eff := Fail[TestContext, string](expectedErr)
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -72,10 +72,10 @@ func TestProvide(t *testing.T) {
|
||||
}
|
||||
|
||||
ctx := SimpleContext{ID: 42}
|
||||
eff := Of[SimpleContext, int](100)
|
||||
eff := Of[SimpleContext](100)
|
||||
|
||||
ioResult := Provide[SimpleContext, int](ctx)(eff)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -85,12 +85,12 @@ func TestProvide(t *testing.T) {
|
||||
t.Run("provides context to chained effects", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "base"}
|
||||
|
||||
eff := Chain[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("result")
|
||||
})(Of[TestContext, int](42))
|
||||
eff := Chain(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext]("result")
|
||||
})(Of[TestContext](42))
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -102,10 +102,10 @@ func TestProvide(t *testing.T) {
|
||||
|
||||
eff := Map[TestContext](func(x int) string {
|
||||
return "mapped"
|
||||
})(Of[TestContext, int](42))
|
||||
})(Of[TestContext](42))
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -116,10 +116,10 @@ func TestProvide(t *testing.T) {
|
||||
func TestRunSync(t *testing.T) {
|
||||
t.Run("runs effect synchronously", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext, int](42)
|
||||
eff := Of[TestContext](42)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -128,10 +128,10 @@ func TestRunSync(t *testing.T) {
|
||||
|
||||
t.Run("runs effect with context.Context", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext, string]("hello")
|
||||
eff := Of[TestContext]("hello")
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
|
||||
bgCtx := context.Background()
|
||||
result, err := readerResult(bgCtx)
|
||||
@@ -146,7 +146,7 @@ func TestRunSync(t *testing.T) {
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -156,14 +156,14 @@ func TestRunSync(t *testing.T) {
|
||||
t.Run("runs complex effect chains", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
|
||||
eff := Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x + 10)
|
||||
})(Of[TestContext, int](5)))
|
||||
eff := Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x + 10)
|
||||
})(Of[TestContext](5)))
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -172,10 +172,10 @@ func TestRunSync(t *testing.T) {
|
||||
|
||||
t.Run("handles multiple sequential runs", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext, int](42)
|
||||
eff := Of[TestContext](42)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
|
||||
// Run multiple times
|
||||
result1, err1 := readerResult(context.Background())
|
||||
@@ -198,10 +198,10 @@ func TestRunSync(t *testing.T) {
|
||||
|
||||
ctx := TestContext{Value: "test"}
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
eff := Of[TestContext, User](user)
|
||||
eff := Of[TestContext](user)
|
||||
|
||||
ioResult := Provide[TestContext, User](ctx)(eff)
|
||||
readerResult := RunSync[User](ioResult)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -219,10 +219,10 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
cfg := AppConfig{APIKey: "secret", Timeout: 30}
|
||||
|
||||
// Create an effect that uses the config
|
||||
eff := Of[AppConfig, string]("API call successful")
|
||||
eff := Of[AppConfig]("API call successful")
|
||||
|
||||
// Provide config and run
|
||||
result, err := RunSync[string](Provide[AppConfig, string](cfg)(eff))(context.Background())
|
||||
result, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "API call successful", result)
|
||||
@@ -238,7 +238,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
|
||||
eff := Fail[AppConfig, string](expectedErr)
|
||||
|
||||
_, err := RunSync[string](Provide[AppConfig, string](cfg)(eff))(context.Background())
|
||||
_, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
@@ -249,11 +249,11 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
|
||||
eff := Map[TestContext](func(x int) string {
|
||||
return "final"
|
||||
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})(Of[TestContext, int](21)))
|
||||
})(Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(Of[TestContext](21)))
|
||||
|
||||
result, err := RunSync[string](Provide[TestContext, string](ctx)(eff))(context.Background())
|
||||
result, err := RunSync(Provide[TestContext, string](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "final", result)
|
||||
@@ -267,7 +267,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
|
||||
ctx := TestContext{Value: "test"}
|
||||
|
||||
eff := Bind[TestContext](
|
||||
eff := Bind(
|
||||
func(y int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Y = y
|
||||
@@ -275,13 +275,13 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
}
|
||||
},
|
||||
func(s State) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](s.X * 2)
|
||||
return Of[TestContext](s.X * 2)
|
||||
},
|
||||
)(BindTo[TestContext](func(x int) State {
|
||||
return State{X: x}
|
||||
})(Of[TestContext, int](10)))
|
||||
})(Of[TestContext](10)))
|
||||
|
||||
result, err := RunSync[State](Provide[TestContext, State](ctx)(eff))(context.Background())
|
||||
result, err := RunSync(Provide[TestContext, State](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result.X)
|
||||
@@ -297,14 +297,14 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
}
|
||||
|
||||
outerCtx := OuterCtx{Value: "outer"}
|
||||
innerEff := Of[InnerCtx, string]("inner result")
|
||||
innerEff := Of[InnerCtx]("inner result")
|
||||
|
||||
// Transform context
|
||||
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
|
||||
return InnerCtx{Data: outer.Value + "-transformed"}
|
||||
})(innerEff)
|
||||
|
||||
result, err := RunSync[string](Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
|
||||
result, err := RunSync(Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "inner result", result)
|
||||
@@ -314,11 +314,11 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
|
||||
eff := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
eff := TraverseArray(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(input)
|
||||
|
||||
result, err := RunSync[[]int](Provide[TestContext, []int](ctx)(eff))(context.Background())
|
||||
result, err := RunSync(Provide[TestContext, []int](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
func TestTraverseArray(t *testing.T) {
|
||||
t.Run("traverses empty array", func(t *testing.T) {
|
||||
input := []int{}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
@@ -39,8 +39,8 @@ func TestTraverseArray(t *testing.T) {
|
||||
|
||||
t.Run("traverses array with single element", func(t *testing.T) {
|
||||
input := []int{42}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
@@ -51,8 +51,8 @@ func TestTraverseArray(t *testing.T) {
|
||||
|
||||
t.Run("traverses array with multiple elements", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
@@ -63,8 +63,8 @@ func TestTraverseArray(t *testing.T) {
|
||||
|
||||
t.Run("transforms to different type", func(t *testing.T) {
|
||||
input := []string{"hello", "world", "test"}
|
||||
kleisli := TraverseArray[TestContext](func(s string) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](len(s))
|
||||
kleisli := TraverseArray(func(s string) Effect[TestContext, int] {
|
||||
return Of[TestContext](len(s))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
@@ -76,11 +76,11 @@ func TestTraverseArray(t *testing.T) {
|
||||
t.Run("stops on first error", func(t *testing.T) {
|
||||
expectedErr := errors.New("traverse error")
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
if x == 3 {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
}
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
@@ -96,8 +96,8 @@ func TestTraverseArray(t *testing.T) {
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3}
|
||||
kleisli := TraverseArray[TestContext](func(id int) Effect[TestContext, User] {
|
||||
return Of[TestContext, User](User{
|
||||
kleisli := TraverseArray(func(id int) Effect[TestContext, User] {
|
||||
return Of[TestContext](User{
|
||||
ID: id,
|
||||
Name: fmt.Sprintf("User%d", id),
|
||||
})
|
||||
@@ -118,15 +118,15 @@ func TestTraverseArray(t *testing.T) {
|
||||
t.Run("chains with other operations", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
eff := Chain[TestContext](func(strings []string) Effect[TestContext, int] {
|
||||
eff := Chain(func(strings []string) Effect[TestContext, int] {
|
||||
total := 0
|
||||
for _, s := range strings {
|
||||
val, _ := strconv.Atoi(s)
|
||||
total += val
|
||||
}
|
||||
return Of[TestContext, int](total)
|
||||
})(TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x * 2))
|
||||
return Of[TestContext](total)
|
||||
})(TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x * 2))
|
||||
})(input))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
@@ -137,10 +137,10 @@ func TestTraverseArray(t *testing.T) {
|
||||
|
||||
t.Run("uses context in transformation", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Chain[TestContext](func(ctx TestContext) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](fmt.Sprintf("%s-%d", ctx.Value, x))
|
||||
})(Of[TestContext, TestContext](TestContext{Value: "prefix"}))
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Chain(func(ctx TestContext) Effect[TestContext, string] {
|
||||
return Of[TestContext](fmt.Sprintf("%s-%d", ctx.Value, x))
|
||||
})(Of[TestContext](TestContext{Value: "prefix"}))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
@@ -151,8 +151,8 @@ func TestTraverseArray(t *testing.T) {
|
||||
|
||||
t.Run("preserves order", func(t *testing.T) {
|
||||
input := []int{5, 3, 8, 1, 9, 2}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 10)
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 10)
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
@@ -168,8 +168,8 @@ func TestTraverseArray(t *testing.T) {
|
||||
input[i] = i
|
||||
}
|
||||
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
@@ -184,16 +184,16 @@ func TestTraverseArray(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
// First traversal: int -> string
|
||||
kleisli1 := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
kleisli1 := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
// Second traversal: string -> int (length)
|
||||
kleisli2 := TraverseArray[TestContext](func(s string) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](len(s))
|
||||
kleisli2 := TraverseArray(func(s string) Effect[TestContext, int] {
|
||||
return Of[TestContext](len(s))
|
||||
})
|
||||
|
||||
eff := Chain[TestContext](kleisli2)(kleisli1(input))
|
||||
eff := Chain(kleisli2)(kleisli1(input))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
@@ -203,8 +203,8 @@ func TestTraverseArray(t *testing.T) {
|
||||
|
||||
t.Run("handles nil array", func(t *testing.T) {
|
||||
var input []int
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
@@ -222,8 +222,8 @@ func TestTraverseArray(t *testing.T) {
|
||||
result += s + ","
|
||||
}
|
||||
return result
|
||||
})(TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
})(TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})(input))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
@@ -235,11 +235,11 @@ func TestTraverseArray(t *testing.T) {
|
||||
t.Run("error in middle of array", func(t *testing.T) {
|
||||
expectedErr := errors.New("middle error")
|
||||
input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
if x == 5 {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
}
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
@@ -251,11 +251,11 @@ func TestTraverseArray(t *testing.T) {
|
||||
t.Run("error at end of array", func(t *testing.T) {
|
||||
expectedErr := errors.New("end error")
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
if x == 5 {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
}
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
@@ -188,6 +188,81 @@ func MonadChain[E, A, B any](fa Either[E, A], f Kleisli[E, A, B]) Either[E, B] {
|
||||
return f(fa.r)
|
||||
}
|
||||
|
||||
// MonadChainLeft sequences a computation on the Left (error) value, allowing error recovery or transformation.
|
||||
// If the Either is Left, applies the provided function to the error value, which returns a new Either.
|
||||
// If the Either is Right, returns the Right value unchanged with the new error type.
|
||||
//
|
||||
// This is the dual of [MonadChain] - while MonadChain operates on Right values (success),
|
||||
// MonadChainLeft operates on Left values (errors). It's useful for error recovery, error transformation,
|
||||
// or chaining alternative computations when an error occurs.
|
||||
//
|
||||
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// The error type can be transformed from EA to EB, allowing flexible error type conversions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Error recovery: convert specific errors to success
|
||||
// result := either.MonadChainLeft(
|
||||
// either.Left[int](errors.New("not found")),
|
||||
// func(err error) either.Either[string, int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return either.Right[string](0) // default value
|
||||
// }
|
||||
// return either.Left[int](err.Error()) // transform error
|
||||
// },
|
||||
// ) // Right(0)
|
||||
//
|
||||
// // Error transformation: change error type
|
||||
// result := either.MonadChainLeft(
|
||||
// either.Left[int](404),
|
||||
// func(code int) either.Either[string, int] {
|
||||
// return either.Left[int](fmt.Sprintf("Error code: %d", code))
|
||||
// },
|
||||
// ) // Left("Error code: 404")
|
||||
//
|
||||
// // Right values pass through unchanged
|
||||
// result := either.MonadChainLeft(
|
||||
// either.Right[error](42),
|
||||
// func(err error) either.Either[string, int] {
|
||||
// return either.Left[int]("error")
|
||||
// },
|
||||
// ) // Right(42)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[EA, EB, A any](fa Either[EA, A], f Kleisli[EB, EA, A]) Either[EB, A] {
|
||||
return MonadFold(fa, f, Of[EB])
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// Returns a function that sequences a computation on the Left (error) value.
|
||||
//
|
||||
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for creating reusable error handlers or transformers that can be
|
||||
// composed with other Either operations using pipes or function composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a reusable error handler
|
||||
// handleNotFound := either.ChainLeft[error, string](func(err error) either.Either[string, int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return either.Right[string](0)
|
||||
// }
|
||||
// return either.Left[int](err.Error())
|
||||
// })
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// result := F.Pipe1(
|
||||
// either.Left[int](errors.New("not found")),
|
||||
// handleNotFound,
|
||||
// ) // Right(0)
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[EA, EB, A any](f Kleisli[EB, EA, A]) Kleisli[EB, Either[EA, A], A] {
|
||||
return Fold(f, Of[EB])
|
||||
}
|
||||
|
||||
// MonadChainFirst executes a side-effect computation but returns the original value.
|
||||
// Useful for performing actions (like logging) without changing the value.
|
||||
//
|
||||
@@ -471,6 +546,8 @@ func Alt[E, A any](that Lazy[Either[E, A]]) Operator[E, A, A] {
|
||||
// If the Either is Left, it applies the provided function to the error value,
|
||||
// which returns a new Either that replaces the original.
|
||||
//
|
||||
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
|
||||
@@ -124,79 +124,52 @@ func TestStringer(t *testing.T) {
|
||||
func TestZeroWithIntegers(t *testing.T) {
|
||||
e := Zero[error, int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, 0, value, "Right value should be zero for int")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](0), e, "Zero should create a Right value with zero for int")
|
||||
}
|
||||
|
||||
// TestZeroWithStrings tests Zero function with string types
|
||||
func TestZeroWithStrings(t *testing.T) {
|
||||
e := Zero[error, string]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, "", value, "Right value should be empty string")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](""), e, "Zero should create a Right value with empty string")
|
||||
}
|
||||
|
||||
// TestZeroWithBooleans tests Zero function with boolean types
|
||||
func TestZeroWithBooleans(t *testing.T) {
|
||||
e := Zero[error, bool]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, false, value, "Right value should be false for bool")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](false), e, "Zero should create a Right value with false for bool")
|
||||
}
|
||||
|
||||
// TestZeroWithFloats tests Zero function with float types
|
||||
func TestZeroWithFloats(t *testing.T) {
|
||||
e := Zero[error, float64]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, 0.0, value, "Right value should be 0.0 for float64")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](0.0), e, "Zero should create a Right value with 0.0 for float64")
|
||||
}
|
||||
|
||||
// TestZeroWithPointers tests Zero function with pointer types
|
||||
func TestZeroWithPointers(t *testing.T) {
|
||||
e := Zero[error, *int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for pointer type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilPtr *int
|
||||
assert.Equal(t, Of[error](nilPtr), e, "Zero should create a Right value with nil pointer")
|
||||
}
|
||||
|
||||
// TestZeroWithSlices tests Zero function with slice types
|
||||
func TestZeroWithSlices(t *testing.T) {
|
||||
e := Zero[error, []int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for slice type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilSlice []int
|
||||
assert.Equal(t, Of[error](nilSlice), e, "Zero should create a Right value with nil slice")
|
||||
}
|
||||
|
||||
// TestZeroWithMaps tests Zero function with map types
|
||||
func TestZeroWithMaps(t *testing.T) {
|
||||
e := Zero[error, map[string]int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for map type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilMap map[string]int
|
||||
assert.Equal(t, Of[error](nilMap), e, "Zero should create a Right value with nil map")
|
||||
}
|
||||
|
||||
// TestZeroWithStructs tests Zero function with struct types
|
||||
@@ -208,23 +181,16 @@ func TestZeroWithStructs(t *testing.T) {
|
||||
|
||||
e := Zero[error, TestStruct]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
expected := TestStruct{Field1: 0, Field2: ""}
|
||||
assert.Equal(t, expected, value, "Right value should be zero value for struct")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](expected), e, "Zero should create a Right value with zero value for struct")
|
||||
}
|
||||
|
||||
// TestZeroWithInterfaces tests Zero function with interface types
|
||||
func TestZeroWithInterfaces(t *testing.T) {
|
||||
e := Zero[error, interface{}]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for interface type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilInterface interface{}
|
||||
assert.Equal(t, Of[error](nilInterface), e, "Zero should create a Right value with nil interface")
|
||||
}
|
||||
|
||||
// TestZeroWithCustomErrorType tests Zero function with custom error types
|
||||
@@ -236,12 +202,7 @@ func TestZeroWithCustomErrorType(t *testing.T) {
|
||||
|
||||
e := Zero[CustomError, string]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, "", value, "Right value should be empty string")
|
||||
assert.Equal(t, CustomError{Code: 0, Message: ""}, err, "Error should be zero value for CustomError")
|
||||
assert.Equal(t, Of[CustomError](""), e, "Zero should create a Right value with empty string")
|
||||
}
|
||||
|
||||
// TestZeroCanBeUsedWithOtherFunctions tests that Zero Eithers work with other either functions
|
||||
@@ -252,17 +213,13 @@ func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
|
||||
mapped := MonadMap(e, func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
})
|
||||
assert.True(t, IsRight(mapped), "Mapped Zero should still be Right")
|
||||
value, _ := Unwrap(mapped)
|
||||
assert.Equal(t, "0", value, "Mapped value should be '0'")
|
||||
assert.Equal(t, Of[error]("0"), mapped, "Mapped Zero should be Right with '0'")
|
||||
|
||||
// Test with Chain
|
||||
chained := MonadChain(e, func(n int) Either[error, string] {
|
||||
return Right[error](fmt.Sprintf("value: %d", n))
|
||||
})
|
||||
assert.True(t, IsRight(chained), "Chained Zero should still be Right")
|
||||
chainedValue, _ := Unwrap(chained)
|
||||
assert.Equal(t, "value: 0", chainedValue, "Chained value should be 'value: 0'")
|
||||
assert.Equal(t, Of[error]("value: 0"), chained, "Chained Zero should be Right with 'value: 0'")
|
||||
|
||||
// Test with Fold
|
||||
folded := MonadFold(e,
|
||||
@@ -295,23 +252,15 @@ func TestZeroWithComplexTypes(t *testing.T) {
|
||||
|
||||
e := Zero[error, ComplexType]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
expected := ComplexType{Nested: nil, Ptr: nil}
|
||||
assert.Equal(t, expected, value, "Right value should be zero value for complex struct")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](expected), e, "Zero should create a Right value with zero value for complex struct")
|
||||
}
|
||||
|
||||
// TestZeroWithOption tests Zero with Option type
|
||||
func TestZeroWithOption(t *testing.T) {
|
||||
e := Zero[error, O.Option[int]]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.True(t, O.IsNone(value), "Right value should be None for Option type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](O.None[int]()), e, "Zero should create a Right value with None option")
|
||||
}
|
||||
|
||||
// TestZeroIsNotLeft tests that Zero never creates a Left value
|
||||
@@ -343,3 +292,211 @@ func TestZeroEqualsDefaultInitialization(t *testing.T) {
|
||||
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
|
||||
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
|
||||
}
|
||||
|
||||
// TestMonadChainLeft tests the MonadChainLeft function with various scenarios
|
||||
func TestMonadChainLeft(t *testing.T) {
|
||||
t.Run("Left value is transformed by function", func(t *testing.T) {
|
||||
// Transform error to success
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("not found")),
|
||||
func(err error) Either[string, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[string](0) // default value
|
||||
}
|
||||
return Left[int](err.Error())
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
})
|
||||
|
||||
t.Run("Left value error type is transformed", func(t *testing.T) {
|
||||
// Transform error type from int to string
|
||||
result := MonadChainLeft(
|
||||
Left[int](404),
|
||||
func(code int) Either[string, int] {
|
||||
return Left[int](fmt.Sprintf("Error code: %d", code))
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Left[int]("Error code: 404"), result)
|
||||
})
|
||||
|
||||
t.Run("Right value passes through unchanged", func(t *testing.T) {
|
||||
// Right value should not be affected
|
||||
result := MonadChainLeft(
|
||||
Right[error](42),
|
||||
func(err error) Either[string, int] {
|
||||
return Left[int]("should not be called")
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("Chain multiple error transformations", func(t *testing.T) {
|
||||
// First transformation
|
||||
step1 := MonadChainLeft(
|
||||
Left[int](errors.New("error1")),
|
||||
func(err error) Either[error, int] {
|
||||
return Left[int](errors.New("error2"))
|
||||
},
|
||||
)
|
||||
// Second transformation
|
||||
step2 := MonadChainLeft(
|
||||
step1,
|
||||
func(err error) Either[string, int] {
|
||||
return Left[int](err.Error())
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Left[int]("error2"), step2)
|
||||
})
|
||||
|
||||
t.Run("Error recovery with fallback", func(t *testing.T) {
|
||||
// Recover from specific errors
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("timeout")),
|
||||
func(err error) Either[error, int] {
|
||||
if err.Error() == "timeout" {
|
||||
return Right[error](999) // fallback value
|
||||
}
|
||||
return Left[int](err)
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of[error](999), result)
|
||||
})
|
||||
|
||||
t.Run("Transform error to different Left", func(t *testing.T) {
|
||||
// Transform one error to another
|
||||
result := MonadChainLeft(
|
||||
Left[string]("original error"),
|
||||
func(s string) Either[int, string] {
|
||||
return Left[string](len(s))
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Left[string](14), result) // length of "original error"
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainLeft tests the curried ChainLeft function
|
||||
func TestChainLeft(t *testing.T) {
|
||||
t.Run("Curried function transforms Left value", func(t *testing.T) {
|
||||
// Create a reusable error handler
|
||||
handleNotFound := ChainLeft[error, string](func(err error) Either[string, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[string](0)
|
||||
}
|
||||
return Left[int](err.Error())
|
||||
})
|
||||
|
||||
result := handleNotFound(Left[int](errors.New("not found")))
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
})
|
||||
|
||||
t.Run("Curried function with Right value", func(t *testing.T) {
|
||||
handler := ChainLeft[error, string](func(err error) Either[string, int] {
|
||||
return Left[int]("should not be called")
|
||||
})
|
||||
|
||||
result := handler(Right[error](42))
|
||||
assert.Equal(t, Of[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("Use in pipeline with Pipe", func(t *testing.T) {
|
||||
// Create error transformer
|
||||
toStringError := ChainLeft[int, string](func(code int) Either[string, string] {
|
||||
return Left[string](fmt.Sprintf("Error: %d", code))
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[string](404),
|
||||
toStringError,
|
||||
)
|
||||
assert.Equal(t, Left[string]("Error: 404"), result)
|
||||
})
|
||||
|
||||
t.Run("Compose multiple ChainLeft operations", func(t *testing.T) {
|
||||
// First handler: convert error to string
|
||||
handler1 := ChainLeft[error, string](func(err error) Either[string, int] {
|
||||
return Left[int](err.Error())
|
||||
})
|
||||
|
||||
// Second handler: add prefix to string error
|
||||
handler2 := ChainLeft[string, string](func(s string) Either[string, int] {
|
||||
return Left[int]("Handled: " + s)
|
||||
})
|
||||
|
||||
result := F.Pipe2(
|
||||
Left[int](errors.New("original")),
|
||||
handler1,
|
||||
handler2,
|
||||
)
|
||||
assert.Equal(t, Left[int]("Handled: original"), result)
|
||||
})
|
||||
|
||||
t.Run("Error recovery in pipeline", func(t *testing.T) {
|
||||
// Handler that recovers from specific errors
|
||||
recoverFromTimeout := ChainLeft(func(err error) Either[error, int] {
|
||||
if err.Error() == "timeout" {
|
||||
return Right[error](0) // recovered value
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
|
||||
// Test with timeout error
|
||||
result1 := F.Pipe1(
|
||||
Left[int](errors.New("timeout")),
|
||||
recoverFromTimeout,
|
||||
)
|
||||
assert.Equal(t, Of[error](0), result1)
|
||||
|
||||
// Test with other error
|
||||
result2 := F.Pipe1(
|
||||
Left[int](errors.New("other error")),
|
||||
recoverFromTimeout,
|
||||
)
|
||||
assert.True(t, IsLeft(result2))
|
||||
})
|
||||
|
||||
t.Run("Transform error type in pipeline", func(t *testing.T) {
|
||||
// Convert numeric error codes to descriptive strings
|
||||
codeToMessage := ChainLeft(func(code int) Either[string, string] {
|
||||
messages := map[int]string{
|
||||
404: "Not Found",
|
||||
500: "Internal Server Error",
|
||||
}
|
||||
if msg, ok := messages[code]; ok {
|
||||
return Left[string](msg)
|
||||
}
|
||||
return Left[string](fmt.Sprintf("Unknown error: %d", code))
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[string](404),
|
||||
codeToMessage,
|
||||
)
|
||||
assert.Equal(t, Left[string]("Not Found"), result)
|
||||
})
|
||||
|
||||
t.Run("ChainLeft with Map combination", func(t *testing.T) {
|
||||
// Combine ChainLeft with Map to handle both channels
|
||||
errorHandler := ChainLeft(func(err error) Either[string, int] {
|
||||
return Left[int]("Error: " + err.Error())
|
||||
})
|
||||
|
||||
valueMapper := Map[string](S.Format[int]("Value: %d"))
|
||||
|
||||
// Test with Left
|
||||
result1 := F.Pipe2(
|
||||
Left[int](errors.New("fail")),
|
||||
errorHandler,
|
||||
valueMapper,
|
||||
)
|
||||
assert.Equal(t, Left[string]("Error: fail"), result1)
|
||||
|
||||
// Test with Right
|
||||
result2 := F.Pipe2(
|
||||
Right[error](42),
|
||||
errorHandler,
|
||||
valueMapper,
|
||||
)
|
||||
assert.Equal(t, Of[string]("Value: 42"), result2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,7 +82,30 @@ func Bind[S1, S2, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// Similar to Bind, but uses the Functor's Map operation instead of the Monad's Chain.
|
||||
// This is useful when you want to add a computed value to the context without needing
|
||||
// the full power of monadic composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// Sum int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// identity.Do(State{X: 10, Y: 20}),
|
||||
// identity.Let(
|
||||
// func(sum int) func(State) State {
|
||||
// return func(s State) State { s.Sum = sum; return s }
|
||||
// },
|
||||
// func(s State) int {
|
||||
// return s.X + s.Y
|
||||
// },
|
||||
// ),
|
||||
// ) // State{X: 10, Y: 20, Sum: 30}
|
||||
func Let[S1, S2, T any](
|
||||
key func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
@@ -94,7 +117,27 @@ func Let[S1, S2, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
|
||||
// This is a specialized version of Let that doesn't require a computation function,
|
||||
// useful when you want to add a known value to the context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// Constant string
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// identity.Do(State{X: 10, Y: 20}),
|
||||
// identity.LetTo(
|
||||
// func(c string) func(State) State {
|
||||
// return func(s State) State { s.Constant = c; return s }
|
||||
// },
|
||||
// "fixed value",
|
||||
// ),
|
||||
// ) // State{X: 10, Y: 20, Constant: "fixed value"}
|
||||
func LetTo[S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
b B,
|
||||
@@ -106,7 +149,31 @@ func LetTo[S1, S2, B any](
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
// BindTo initializes a new state [S1] from a value [T].
|
||||
// This is typically used as the first operation in a do-notation chain to convert
|
||||
// a plain value into a context that can be used with subsequent Bind operations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// 42,
|
||||
// identity.BindTo(func(x int) State {
|
||||
// return State{X: x}
|
||||
// }),
|
||||
// identity.Bind(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) int {
|
||||
// return s.X * 2
|
||||
// },
|
||||
// ),
|
||||
// ) // State{X: 42, Y: 84}
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(T) S1 {
|
||||
|
||||
@@ -489,6 +489,8 @@ func After[E, A any](timestamp time.Time) Operator[E, A, A] {
|
||||
// If the input is a Left value, it applies the function f to transform the error and potentially
|
||||
// change the error type from EA to EB. If the input is a Right value, it passes through unchanged.
|
||||
//
|
||||
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery or error transformation scenarios where you want to handle
|
||||
// errors by performing another computation that may also fail.
|
||||
//
|
||||
@@ -523,6 +525,8 @@ func MonadChainLeft[EA, EB, A any](fa IOEither[EA, A], f Kleisli[EB, EA, A]) IOE
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// It returns a function that chains a computation on the left (error) side of an [IOEither].
|
||||
//
|
||||
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is particularly useful in functional composition pipelines where you want to handle
|
||||
// errors by performing another computation that may also fail.
|
||||
//
|
||||
@@ -644,6 +648,8 @@ func TapLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
|
||||
// If the IOEither is Left, it applies the provided function to the error value,
|
||||
// which returns a new IOEither that replaces the original.
|
||||
//
|
||||
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative IO computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
|
||||
@@ -490,3 +490,148 @@ func TestOrElseW(t *testing.T) {
|
||||
preserved := preserveRecover(preservedRight)()
|
||||
assert.Equal(t, E.Right[AppError](42), preserved)
|
||||
}
|
||||
|
||||
// TestChainLeftIdenticalToOrElse proves that ChainLeft and OrElse are identical functions.
|
||||
// This test verifies that both functions produce the same results for all scenarios:
|
||||
// - Left values with error recovery
|
||||
// - Left values with error transformation
|
||||
// - Right values passing through unchanged
|
||||
func TestChainLeftIdenticalToOrElse(t *testing.T) {
|
||||
// Test 1: Left value with error recovery - both should recover to Right
|
||||
t.Run("Left value recovery - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
recoveryFn := func(e string) IOEither[string, int] {
|
||||
if e == "recoverable" {
|
||||
return Right[string](42)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
input := Left[int]("recoverable")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(recoveryFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(recoveryFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](42), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 2: Left value with error transformation - both should transform error
|
||||
t.Run("Left value transformation - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
transformFn := func(e string) IOEither[string, int] {
|
||||
return Left[int]("transformed: " + e)
|
||||
}
|
||||
|
||||
input := Left[int]("original error")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(transformFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(transformFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Left[int]("transformed: original error"), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 3: Right value - both should pass through unchanged
|
||||
t.Run("Right value passthrough - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
handlerFn := func(e string) IOEither[string, int] {
|
||||
return Left[int]("should not be called")
|
||||
}
|
||||
|
||||
input := Right[string](100)
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(handlerFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(handlerFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](100), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 4: Error type widening - both should handle type transformation
|
||||
t.Run("Error type widening - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
widenFn := func(e string) IOEither[int, int] {
|
||||
return Left[int](404)
|
||||
}
|
||||
|
||||
input := Left[int]("not found")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(widenFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(widenFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Left[int](404), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 5: Composition in pipeline - both should work identically in F.Pipe
|
||||
t.Run("Pipeline composition - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
recoveryFn := func(e string) IOEither[string, int] {
|
||||
if e == "network error" {
|
||||
return Right[string](0)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
input := Left[int]("network error")
|
||||
|
||||
// Using ChainLeft in pipeline
|
||||
resultChainLeft := F.Pipe1(input, ChainLeft(recoveryFn))()
|
||||
|
||||
// Using OrElse in pipeline
|
||||
resultOrElse := F.Pipe1(input, OrElse(recoveryFn))()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](0), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 6: Multiple chained operations - both should behave identically
|
||||
t.Run("Multiple operations - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
handler1 := func(e string) IOEither[string, int] {
|
||||
if e == "error1" {
|
||||
return Right[string](1)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
handler2 := func(e string) IOEither[string, int] {
|
||||
if e == "error2" {
|
||||
return Right[string](2)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
input := Left[int]("error2")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := F.Pipe2(
|
||||
input,
|
||||
ChainLeft(handler1),
|
||||
ChainLeft(handler2),
|
||||
)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := F.Pipe2(
|
||||
input,
|
||||
OrElse(handler1),
|
||||
OrElse(handler2),
|
||||
)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](2), resultChainLeft)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// MakeIORef creates a new IORef containing the given initial value.
|
||||
@@ -50,6 +51,32 @@ func MakeIORef[A any](a A) IO[IORef[A]] {
|
||||
}
|
||||
}
|
||||
|
||||
// Write atomically writes a new value to an IORef and returns the written value.
|
||||
//
|
||||
// This function returns a Kleisli arrow that takes an IORef and produces an IO
|
||||
// computation that writes the given value to the reference. The write operation
|
||||
// is atomic and thread-safe, using a write lock to ensure exclusive access.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The new value to write to the IORef
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow from IORef[A] to IO[A] that writes the value and returns it
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ref := ioref.MakeIORef(42)()
|
||||
//
|
||||
// // Write a new value
|
||||
// newValue := ioref.Write(100)(ref)() // Returns 100, ref now contains 100
|
||||
//
|
||||
// // Chain writes
|
||||
// pipe.Pipe2(
|
||||
// ref,
|
||||
// ioref.Write(50),
|
||||
// io.Chain(ioref.Write(75)),
|
||||
// )() // ref now contains 75
|
||||
//
|
||||
//go:inline
|
||||
func Write[A any](a A) io.Kleisli[IORef[A], A] {
|
||||
return func(ref IORef[A]) IO[A] {
|
||||
@@ -180,6 +207,57 @@ func ModifyIOK[A any](f io.Kleisli[A, A]) io.Kleisli[IORef[A], A] {
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyReaderIOK atomically modifies the value in an IORef using a ReaderIO-based transformation.
|
||||
//
|
||||
// This is a variant of ModifyIOK that works with ReaderIO computations, allowing the
|
||||
// transformation function to access an environment of type R while performing IO effects.
|
||||
// This is useful when the modification logic needs access to configuration, context,
|
||||
// or other shared resources.
|
||||
//
|
||||
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
|
||||
// access during the read-modify-write cycle. The ReaderIO effect in the transformation
|
||||
// function is executed while holding the lock.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A ReaderIO Kleisli arrow (readerio.Kleisli[R, A, A]) that takes the current value
|
||||
// and an environment R, and returns an IO computation producing the new value
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderIO Kleisli arrow from IORef[A] to ReaderIO[R, A] that returns the new value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// multiplier int
|
||||
// }
|
||||
//
|
||||
// ref := ioref.MakeIORef(10)()
|
||||
//
|
||||
// // Modify using environment
|
||||
// modifyWithConfig := ioref.ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
// return func(cfg Config) io.IO[int] {
|
||||
// return func() int {
|
||||
// return x * cfg.multiplier
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// config := Config{multiplier: 5}
|
||||
// newValue := modifyWithConfig(ref)(config)() // Returns 50, ref now contains 50
|
||||
func ModifyReaderIOK[R, A any](f readerio.Kleisli[R, A, A]) readerio.Kleisli[R, IORef[A], A] {
|
||||
return func(ref IORef[A]) ReaderIO[R, A] {
|
||||
return func(r R) readerio.IO[A] {
|
||||
return func() A {
|
||||
ref.mu.Lock()
|
||||
defer ref.mu.Unlock()
|
||||
|
||||
ref.a = f(ref.a)(r)()
|
||||
return ref.a
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyWithResult atomically modifies the value in an IORef and returns both
|
||||
// the new value and an additional result computed from the old value.
|
||||
//
|
||||
@@ -269,3 +347,62 @@ func ModifyIOKWithResult[A, B any](f io.Kleisli[A, Pair[A, B]]) io.Kleisli[IORef
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyReaderIOKWithResult atomically modifies the value in an IORef and returns a result,
|
||||
// using a ReaderIO-based transformation function.
|
||||
//
|
||||
// This combines the capabilities of ModifyIOKWithResult and ModifyReaderIOK, allowing the
|
||||
// transformation function to:
|
||||
// - Access an environment of type R (like configuration or context)
|
||||
// - Perform IO effects during the transformation
|
||||
// - Both update the stored value and compute a result based on the old value
|
||||
// - Ensure atomicity of the entire read-transform-write-compute cycle
|
||||
//
|
||||
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
|
||||
// access. The ReaderIO effect in the transformation function is executed while holding the lock.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A ReaderIO Kleisli arrow (readerio.Kleisli[R, A, Pair[A, B]]) that takes the old value
|
||||
// and an environment R, and returns an IO computation producing a Pair of (new value, result)
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderIO Kleisli arrow from IORef[A] to ReaderIO[R, B] that produces the result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// logEnabled bool
|
||||
// }
|
||||
//
|
||||
// ref := ioref.MakeIORef(42)()
|
||||
//
|
||||
// // Increment with conditional logging, return old value
|
||||
// incrementWithLog := ioref.ModifyReaderIOKWithResult(
|
||||
// func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
|
||||
// return func(cfg Config) io.IO[pair.Pair[int, int]] {
|
||||
// return func() pair.Pair[int, int] {
|
||||
// if cfg.logEnabled {
|
||||
// fmt.Printf("Incrementing from %d\n", x)
|
||||
// }
|
||||
// return pair.MakePair(x+1, x)
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// config := Config{logEnabled: true}
|
||||
// oldValue := incrementWithLog(ref)(config)() // Logs and returns 42, ref now contains 43
|
||||
func ModifyReaderIOKWithResult[R, A, B any](f readerio.Kleisli[R, A, Pair[A, B]]) readerio.Kleisli[R, IORef[A], B] {
|
||||
return func(ref IORef[A]) ReaderIO[R, B] {
|
||||
return func(r R) readerio.IO[B] {
|
||||
return func() B {
|
||||
ref.mu.Lock()
|
||||
defer ref.mu.Unlock()
|
||||
|
||||
result := f(ref.a)(r)()
|
||||
ref.a = pair.Head(result)
|
||||
return pair.Tail(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,588 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMakeIORef(t *testing.T) {
|
||||
t.Run("creates IORef with integer value", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Equal(t, 42, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("creates IORef with string value", func(t *testing.T) {
|
||||
ref := MakeIORef("hello")()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Equal(t, "hello", Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("creates IORef with slice value", func(t *testing.T) {
|
||||
slice := []int{1, 2, 3}
|
||||
ref := MakeIORef(slice)()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Equal(t, slice, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("creates IORef with struct value", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
ref := MakeIORef(person)()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Equal(t, person, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("creates IORef with zero value", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Equal(t, 0, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("creates IORef with nil pointer", func(t *testing.T) {
|
||||
var ptr *int
|
||||
ref := MakeIORef(ptr)()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Nil(t, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("multiple IORefs are independent", func(t *testing.T) {
|
||||
ref1 := MakeIORef(10)()
|
||||
ref2 := MakeIORef(20)()
|
||||
|
||||
assert.Equal(t, 10, Read(ref1)())
|
||||
assert.Equal(t, 20, Read(ref2)())
|
||||
|
||||
Write(30)(ref1)()
|
||||
assert.Equal(t, 30, Read(ref1)())
|
||||
assert.Equal(t, 20, Read(ref2)()) // ref2 unchanged
|
||||
})
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
t.Run("reads initial value", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
value := Read(ref)()
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("reads updated value", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
Write(20)(ref)()
|
||||
value := Read(ref)()
|
||||
assert.Equal(t, 20, value)
|
||||
})
|
||||
|
||||
t.Run("multiple reads return same value", func(t *testing.T) {
|
||||
ref := MakeIORef(100)()
|
||||
value1 := Read(ref)()
|
||||
value2 := Read(ref)()
|
||||
value3 := Read(ref)()
|
||||
assert.Equal(t, 100, value1)
|
||||
assert.Equal(t, 100, value2)
|
||||
assert.Equal(t, 100, value3)
|
||||
})
|
||||
|
||||
t.Run("concurrent reads are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
results := make([]int, iterations)
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx] = Read(ref)()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// All reads should return the same value
|
||||
for _, v := range results {
|
||||
assert.Equal(t, 42, v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reads during concurrent writes", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 50
|
||||
|
||||
// Start concurrent writes
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(val int) {
|
||||
defer wg.Done()
|
||||
Write(val)(ref)()
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Start concurrent reads
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
value := Read(ref)()
|
||||
// Value should be valid (between 0 and iterations-1)
|
||||
assert.GreaterOrEqual(t, value, 0)
|
||||
assert.Less(t, value, iterations)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
t.Run("writes new value", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
result := Write(100)(ref)()
|
||||
assert.Equal(t, 100, result)
|
||||
assert.Equal(t, 100, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("overwrites existing value", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
Write(20)(ref)()
|
||||
Write(30)(ref)()
|
||||
assert.Equal(t, 30, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("returns written value", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
result := Write(42)(ref)()
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("writes string value", func(t *testing.T) {
|
||||
ref := MakeIORef("hello")()
|
||||
result := Write("world")(ref)()
|
||||
assert.Equal(t, "world", result)
|
||||
assert.Equal(t, "world", Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained writes", func(t *testing.T) {
|
||||
ref := MakeIORef(1)()
|
||||
Write(2)(ref)()
|
||||
Write(3)(ref)()
|
||||
result := Write(4)(ref)()
|
||||
assert.Equal(t, 4, result)
|
||||
assert.Equal(t, 4, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent writes are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(val int) {
|
||||
defer wg.Done()
|
||||
Write(val)(ref)()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Final value should be one of the written values
|
||||
finalValue := Read(ref)()
|
||||
assert.GreaterOrEqual(t, finalValue, 0)
|
||||
assert.Less(t, finalValue, iterations)
|
||||
})
|
||||
|
||||
t.Run("write with zero value", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
Write(0)(ref)()
|
||||
assert.Equal(t, 0, Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModify(t *testing.T) {
|
||||
t.Run("modifies value with simple function", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
result := Modify(func(x int) int { return x * 2 })(ref)()
|
||||
assert.Equal(t, 20, result)
|
||||
assert.Equal(t, 20, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modifies with addition", func(t *testing.T) {
|
||||
ref := MakeIORef(5)()
|
||||
Modify(func(x int) int { return x + 10 })(ref)()
|
||||
assert.Equal(t, 15, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modifies string value", func(t *testing.T) {
|
||||
ref := MakeIORef("hello")()
|
||||
result := Modify(func(s string) string { return s + " world" })(ref)()
|
||||
assert.Equal(t, "hello world", result)
|
||||
assert.Equal(t, "hello world", Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained modifications", func(t *testing.T) {
|
||||
ref := MakeIORef(2)()
|
||||
Modify(func(x int) int { return x * 3 })(ref)() // 6
|
||||
Modify(func(x int) int { return x + 4 })(ref)() // 10
|
||||
result := Modify(func(x int) int { return x / 2 })(ref)()
|
||||
assert.Equal(t, 5, result)
|
||||
assert.Equal(t, 5, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent modifications are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
Modify(func(x int) int { return x + 1 })(ref)()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, iterations, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modify with identity function", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
result := Modify(func(x int) int { return x })(ref)()
|
||||
assert.Equal(t, 42, result)
|
||||
assert.Equal(t, 42, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modify returns new value", func(t *testing.T) {
|
||||
ref := MakeIORef(100)()
|
||||
result := Modify(func(x int) int { return x - 50 })(ref)()
|
||||
assert.Equal(t, 50, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyWithResult(t *testing.T) {
|
||||
t.Run("modifies and returns old value", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(x+1, x)
|
||||
})(ref)()
|
||||
assert.Equal(t, 42, oldValue)
|
||||
assert.Equal(t, 43, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("swaps value and returns old", func(t *testing.T) {
|
||||
ref := MakeIORef(100)()
|
||||
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(200, x)
|
||||
})(ref)()
|
||||
assert.Equal(t, 100, oldValue)
|
||||
assert.Equal(t, 200, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("returns different type", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
message := ModifyWithResult(func(x int) pair.Pair[int, string] {
|
||||
return pair.MakePair(x*2, fmt.Sprintf("doubled from %d", x))
|
||||
})(ref)()
|
||||
assert.Equal(t, "doubled from 42", message)
|
||||
assert.Equal(t, 84, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("computes result based on old value", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
wasPositive := ModifyWithResult(func(x int) pair.Pair[int, bool] {
|
||||
return pair.MakePair(x+5, x > 0)
|
||||
})(ref)()
|
||||
assert.True(t, wasPositive)
|
||||
assert.Equal(t, 15, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained modifications with results", func(t *testing.T) {
|
||||
ref := MakeIORef(5)()
|
||||
result1 := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(x*2, x)
|
||||
})(ref)()
|
||||
result2 := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(x+10, x)
|
||||
})(ref)()
|
||||
assert.Equal(t, 5, result1)
|
||||
assert.Equal(t, 10, result2)
|
||||
assert.Equal(t, 20, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent modifications with results are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
results := make([]int, iterations)
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(x+1, x)
|
||||
})(ref)()
|
||||
results[idx] = oldValue
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, iterations, Read(ref)())
|
||||
|
||||
// All old values should be unique
|
||||
seen := make(map[int]bool)
|
||||
for _, v := range results {
|
||||
assert.False(t, seen[v])
|
||||
seen[v] = true
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("extract and replace pattern", func(t *testing.T) {
|
||||
ref := MakeIORef([]int{1, 2, 3})()
|
||||
first := ModifyWithResult(func(xs []int) pair.Pair[[]int, int] {
|
||||
if len(xs) == 0 {
|
||||
return pair.MakePair(xs, 0)
|
||||
}
|
||||
return pair.MakePair(xs[1:], xs[0])
|
||||
})(ref)()
|
||||
assert.Equal(t, 1, first)
|
||||
assert.Equal(t, []int{2, 3}, Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyReaderIOK(t *testing.T) {
|
||||
type Config struct {
|
||||
multiplier int
|
||||
}
|
||||
|
||||
t.Run("modifies with environment", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
config := Config{multiplier: 5}
|
||||
|
||||
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
return func(cfg Config) io.IO[int] {
|
||||
return io.Of(x * cfg.multiplier)
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, 50, result)
|
||||
assert.Equal(t, 50, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("uses environment for computation", func(t *testing.T) {
|
||||
ref := MakeIORef(100)()
|
||||
config := Config{multiplier: 2}
|
||||
|
||||
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
return func(cfg Config) io.IO[int] {
|
||||
return func() int {
|
||||
return x / cfg.multiplier
|
||||
}
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, 50, result)
|
||||
assert.Equal(t, 50, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained modifications with different configs", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
config1 := Config{multiplier: 2}
|
||||
config2 := Config{multiplier: 3}
|
||||
|
||||
ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
return func(cfg Config) io.IO[int] {
|
||||
return io.Of(x * cfg.multiplier)
|
||||
}
|
||||
})(ref)(config1)()
|
||||
|
||||
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
return func(cfg Config) io.IO[int] {
|
||||
return io.Of(x + cfg.multiplier)
|
||||
}
|
||||
})(ref)(config2)()
|
||||
|
||||
assert.Equal(t, 23, result) // (10 * 2) + 3
|
||||
assert.Equal(t, 23, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent modifications with environment are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
config := Config{multiplier: 1}
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
return func(cfg Config) io.IO[int] {
|
||||
return io.Of(x + cfg.multiplier)
|
||||
}
|
||||
})(ref)(config)()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, iterations, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("environment provides configuration", func(t *testing.T) {
|
||||
type Settings struct {
|
||||
prefix string
|
||||
}
|
||||
ref := MakeIORef("world")()
|
||||
settings := Settings{prefix: "hello "}
|
||||
|
||||
result := ModifyReaderIOK(func(s string) readerio.ReaderIO[Settings, string] {
|
||||
return func(cfg Settings) io.IO[string] {
|
||||
return io.Of(cfg.prefix + s)
|
||||
}
|
||||
})(ref)(settings)()
|
||||
|
||||
assert.Equal(t, "hello world", result)
|
||||
assert.Equal(t, "hello world", Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyReaderIOKWithResult(t *testing.T) {
|
||||
type Config struct {
|
||||
logEnabled bool
|
||||
multiplier int
|
||||
}
|
||||
|
||||
t.Run("modifies with environment and returns result", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
config := Config{logEnabled: false, multiplier: 2}
|
||||
|
||||
oldValue := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x*cfg.multiplier, x))
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, 42, oldValue)
|
||||
assert.Equal(t, 84, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("returns different type based on environment", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
config := Config{logEnabled: true, multiplier: 3}
|
||||
|
||||
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, string]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, string]] {
|
||||
return func() pair.Pair[int, string] {
|
||||
newVal := x * cfg.multiplier
|
||||
msg := fmt.Sprintf("multiplied %d by %d", x, cfg.multiplier)
|
||||
return pair.MakePair(newVal, msg)
|
||||
}
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, "multiplied 10 by 3", message)
|
||||
assert.Equal(t, 30, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("conditional logic based on environment", func(t *testing.T) {
|
||||
ref := MakeIORef(-10)()
|
||||
config := Config{logEnabled: true, multiplier: 2}
|
||||
|
||||
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, string]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, string]] {
|
||||
return func() pair.Pair[int, string] {
|
||||
if x < 0 {
|
||||
return pair.MakePair(0, "reset negative value")
|
||||
}
|
||||
return pair.MakePair(x*cfg.multiplier, "multiplied positive value")
|
||||
}
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, "reset negative value", message)
|
||||
assert.Equal(t, 0, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained modifications with results", func(t *testing.T) {
|
||||
ref := MakeIORef(5)()
|
||||
config := Config{logEnabled: false, multiplier: 2}
|
||||
|
||||
result1 := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x*cfg.multiplier, x))
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
result2 := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x+cfg.multiplier, x))
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, 5, result1)
|
||||
assert.Equal(t, 10, result2)
|
||||
assert.Equal(t, 12, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent modifications with environment are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
config := Config{logEnabled: false, multiplier: 1}
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
results := make([]int, iterations)
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
oldValue := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x+cfg.multiplier, x))
|
||||
}
|
||||
})(ref)(config)()
|
||||
results[idx] = oldValue
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, iterations, Read(ref)())
|
||||
|
||||
// All old values should be unique
|
||||
seen := make(map[int]bool)
|
||||
for _, v := range results {
|
||||
assert.False(t, seen[v])
|
||||
seen[v] = true
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("environment provides validation rules", func(t *testing.T) {
|
||||
type ValidationConfig struct {
|
||||
maxValue int
|
||||
}
|
||||
ref := MakeIORef(100)()
|
||||
config := ValidationConfig{maxValue: 50}
|
||||
|
||||
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[ValidationConfig, pair.Pair[int, string]] {
|
||||
return func(cfg ValidationConfig) io.IO[pair.Pair[int, string]] {
|
||||
return func() pair.Pair[int, string] {
|
||||
if x > cfg.maxValue {
|
||||
return pair.MakePair(cfg.maxValue, fmt.Sprintf("capped at %d", cfg.maxValue))
|
||||
}
|
||||
return pair.MakePair(x, "value within limits")
|
||||
}
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, "capped at 50", message)
|
||||
assert.Equal(t, 50, Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyIOK(t *testing.T) {
|
||||
t.Run("basic modification with IO effect", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
|
||||
@@ -45,29 +45,110 @@ import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
type (
|
||||
// ioRef is the internal implementation of a mutable reference.
|
||||
// It uses a read-write mutex to ensure thread-safe access.
|
||||
// It uses a read-write mutex to ensure thread-safe access to the stored value.
|
||||
//
|
||||
// The mutex allows multiple concurrent readers (using RLock) but ensures
|
||||
// exclusive access for writers (using Lock), preventing race conditions
|
||||
// when reading or modifying the stored value.
|
||||
//
|
||||
// This type is not exported; users interact with it through the IORef type alias.
|
||||
ioRef[A any] struct {
|
||||
mu sync.RWMutex
|
||||
a A
|
||||
mu sync.RWMutex // Protects concurrent access to the stored value
|
||||
a A // The stored value
|
||||
}
|
||||
|
||||
// IO represents a synchronous computation that may have side effects.
|
||||
// It's a function that takes no arguments and returns a value of type A.
|
||||
//
|
||||
// IO computations are lazy - they don't execute until explicitly invoked
|
||||
// by calling the function. This allows for composing and chaining effects
|
||||
// before execution.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Define an IO computation
|
||||
// computation := func() int {
|
||||
// fmt.Println("Computing...")
|
||||
// return 42
|
||||
// }
|
||||
//
|
||||
// // Nothing happens yet - the computation is lazy
|
||||
// result := computation() // Now it executes and prints "Computing..."
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// ReaderIO represents a computation that requires an environment of type R
|
||||
// and produces an IO effect that yields a value of type A.
|
||||
//
|
||||
// This combines the Reader pattern (dependency injection) with IO effects,
|
||||
// allowing computations to access shared configuration or context while
|
||||
// performing side effects.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// multiplier int
|
||||
// }
|
||||
//
|
||||
// // A ReaderIO that uses config to compute a value
|
||||
// computation := func(cfg Config) io.IO[int] {
|
||||
// return func() int {
|
||||
// return 42 * cfg.multiplier
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute with specific config
|
||||
// result := computation(Config{multiplier: 2})() // Returns 84
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
|
||||
// IORef represents a mutable reference to a value of type A.
|
||||
// Operations on IORef are thread-safe and performed within the IO monad.
|
||||
//
|
||||
// IORef provides a way to work with mutable state in a functional style,
|
||||
// where mutations are explicit and contained within IO computations.
|
||||
// This makes side effects visible in the type system and allows for
|
||||
// better reasoning about code that uses mutable state.
|
||||
//
|
||||
// All operations on IORef (Read, Write, Modify, etc.) are atomic and
|
||||
// thread-safe, making it safe to share IORefs across goroutines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a new IORef
|
||||
// ref := ioref.MakeIORef(42)()
|
||||
//
|
||||
// // Read the current value
|
||||
// value := ioref.Read(ref)() // 42
|
||||
//
|
||||
// // Write a new value
|
||||
// ioref.Write(100)(ref)()
|
||||
//
|
||||
// // Modify the value atomically
|
||||
// ioref.Modify(func(x int) int { return x * 2 })(ref)()
|
||||
IORef[A any] = *ioRef[A]
|
||||
|
||||
// Endomorphism represents a function from A to A.
|
||||
// It's commonly used with Modify to transform the value in an IORef.
|
||||
//
|
||||
// An endomorphism is a morphism (structure-preserving map) from a
|
||||
// mathematical object to itself. In programming terms, it's simply
|
||||
// a function that takes a value and returns a value of the same type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // An endomorphism that doubles an integer
|
||||
// double := func(x int) int { return x * 2 }
|
||||
//
|
||||
// // An endomorphism that uppercases a string
|
||||
// upper := func(s string) string { return strings.ToUpper(s) }
|
||||
//
|
||||
// // Use with IORef
|
||||
// ref := ioref.MakeIORef(21)()
|
||||
// ioref.Modify(double)(ref)() // ref now contains 42
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Pair represents a tuple of two values of types A and B.
|
||||
@@ -76,6 +157,8 @@ type (
|
||||
//
|
||||
// The head of the pair contains the new value to store in the IORef,
|
||||
// while the tail contains the result to return from the operation.
|
||||
// This allows atomic operations that both update the reference and
|
||||
// compute a result based on the old value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -85,5 +168,11 @@ type (
|
||||
// // Extract values
|
||||
// newVal := pair.Head(p) // Gets the head (new value)
|
||||
// oldVal := pair.Tail(p) // Gets the tail (old value)
|
||||
//
|
||||
// // Use with ModifyWithResult to swap and return old value
|
||||
// ref := ioref.MakeIORef(42)()
|
||||
// oldValue := ioref.ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
// return pair.MakePair(100, x) // Store 100, return old value
|
||||
// })(ref)() // oldValue is 42, ref now contains 100
|
||||
Pair[A, B any] = pair.Pair[A, B]
|
||||
)
|
||||
|
||||
@@ -710,6 +710,146 @@ func TestTranscodeEither(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestTranscodeEitherValidation(t *testing.T) {
|
||||
t.Run("validates Left value with context", func(t *testing.T) {
|
||||
eitherCodec := TranscodeEither(String(), Int())
|
||||
result := eitherCodec.Decode(either.Left[any, any](123)) // Invalid: should be string
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(either.Either[string, int]) validation.Errors { return nil },
|
||||
)
|
||||
assert.NotEmpty(t, errors)
|
||||
// Verify error contains type information
|
||||
assert.Contains(t, fmt.Sprintf("%v", errors[0]), "string")
|
||||
})
|
||||
|
||||
t.Run("validates Right value with context", func(t *testing.T) {
|
||||
eitherCodec := TranscodeEither(String(), Int())
|
||||
result := eitherCodec.Decode(either.Right[any, any]("not a number"))
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(either.Either[string, int]) validation.Errors { return nil },
|
||||
)
|
||||
assert.NotEmpty(t, errors)
|
||||
// Verify error contains type information
|
||||
assert.Contains(t, fmt.Sprintf("%v", errors[0]), "int")
|
||||
})
|
||||
|
||||
t.Run("preserves Either structure on validation failure", func(t *testing.T) {
|
||||
eitherCodec := TranscodeEither(String(), Int())
|
||||
|
||||
// Left with wrong type
|
||||
leftResult := eitherCodec.Decode(either.Left[any, any]([]int{1, 2, 3}))
|
||||
assert.True(t, either.IsLeft(leftResult))
|
||||
|
||||
// Right with wrong type
|
||||
rightResult := eitherCodec.Decode(either.Right[any, any](true))
|
||||
assert.True(t, either.IsLeft(rightResult))
|
||||
})
|
||||
|
||||
t.Run("validates with custom codec that can fail", func(t *testing.T) {
|
||||
// Create a codec that only accepts positive integers
|
||||
positiveInt := MakeType(
|
||||
"PositiveInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i <= 0 {
|
||||
return either.Left[int](fmt.Errorf("not a positive integer"))
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i <= 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
eitherCodec := TranscodeEither(String(), positiveInt)
|
||||
|
||||
// Valid positive integer
|
||||
validResult := eitherCodec.Decode(either.Right[any](42))
|
||||
assert.True(t, either.IsRight(validResult))
|
||||
|
||||
// Invalid: zero
|
||||
zeroResult := eitherCodec.Decode(either.Right[any](0))
|
||||
assert.True(t, either.IsLeft(zeroResult))
|
||||
|
||||
// Invalid: negative
|
||||
negativeResult := eitherCodec.Decode(either.Right[any](-5))
|
||||
assert.True(t, either.IsLeft(negativeResult))
|
||||
})
|
||||
|
||||
t.Run("validates both branches independently", func(t *testing.T) {
|
||||
// Create codecs with specific validation rules
|
||||
nonEmptyString := MakeType(
|
||||
"NonEmptyString",
|
||||
func(u any) either.Either[error, string] {
|
||||
s, ok := u.(string)
|
||||
if !ok || len(s) == 0 {
|
||||
return either.Left[string](fmt.Errorf("not a non-empty string"))
|
||||
}
|
||||
return either.Of[error](s)
|
||||
},
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) == 0 {
|
||||
return validation.FailureWithMessage[string](s, "must not be empty")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
evenInt := MakeType(
|
||||
"EvenInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i%2 != 0 {
|
||||
return either.Left[int](fmt.Errorf("not an even integer"))
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i%2 != 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be even")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
eitherCodec := TranscodeEither(nonEmptyString, evenInt)
|
||||
|
||||
// Valid Left: non-empty string
|
||||
validLeft := eitherCodec.Decode(either.Left[int]("hello"))
|
||||
assert.True(t, either.IsRight(validLeft))
|
||||
|
||||
// Invalid Left: empty string
|
||||
invalidLeft := eitherCodec.Decode(either.Left[int](""))
|
||||
assert.True(t, either.IsLeft(invalidLeft))
|
||||
|
||||
// Valid Right: even integer
|
||||
validRight := eitherCodec.Decode(either.Right[string](42))
|
||||
assert.True(t, either.IsRight(validRight))
|
||||
|
||||
// Invalid Right: odd integer
|
||||
invalidRight := eitherCodec.Decode(either.Right[string](43))
|
||||
assert.True(t, either.IsLeft(invalidRight))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTranscodeEitherWithTransformation(t *testing.T) {
|
||||
// Create a codec that transforms strings to their lengths
|
||||
stringToLength := MakeType(
|
||||
|
||||
321
v2/optics/codec/decode/OrElse_ChainLeft_explanation.md
Normal file
321
v2/optics/codec/decode/OrElse_ChainLeft_explanation.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# ChainLeft and OrElse in the Decode Package
|
||||
|
||||
## Overview
|
||||
|
||||
In [`optics/codec/decode/monad.go`](monad.go:53-62), the [`ChainLeft`](monad.go:53) and [`OrElse`](monad.go:60) functions work with decoders that may fail during decoding operations.
|
||||
|
||||
```go
|
||||
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return readert.Chain[Decode[I, A]](
|
||||
validation.ChainLeft,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
```
|
||||
|
||||
## Key Insight: OrElse is ChainLeft
|
||||
|
||||
**`OrElse` is exactly the same as `ChainLeft`** - they are aliases with identical implementations and behavior. The choice between them is purely about **code readability and semantic intent**.
|
||||
|
||||
## Understanding the Types
|
||||
|
||||
### Decode[I, A]
|
||||
A decoder that takes input of type `I` and produces a `Validation[A]`:
|
||||
```go
|
||||
type Decode[I, A any] = func(I) Validation[A]
|
||||
```
|
||||
|
||||
### Kleisli[I, Errors, A]
|
||||
A function that takes `Errors` and produces a `Decode[I, A]`:
|
||||
```go
|
||||
type Kleisli[I, Errors, A] = func(Errors) Decode[I, A]
|
||||
```
|
||||
|
||||
This allows error handlers to:
|
||||
1. Access the validation errors that occurred
|
||||
2. Access the original input (via the returned Decode function)
|
||||
3. Either recover with a success value or produce new errors
|
||||
|
||||
### Operator[I, A, A]
|
||||
A function that transforms one decoder into another:
|
||||
```go
|
||||
type Operator[I, A, A] = func(Decode[I, A]) Decode[I, A]
|
||||
```
|
||||
|
||||
## Core Behavior
|
||||
|
||||
Both [`ChainLeft`](monad.go:53) and [`OrElse`](monad.go:60) delegate to [`validation.ChainLeft`](../validation/monad.go:304), which provides:
|
||||
|
||||
### 1. Error Aggregation
|
||||
When the transformation function returns a failure, **both the original errors AND the new errors are combined** using the Errors monoid:
|
||||
|
||||
```go
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "original error"},
|
||||
})
|
||||
}
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "additional error"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := handler(failingDecoder)
|
||||
result := decoder("input")
|
||||
// Result contains BOTH errors: ["original error", "additional error"]
|
||||
```
|
||||
|
||||
### 2. Success Pass-Through
|
||||
Success values pass through unchanged - the handler is never called:
|
||||
|
||||
```go
|
||||
successDecoder := Of[string](42)
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "never called"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := handler(successDecoder)
|
||||
result := decoder("input")
|
||||
// Result: Success(42) - unchanged
|
||||
```
|
||||
|
||||
### 3. Error Recovery
|
||||
The handler can recover from failures by returning a successful decoder:
|
||||
|
||||
```go
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "not found"},
|
||||
})
|
||||
}
|
||||
|
||||
recoverFromNotFound := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "not found" {
|
||||
return Of[string](0) // recover with default
|
||||
}
|
||||
}
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
})
|
||||
|
||||
decoder := recoverFromNotFound(failingDecoder)
|
||||
result := decoder("input")
|
||||
// Result: Success(0) - recovered from failure
|
||||
```
|
||||
|
||||
### 4. Access to Original Input
|
||||
The handler returns a `Decode[I, A]` function, giving it access to the original input:
|
||||
|
||||
```go
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
// Can access both errs and input here
|
||||
if input == "special" {
|
||||
return validation.Of(999)
|
||||
}
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Fallback Decoding (OrElse reads better)
|
||||
|
||||
```go
|
||||
// Primary decoder that may fail
|
||||
primaryDecoder := func(input string) Validation[int] {
|
||||
n, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "not a valid integer"},
|
||||
})
|
||||
}
|
||||
return validation.Of(n)
|
||||
}
|
||||
|
||||
// Use OrElse for semantic clarity - "try primary, or else use default"
|
||||
withDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
return Of[string](0) // default to 0 if decoding fails
|
||||
})
|
||||
|
||||
decoder := withDefault(primaryDecoder)
|
||||
|
||||
result1 := decoder("42") // Success(42)
|
||||
result2 := decoder("abc") // Success(0) - fallback
|
||||
```
|
||||
|
||||
### 2. Error Context Addition (ChainLeft reads better)
|
||||
|
||||
```go
|
||||
decodeUserAge := func(data map[string]any) Validation[int] {
|
||||
age, ok := data["age"].(int)
|
||||
if !ok {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: data["age"], Messsage: "invalid type"},
|
||||
})
|
||||
}
|
||||
return validation.Of(age)
|
||||
}
|
||||
|
||||
// Use ChainLeft when emphasizing error transformation
|
||||
addContext := ChainLeft(func(errs Errors) Decode[map[string]any, int] {
|
||||
return func(data map[string]any) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
Messsage: "failed to decode user age",
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := addContext(decodeUserAge)
|
||||
// Errors will include both original error and context
|
||||
```
|
||||
|
||||
### 3. Conditional Recovery Based on Input
|
||||
|
||||
```go
|
||||
decodePort := func(input string) Validation[int] {
|
||||
port, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "invalid port"},
|
||||
})
|
||||
}
|
||||
return validation.Of(port)
|
||||
}
|
||||
|
||||
// Recover with different defaults based on input
|
||||
smartDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
// Check input to determine appropriate default
|
||||
if strings.Contains(input, "http") {
|
||||
return validation.Of(80)
|
||||
}
|
||||
if strings.Contains(input, "https") {
|
||||
return validation.Of(443)
|
||||
}
|
||||
return validation.Of(8080)
|
||||
}
|
||||
})
|
||||
|
||||
decoder := smartDefault(decodePort)
|
||||
result1 := decoder("http-server") // Success(80)
|
||||
result2 := decoder("https-server") // Success(443)
|
||||
result3 := decoder("other") // Success(8080)
|
||||
```
|
||||
|
||||
### 4. Pipeline Composition
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
DatabaseURL string
|
||||
}
|
||||
|
||||
decodeConfig := func(data map[string]any) Validation[Config] {
|
||||
url, ok := data["db_url"].(string)
|
||||
if !ok {
|
||||
return either.Left[Config](validation.Errors{
|
||||
{Messsage: "missing db_url"},
|
||||
})
|
||||
}
|
||||
return validation.Of(Config{DatabaseURL: url})
|
||||
}
|
||||
|
||||
// Build a pipeline with multiple error handlers
|
||||
decoder := F.Pipe2(
|
||||
decodeConfig,
|
||||
OrElse(func(errs Errors) Decode[map[string]any, Config] {
|
||||
// Try environment variable as fallback
|
||||
return func(data map[string]any) Validation[Config] {
|
||||
if url := os.Getenv("DATABASE_URL"); url != "" {
|
||||
return validation.Of(Config{DatabaseURL: url})
|
||||
}
|
||||
return either.Left[Config](errs)
|
||||
}
|
||||
}),
|
||||
OrElse(func(errs Errors) Decode[map[string]any, Config] {
|
||||
// Final fallback to default
|
||||
return Of[map[string]any](Config{
|
||||
DatabaseURL: "localhost:5432",
|
||||
})
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
## Comparison with validation.ChainLeft
|
||||
|
||||
The decode package's [`ChainLeft`](monad.go:53) wraps [`validation.ChainLeft`](../validation/monad.go:304) using the Reader transformer pattern:
|
||||
|
||||
| Aspect | validation.ChainLeft | decode.ChainLeft |
|
||||
|--------|---------------------|------------------|
|
||||
| **Input** | `Validation[A]` | `Decode[I, A]` (function) |
|
||||
| **Handler** | `func(Errors) Validation[A]` | `func(Errors) Decode[I, A]` |
|
||||
| **Output** | `Validation[A]` | `Decode[I, A]` (function) |
|
||||
| **Context** | No input access | Access to original input `I` |
|
||||
| **Use Case** | Pure validation logic | Decoding with input-dependent recovery |
|
||||
|
||||
The key difference is that decode's version gives handlers access to the original input through the returned `Decode[I, A]` function.
|
||||
|
||||
## When to Use Which Name
|
||||
|
||||
### Use **OrElse** when:
|
||||
- Emphasizing fallback/alternative decoding logic
|
||||
- Providing default values on decode failure
|
||||
- The intent is "try this, or else try that"
|
||||
- Code reads more naturally with "or else"
|
||||
|
||||
### Use **ChainLeft** when:
|
||||
- Emphasizing technical error channel transformation
|
||||
- Adding context or enriching error information
|
||||
- The focus is on error handling mechanics
|
||||
- Working with other functional programming concepts
|
||||
|
||||
## Verification
|
||||
|
||||
The test suite in [`monad_test.go`](monad_test.go:385) includes comprehensive tests proving that `OrElse` and `ChainLeft` are equivalent:
|
||||
|
||||
- ✅ Identical behavior for Success values
|
||||
- ✅ Identical behavior for error recovery
|
||||
- ✅ Identical behavior for error aggregation
|
||||
- ✅ Identical behavior in pipeline composition
|
||||
- ✅ Identical behavior for multiple error scenarios
|
||||
- ✅ Both provide access to original input
|
||||
|
||||
Run the tests:
|
||||
```bash
|
||||
go test -v -run "TestChainLeft|TestOrElse" ./optics/codec/decode
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
**`OrElse` is exactly the same as `ChainLeft`** in the decode package - they are aliases with identical implementations and behavior. Both:
|
||||
|
||||
1. **Delegate to validation.ChainLeft** for error handling logic
|
||||
2. **Aggregate errors** when transformations fail
|
||||
3. **Preserve successes** unchanged
|
||||
4. **Enable recovery** from decode failures
|
||||
5. **Provide access** to the original input
|
||||
|
||||
The choice between them is purely about **code readability and semantic intent**:
|
||||
- Use **`OrElse`** when emphasizing fallback/alternative decoding
|
||||
- Use **`ChainLeft`** when emphasizing error transformation
|
||||
|
||||
Both maintain the critical property of **error aggregation**, ensuring all validation failures are preserved and reported together.
|
||||
335
v2/optics/codec/decode/bind.go
Normal file
335
v2/optics/codec/decode/bind.go
Normal file
@@ -0,0 +1,335 @@
|
||||
// Copyright (c) 2025 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 decode
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type S to be used with the Bind operation.
|
||||
// This is the starting point for building up a context using do-notation style.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Result struct {
|
||||
// x int
|
||||
// y string
|
||||
// }
|
||||
// result := Do(Result{})
|
||||
func Do[I, S any](
|
||||
empty S,
|
||||
) Decode[I, S] {
|
||||
return Of[I](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context S1 to produce a context S2.
|
||||
// This is used in do-notation style to sequentially build up a context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{}),
|
||||
// Bind(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, func(s State) Decode[string, int] {
|
||||
// return Of[string](42)
|
||||
// }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 42})
|
||||
func Bind[I, S1, S2, A any](
|
||||
setter func(A) func(S1) S2,
|
||||
f Kleisli[I, S1, A],
|
||||
) Operator[I, S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[I, S1, S2],
|
||||
Map[I, A, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
|
||||
// Unlike Bind, the computation function returns a plain value, not wrapped in Decode.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; computed int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{x: 5}),
|
||||
// Let[string](func(c int) func(State) State {
|
||||
// return func(s State) State { s.computed = c; return s }
|
||||
// }, func(s State) int { return s.x * 2 }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 5, computed: 10})
|
||||
func Let[I, S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
f func(S1) B,
|
||||
) Operator[I, S1, S2] {
|
||||
return F.Let(
|
||||
Map[I, S1, S2],
|
||||
key,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context S1 to produce a context S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; name string }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{x: 5}),
|
||||
// LetTo(func(n string) func(State) State {
|
||||
// return func(s State) State { s.name = n; return s }
|
||||
// }, "example"),
|
||||
// )
|
||||
func LetTo[I, S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
b B,
|
||||
) Operator[I, S1, S2] {
|
||||
return F.LetTo(
|
||||
Map[I, S1, S2],
|
||||
key,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state S1 from a value T.
|
||||
// This is typically used as the first operation after creating a Decode value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { value int }
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](42),
|
||||
// BindTo[string](func(x int) State { return State{value: x} }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{value: 42})
|
||||
func BindTo[I, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[I, T, S1] {
|
||||
return C.BindTo(
|
||||
Map[I, T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
|
||||
// This uses the applicative functor pattern, allowing parallel composition.
|
||||
//
|
||||
// IMPORTANT: Unlike Bind which fails fast, ApS aggregates ALL validation errors from both the context
|
||||
// and the value. If both validations fail, all errors are collected and returned together.
|
||||
// This is useful for validating multiple independent fields and reporting all errors at once.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{}),
|
||||
// ApS(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, Of[string](42)),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 42})
|
||||
//
|
||||
// Error aggregation example:
|
||||
//
|
||||
// // Both decoders fail - errors are aggregated
|
||||
// decoder1 := func(input string) Validation[State] {
|
||||
// return validation.Failures[State](/* errors */)
|
||||
// }
|
||||
// decoder2 := func(input string) Validation[int] {
|
||||
// return validation.Failures[int](/* errors */)
|
||||
// }
|
||||
// combined := ApS(setter, decoder2)(decoder1)
|
||||
// result := combined("input") // Contains BOTH sets of errors
|
||||
func ApS[I, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Decode[I, T],
|
||||
) Operator[I, S1, S2] {
|
||||
return A.ApS(
|
||||
Ap[S2, I, T],
|
||||
Map[I, S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// IMPORTANT: Like ApS, this function aggregates ALL validation errors. If both the context
|
||||
// and the value fail validation, all errors are collected and returned together.
|
||||
// This enables comprehensive error reporting for complex nested structures.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Address struct {
|
||||
// Street string
|
||||
// City string
|
||||
// }
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Address Address
|
||||
// }
|
||||
//
|
||||
// // Create a lens for the Address field
|
||||
// addressLens := lens.MakeLens(
|
||||
// func(p Person) Address { return p.Address },
|
||||
// func(p Person, a Address) Person { p.Address = a; return p },
|
||||
// )
|
||||
//
|
||||
// // Use ApSL to update the address
|
||||
// decoder := F.Pipe2(
|
||||
// Of[string](Person{Name: "Alice"}),
|
||||
// ApSL(
|
||||
// addressLens,
|
||||
// Of[string](Address{Street: "Main St", City: "NYC"}),
|
||||
// ),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Person{...})
|
||||
func ApSL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Decode[I, T],
|
||||
) Operator[I, S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// a Validation that produces the new value.
|
||||
//
|
||||
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
|
||||
// the current value of the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but fail if it would exceed 100
|
||||
// increment := func(v int) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// if v >= 100 {
|
||||
// return validation.Failures[int](/* errors */)
|
||||
// }
|
||||
// return validation.Success(v + 1)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Counter{Value: 42}),
|
||||
// BindL(valueLens, increment),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Counter{Value: 43})
|
||||
func BindL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[I, T, T],
|
||||
) Operator[I, S, S] {
|
||||
return Bind(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in Validation).
|
||||
//
|
||||
// This is useful for pure transformations that cannot fail, such as mathematical operations,
|
||||
// string manipulations, or other deterministic updates.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Counter{Value: 21}),
|
||||
// LetL(valueLens, double),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Counter{Value: 42})
|
||||
func LetL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[I, S, S] {
|
||||
return Let[I](lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// This is useful for resetting fields, initializing values, or setting fields to
|
||||
// predetermined constants.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Config{Debug: true, Timeout: 30}),
|
||||
// LetToL(debugLens, false),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Config{Debug: false, Timeout: 30})
|
||||
func LetToL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[I, S, S] {
|
||||
return LetTo[I](lens.Set, b)
|
||||
}
|
||||
665
v2/optics/codec/decode/bind_test.go
Normal file
665
v2/optics/codec/decode/bind_test.go
Normal file
@@ -0,0 +1,665 @@
|
||||
package decode
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
t.Run("creates decoder with empty state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
decoder := Do[string](State{})
|
||||
result := decoder("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{}, value)
|
||||
})
|
||||
|
||||
t.Run("creates decoder with initialized state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
initial := State{x: 42, y: "hello"}
|
||||
decoder := Do[string](initial)
|
||||
result := decoder("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, initial, value)
|
||||
})
|
||||
|
||||
t.Run("works with different input types", func(t *testing.T) {
|
||||
intDecoder := Do[int](0)
|
||||
assert.True(t, either.IsRight(intDecoder(42)))
|
||||
|
||||
strDecoder := Do[string]("")
|
||||
assert.True(t, either.IsRight(strDecoder("test")))
|
||||
|
||||
type Custom struct{ Value int }
|
||||
customDecoder := Do[[]byte](Custom{Value: 100})
|
||||
assert.True(t, either.IsRight(customDecoder([]byte("data"))))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("binds successful decode to state", func(t *testing.T) {
|
||||
decoder := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return Of[string](42)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return Of[string](10)
|
||||
}),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 42, y: 10}, value)
|
||||
})
|
||||
|
||||
t.Run("propagates failure", func(t *testing.T) {
|
||||
decoder := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return Of[string](42)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return func(input string) validation.Validation[int] {
|
||||
return validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "y failed"},
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(State) validation.Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "y failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("can access previous state values", func(t *testing.T) {
|
||||
decoder := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return Of[string](10)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
// y depends on x
|
||||
return Of[string](s.x * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 10, y: 20}, value)
|
||||
})
|
||||
|
||||
t.Run("can access input in decoder", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return func(input string) validation.Validation[int] {
|
||||
// Use input to determine value
|
||||
if input == "large" {
|
||||
return validation.Success(100)
|
||||
}
|
||||
return validation.Success(10)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result1 := decoder("large")
|
||||
value1 := either.MonadFold(result1,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, 100, value1.x)
|
||||
|
||||
result2 := decoder("small")
|
||||
value2 := either.MonadFold(result2,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, 10, value2.x)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
computed int
|
||||
}
|
||||
|
||||
t.Run("attaches pure computation result to state", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Do[string](State{x: 5}),
|
||||
Let[string](func(c int) func(State) State {
|
||||
return func(s State) State { s.computed = c; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 5, computed: 10}, value)
|
||||
})
|
||||
|
||||
t.Run("chains multiple Let operations", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
z int
|
||||
}
|
||||
decoder := F.Pipe3(
|
||||
Do[string](State{x: 5}),
|
||||
Let[string](func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
Let[string](func(z int) func(State) State {
|
||||
return func(s State) State { s.z = z; return s }
|
||||
}, func(s State) int { return s.y + 10 }),
|
||||
Let[string](func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) int { return s.z * 3 }),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
name string
|
||||
}
|
||||
|
||||
t.Run("attaches constant value to state", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Do[string](State{x: 5}),
|
||||
LetTo[string](func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "example"),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 5, name: "example"}, value)
|
||||
})
|
||||
|
||||
t.Run("sets multiple constant values", func(t *testing.T) {
|
||||
type State struct {
|
||||
name string
|
||||
version int
|
||||
active bool
|
||||
}
|
||||
decoder := F.Pipe3(
|
||||
Do[string](State{}),
|
||||
LetTo[string](func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "app"),
|
||||
LetTo[string](func(v int) func(State) State {
|
||||
return func(s State) State { s.version = v; return s }
|
||||
}, 2),
|
||||
LetTo[string](func(a bool) func(State) State {
|
||||
return func(s State) State { s.active = a; return s }
|
||||
}, true),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{name: "app", version: 2, active: true}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
}
|
||||
|
||||
t.Run("initializes state from value", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Of[string](42),
|
||||
BindTo[string](func(x int) State { return State{value: x} }),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{value: 42}, value)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
type StringState struct {
|
||||
text string
|
||||
}
|
||||
decoder := F.Pipe1(
|
||||
Of[string]("hello"),
|
||||
BindTo[string](func(s string) StringState { return StringState{text: s} }),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) StringState { return StringState{} },
|
||||
F.Identity[StringState],
|
||||
)
|
||||
assert.Equal(t, StringState{text: "hello"}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("attaches value using applicative pattern", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Do[string](State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Of[string](42)),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 42}, value)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors from both decoders", func(t *testing.T) {
|
||||
stateDecoder := func(input string) validation.Validation[State] {
|
||||
return validation.Failures[State](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "state error"},
|
||||
})
|
||||
}
|
||||
valueDecoder := func(input string) validation.Validation[int] {
|
||||
return validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "value error"},
|
||||
})
|
||||
}
|
||||
|
||||
decoder := ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, valueDecoder)(stateDecoder)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(State) validation.Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "state error")
|
||||
assert.Contains(t, messages, "value error")
|
||||
})
|
||||
|
||||
t.Run("combines multiple ApS operations", func(t *testing.T) {
|
||||
decoder := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Of[string](10)),
|
||||
ApS(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, Of[string](20)),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 10, y: 20}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
t.Run("updates nested structure using lens", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
decoder := F.Pipe1(
|
||||
Of[string](Person{Name: "Alice"}),
|
||||
ApSL(
|
||||
addressLens,
|
||||
Of[string](Address{Street: "Main St", City: "NYC"}),
|
||||
),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Person { return Person{} },
|
||||
F.Identity[Person],
|
||||
)
|
||||
assert.Equal(t, "Alice", value.Name)
|
||||
assert.Equal(t, "Main St", value.Address.Street)
|
||||
assert.Equal(t, "NYC", value.Address.City)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
personDecoder := func(input string) validation.Validation[Person] {
|
||||
return validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "person error"},
|
||||
})
|
||||
}
|
||||
addressDecoder := func(input string) validation.Validation[Address] {
|
||||
return validation.Failures[Address](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "address error"},
|
||||
})
|
||||
}
|
||||
|
||||
decoder := ApSL(addressLens, addressDecoder)(personDecoder)
|
||||
result := decoder("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(Person) validation.Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("updates field based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Decode[string, int] {
|
||||
return Of[string](v + 1)
|
||||
}
|
||||
|
||||
decoder := F.Pipe1(
|
||||
Of[string](Counter{Value: 42}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, Counter{Value: 43}, value)
|
||||
})
|
||||
|
||||
t.Run("fails validation based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Decode[string, int] {
|
||||
return func(input string) validation.Validation[int] {
|
||||
if v >= 100 {
|
||||
return validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "exceeds limit"},
|
||||
})
|
||||
}
|
||||
return validation.Success(v + 1)
|
||||
}
|
||||
}
|
||||
|
||||
decoder := F.Pipe1(
|
||||
Of[string](Counter{Value: 100}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(Counter) validation.Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "exceeds limit", errors[0].Messsage)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("transforms field with pure function", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
decoder := F.Pipe1(
|
||||
Of[string](Counter{Value: 21}),
|
||||
LetL[string](valueLens, double),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, Counter{Value: 42}, value)
|
||||
})
|
||||
|
||||
t.Run("chains multiple transformations", func(t *testing.T) {
|
||||
add10 := func(v int) int { return v + 10 }
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
decoder := F.Pipe2(
|
||||
Of[string](Counter{Value: 5}),
|
||||
LetL[string](valueLens, add10),
|
||||
LetL[string](valueLens, double),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, Counter{Value: 30}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Timeout int
|
||||
}
|
||||
|
||||
debugLens := L.MakeLens(
|
||||
func(c Config) bool { return c.Debug },
|
||||
func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
)
|
||||
|
||||
t.Run("sets field to constant value", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Of[string](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[string](debugLens, false),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Config { return Config{} },
|
||||
F.Identity[Config],
|
||||
)
|
||||
assert.Equal(t, Config{Debug: false, Timeout: 30}, value)
|
||||
})
|
||||
|
||||
t.Run("sets multiple fields", func(t *testing.T) {
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) int { return c.Timeout },
|
||||
func(c Config, t int) Config { c.Timeout = t; return c },
|
||||
)
|
||||
|
||||
decoder := F.Pipe2(
|
||||
Of[string](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[string](debugLens, false),
|
||||
LetToL[string](timeoutLens, 60),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Config { return Config{} },
|
||||
F.Identity[Config],
|
||||
)
|
||||
assert.Equal(t, Config{Debug: false, Timeout: 60}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindOperationsComposition(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
t.Run("combines Do, Bind, Let, and LetTo", func(t *testing.T) {
|
||||
decoder := F.Pipe4(
|
||||
Do[string](User{}),
|
||||
LetTo[string](func(n string) func(User) User {
|
||||
return func(u User) User { u.Name = n; return u }
|
||||
}, "Alice"),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Decode[string, int] {
|
||||
// Age validation
|
||||
if len(u.Name) > 0 {
|
||||
return Of[string](25)
|
||||
}
|
||||
return func(input string) validation.Validation[int] {
|
||||
return validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "name required"},
|
||||
})
|
||||
}
|
||||
}),
|
||||
Let[string](func(e string) func(User) User {
|
||||
return func(u User) User { u.Email = e; return u }
|
||||
}, func(u User) string {
|
||||
// Derive email from name
|
||||
return u.Name + "@example.com"
|
||||
}),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Decode[string, int] {
|
||||
// Validate age is positive
|
||||
if u.Age > 0 {
|
||||
return Of[string](u.Age)
|
||||
}
|
||||
return func(input string) validation.Validation[int] {
|
||||
return validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "age must be positive"},
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) User { return User{} },
|
||||
F.Identity[User],
|
||||
)
|
||||
assert.Equal(t, "Alice", value.Name)
|
||||
assert.Equal(t, 25, value.Age)
|
||||
assert.Equal(t, "Alice@example.com", value.Email)
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package decode
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
// Of creates a Decode that always succeeds with the given value.
|
||||
@@ -14,7 +15,82 @@ import (
|
||||
// decoder := decode.Of[string](42)
|
||||
// result := decoder("any input") // Always returns validation.Success(42)
|
||||
func Of[I, A any](a A) Decode[I, A] {
|
||||
return reader.Of[I](validation.Of(a))
|
||||
return readereither.Of[I, Errors](a)
|
||||
}
|
||||
|
||||
// Left creates a Decode that always fails with the given validation errors.
|
||||
// This is the dual of Of - while Of lifts a success value, Left lifts failure errors
|
||||
// into the Decode context.
|
||||
//
|
||||
// Left is useful for:
|
||||
// - Creating decoders that represent known failure states
|
||||
// - Short-circuiting decode pipelines with specific errors
|
||||
// - Building custom validation error responses
|
||||
// - Testing error handling paths
|
||||
//
|
||||
// The returned decoder ignores its input and always returns a validation failure
|
||||
// containing the provided errors. This makes it the identity element for the
|
||||
// Alt/OrElse operations when used as a fallback.
|
||||
//
|
||||
// Type signature: func(Errors) Decode[I, A]
|
||||
// - Takes validation errors
|
||||
// - Returns a decoder that always fails with those errors
|
||||
// - The decoder ignores its input of type I
|
||||
// - The failure type A can be any type (phantom type)
|
||||
//
|
||||
// Example - Creating a failing decoder:
|
||||
//
|
||||
// failDecoder := decode.Left[string, int](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: nil,
|
||||
// Messsage: "operation not supported",
|
||||
// },
|
||||
// })
|
||||
// result := failDecoder("any input") // Always fails with the error
|
||||
//
|
||||
// Example - Short-circuiting with specific errors:
|
||||
//
|
||||
// validateAge := func(age int) Decode[map[string]any, int] {
|
||||
// if age < 0 {
|
||||
// return decode.Left[map[string]any, int](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: age,
|
||||
// Context: validation.Context{{Key: "age", Type: "int"}},
|
||||
// Messsage: "age cannot be negative",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// return decode.Of[map[string]any](age)
|
||||
// }
|
||||
//
|
||||
// Example - Building error responses:
|
||||
//
|
||||
// notFoundError := decode.Left[string, User](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Messsage: "user not found",
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// decoder := decode.MonadAlt(
|
||||
// tryFindUser,
|
||||
// func() Decode[string, User] { return notFoundError },
|
||||
// )
|
||||
//
|
||||
// Example - Testing error paths:
|
||||
//
|
||||
// // Create a decoder that always fails for testing
|
||||
// alwaysFails := decode.Left[string, int](validation.Errors{
|
||||
// &validation.ValidationError{Messsage: "test error"},
|
||||
// })
|
||||
//
|
||||
// // Test error recovery logic
|
||||
// recovered := decode.OrElse(func(errs Errors) Decode[string, int] {
|
||||
// return decode.Of[string](0) // recover with default
|
||||
// })(alwaysFails)
|
||||
//
|
||||
// result := recovered("input") // Success(0)
|
||||
func Left[I, A any](err Errors) Decode[I, A] {
|
||||
return readereither.Left[I, A](err)
|
||||
}
|
||||
|
||||
// MonadChain sequences two decode operations, passing the result of the first to the second.
|
||||
@@ -50,6 +126,212 @@ func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// ChainLeft transforms the error channel of a decoder, enabling error recovery and context addition.
|
||||
// This is the left-biased monadic chain operation that operates on validation failures.
|
||||
//
|
||||
// **Key behaviors**:
|
||||
// - Success values pass through unchanged - the handler is never called
|
||||
// - On failure, the handler receives the errors and can recover or add context
|
||||
// - When the handler also fails, **both original and new errors are aggregated**
|
||||
// - The handler returns a Decode[I, A], giving it access to the original input
|
||||
//
|
||||
// **Error Aggregation**: Unlike standard Either operations, when the transformation function
|
||||
// returns a failure, both the original errors AND the new errors are combined using the
|
||||
// Errors monoid. This ensures no validation errors are lost.
|
||||
//
|
||||
// Use cases:
|
||||
// - Adding contextual information to validation errors
|
||||
// - Recovering from specific error conditions
|
||||
// - Transforming error messages while preserving original errors
|
||||
// - Implementing conditional recovery based on error types
|
||||
//
|
||||
// Example - Error recovery:
|
||||
//
|
||||
// failingDecoder := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not found"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// recoverFromNotFound := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "not found" {
|
||||
// return Of[string](0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](errs)
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// decoder := recoverFromNotFound(failingDecoder)
|
||||
// result := decoder("input") // Success(0) - recovered from failure
|
||||
//
|
||||
// Example - Adding context:
|
||||
//
|
||||
// addContext := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {
|
||||
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to decode user age",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// // Result will contain BOTH original error and context error
|
||||
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return readert.Chain[Decode[I, A]](
|
||||
validation.ChainLeft,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainLeft transforms the error channel of a decoder, enabling error recovery and context addition.
|
||||
// This is the uncurried version of ChainLeft, taking both the decoder and the transformation function directly.
|
||||
//
|
||||
// **Key behaviors**:
|
||||
// - Success values pass through unchanged - the handler is never called
|
||||
// - On failure, the handler receives the errors and can recover or add context
|
||||
// - When the handler also fails, **both original and new errors are aggregated**
|
||||
// - The handler returns a Decode[I, A], giving it access to the original input
|
||||
//
|
||||
// **Error Aggregation**: Unlike standard Either operations, when the transformation function
|
||||
// returns a failure, both the original errors AND the new errors are combined using the
|
||||
// Errors monoid. This ensures no validation errors are lost.
|
||||
//
|
||||
// This function is the direct, uncurried form of ChainLeft. Use ChainLeft when you need
|
||||
// a curried operator for composition pipelines, and use MonadChainLeft when you have both
|
||||
// the decoder and transformation function available at once.
|
||||
//
|
||||
// Use cases:
|
||||
// - Adding contextual information to validation errors
|
||||
// - Recovering from specific error conditions
|
||||
// - Transforming error messages while preserving original errors
|
||||
// - Implementing conditional recovery based on error types
|
||||
//
|
||||
// Example - Error recovery:
|
||||
//
|
||||
// failingDecoder := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not found"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// recoverFromNotFound := func(errs Errors) Decode[string, int] {
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "not found" {
|
||||
// return Of[string](0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](errs)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadChainLeft(failingDecoder, recoverFromNotFound)
|
||||
// result := decoder("input") // Success(0) - recovered from failure
|
||||
//
|
||||
// Example - Adding context:
|
||||
//
|
||||
// addContext := func(errs Errors) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {
|
||||
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to decode user age",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadChainLeft(failingDecoder, addContext)
|
||||
// result := decoder("abc")
|
||||
// // Result will contain BOTH original error and context error
|
||||
//
|
||||
// Example - Comparison with ChainLeft:
|
||||
//
|
||||
// // MonadChainLeft - direct application
|
||||
// result1 := MonadChainLeft(decoder, handler)("input")
|
||||
//
|
||||
// // ChainLeft - curried for pipelines
|
||||
// result2 := ChainLeft(handler)(decoder)("input")
|
||||
//
|
||||
// // Both produce identical results
|
||||
func MonadChainLeft[I, A any](fa Decode[I, A], f Kleisli[I, Errors, A]) Decode[I, A] {
|
||||
return readert.MonadChain(
|
||||
validation.MonadChainLeft,
|
||||
fa,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// OrElse provides fallback decoding logic when the primary decoder fails.
|
||||
// This is an alias for ChainLeft with a more semantic name for fallback scenarios.
|
||||
//
|
||||
// **OrElse is exactly the same as ChainLeft** - they are aliases with identical implementations
|
||||
// and behavior. The choice between them is purely about code readability and semantic intent:
|
||||
// - Use **OrElse** when emphasizing fallback/alternative decoding logic
|
||||
// - Use **ChainLeft** when emphasizing technical error channel transformation
|
||||
//
|
||||
// **Key behaviors** (identical to ChainLeft):
|
||||
// - Success values pass through unchanged - the handler is never called
|
||||
// - On failure, the handler receives the errors and can provide an alternative
|
||||
// - When the handler also fails, **both original and new errors are aggregated**
|
||||
// - The handler returns a Decode[I, A], giving it access to the original input
|
||||
//
|
||||
// The name "OrElse" reads naturally in code: "try this decoder, or else try this alternative."
|
||||
// This makes it ideal for expressing fallback logic and default values.
|
||||
//
|
||||
// Use cases:
|
||||
// - Providing default values when decoding fails
|
||||
// - Trying alternative decoding strategies
|
||||
// - Implementing fallback chains with multiple alternatives
|
||||
// - Input-dependent recovery (using access to original input)
|
||||
//
|
||||
// Example - Simple fallback:
|
||||
//
|
||||
// primaryDecoder := func(input string) Validation[int] {
|
||||
// n, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not a valid integer"},
|
||||
// })
|
||||
// }
|
||||
// return validation.Of(n)
|
||||
// }
|
||||
//
|
||||
// withDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
// return Of[string](0) // default to 0 if decoding fails
|
||||
// })
|
||||
//
|
||||
// decoder := withDefault(primaryDecoder)
|
||||
// result1 := decoder("42") // Success(42)
|
||||
// result2 := decoder("abc") // Success(0) - fallback
|
||||
//
|
||||
// Example - Input-dependent fallback:
|
||||
//
|
||||
// smartDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// // Access original input to determine appropriate default
|
||||
// if strings.Contains(input, "http") {
|
||||
// return validation.Of(80)
|
||||
// }
|
||||
// if strings.Contains(input, "https") {
|
||||
// return validation.Of(443)
|
||||
// }
|
||||
// return validation.Of(8080)
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// decoder := smartDefault(decodePort)
|
||||
// result1 := decoder("http-server") // Success(80)
|
||||
// result2 := decoder("https-server") // Success(443)
|
||||
// result3 := decoder("other") // Success(8080)
|
||||
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
|
||||
// MonadMap transforms the decoded value using the provided function.
|
||||
// This is the functor map operation that applies a transformation to successful decode results.
|
||||
//
|
||||
@@ -127,3 +409,155 @@ func Ap[B, I, A any](fa Decode[I, A]) Operator[I, func(A) B, B] {
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAlt provides alternative/fallback decoding with error aggregation.
|
||||
// This is the Alternative pattern's core operation that tries the first decoder,
|
||||
// and if it fails, tries the second decoder as a fallback.
|
||||
//
|
||||
// **Key behaviors**:
|
||||
// - If first succeeds: returns the first result (second is never evaluated)
|
||||
// - If first fails and second succeeds: returns the second result
|
||||
// - If both fail: **aggregates errors from both decoders**
|
||||
//
|
||||
// **Error Aggregation**: Unlike simple fallback patterns, when both decoders fail,
|
||||
// MonadAlt combines ALL errors from both attempts using the Errors monoid. This ensures
|
||||
// complete visibility into why all alternatives failed, which is crucial for debugging
|
||||
// and providing comprehensive error messages to users.
|
||||
//
|
||||
// The name "Alt" comes from the Alternative type class in functional programming,
|
||||
// which represents computations with a notion of choice and failure.
|
||||
//
|
||||
// Use cases:
|
||||
// - Trying multiple decoding strategies for the same input
|
||||
// - Providing fallback decoders when primary decoder fails
|
||||
// - Building validation pipelines with multiple alternatives
|
||||
// - Implementing "try this, or else try that" logic
|
||||
//
|
||||
// Example - Simple fallback:
|
||||
//
|
||||
// primaryDecoder := func(input string) Validation[int] {
|
||||
// n, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not a valid integer"},
|
||||
// })
|
||||
// }
|
||||
// return validation.Of(n)
|
||||
// }
|
||||
//
|
||||
// fallbackDecoder := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// // Try parsing as float and converting to int
|
||||
// f, err := strconv.ParseFloat(input, 64)
|
||||
// if err != nil {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not a valid number"},
|
||||
// })
|
||||
// }
|
||||
// return validation.Of(int(f))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadAlt(primaryDecoder, fallbackDecoder)
|
||||
// result1 := decoder("42") // Success(42) - primary succeeds
|
||||
// result2 := decoder("42.5") // Success(42) - fallback succeeds
|
||||
// result3 := decoder("abc") // Failures with both errors aggregated
|
||||
//
|
||||
// Example - Multiple alternatives:
|
||||
//
|
||||
// decoder1 := parseAsJSON
|
||||
// decoder2 := func() Decode[string, Config] { return parseAsYAML }
|
||||
// decoder3 := func() Decode[string, Config] { return parseAsINI }
|
||||
//
|
||||
// // Try JSON, then YAML, then INI
|
||||
// decoder := MonadAlt(MonadAlt(decoder1, decoder2), decoder3)
|
||||
// // If all fail, errors from all three attempts are aggregated
|
||||
//
|
||||
// Example - Error aggregation:
|
||||
//
|
||||
// failing1 := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Messsage: "primary decoder failed"},
|
||||
// })
|
||||
// }
|
||||
// failing2 := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Messsage: "fallback decoder failed"},
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadAlt(failing1, failing2)
|
||||
// result := decoder("input")
|
||||
// // Result contains BOTH errors: ["primary decoder failed", "fallback decoder failed"]
|
||||
func MonadAlt[I, A any](first Decode[I, A], second Lazy[Decode[I, A]]) Decode[I, A] {
|
||||
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
// Alt creates an operator that provides alternative/fallback decoding with error aggregation.
|
||||
// This is the curried version of MonadAlt, useful for composition pipelines.
|
||||
//
|
||||
// **Key behaviors** (identical to MonadAlt):
|
||||
// - If first succeeds: returns the first result (second is never evaluated)
|
||||
// - If first fails and second succeeds: returns the second result
|
||||
// - If both fail: **aggregates errors from both decoders**
|
||||
//
|
||||
// The Alt operator enables building reusable fallback chains that can be applied
|
||||
// to different decoders. It reads naturally in pipelines: "apply this decoder,
|
||||
// with this alternative if it fails."
|
||||
//
|
||||
// Use cases:
|
||||
// - Creating reusable fallback strategies
|
||||
// - Building decoder combinators with alternatives
|
||||
// - Composing multiple fallback layers
|
||||
// - Implementing retry logic with different strategies
|
||||
//
|
||||
// Example - Creating a reusable fallback:
|
||||
//
|
||||
// // Create an operator that falls back to a default value
|
||||
// withDefault := Alt(func() Decode[string, int] {
|
||||
// return Of[string](0)
|
||||
// })
|
||||
//
|
||||
// // Apply to any decoder
|
||||
// decoder1 := withDefault(parseInteger)
|
||||
// decoder2 := withDefault(parseFromJSON)
|
||||
//
|
||||
// result1 := decoder1("42") // Success(42)
|
||||
// result2 := decoder1("abc") // Success(0) - fallback
|
||||
//
|
||||
// Example - Composing multiple alternatives:
|
||||
//
|
||||
// tryYAML := Alt(func() Decode[string, Config] { return parseAsYAML })
|
||||
// tryINI := Alt(func() Decode[string, Config] { return parseAsINI })
|
||||
// useDefault := Alt(func() Decode[string, Config] {
|
||||
// return Of[string](defaultConfig)
|
||||
// })
|
||||
//
|
||||
// // Build a pipeline: try JSON, then YAML, then INI, then default
|
||||
// decoder := useDefault(tryINI(tryYAML(parseAsJSON)))
|
||||
//
|
||||
// Example - Error aggregation in pipeline:
|
||||
//
|
||||
// failing1 := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{{Messsage: "error 1"}})
|
||||
// }
|
||||
// failing2 := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{{Messsage: "error 2"}})
|
||||
// }
|
||||
// }
|
||||
// failing3 := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{{Messsage: "error 3"}})
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Chain multiple alternatives
|
||||
// decoder := Alt(failing3)(Alt(failing2)(failing1))
|
||||
// result := decoder("input")
|
||||
// // Result contains ALL errors: ["error 1", "error 2", "error 3"]
|
||||
func Alt[I, A any](second Lazy[Decode[I, A]]) Operator[I, A, A] {
|
||||
return ChainLeft(function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
368
v2/optics/codec/decode/monoid.go
Normal file
368
v2/optics/codec/decode/monoid.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package decode
|
||||
|
||||
import "github.com/IBM/fp-go/v2/monoid"
|
||||
|
||||
// ApplicativeMonoid creates a Monoid instance for Decode[I, A] given a Monoid for A.
|
||||
// This allows combining decoders where both the decoded values and validation errors
|
||||
// are combined according to their respective monoid operations.
|
||||
//
|
||||
// The resulting monoid enables:
|
||||
// - Combining multiple decoders that produce monoidal values
|
||||
// - Accumulating validation errors when any decoder fails
|
||||
// - Building complex decoders from simpler ones through composition
|
||||
//
|
||||
// **Behavior**:
|
||||
// - Empty: Returns a decoder that always succeeds with the empty value from the inner monoid
|
||||
// - Concat: Combines two decoders:
|
||||
// - Both succeed: Combines decoded values using the inner monoid
|
||||
// - Any fails: Accumulates all validation errors using the Errors monoid
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Aggregating results from multiple independent decoders
|
||||
// - Building decoders that combine partial results
|
||||
// - Validating and combining configuration from multiple sources
|
||||
// - Parallel validation with result accumulation
|
||||
//
|
||||
// Example - Combining string decoders:
|
||||
//
|
||||
// import S "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// // Create a monoid for decoders that produce strings
|
||||
// m := ApplicativeMonoid[map[string]any](S.Monoid)
|
||||
//
|
||||
// decoder1 := func(data map[string]any) Validation[string] {
|
||||
// if name, ok := data["firstName"].(string); ok {
|
||||
// return validation.Of(name)
|
||||
// }
|
||||
// return either.Left[string](validation.Errors{
|
||||
// {Messsage: "missing firstName"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// decoder2 := func(data map[string]any) Validation[string] {
|
||||
// if name, ok := data["lastName"].(string); ok {
|
||||
// return validation.Of(" " + name)
|
||||
// }
|
||||
// return either.Left[string](validation.Errors{
|
||||
// {Messsage: "missing lastName"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Combine decoders - will concatenate strings if both succeed
|
||||
// combined := m.Concat(decoder1, decoder2)
|
||||
// result := combined(map[string]any{
|
||||
// "firstName": "John",
|
||||
// "lastName": "Doe",
|
||||
// }) // Success("John Doe")
|
||||
//
|
||||
// Example - Error accumulation:
|
||||
//
|
||||
// // If any decoder fails, errors are accumulated
|
||||
// result := combined(map[string]any{}) // Failures with both error messages
|
||||
//
|
||||
// Example - Numeric aggregation:
|
||||
//
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// intMonoid := monoid.MakeMonoid(N.Add[int], 0)
|
||||
// m := ApplicativeMonoid[string](intMonoid)
|
||||
//
|
||||
// decoder1 := func(input string) Validation[int] {
|
||||
// return validation.Of(10)
|
||||
// }
|
||||
// decoder2 := func(input string) Validation[int] {
|
||||
// return validation.Of(32)
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(decoder1, decoder2)
|
||||
// result := combined("input") // Success(42) - values are added
|
||||
func ApplicativeMonoid[I, A any](m Monoid[A]) Monoid[Decode[I, A]] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[I, A],
|
||||
MonadMap[I, A, Endomorphism[A]],
|
||||
MonadAp[A, I, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a Monoid instance for Decode[I, A] using the Alternative pattern.
|
||||
// This combines applicative error-accumulation behavior with alternative fallback behavior,
|
||||
// allowing you to both accumulate errors and provide fallback alternatives when combining decoders.
|
||||
//
|
||||
// The Alternative pattern provides two key operations:
|
||||
// - Applicative operations (Of, Map, Ap): accumulate errors when combining decoders
|
||||
// - Alternative operation (Alt): provide fallback when a decoder fails
|
||||
//
|
||||
// This monoid is particularly useful when you want to:
|
||||
// - Try multiple decoding strategies and fall back to alternatives
|
||||
// - Combine successful values using the provided monoid
|
||||
// - Accumulate all errors from failed attempts
|
||||
// - Build decoding pipelines with fallback logic
|
||||
//
|
||||
// **Behavior**:
|
||||
// - Empty: Returns a decoder that always succeeds with the empty value from the inner monoid
|
||||
// - Concat: Combines two decoders using both applicative and alternative semantics:
|
||||
// - If first succeeds and second succeeds: combines decoded values using inner monoid
|
||||
// - If first fails: tries second as fallback (alternative behavior)
|
||||
// - If both fail: **accumulates all errors from both decoders**
|
||||
//
|
||||
// **Error Aggregation**: When both decoders fail, all validation errors from both attempts
|
||||
// are combined using the Errors monoid. This provides complete visibility into why all
|
||||
// alternatives failed, which is essential for debugging and user feedback.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - I: The input type being decoded
|
||||
// - A: The output type after successful decoding
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The monoid for combining successful decoded values of type A
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Monoid[Decode[I, A]] that combines applicative and alternative behaviors
|
||||
//
|
||||
// Example - Combining successful decoders:
|
||||
//
|
||||
// import S "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// m := AlternativeMonoid[string](S.Monoid)
|
||||
//
|
||||
// decoder1 := func(input string) Validation[string] {
|
||||
// return validation.Of("Hello")
|
||||
// }
|
||||
// decoder2 := func(input string) Validation[string] {
|
||||
// return validation.Of(" World")
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(decoder1, decoder2)
|
||||
// result := combined("input")
|
||||
// // Result: Success("Hello World") - values combined using string monoid
|
||||
//
|
||||
// Example - Fallback behavior:
|
||||
//
|
||||
// m := AlternativeMonoid[string](S.Monoid)
|
||||
//
|
||||
// failing := func(input string) Validation[string] {
|
||||
// return either.Left[string](validation.Errors{
|
||||
// {Value: input, Messsage: "primary failed"},
|
||||
// })
|
||||
// }
|
||||
// fallback := func(input string) Validation[string] {
|
||||
// return validation.Of("fallback value")
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(failing, fallback)
|
||||
// result := combined("input")
|
||||
// // Result: Success("fallback value") - second decoder used as fallback
|
||||
//
|
||||
// Example - Error accumulation when both fail:
|
||||
//
|
||||
// m := AlternativeMonoid[string](S.Monoid)
|
||||
//
|
||||
// failing1 := func(input string) Validation[string] {
|
||||
// return either.Left[string](validation.Errors{
|
||||
// {Value: input, Messsage: "error 1"},
|
||||
// })
|
||||
// }
|
||||
// failing2 := func(input string) Validation[string] {
|
||||
// return either.Left[string](validation.Errors{
|
||||
// {Value: input, Messsage: "error 2"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(failing1, failing2)
|
||||
// result := combined("input")
|
||||
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
|
||||
//
|
||||
// Example - Building decoder with multiple fallbacks:
|
||||
//
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// m := AlternativeMonoid[string](N.MonoidSum[int]())
|
||||
//
|
||||
// // Try to parse from different formats
|
||||
// parseJSON := func(input string) Validation[int] { /* ... */ }
|
||||
// parseYAML := func(input string) Validation[int] { /* ... */ }
|
||||
// parseINI := func(input string) Validation[int] { /* ... */ }
|
||||
//
|
||||
// // Combine with fallback chain
|
||||
// decoder := m.Concat(m.Concat(parseJSON, parseYAML), parseINI)
|
||||
// // Uses first successful parser, or accumulates all errors if all fail
|
||||
//
|
||||
// Example - Combining multiple configuration sources:
|
||||
//
|
||||
// type Config struct{ Port int }
|
||||
// configMonoid := monoid.MakeMonoid(
|
||||
// func(a, b Config) Config {
|
||||
// if b.Port != 0 { return b }
|
||||
// return a
|
||||
// },
|
||||
// Config{Port: 0},
|
||||
// )
|
||||
//
|
||||
// m := AlternativeMonoid[map[string]any](configMonoid)
|
||||
//
|
||||
// fromEnv := func(data map[string]any) Validation[Config] { /* ... */ }
|
||||
// fromFile := func(data map[string]any) Validation[Config] { /* ... */ }
|
||||
// fromDefault := func(data map[string]any) Validation[Config] {
|
||||
// return validation.Of(Config{Port: 8080})
|
||||
// }
|
||||
//
|
||||
// // Try env, then file, then default
|
||||
// decoder := m.Concat(m.Concat(fromEnv, fromFile), fromDefault)
|
||||
// // Returns first successful config, or all errors if all fail
|
||||
func AlternativeMonoid[I, A any](m Monoid[A]) Monoid[Decode[I, A]] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[I, A],
|
||||
MonadMap[I, A, func(A) A],
|
||||
MonadAp[A, I, A],
|
||||
MonadAlt[I, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a Monoid instance for Decode[I, A] using the Alt (alternative) operation.
|
||||
// This monoid provides a way to combine decoders with fallback behavior, where the second
|
||||
// decoder is used as an alternative if the first one fails.
|
||||
//
|
||||
// The Alt operation implements the "try first, fallback to second" pattern, which is useful
|
||||
// for decoding scenarios where you want to attempt multiple decoding strategies in sequence
|
||||
// and use the first one that succeeds.
|
||||
//
|
||||
// **Behavior**:
|
||||
// - Empty: Returns the provided zero value (a lazy computation that produces a Decode[I, A])
|
||||
// - Concat: Combines two decoders using Alt semantics:
|
||||
// - If first succeeds: returns the first result (second is never evaluated)
|
||||
// - If first fails: tries the second decoder as fallback
|
||||
// - If both fail: **aggregates errors from both decoders**
|
||||
//
|
||||
// **Error Aggregation**: When both decoders fail, all validation errors from both attempts
|
||||
// are combined using the Errors monoid. This ensures complete visibility into why all
|
||||
// alternatives failed.
|
||||
//
|
||||
// This is different from [AlternativeMonoid] in that:
|
||||
// - AltMonoid uses a custom zero value (provided by the user)
|
||||
// - AlternativeMonoid derives the zero from an inner monoid
|
||||
// - AltMonoid is simpler and only provides fallback behavior
|
||||
// - AlternativeMonoid combines applicative and alternative behaviors
|
||||
//
|
||||
// Type Parameters:
|
||||
// - I: The input type being decoded
|
||||
// - A: The output type after successful decoding
|
||||
//
|
||||
// Parameters:
|
||||
// - zero: A lazy computation that produces the identity/empty Decode[I, A].
|
||||
// This is typically a decoder that always succeeds with a default value, or could be
|
||||
// a decoder that always fails representing "no decoding attempted"
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Monoid[Decode[I, A]] that combines decoders with fallback behavior
|
||||
//
|
||||
// Example - Using default value as zero:
|
||||
//
|
||||
// m := AltMonoid(func() Decode[string, int] {
|
||||
// return Of[string](0)
|
||||
// })
|
||||
//
|
||||
// failing := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "failed"},
|
||||
// })
|
||||
// }
|
||||
// succeeding := func(input string) Validation[int] {
|
||||
// return validation.Of(42)
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(failing, succeeding)
|
||||
// result := combined("input")
|
||||
// // Result: Success(42) - falls back to second decoder
|
||||
//
|
||||
// empty := m.Empty()
|
||||
// result2 := empty("input")
|
||||
// // Result: Success(0) - the provided zero value
|
||||
//
|
||||
// Example - Chaining multiple fallbacks:
|
||||
//
|
||||
// m := AltMonoid(func() Decode[string, Config] {
|
||||
// return Of[string](defaultConfig)
|
||||
// })
|
||||
//
|
||||
// primary := parseFromPrimarySource // Fails
|
||||
// secondary := parseFromSecondarySource // Fails
|
||||
// tertiary := parseFromTertiarySource // Succeeds
|
||||
//
|
||||
// // Chain fallbacks
|
||||
// decoder := m.Concat(m.Concat(primary, secondary), tertiary)
|
||||
// result := decoder("input")
|
||||
// // Result: Success from tertiary - uses first successful decoder
|
||||
//
|
||||
// Example - Error aggregation when all fail:
|
||||
//
|
||||
// m := AltMonoid(func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Messsage: "no default available"},
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// failing1 := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "error 1"},
|
||||
// })
|
||||
// }
|
||||
// failing2 := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "error 2"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(failing1, failing2)
|
||||
// result := combined("input")
|
||||
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
|
||||
//
|
||||
// Example - Building a decoder pipeline with fallbacks:
|
||||
//
|
||||
// m := AltMonoid(func() Decode[string, Config] {
|
||||
// return Of[string](defaultConfig)
|
||||
// })
|
||||
//
|
||||
// // Try multiple decoding sources in order
|
||||
// decoders := []Decode[string, Config]{
|
||||
// loadFromFile("config.json"), // Try file first
|
||||
// loadFromEnv, // Then environment
|
||||
// loadFromRemote("api.example.com"), // Then remote API
|
||||
// }
|
||||
//
|
||||
// // Fold using the monoid to get first successful config
|
||||
// result := array.MonoidFold(m)(decoders)
|
||||
// // Result: First successful config, or defaultConfig if all fail
|
||||
//
|
||||
// Example - Comparing with AlternativeMonoid:
|
||||
//
|
||||
// // AltMonoid - simple fallback with custom zero
|
||||
// altM := AltMonoid(func() Decode[string, int] {
|
||||
// return Of[string](0)
|
||||
// })
|
||||
//
|
||||
// // AlternativeMonoid - combines values when both succeed
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
// altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
|
||||
//
|
||||
// decoder1 := Of[string](10)
|
||||
// decoder2 := Of[string](32)
|
||||
//
|
||||
// // AltMonoid: returns first success (10)
|
||||
// result1 := altM.Concat(decoder1, decoder2)("input")
|
||||
// // Result: Success(10)
|
||||
//
|
||||
// // AlternativeMonoid: combines both successes (10 + 32 = 42)
|
||||
// result2 := altMonoid.Concat(decoder1, decoder2)("input")
|
||||
// // Result: Success(42)
|
||||
func AltMonoid[I, A any](zero Lazy[Decode[I, A]]) Monoid[Decode[I, A]] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[I, A],
|
||||
)
|
||||
}
|
||||
970
v2/optics/codec/decode/monoid_test.go
Normal file
970
v2/optics/codec/decode/monoid_test.go
Normal file
@@ -0,0 +1,970 @@
|
||||
package decode
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
MO "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
t.Run("with string monoid", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](S.Monoid)
|
||||
|
||||
t.Run("empty returns decoder that succeeds with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("any input")
|
||||
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
|
||||
t.Run("concat combines successful decoders", func(t *testing.T) {
|
||||
decoder1 := Of[string]("Hello")
|
||||
decoder2 := Of[string](" World")
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of("Hello World"), result)
|
||||
})
|
||||
|
||||
t.Run("concat with failure returns failure", func(t *testing.T) {
|
||||
decoder1 := Of[string]("Hello")
|
||||
decoder2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "decode failed"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "decode failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("concat accumulates all errors from both failures", func(t *testing.T) {
|
||||
decoder1 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
decoder2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves decoder", func(t *testing.T) {
|
||||
decoder := Of[string]("test")
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(decoder, empty)("input")
|
||||
result2 := m.Concat(empty, decoder)("input")
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "test", val1)
|
||||
assert.Equal(t, "test", val2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with int addition monoid", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := ApplicativeMonoid[string](intMonoid)
|
||||
|
||||
t.Run("empty returns decoder with zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("concat adds decoded values", func(t *testing.T) {
|
||||
decoder1 := Of[string](10)
|
||||
decoder2 := Of[string](32)
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
decoder1 := Of[string](1)
|
||||
decoder2 := Of[string](2)
|
||||
decoder3 := Of[string](3)
|
||||
decoder4 := Of[string](4)
|
||||
|
||||
combined := m.Concat(m.Concat(m.Concat(decoder1, decoder2), decoder3), decoder4)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with map input type", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[map[string]any](S.Monoid)
|
||||
|
||||
t.Run("combines decoders with different inputs", func(t *testing.T) {
|
||||
decoder1 := func(data map[string]any) Validation[string] {
|
||||
if name, ok := data["firstName"].(string); ok {
|
||||
return validation.Of(name)
|
||||
}
|
||||
return either.Left[string](validation.Errors{
|
||||
{Messsage: "missing firstName"},
|
||||
})
|
||||
}
|
||||
|
||||
decoder2 := func(data map[string]any) Validation[string] {
|
||||
if name, ok := data["lastName"].(string); ok {
|
||||
return validation.Of(" " + name)
|
||||
}
|
||||
return either.Left[string](validation.Errors{
|
||||
{Messsage: "missing lastName"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
|
||||
// Test success case
|
||||
result1 := combined(map[string]any{
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
})
|
||||
value1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "John Doe", value1)
|
||||
|
||||
// Test failure case - both fields missing
|
||||
result2 := combined(map[string]any{})
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
errors := either.MonadFold(result2,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
t.Run("ApplicativeMonoid satisfies monoid laws", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](S.Monoid)
|
||||
|
||||
decoder1 := Of[string]("a")
|
||||
decoder2 := Of[string]("b")
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// empty + a = a
|
||||
result := m.Concat(m.Empty(), decoder1)("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// a + empty = a
|
||||
result := m.Concat(decoder1, m.Empty())("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
decoder3 := Of[string]("c")
|
||||
// (a + b) + c = a + (b + c)
|
||||
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
|
||||
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "abc", leftVal)
|
||||
assert.Equal(t, "abc", rightVal)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidWithFailures(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](S.Monoid)
|
||||
|
||||
t.Run("failure propagates through concat", func(t *testing.T) {
|
||||
decoder1 := Of[string]("a")
|
||||
decoder2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error"},
|
||||
})
|
||||
}
|
||||
decoder3 := Of[string]("c")
|
||||
|
||||
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
})
|
||||
|
||||
t.Run("multiple failures accumulate", func(t *testing.T) {
|
||||
decoder1 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
decoder2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
decoder3 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 3"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
|
||||
result := combined("input")
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 3)
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
assert.Contains(t, messages, "error 3")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidEdgeCases(t *testing.T) {
|
||||
t.Run("with custom struct monoid", func(t *testing.T) {
|
||||
type Counter struct{ Count int }
|
||||
|
||||
counterMonoid := MO.MakeMonoid(
|
||||
func(a, b Counter) Counter { return Counter{Count: a.Count + b.Count} },
|
||||
Counter{Count: 0},
|
||||
)
|
||||
|
||||
m := ApplicativeMonoid[string](counterMonoid)
|
||||
|
||||
decoder1 := Of[string](Counter{Count: 5})
|
||||
decoder2 := Of[string](Counter{Count: 10})
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, 15, value.Count)
|
||||
})
|
||||
|
||||
t.Run("empty concat empty", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](S.Monoid)
|
||||
|
||||
combined := m.Concat(m.Empty(), m.Empty())
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "ERROR" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
})
|
||||
|
||||
t.Run("with different input types", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := ApplicativeMonoid[int](intMonoid)
|
||||
|
||||
decoder1 := func(input int) Validation[int] {
|
||||
return validation.Of(input * 2)
|
||||
}
|
||||
decoder2 := func(input int) Validation[int] {
|
||||
return validation.Of(input + 10)
|
||||
}
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined(5)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
// (5 * 2) + (5 + 10) = 10 + 15 = 25
|
||||
assert.Equal(t, 25, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoidRealWorldScenarios(t *testing.T) {
|
||||
t.Run("combining configuration from multiple sources", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// Monoid that combines configs (last non-empty wins for strings, sum for ints)
|
||||
configMonoid := MO.MakeMonoid(
|
||||
func(a, b Config) Config {
|
||||
host := a.Host
|
||||
if b.Host != "" {
|
||||
host = b.Host
|
||||
}
|
||||
return Config{
|
||||
Host: host,
|
||||
Port: a.Port + b.Port,
|
||||
}
|
||||
},
|
||||
Config{Host: "", Port: 0},
|
||||
)
|
||||
|
||||
m := ApplicativeMonoid[map[string]any](configMonoid)
|
||||
|
||||
decoder1 := func(data map[string]any) Validation[Config] {
|
||||
if host, ok := data["host"].(string); ok {
|
||||
return validation.Of(Config{Host: host, Port: 0})
|
||||
}
|
||||
return either.Left[Config](validation.Errors{
|
||||
{Messsage: "missing host"},
|
||||
})
|
||||
}
|
||||
|
||||
decoder2 := func(data map[string]any) Validation[Config] {
|
||||
if port, ok := data["port"].(int); ok {
|
||||
return validation.Of(Config{Host: "", Port: port})
|
||||
}
|
||||
return either.Left[Config](validation.Errors{
|
||||
{Messsage: "missing port"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
|
||||
// Success case
|
||||
result := combined(map[string]any{
|
||||
"host": "localhost",
|
||||
"port": 8080,
|
||||
})
|
||||
|
||||
config := either.MonadFold(result,
|
||||
func(Errors) Config { return Config{} },
|
||||
F.Identity[Config],
|
||||
)
|
||||
assert.Equal(t, "localhost", config.Host)
|
||||
assert.Equal(t, 8080, config.Port)
|
||||
})
|
||||
|
||||
t.Run("aggregating validation results", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := ApplicativeMonoid[string](intMonoid)
|
||||
|
||||
// Decoder that extracts and validates a number
|
||||
makeDecoder := func(value int, shouldFail bool) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
if shouldFail {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "validation failed"},
|
||||
})
|
||||
}
|
||||
return validation.Of(value)
|
||||
}
|
||||
}
|
||||
|
||||
// All succeed - values are summed
|
||||
decoder1 := makeDecoder(10, false)
|
||||
decoder2 := makeDecoder(20, false)
|
||||
decoder3 := makeDecoder(12, false)
|
||||
|
||||
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
|
||||
// Some fail - errors are accumulated
|
||||
decoder4 := makeDecoder(10, true)
|
||||
decoder5 := makeDecoder(20, true)
|
||||
|
||||
combinedFail := m.Concat(decoder4, decoder5)
|
||||
resultFail := combinedFail("input")
|
||||
|
||||
assert.True(t, either.IsLeft(resultFail))
|
||||
errors := either.MonadFold(resultFail,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlternativeMonoid tests the AlternativeMonoid function
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
t.Run("with string monoid", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string](S.Monoid)
|
||||
|
||||
t.Run("empty returns decoder that succeeds with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")
|
||||
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
|
||||
t.Run("concat combines successful decoders using monoid", func(t *testing.T) {
|
||||
decoder1 := Of[string]("Hello")
|
||||
decoder2 := Of[string](" World")
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of("Hello World"), result)
|
||||
})
|
||||
|
||||
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "first failed"},
|
||||
})
|
||||
}
|
||||
succeeding := Of[string]("fallback")
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of("fallback"), result)
|
||||
})
|
||||
|
||||
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
failing2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both decoders")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves decoder", func(t *testing.T) {
|
||||
decoder := Of[string]("test")
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(decoder, empty)("input")
|
||||
result2 := m.Concat(empty, decoder)("input")
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "test", val1)
|
||||
assert.Equal(t, "test", val2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with int addition monoid", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := AlternativeMonoid[string](intMonoid)
|
||||
|
||||
t.Run("empty returns decoder with zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
|
||||
decoder1 := Of[string](10)
|
||||
decoder2 := Of[string](32)
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("concat uses fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
succeeding := Of[string](42)
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
decoder1 := Of[string](1)
|
||||
decoder2 := Of[string](2)
|
||||
decoder3 := Of[string](3)
|
||||
decoder4 := Of[string](4)
|
||||
|
||||
combined := m.Concat(m.Concat(m.Concat(decoder1, decoder2), decoder3), decoder4)
|
||||
result := combined("input")
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string](S.Monoid)
|
||||
|
||||
decoder1 := Of[string]("a")
|
||||
decoder2 := Of[string]("b")
|
||||
decoder3 := Of[string]("c")
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
result := m.Concat(m.Empty(), decoder1)("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
result := m.Concat(decoder1, m.Empty())("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
|
||||
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "abc", leftVal)
|
||||
assert.Equal(t, "abc", rightVal)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("error aggregation with multiple failures", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string](S.Monoid)
|
||||
|
||||
failing1 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
failing2 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
failing3 := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 3"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(m.Concat(failing1, failing2), failing3)
|
||||
result := combined("input")
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 3, "Should aggregate errors from all decoders")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
assert.Contains(t, messages, "error 3")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltMonoid tests the AltMonoid function
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
t.Run("with default value as zero", func(t *testing.T) {
|
||||
m := AltMonoid(func() Decode[string, int] {
|
||||
return Of[string](0)
|
||||
})
|
||||
|
||||
t.Run("empty returns the provided zero decoder", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")
|
||||
|
||||
assert.Equal(t, validation.Of(0), result)
|
||||
})
|
||||
|
||||
t.Run("concat returns first decoder when it succeeds", func(t *testing.T) {
|
||||
decoder1 := Of[string](42)
|
||||
decoder2 := Of[string](100)
|
||||
|
||||
combined := m.Concat(decoder1, decoder2)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
succeeding := Of[string](42)
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
failing2 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both decoders")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with failing zero", func(t *testing.T) {
|
||||
m := AltMonoid(func() Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "no default available"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty returns the failing zero decoder", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("concat with all failures aggregates errors", func(t *testing.T) {
|
||||
failing1 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
failing2 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("chaining multiple fallbacks", func(t *testing.T) {
|
||||
m := AltMonoid(func() Decode[string, string] {
|
||||
return Of[string]("default")
|
||||
})
|
||||
|
||||
primary := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "primary failed"},
|
||||
})
|
||||
}
|
||||
secondary := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "secondary failed"},
|
||||
})
|
||||
}
|
||||
tertiary := Of[string]("tertiary value")
|
||||
|
||||
combined := m.Concat(m.Concat(primary, secondary), tertiary)
|
||||
result := combined("input")
|
||||
|
||||
assert.Equal(t, validation.Of("tertiary value"), result)
|
||||
})
|
||||
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := AltMonoid(func() Decode[string, int] {
|
||||
return Of[string](0)
|
||||
})
|
||||
|
||||
decoder1 := Of[string](1)
|
||||
decoder2 := Of[string](2)
|
||||
decoder3 := Of[string](3)
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
result := m.Concat(m.Empty(), decoder1)("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
// With AltMonoid, first success wins, so empty (0) is returned
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
result := m.Concat(decoder1, m.Empty())("input")
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
// First decoder succeeds, so 1 is returned
|
||||
assert.Equal(t, 1, value)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
// For AltMonoid, first success wins
|
||||
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
|
||||
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Both should return 1 (first success)
|
||||
assert.Equal(t, 1, leftVal)
|
||||
assert.Equal(t, 1, rightVal)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
|
||||
// AltMonoid - first success wins
|
||||
altM := AltMonoid(func() Decode[string, int] {
|
||||
return Of[string](0)
|
||||
})
|
||||
|
||||
// AlternativeMonoid - combines successes
|
||||
altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
|
||||
|
||||
decoder1 := Of[string](10)
|
||||
decoder2 := Of[string](32)
|
||||
|
||||
// AltMonoid: returns first success (10)
|
||||
result1 := altM.Concat(decoder1, decoder2)("input")
|
||||
value1 := either.MonadFold(result1,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value1, "AltMonoid returns first success")
|
||||
|
||||
// AlternativeMonoid: combines both successes (10 + 32 = 42)
|
||||
result2 := altMonoid.Concat(decoder1, decoder2)("input")
|
||||
value2 := either.MonadFold(result2,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value2, "AlternativeMonoid combines successes")
|
||||
})
|
||||
|
||||
t.Run("error aggregation with context", func(t *testing.T) {
|
||||
m := AltMonoid(func() Decode[string, int] {
|
||||
return Of[string](0)
|
||||
})
|
||||
|
||||
failing1 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Value: input,
|
||||
Messsage: "parse error",
|
||||
Context: validation.Context{{Key: "field", Type: "int"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
failing2 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Value: input,
|
||||
Messsage: "validation error",
|
||||
Context: validation.Context{{Key: "value", Type: "int"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("abc")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should have errors from both decoders")
|
||||
|
||||
// Verify that errors with context are present
|
||||
hasParseError := false
|
||||
hasValidationError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "parse error" {
|
||||
hasParseError = true
|
||||
assert.NotNil(t, err.Context)
|
||||
}
|
||||
if err.Messsage == "validation error" {
|
||||
hasValidationError = true
|
||||
assert.NotNil(t, err.Context)
|
||||
}
|
||||
}
|
||||
assert.True(t, hasParseError, "Should have parse error")
|
||||
assert.True(t, hasValidationError, "Should have validation error")
|
||||
})
|
||||
}
|
||||
@@ -1,30 +1,346 @@
|
||||
// Copyright (c) 2025 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 decode
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
// Errors is a collection of validation errors that occurred during decoding.
|
||||
// This is an alias for validation.Errors, which is []*ValidationError.
|
||||
//
|
||||
// Errors accumulates multiple validation failures, allowing decoders to report
|
||||
// all problems at once rather than failing on the first error. This is particularly
|
||||
// useful for form validation, API request validation, and configuration parsing
|
||||
// where users benefit from seeing all issues simultaneously.
|
||||
//
|
||||
// The Errors type forms a Semigroup and Monoid, enabling:
|
||||
// - Concatenation: Combining errors from multiple decoders
|
||||
// - Accumulation: Collecting errors through applicative operations
|
||||
// - Empty value: An empty slice representing no errors (success)
|
||||
//
|
||||
// Each error in the collection is a *ValidationError containing:
|
||||
// - Value: The actual value that failed validation
|
||||
// - Context: The path to the value in nested structures
|
||||
// - Message: Human-readable error description
|
||||
// - Cause: Optional underlying error
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Multiple validation failures
|
||||
// errors := Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: "",
|
||||
// Context: []validation.ContextEntry{{Key: "name"}},
|
||||
// Messsage: "name is required",
|
||||
// },
|
||||
// &validation.ValidationError{
|
||||
// Value: "invalid@",
|
||||
// Context: []validation.ContextEntry{{Key: "email"}},
|
||||
// Messsage: "invalid email format",
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // Create a failed validation with these errors
|
||||
// result := validation.Failures[User](errors)
|
||||
//
|
||||
// // Errors can be combined using the monoid
|
||||
// moreErrors := Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: -1,
|
||||
// Context: []validation.ContextEntry{{Key: "age"}},
|
||||
// Messsage: "age must be positive",
|
||||
// },
|
||||
// }
|
||||
// allErrors := append(errors, moreErrors...)
|
||||
Errors = validation.Errors
|
||||
|
||||
// Validation represents the result of a validation operation that may contain
|
||||
// validation errors or a successfully validated value of type A.
|
||||
// This is an alias for validation.Validation[A], which is Either[Errors, A].
|
||||
//
|
||||
// In the decode context:
|
||||
// - Left(Errors): Decoding failed with one or more validation errors
|
||||
// - Right(A): Successfully decoded value of type A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Success case
|
||||
// valid := validation.Success(42) // Right(42)
|
||||
//
|
||||
// // Failure case
|
||||
// invalid := validation.Failures[int](validation.Errors{
|
||||
// &validation.ValidationError{Messsage: "invalid format"},
|
||||
// }) // Left([...])
|
||||
Validation[A any] = validation.Validation[A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
// This is an alias for reader.Reader[R, A], which is func(R) A.
|
||||
//
|
||||
// In the decode context, Reader is used to access the input data being decoded.
|
||||
// The environment R is typically the raw input (e.g., JSON, string, bytes) that
|
||||
// needs to be decoded into a structured type A.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A reader that extracts a field from a map
|
||||
// getField := func(data map[string]any) string {
|
||||
// return data["name"].(string)
|
||||
// } // Reader[map[string]any, string]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// Decode is a function that decodes input I to type A with validation.
|
||||
// It returns a Validation result directly.
|
||||
// It combines the Reader pattern (for accessing input) with Validation (for error handling).
|
||||
//
|
||||
// Type: func(I) Validation[A]
|
||||
//
|
||||
// A Decode function:
|
||||
// 1. Takes raw input of type I (e.g., JSON, string, bytes)
|
||||
// 2. Attempts to decode/parse it into type A
|
||||
// 3. Returns a Validation[A] with either:
|
||||
// - Success(A): Successfully decoded value
|
||||
// - Failures(Errors): Validation errors describing what went wrong
|
||||
//
|
||||
// This type is the foundation of the decode package, enabling composable,
|
||||
// type-safe decoding with comprehensive error reporting.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Decode a string to an integer
|
||||
// decodeInt := func(input string) Validation[int] {
|
||||
// n, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return validation.Failures[int](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: input,
|
||||
// Messsage: "not a valid integer",
|
||||
// Cause: err,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// return validation.Success(n)
|
||||
// } // Decode[string, int]
|
||||
//
|
||||
// result := decodeInt("42") // Success(42)
|
||||
// result := decodeInt("abc") // Failures([...])
|
||||
Decode[I, A any] = Reader[I, Validation[A]]
|
||||
|
||||
// Kleisli represents a function from A to a decoded B given input type I.
|
||||
// It's a Reader that takes an input A and produces a Decode[I, B] function.
|
||||
// This enables composition of decoding operations in a functional style.
|
||||
//
|
||||
// Type: func(A) Decode[I, B]
|
||||
// which expands to: func(A) func(I) Validation[B]
|
||||
//
|
||||
// Kleisli arrows are the fundamental building blocks for composing decoders.
|
||||
// They allow you to chain decoding operations where each step can:
|
||||
// 1. Depend on the result of the previous step (the A parameter)
|
||||
// 2. Access the original input (the I parameter via Decode)
|
||||
// 3. Fail with validation errors (via Validation[B])
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Conditional decoding based on previously decoded values
|
||||
// - Multi-stage decoding pipelines
|
||||
// - Dependent field validation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Decode a user, then decode their age based on their type
|
||||
// decodeAge := func(userType string) Decode[map[string]any, int] {
|
||||
// return func(data map[string]any) Validation[int] {
|
||||
// if userType == "admin" {
|
||||
// // Admins must be 18+
|
||||
// age := data["age"].(int)
|
||||
// if age < 18 {
|
||||
// return validation.Failures[int](/* error */)
|
||||
// }
|
||||
// return validation.Success(age)
|
||||
// }
|
||||
// // Regular users can be any age
|
||||
// return validation.Success(data["age"].(int))
|
||||
// }
|
||||
// } // Kleisli[map[string]any, string, int]
|
||||
//
|
||||
// // Use with Chain to compose decoders
|
||||
// decoder := F.Pipe2(
|
||||
// decodeUserType, // Decode[map[string]any, string]
|
||||
// Chain(decodeAge), // Chains with Kleisli
|
||||
// Map(func(age int) User { // Transform to final type
|
||||
// return User{Age: age}
|
||||
// }),
|
||||
// )
|
||||
Kleisli[I, A, B any] = Reader[A, Decode[I, B]]
|
||||
|
||||
// Operator represents a decoding transformation that takes a decoded A and produces a decoded B.
|
||||
// It's a specialized Kleisli arrow for composing decode operations where the input is already decoded.
|
||||
// This allows chaining multiple decode transformations together.
|
||||
//
|
||||
// Type: func(Decode[I, A]) Decode[I, B]
|
||||
//
|
||||
// Operators are higher-order functions that transform one decoder into another.
|
||||
// They are the result of partially applying functions like Map, Chain, and Ap,
|
||||
// making them ideal for use in composition pipelines with F.Pipe.
|
||||
//
|
||||
// Key characteristics:
|
||||
// - Takes a Decode[I, A] as input
|
||||
// - Returns a Decode[I, B] as output
|
||||
// - Preserves the input type I (the raw data being decoded)
|
||||
// - Transforms the output type from A to B
|
||||
//
|
||||
// Common operators:
|
||||
// - Map(f): Transforms successful decode results
|
||||
// - Chain(f): Sequences dependent decode operations
|
||||
// - Ap(fa): Applies function decoders to value decoders
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create reusable operators
|
||||
// toString := Map(func(n int) string {
|
||||
// return strconv.Itoa(n)
|
||||
// }) // Operator[string, int, string]
|
||||
//
|
||||
// validatePositive := Chain(func(n int) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// if n <= 0 {
|
||||
// return validation.Failures[int](/* error */)
|
||||
// }
|
||||
// return validation.Success(n)
|
||||
// }
|
||||
// }) // Operator[string, int, int]
|
||||
//
|
||||
// // Compose operators in a pipeline
|
||||
// decoder := F.Pipe2(
|
||||
// decodeInt, // Decode[string, int]
|
||||
// validatePositive, // Operator[string, int, int]
|
||||
// toString, // Operator[string, int, string]
|
||||
// ) // Decode[string, string]
|
||||
//
|
||||
// result := decoder("42") // Success("42")
|
||||
// result := decoder("-5") // Failures([...])
|
||||
Operator[I, A, B any] = Kleisli[I, Decode[I, A], B]
|
||||
|
||||
// Endomorphism represents a function from a type to itself: func(A) A.
|
||||
// This is an alias for endomorphism.Endomorphism[A].
|
||||
//
|
||||
// In the decode context, endomorphisms are used with LetL to transform
|
||||
// decoded values using pure functions that don't change the type.
|
||||
//
|
||||
// Endomorphisms are useful for:
|
||||
// - Normalizing data (e.g., trimming strings, rounding numbers)
|
||||
// - Applying business rules (e.g., clamping values to ranges)
|
||||
// - Data sanitization (e.g., removing special characters)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Normalize a string by trimming and lowercasing
|
||||
// normalize := func(s string) string {
|
||||
// return strings.ToLower(strings.TrimSpace(s))
|
||||
// } // Endomorphism[string]
|
||||
//
|
||||
// // Clamp an integer to a range
|
||||
// clamp := func(n int) int {
|
||||
// if n < 0 { return 0 }
|
||||
// if n > 100 { return 100 }
|
||||
// return n
|
||||
// } // Endomorphism[int]
|
||||
//
|
||||
// // Use with LetL to transform decoded values
|
||||
// decoder := F.Pipe1(
|
||||
// decodeString,
|
||||
// LetL(nameLens, normalize),
|
||||
// )
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Monoid represents an algebraic structure with an associative binary operation
|
||||
// and an identity element. This is an alias for monoid.Monoid[A].
|
||||
//
|
||||
// A Monoid[A] consists of:
|
||||
// - Concat: func(A, A) A - An associative binary operation
|
||||
// - Empty: func() A - An identity element
|
||||
//
|
||||
// In the decode context, monoids are used to combine multiple decoders or
|
||||
// validation results. The most common use case is combining validation errors
|
||||
// from multiple decoders using the Errors monoid.
|
||||
//
|
||||
// Properties:
|
||||
// - Associativity: Concat(Concat(a, b), c) == Concat(a, Concat(b, c))
|
||||
// - Identity: Concat(Empty(), a) == a == Concat(a, Empty())
|
||||
//
|
||||
// Common monoid instances:
|
||||
// - Errors: Combines validation errors from multiple sources
|
||||
// - Array: Concatenates arrays of decoded values
|
||||
// - String: Concatenates strings
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Combine validation errors from multiple decoders
|
||||
// errorsMonoid := validation.GetMonoid[int]()
|
||||
//
|
||||
// // Decode multiple fields and combine errors
|
||||
// result1 := decodeField1(data) // Validation[string]
|
||||
// result2 := decodeField2(data) // Validation[int]
|
||||
//
|
||||
// // If both fail, errors are combined using the monoid
|
||||
// combined := errorsMonoid.Concat(result1, result2)
|
||||
//
|
||||
// // The monoid's Empty() provides a successful validation with no errors
|
||||
// empty := errorsMonoid.Empty() // Success with no value
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
|
||||
// Lazy represents a deferred computation that produces a value of type A.
|
||||
// This is an alias for lazy.Lazy[A], which is func() A.
|
||||
//
|
||||
// In the decode context, Lazy is used to defer expensive computations or
|
||||
// recursive decoder definitions until they are actually needed. This is
|
||||
// particularly important for:
|
||||
// - Recursive data structures (e.g., trees, linked lists)
|
||||
// - Expensive default values
|
||||
// - Breaking circular dependencies in decoder definitions
|
||||
//
|
||||
// A Lazy[A] is simply a function that takes no arguments and returns A.
|
||||
// The computation is only executed when the function is called, allowing
|
||||
// for lazy evaluation and recursive definitions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Define a recursive decoder for a tree structure
|
||||
// type Tree struct {
|
||||
// Value int
|
||||
// Children []Tree
|
||||
// }
|
||||
//
|
||||
// // Use Lazy to break the circular dependency
|
||||
// var decodeTree Decode[map[string]any, Tree]
|
||||
// decodeTree = func(data map[string]any) Validation[Tree] {
|
||||
// // Lazy evaluation allows referencing decodeTree within itself
|
||||
// childrenDecoder := Array(Lazy(func() Decode[map[string]any, Tree] {
|
||||
// return decodeTree
|
||||
// }))
|
||||
// // ... rest of decoder implementation
|
||||
// }
|
||||
//
|
||||
// // Lazy default value that's only computed if needed
|
||||
// expensiveDefault := Lazy(func() Config {
|
||||
// // This computation only runs if the decode fails
|
||||
// return computeExpensiveDefaultConfig()
|
||||
// })
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
)
|
||||
|
||||
282
v2/optics/codec/either.go
Normal file
282
v2/optics/codec/either.go
Normal file
@@ -0,0 +1,282 @@
|
||||
// Copyright (c) 2023 - 2025 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 codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
)
|
||||
|
||||
// encodeEither creates an encoder for Either[A, B] values.
|
||||
//
|
||||
// This function produces an encoder that handles both Left and Right cases of an Either value.
|
||||
// It uses the provided codecs to encode the Left (A) and Right (B) values respectively.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the Left value
|
||||
// - B: The type of the Right value
|
||||
// - O: The output type after encoding
|
||||
// - I: The input type for validation (not used in encoding)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - leftItem: The codec for encoding Left values of type A
|
||||
// - rightItem: The codec for encoding Right values of type B
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Encode function that takes an Either[A, B] and returns O by encoding
|
||||
// either the Left or Right value using the appropriate codec.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// stringCodec := String()
|
||||
// intCodec := Int()
|
||||
// encoder := encodeEither(stringCodec, intCodec)
|
||||
//
|
||||
// // Encode a Left value
|
||||
// leftResult := encoder(either.Left[int]("error"))
|
||||
// // leftResult contains the encoded string "error"
|
||||
//
|
||||
// // Encode a Right value
|
||||
// rightResult := encoder(either.Right[string](42))
|
||||
// // rightResult contains the encoded int 42
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Uses either.Fold to pattern match on the Either value
|
||||
// - Left values are encoded using leftItem.Encode
|
||||
// - Right values are encoded using rightItem.Encode
|
||||
func encodeEither[A, B, O, I any](
|
||||
leftItem Type[A, O, I],
|
||||
rightItem Type[B, O, I],
|
||||
) Encode[either.Either[A, B], O] {
|
||||
return either.Fold(
|
||||
leftItem.Encode,
|
||||
rightItem.Encode,
|
||||
)
|
||||
}
|
||||
|
||||
// validateEither creates a validator for Either[A, B] values.
|
||||
//
|
||||
// This function produces a validator that attempts to validate the input as both
|
||||
// a Left (A) and Right (B) value. The validation strategy is:
|
||||
// 1. First, try to validate as a Right value (B)
|
||||
// 2. If Right validation succeeds, return Either.Right[A](B)
|
||||
// 3. If Right validation fails, try to validate as a Left value (A)
|
||||
// 4. If Left validation succeeds, return Either.Left[B](A)
|
||||
// 5. If both validations fail, concatenate all errors from both attempts
|
||||
//
|
||||
// This approach ensures that the validator tries both branches and provides
|
||||
// comprehensive error information when both fail.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the Left value
|
||||
// - B: The type of the Right value
|
||||
// - O: The output type after encoding (not used in validation)
|
||||
// - I: The input type to validate
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - leftItem: The codec for validating Left values of type A
|
||||
// - rightItem: The codec for validating Right values of type B
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate function that takes an input I and returns a Decode function.
|
||||
// The Decode function takes a Context and returns a Validation[Either[A, B]].
|
||||
//
|
||||
// # Validation Logic
|
||||
//
|
||||
// The validator follows this decision tree:
|
||||
//
|
||||
// Input I
|
||||
// |
|
||||
// +--> Validate as Right (B)
|
||||
// |
|
||||
// +-- Success --> Return Either.Right[A](B)
|
||||
// |
|
||||
// +-- Failure --> Validate as Left (A)
|
||||
// |
|
||||
// +-- Success --> Return Either.Left[B](A)
|
||||
// |
|
||||
// +-- Failure --> Return all errors (Left + Right)
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// stringCodec := String()
|
||||
// intCodec := Int()
|
||||
// validator := validateEither(stringCodec, intCodec)
|
||||
//
|
||||
// // Validate a string (will succeed as Left)
|
||||
// result1 := validator("hello")(validation.Context{})
|
||||
// // result1 is Success(Either.Left[int]("hello"))
|
||||
//
|
||||
// // Validate an int (will succeed as Right)
|
||||
// result2 := validator(42)(validation.Context{})
|
||||
// // result2 is Success(Either.Right[string](42))
|
||||
//
|
||||
// // Validate something that's neither (will fail with both errors)
|
||||
// result3 := validator([]int{1, 2, 3})(validation.Context{})
|
||||
// // result3 is Failure with errors from both string and int validation
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Prioritizes Right validation over Left validation
|
||||
// - Accumulates errors from both branches when both fail
|
||||
// - Uses the validation context to provide detailed error messages
|
||||
// - The validator is lazy: it only evaluates Left if Right fails
|
||||
func validateEither[A, B, O, I any](
|
||||
leftItem Type[A, O, I],
|
||||
rightItem Type[B, O, I],
|
||||
) Validate[I, either.Either[A, B]] {
|
||||
|
||||
// F.Pipe1(
|
||||
// leftItem.Decode,
|
||||
// decode.OrElse()
|
||||
// )
|
||||
|
||||
return func(i I) Decode[Context, either.Either[A, B]] {
|
||||
valRight := rightItem.Validate(i)
|
||||
valLeft := leftItem.Validate(i)
|
||||
|
||||
return func(ctx Context) Validation[either.Either[A, B]] {
|
||||
|
||||
resRight := valRight(ctx)
|
||||
|
||||
return either.Fold(
|
||||
func(rightErrors validate.Errors) Validation[either.Either[A, B]] {
|
||||
resLeft := valLeft(ctx)
|
||||
return either.Fold(
|
||||
func(leftErrors validate.Errors) Validation[either.Either[A, B]] {
|
||||
return validation.Failures[either.Either[A, B]](array.Concat(leftErrors)(rightErrors))
|
||||
},
|
||||
F.Flow2(either.Left[B, A], validation.Of),
|
||||
)(resLeft)
|
||||
},
|
||||
F.Flow2(either.Right[A, B], validation.Of),
|
||||
)(resRight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Either creates a codec for Either[A, B] values.
|
||||
//
|
||||
// This function constructs a complete codec that can encode, decode, and validate
|
||||
// Either values. An Either represents a value that can be one of two types: Left (A)
|
||||
// or Right (B). This is commonly used for error handling, where Left represents an
|
||||
// error and Right represents a success value.
|
||||
//
|
||||
// The codec handles both branches of the Either type using the provided codecs for
|
||||
// each branch. During validation, it attempts to validate the input as both types
|
||||
// and succeeds if either validation passes.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the Left value
|
||||
// - B: The type of the Right value
|
||||
// - O: The output type after encoding
|
||||
// - I: The input type for validation
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - leftItem: The codec for handling Left values of type A
|
||||
// - rightItem: The codec for handling Right values of type B
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Type[either.Either[A, B], O, I] that can encode, decode, and validate Either values.
|
||||
//
|
||||
// # Codec Behavior
|
||||
//
|
||||
// Encoding:
|
||||
// - Left values are encoded using leftItem.Encode
|
||||
// - Right values are encoded using rightItem.Encode
|
||||
//
|
||||
// Validation:
|
||||
// - First attempts to validate as Right (B)
|
||||
// - If Right fails, attempts to validate as Left (A)
|
||||
// - If both fail, returns all accumulated errors
|
||||
// - If either succeeds, returns the corresponding Either value
|
||||
//
|
||||
// Type Checking:
|
||||
// - Uses Is[either.Either[A, B]]() to verify the value is an Either
|
||||
//
|
||||
// Naming:
|
||||
// - The codec name is "Either[<leftName>, <rightName>]"
|
||||
// - Example: "Either[string, int]"
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Create a codec for Either[string, int]
|
||||
// stringCodec := String()
|
||||
// intCodec := Int()
|
||||
// eitherCodec := Either(stringCodec, intCodec)
|
||||
//
|
||||
// // Encode a Left value
|
||||
// leftEncoded := eitherCodec.Encode(either.Left[int]("error"))
|
||||
// // leftEncoded contains the encoded string
|
||||
//
|
||||
// // Encode a Right value
|
||||
// rightEncoded := eitherCodec.Encode(either.Right[string](42))
|
||||
// // rightEncoded contains the encoded int
|
||||
//
|
||||
// // Decode/validate an input
|
||||
// result := eitherCodec.Decode("hello")
|
||||
// // result is Success(Either.Left[int]("hello"))
|
||||
//
|
||||
// result2 := eitherCodec.Decode(42)
|
||||
// // result2 is Success(Either.Right[string](42))
|
||||
//
|
||||
// // Get the codec name
|
||||
// name := eitherCodec.Name()
|
||||
// // name is "Either[string, int]"
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Error handling: Either[Error, Value]
|
||||
// - Alternative values: Either[DefaultValue, CustomValue]
|
||||
// - Union types: Either[TypeA, TypeB]
|
||||
// - Validation results: Either[ValidationError, ValidatedValue]
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The codec prioritizes Right validation over Left validation
|
||||
// - Both branches must have compatible encoding output types (O)
|
||||
// - Both branches must have compatible validation input types (I)
|
||||
// - The codec name includes the names of both branch codecs
|
||||
// - This is a building block for more complex sum types
|
||||
func Either[A, B, O, I any](
|
||||
leftItem Type[A, O, I],
|
||||
rightItem Type[B, O, I],
|
||||
) Type[either.Either[A, B], O, I] {
|
||||
name := fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name())
|
||||
isEither := Is[either.Either[A, B]]()
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
isEither,
|
||||
validateEither(leftItem, rightItem),
|
||||
encodeEither(leftItem, rightItem),
|
||||
)
|
||||
}
|
||||
347
v2/optics/codec/either_test.go
Normal file
347
v2/optics/codec/either_test.go
Normal file
@@ -0,0 +1,347 @@
|
||||
// Copyright (c) 2023 - 2025 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 codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestEitherWithIdentityCodecs tests the Either function with identity codecs
|
||||
// where both branches have the same output and input types
|
||||
func TestEitherWithIdentityCodecs(t *testing.T) {
|
||||
t.Run("creates codec with correct name", func(t *testing.T) {
|
||||
// The Either function is designed for cases where both branches encode to the same type
|
||||
// For example, both encode to string or both encode to JSON
|
||||
|
||||
// Create codecs that both encode to string
|
||||
stringToString := Id[string]()
|
||||
intToString := IntFromString()
|
||||
|
||||
eitherCodec := Either(stringToString, intToString)
|
||||
|
||||
assert.Equal(t, "Either[string, IntFromString]", eitherCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherEncode tests encoding of Either values
|
||||
func TestEitherEncode(t *testing.T) {
|
||||
// Create codecs that both encode to string
|
||||
stringToString := Id[string]()
|
||||
intToString := IntFromString()
|
||||
|
||||
eitherCodec := Either(stringToString, intToString)
|
||||
|
||||
t.Run("encodes Left value", func(t *testing.T) {
|
||||
leftValue := either.Left[int]("hello")
|
||||
encoded := eitherCodec.Encode(leftValue)
|
||||
|
||||
assert.Equal(t, "hello", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes Right value", func(t *testing.T) {
|
||||
rightValue := either.Right[string](42)
|
||||
encoded := eitherCodec.Encode(rightValue)
|
||||
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherDecode tests decoding/validation of Either values
|
||||
func TestEitherDecode(t *testing.T) {
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, either.Either[string, int]](either.Left[int]("")))
|
||||
|
||||
// Create codecs that both work with string input
|
||||
stringCodec := Id[string]()
|
||||
intFromString := IntFromString()
|
||||
|
||||
eitherCodec := Either(stringCodec, intFromString)
|
||||
|
||||
t.Run("decodes integer string as Right", func(t *testing.T) {
|
||||
result := eitherCodec.Decode("42")
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode integer string")
|
||||
|
||||
value := getOrElseNull(result)
|
||||
assert.True(t, either.IsRight(value), "should be Right")
|
||||
|
||||
rightValue := either.MonadFold(value,
|
||||
func(string) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, rightValue)
|
||||
})
|
||||
|
||||
t.Run("decodes non-integer string as Left", func(t *testing.T) {
|
||||
result := eitherCodec.Decode("hello")
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode string")
|
||||
|
||||
value := getOrElseNull(result)
|
||||
assert.True(t, either.IsLeft(value), "should be Left")
|
||||
|
||||
leftValue := either.MonadFold(value,
|
||||
F.Identity[string],
|
||||
func(int) string { return "" },
|
||||
)
|
||||
assert.Equal(t, "hello", leftValue)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherValidation tests validation behavior
|
||||
func TestEitherValidation(t *testing.T) {
|
||||
t.Run("validates with custom codecs", func(t *testing.T) {
|
||||
// Create a codec that only accepts non-empty strings
|
||||
nonEmptyString := MakeType(
|
||||
"NonEmptyString",
|
||||
func(u any) either.Either[error, string] {
|
||||
s, ok := u.(string)
|
||||
if !ok || len(s) == 0 {
|
||||
return either.Left[string](fmt.Errorf("not a non-empty string"))
|
||||
}
|
||||
return either.Of[error](s)
|
||||
},
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) == 0 {
|
||||
return validation.FailureWithMessage[string](s, "must not be empty")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Create a codec that only accepts positive integers from strings
|
||||
positiveIntFromString := MakeType(
|
||||
"PositiveInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i <= 0 {
|
||||
return either.Left[int](fmt.Errorf("not a positive integer"))
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
var n int
|
||||
_, err := fmt.Sscanf(s, "%d", &n)
|
||||
if err != nil {
|
||||
return validation.FailureWithError[int](s, "expected integer string")(err)(c)
|
||||
}
|
||||
if n <= 0 {
|
||||
return validation.FailureWithMessage[int](n, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
},
|
||||
func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
},
|
||||
)
|
||||
|
||||
eitherCodec := Either(nonEmptyString, positiveIntFromString)
|
||||
|
||||
// Valid non-empty string
|
||||
validLeft := eitherCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(validLeft))
|
||||
|
||||
// Valid positive integer
|
||||
validRight := eitherCodec.Decode("42")
|
||||
assert.True(t, either.IsRight(validRight))
|
||||
|
||||
// Invalid empty string - should fail both validations
|
||||
invalidEmpty := eitherCodec.Decode("")
|
||||
assert.True(t, either.IsLeft(invalidEmpty))
|
||||
|
||||
// Invalid zero - should fail Right validation, succeed as Left
|
||||
zeroResult := eitherCodec.Decode("0")
|
||||
// "0" is a valid non-empty string, so it should succeed as Left
|
||||
assert.True(t, either.IsRight(zeroResult))
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherRoundTrip tests encoding and decoding round trips
|
||||
func TestEitherRoundTrip(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
intFromString := IntFromString()
|
||||
|
||||
eitherCodec := Either(stringCodec, intFromString)
|
||||
|
||||
t.Run("round-trip Left value", func(t *testing.T) {
|
||||
original := "hello"
|
||||
|
||||
// Decode
|
||||
decodeResult := eitherCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
decoded := either.MonadFold(decodeResult,
|
||||
func(validation.Errors) either.Either[string, int] { return either.Left[int]("") },
|
||||
F.Identity[either.Either[string, int]],
|
||||
)
|
||||
|
||||
// Encode
|
||||
encoded := eitherCodec.Encode(decoded)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip Right value", func(t *testing.T) {
|
||||
original := "42"
|
||||
|
||||
// Decode
|
||||
decodeResult := eitherCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
decoded := either.MonadFold(decodeResult,
|
||||
func(validation.Errors) either.Either[string, int] { return either.Right[string](0) },
|
||||
F.Identity[either.Either[string, int]],
|
||||
)
|
||||
|
||||
// Encode
|
||||
encoded := eitherCodec.Encode(decoded)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherPrioritization tests that Right validation is prioritized over Left
|
||||
func TestEitherPrioritization(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
intFromString := IntFromString()
|
||||
|
||||
eitherCodec := Either(stringCodec, intFromString)
|
||||
|
||||
t.Run("prioritizes Right over Left when both could succeed", func(t *testing.T) {
|
||||
// "42" can be validated as both string (Left) and int (Right)
|
||||
// The codec should prioritize Right
|
||||
result := eitherCodec.Decode("42")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) either.Either[string, int] { return either.Left[int]("") },
|
||||
F.Identity[either.Either[string, int]],
|
||||
)
|
||||
|
||||
// Should be Right because int validation succeeds and is prioritized
|
||||
assert.True(t, either.IsRight(value))
|
||||
|
||||
rightValue := either.MonadFold(value,
|
||||
func(string) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, rightValue)
|
||||
})
|
||||
|
||||
t.Run("falls back to Left when Right fails", func(t *testing.T) {
|
||||
// "hello" can only be validated as string (Left), not as int (Right)
|
||||
result := eitherCodec.Decode("hello")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) either.Either[string, int] { return either.Left[int]("") },
|
||||
F.Identity[either.Either[string, int]],
|
||||
)
|
||||
|
||||
// Should be Left because int validation failed
|
||||
assert.True(t, either.IsLeft(value))
|
||||
|
||||
leftValue := either.MonadFold(value,
|
||||
F.Identity[string],
|
||||
func(int) string { return "" },
|
||||
)
|
||||
assert.Equal(t, "hello", leftValue)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherErrorAccumulation tests that errors from both branches are accumulated
|
||||
func TestEitherErrorAccumulation(t *testing.T) {
|
||||
// Create codecs with specific validation rules that will both fail
|
||||
nonEmptyString := MakeType(
|
||||
"NonEmptyString",
|
||||
func(u any) either.Either[error, string] {
|
||||
s, ok := u.(string)
|
||||
if !ok || len(s) == 0 {
|
||||
return either.Left[string](fmt.Errorf("not a non-empty string"))
|
||||
}
|
||||
return either.Of[error](s)
|
||||
},
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) == 0 {
|
||||
return validation.FailureWithMessage[string](s, "must not be empty")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
positiveIntFromString := MakeType(
|
||||
"PositiveInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i <= 0 {
|
||||
return either.Left[int](fmt.Errorf("not a positive integer"))
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
var n int
|
||||
_, err := fmt.Sscanf(s, "%d", &n)
|
||||
if err != nil {
|
||||
return validation.FailureWithError[int](s, "expected integer string")(err)(c)
|
||||
}
|
||||
if n <= 0 {
|
||||
return validation.FailureWithMessage[int](n, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
eitherCodec := Either(nonEmptyString, positiveIntFromString)
|
||||
|
||||
t.Run("accumulates errors from both branches when both fail", func(t *testing.T) {
|
||||
// Empty string will fail both validations
|
||||
result := eitherCodec.Decode("")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(either.Either[string, int]) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
// Should have errors from both string and int validation attempts
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
}
|
||||
335
v2/optics/codec/validate/bind.go
Normal file
335
v2/optics/codec/validate/bind.go
Normal file
@@ -0,0 +1,335 @@
|
||||
// Copyright (c) 2025 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 validate
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type S to be used with the Bind operation.
|
||||
// This is the starting point for building up a context using do-notation style.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Result struct {
|
||||
// x int
|
||||
// y string
|
||||
// }
|
||||
// result := Do(Result{})
|
||||
func Do[I, S any](
|
||||
empty S,
|
||||
) Validate[I, S] {
|
||||
return Of[I](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context S1 to produce a context S2.
|
||||
// This is used in do-notation style to sequentially build up a context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{}),
|
||||
// Bind(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, func(s State) Validate[string, int] {
|
||||
// return Of[string](42)
|
||||
// }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 42})
|
||||
func Bind[I, S1, S2, A any](
|
||||
setter func(A) func(S1) S2,
|
||||
f Kleisli[I, S1, A],
|
||||
) Operator[I, S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[I, S1, S2],
|
||||
Map[I, A, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
|
||||
// Unlike Bind, the computation function returns a plain value, not wrapped in Validate.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; computed int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{x: 5}),
|
||||
// Let[string](func(c int) func(State) State {
|
||||
// return func(s State) State { s.computed = c; return s }
|
||||
// }, func(s State) int { return s.x * 2 }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 5, computed: 10})
|
||||
func Let[I, S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
f func(S1) B,
|
||||
) Operator[I, S1, S2] {
|
||||
return F.Let(
|
||||
Map[I, S1, S2],
|
||||
key,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context S1 to produce a context S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; name string }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{x: 5}),
|
||||
// LetTo(func(n string) func(State) State {
|
||||
// return func(s State) State { s.name = n; return s }
|
||||
// }, "example"),
|
||||
// )
|
||||
func LetTo[I, S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
b B,
|
||||
) Operator[I, S1, S2] {
|
||||
return F.LetTo(
|
||||
Map[I, S1, S2],
|
||||
key,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state S1 from a value T.
|
||||
// This is typically used as the first operation after creating a Validate value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { value int }
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](42),
|
||||
// BindTo[string](func(x int) State { return State{value: x} }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{value: 42})
|
||||
func BindTo[I, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[I, T, S1] {
|
||||
return C.BindTo(
|
||||
Map[I, T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
|
||||
// This uses the applicative functor pattern, allowing parallel composition.
|
||||
//
|
||||
// IMPORTANT: Unlike Bind which fails fast, ApS aggregates ALL validation errors from both the context
|
||||
// and the value. If both validations fail, all errors are collected and returned together.
|
||||
// This is useful for validating multiple independent fields and reporting all errors at once.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{}),
|
||||
// ApS(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, Of[string](42)),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 42})
|
||||
//
|
||||
// Error aggregation example:
|
||||
//
|
||||
// // Both decoders fail - errors are aggregated
|
||||
// decoder1 := func(input string) Validation[State] {
|
||||
// return validation.Failures[State](/* errors */)
|
||||
// }
|
||||
// decoder2 := func(input string) Validation[int] {
|
||||
// return validation.Failures[int](/* errors */)
|
||||
// }
|
||||
// combined := ApS(setter, decoder2)(decoder1)
|
||||
// result := combined("input") // Contains BOTH sets of errors
|
||||
func ApS[I, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Validate[I, T],
|
||||
) Operator[I, S1, S2] {
|
||||
return A.ApS(
|
||||
Ap[S2, I, T],
|
||||
Map[I, S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// IMPORTANT: Like ApS, this function aggregates ALL validation errors. If both the context
|
||||
// and the value fail validation, all errors are collected and returned together.
|
||||
// This enables comprehensive error reporting for complex nested structures.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Address struct {
|
||||
// Street string
|
||||
// City string
|
||||
// }
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Address Address
|
||||
// }
|
||||
//
|
||||
// // Create a lens for the Address field
|
||||
// addressLens := lens.MakeLens(
|
||||
// func(p Person) Address { return p.Address },
|
||||
// func(p Person, a Address) Person { p.Address = a; return p },
|
||||
// )
|
||||
//
|
||||
// // Use ApSL to update the address
|
||||
// decoder := F.Pipe2(
|
||||
// Of[string](Person{Name: "Alice"}),
|
||||
// ApSL(
|
||||
// addressLens,
|
||||
// Of[string](Address{Street: "Main St", City: "NYC"}),
|
||||
// ),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Person{...})
|
||||
func ApSL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Validate[I, T],
|
||||
) Operator[I, S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// a Validation that produces the new value.
|
||||
//
|
||||
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
|
||||
// the current value of the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but fail if it would exceed 100
|
||||
// increment := func(v int) Validate[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// if v >= 100 {
|
||||
// return validation.Failures[int](/* errors */)
|
||||
// }
|
||||
// return validation.Success(v + 1)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Counter{Value: 42}),
|
||||
// BindL(valueLens, increment),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Counter{Value: 43})
|
||||
func BindL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[I, T, T],
|
||||
) Operator[I, S, S] {
|
||||
return Bind(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in Validation).
|
||||
//
|
||||
// This is useful for pure transformations that cannot fail, such as mathematical operations,
|
||||
// string manipulations, or other deterministic updates.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Counter{Value: 21}),
|
||||
// LetL(valueLens, double),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Counter{Value: 42})
|
||||
func LetL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[I, S, S] {
|
||||
return Let[I](lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// This is useful for resetting fields, initializing values, or setting fields to
|
||||
// predetermined constants.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Config{Debug: true, Timeout: 30}),
|
||||
// LetToL(debugLens, false),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Config{Debug: false, Timeout: 30})
|
||||
func LetToL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[I, S, S] {
|
||||
return LetTo[I](lens.Set, b)
|
||||
}
|
||||
733
v2/optics/codec/validate/bind_test.go
Normal file
733
v2/optics/codec/validate/bind_test.go
Normal file
@@ -0,0 +1,733 @@
|
||||
// Copyright (c) 2025 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 validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
t.Run("creates successful validation with empty state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
validator := Do[string](State{})
|
||||
result := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, either.Of[Errors](State{}), result)
|
||||
})
|
||||
|
||||
t.Run("creates successful validation with initialized state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
initial := State{x: 42, y: "hello"}
|
||||
validator := Do[string](initial)
|
||||
result := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, either.Of[Errors](initial), result)
|
||||
})
|
||||
|
||||
t.Run("works with different input types", func(t *testing.T) {
|
||||
intValidator := Do[int](0)
|
||||
assert.Equal(t, either.Of[Errors](0), intValidator(42)(nil))
|
||||
|
||||
strValidator := Do[string]("")
|
||||
assert.Equal(t, either.Of[Errors](""), strValidator("test")(nil))
|
||||
|
||||
type Custom struct{ Value int }
|
||||
customValidator := Do[[]byte](Custom{Value: 100})
|
||||
assert.Equal(t, either.Of[Errors](Custom{Value: 100}), customValidator([]byte("data"))(nil))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("binds successful validation to state", func(t *testing.T) {
|
||||
validator := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return Of[string](42)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return Of[string](10)
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](State{x: 42, y: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("propagates failure", func(t *testing.T) {
|
||||
validator := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return Of[string](42)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "y failed"}})
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "y failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("can access previous state values", func(t *testing.T) {
|
||||
validator := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return Of[string](10)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
// y depends on x
|
||||
return Of[string](s.x * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](State{x: 10, y: 20}), result)
|
||||
})
|
||||
|
||||
t.Run("can access input value", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Do[int](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validate[int, int] {
|
||||
return func(input int) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Success(input * 2)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator(21)(nil)
|
||||
assert.Equal(t, either.Of[Errors](State{x: 42}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
computed int
|
||||
}
|
||||
|
||||
t.Run("attaches pure computation result to state", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Do[string](State{x: 5}),
|
||||
Let[string](func(c int) func(State) State {
|
||||
return func(s State) State { s.computed = c; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 5, computed: 10}, value)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := func(input string) Reader[Context, Validation[State]] {
|
||||
return func(ctx Context) Validation[State] {
|
||||
return validation.Failures[State](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := Let[string](func(c int) func(State) State {
|
||||
return func(s State) State { s.computed = c; return s }
|
||||
}, func(s State) int { return s.x * 2 })
|
||||
|
||||
result := validator(failure)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("chains multiple Let operations", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
z int
|
||||
}
|
||||
validator := F.Pipe3(
|
||||
Do[string](State{x: 5}),
|
||||
Let[string](func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
Let[string](func(z int) func(State) State {
|
||||
return func(s State) State { s.z = z; return s }
|
||||
}, func(s State) int { return s.y + 10 }),
|
||||
Let[string](func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) int { return s.z * 3 }),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
name string
|
||||
}
|
||||
|
||||
t.Run("attaches constant value to state", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Do[string](State{x: 5}),
|
||||
LetTo[string](func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "example"),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 5, name: "example"}, value)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := func(input string) Reader[Context, Validation[State]] {
|
||||
return func(ctx Context) Validation[State] {
|
||||
return validation.Failures[State](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := LetTo[string](func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "example")
|
||||
|
||||
result := validator(failure)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("sets multiple constant values", func(t *testing.T) {
|
||||
type State struct {
|
||||
name string
|
||||
version int
|
||||
active bool
|
||||
}
|
||||
validator := F.Pipe3(
|
||||
Do[string](State{}),
|
||||
LetTo[string](func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "app"),
|
||||
LetTo[string](func(v int) func(State) State {
|
||||
return func(s State) State { s.version = v; return s }
|
||||
}, 2),
|
||||
LetTo[string](func(a bool) func(State) State {
|
||||
return func(s State) State { s.active = a; return s }
|
||||
}, true),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{name: "app", version: 2, active: true}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
}
|
||||
|
||||
t.Run("initializes state from value", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Of[string](42),
|
||||
BindTo[string](func(x int) State { return State{value: x} }),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{value: 42}, value)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := BindTo[string](func(x int) State { return State{value: x} })
|
||||
|
||||
result := validator(failure)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
type StringState struct {
|
||||
text string
|
||||
}
|
||||
validator := F.Pipe1(
|
||||
Of[int]("hello"),
|
||||
BindTo[int](func(s string) StringState { return StringState{text: s} }),
|
||||
)
|
||||
|
||||
result := validator(42)(nil)
|
||||
assert.Equal(t, either.Of[Errors](StringState{text: "hello"}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("attaches value using applicative pattern", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Do[string](State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Of[string](42)),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](State{x: 42}), result)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors from both validations", func(t *testing.T) {
|
||||
stateFailure := func(input string) Reader[Context, Validation[State]] {
|
||||
return func(ctx Context) Validation[State] {
|
||||
return validation.Failures[State](Errors{&validation.ValidationError{Messsage: "state error"}})
|
||||
}
|
||||
}
|
||||
valueFailure := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "value error"}})
|
||||
}
|
||||
}
|
||||
|
||||
validator := ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, valueFailure)
|
||||
|
||||
result := validator(stateFailure)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "state error")
|
||||
assert.Contains(t, messages, "value error")
|
||||
})
|
||||
|
||||
t.Run("combines multiple ApS operations", func(t *testing.T) {
|
||||
validator := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Of[string](10)),
|
||||
ApS(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, Of[string](20)),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](State{x: 10, y: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
t.Run("updates nested structure using lens", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
validator := F.Pipe1(
|
||||
Of[string](Person{Name: "Alice"}),
|
||||
ApSL(
|
||||
addressLens,
|
||||
Of[string](Address{Street: "Main St", City: "NYC"}),
|
||||
),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
expected := Person{
|
||||
Name: "Alice",
|
||||
Address: Address{Street: "Main St", City: "NYC"},
|
||||
}
|
||||
assert.Equal(t, either.Of[Errors](expected), result)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
personFailure := func(input string) Reader[Context, Validation[Person]] {
|
||||
return func(ctx Context) Validation[Person] {
|
||||
return validation.Failures[Person](Errors{&validation.ValidationError{Messsage: "person error"}})
|
||||
}
|
||||
}
|
||||
addressFailure := func(input string) Reader[Context, Validation[Address]] {
|
||||
return func(ctx Context) Validation[Address] {
|
||||
return validation.Failures[Address](Errors{&validation.ValidationError{Messsage: "address error"}})
|
||||
}
|
||||
}
|
||||
|
||||
validator := ApSL(addressLens, addressFailure)
|
||||
result := validator(personFailure)("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(Person) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("updates field based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Validate[string, int] {
|
||||
return Of[string](v + 1)
|
||||
}
|
||||
|
||||
validator := F.Pipe1(
|
||||
Of[string](Counter{Value: 42}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](Counter{Value: 43}), result)
|
||||
})
|
||||
|
||||
t.Run("fails validation based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
if v >= 100 {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "exceeds limit"}})
|
||||
}
|
||||
return validation.Success(v + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := F.Pipe1(
|
||||
Of[string](Counter{Value: 100}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(Counter) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "exceeds limit", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
increment := func(v int) Validate[string, int] {
|
||||
return Of[string](v + 1)
|
||||
}
|
||||
|
||||
failure := func(input string) Reader[Context, Validation[Counter]] {
|
||||
return func(ctx Context) Validation[Counter] {
|
||||
return validation.Failures[Counter](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := BindL(valueLens, increment)
|
||||
result := validator(failure)("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("transforms field with pure function", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
validator := F.Pipe1(
|
||||
Of[string](Counter{Value: 21}),
|
||||
LetL[string](valueLens, double),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](Counter{Value: 42}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
failure := func(input string) Reader[Context, Validation[Counter]] {
|
||||
return func(ctx Context) Validation[Counter] {
|
||||
return validation.Failures[Counter](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := LetL[string](valueLens, double)
|
||||
result := validator(failure)("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("chains multiple transformations", func(t *testing.T) {
|
||||
add10 := func(v int) int { return v + 10 }
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
validator := F.Pipe2(
|
||||
Of[string](Counter{Value: 5}),
|
||||
LetL[string](valueLens, add10),
|
||||
LetL[string](valueLens, double),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](Counter{Value: 30}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Timeout int
|
||||
}
|
||||
|
||||
debugLens := L.MakeLens(
|
||||
func(c Config) bool { return c.Debug },
|
||||
func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
)
|
||||
|
||||
t.Run("sets field to constant value", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Of[string](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[string](debugLens, false),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](Config{Debug: false, Timeout: 30}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := func(input string) Reader[Context, Validation[Config]] {
|
||||
return func(ctx Context) Validation[Config] {
|
||||
return validation.Failures[Config](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := LetToL[string](debugLens, false)
|
||||
result := validator(failure)("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("sets multiple fields", func(t *testing.T) {
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) int { return c.Timeout },
|
||||
func(c Config, t int) Config { c.Timeout = t; return c },
|
||||
)
|
||||
|
||||
validator := F.Pipe2(
|
||||
Of[string](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[string](debugLens, false),
|
||||
LetToL[string](timeoutLens, 60),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](Config{Debug: false, Timeout: 60}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindOperationsComposition(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
t.Run("combines Do, Bind, Let, and LetTo", func(t *testing.T) {
|
||||
validator := F.Pipe4(
|
||||
Do[string](User{}),
|
||||
LetTo[string](func(n string) func(User) User {
|
||||
return func(u User) User { u.Name = n; return u }
|
||||
}, "Alice"),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Validate[string, int] {
|
||||
// Age validation
|
||||
if len(u.Name) > 0 {
|
||||
return Of[string](25)
|
||||
}
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "name required"}})
|
||||
}
|
||||
}
|
||||
}),
|
||||
Let[string](func(e string) func(User) User {
|
||||
return func(u User) User { u.Email = e; return u }
|
||||
}, func(u User) string {
|
||||
// Derive email from name
|
||||
return u.Name + "@example.com"
|
||||
}),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Validate[string, int] {
|
||||
// Validate age is positive
|
||||
if u.Age > 0 {
|
||||
return Of[string](u.Age)
|
||||
}
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "age must be positive"}})
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
expected := User{
|
||||
Name: "Alice",
|
||||
Age: 25,
|
||||
Email: "Alice@example.com",
|
||||
}
|
||||
assert.Equal(t, either.Of[Errors](expected), result)
|
||||
})
|
||||
|
||||
t.Run("validates with input-dependent logic", func(t *testing.T) {
|
||||
type Config struct {
|
||||
MaxValue int
|
||||
Value int
|
||||
}
|
||||
|
||||
validator := F.Pipe2(
|
||||
Do[int](Config{}),
|
||||
Bind(func(max int) func(Config) Config {
|
||||
return func(c Config) Config { c.MaxValue = max; return c }
|
||||
}, func(c Config) Validate[int, int] {
|
||||
// Extract max from input
|
||||
return func(input int) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Success(input)
|
||||
}
|
||||
}
|
||||
}),
|
||||
Bind(func(val int) func(Config) Config {
|
||||
return func(c Config) Config { c.Value = val; return c }
|
||||
}, func(c Config) Validate[int, int] {
|
||||
// Validate value against max
|
||||
return func(input int) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
if input/2 <= c.MaxValue {
|
||||
return validation.Success(input / 2)
|
||||
}
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "value exceeds max"}})
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator(100)(nil)
|
||||
assert.Equal(t, either.Of[Errors](Config{MaxValue: 100, Value: 50}), result)
|
||||
})
|
||||
}
|
||||
661
v2/optics/codec/validate/monad_test.go
Normal file
661
v2/optics/codec/validate/monad_test.go
Normal file
@@ -0,0 +1,661 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestMonadChainLeft tests the MonadChainLeft function
|
||||
func TestMonadChainLeft(t *testing.T) {
|
||||
t.Run("transforms failures while preserving successes", func(t *testing.T) {
|
||||
// Create a failing validator
|
||||
failingValidator := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "validation failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Handler that recovers from specific errors
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "validation failed" {
|
||||
return Of[string, int](0) // recover with default
|
||||
}
|
||||
}
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, handler)
|
||||
res := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(0), res, "Should recover from failure")
|
||||
})
|
||||
|
||||
t.Run("preserves success values unchanged", func(t *testing.T) {
|
||||
successValidator := Of[string, int](42)
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "should not be called"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(successValidator, handler)
|
||||
res := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), res, "Success should pass through unchanged")
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when transformation also fails", func(t *testing.T) {
|
||||
failingValidator := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "original error"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Validate[string, string] {
|
||||
return func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Messsage: "additional error"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, handler)
|
||||
res := validator("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(res))
|
||||
errors := either.MonadFold(res,
|
||||
reader.Ask[Errors](),
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2, "Should aggregate both errors")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "original error")
|
||||
assert.Contains(t, messages, "additional error")
|
||||
})
|
||||
|
||||
t.Run("adds context to errors", func(t *testing.T) {
|
||||
failingValidator := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "invalid format"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
addContext := func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
Messsage: "failed to validate user age",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, addContext)
|
||||
res := validator("abc")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(res))
|
||||
errors := either.MonadFold(res,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2, "Should have both original and context errors")
|
||||
})
|
||||
|
||||
t.Run("works with different input types", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
failingValidator := func(cfg Config) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: cfg.Port, Messsage: "invalid port"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Validate[Config, string] {
|
||||
return Of[Config, string]("default-value")
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, handler)
|
||||
res := validator(Config{Port: 9999})(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("default-value"), res)
|
||||
})
|
||||
|
||||
t.Run("handler can access original input", func(t *testing.T) {
|
||||
failingValidator := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "parse failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
// Handler can use the original input to make decisions
|
||||
if input == "special" {
|
||||
return validation.Of(999)
|
||||
}
|
||||
return validation.Of(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, handler)
|
||||
|
||||
res1 := validator("special")(nil)
|
||||
assert.Equal(t, validation.Of(999), res1)
|
||||
|
||||
res2 := validator("other")(nil)
|
||||
assert.Equal(t, validation.Of(0), res2)
|
||||
})
|
||||
|
||||
t.Run("is equivalent to ChainLeft", func(t *testing.T) {
|
||||
failingValidator := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
// MonadChainLeft - direct application
|
||||
result1 := MonadChainLeft(failingValidator, handler)("input")(nil)
|
||||
|
||||
// ChainLeft - curried for pipelines
|
||||
result2 := ChainLeft(handler)(failingValidator)("input")(nil)
|
||||
|
||||
assert.Equal(t, result1, result2, "MonadChainLeft and ChainLeft should produce identical results")
|
||||
})
|
||||
|
||||
t.Run("chains multiple error transformations", func(t *testing.T) {
|
||||
failingValidator := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handler1 := func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "error2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler2 := func(errs Errors) Validate[string, int] {
|
||||
// Check if we can recover
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "error1" {
|
||||
return Of[string, int](100) // recover
|
||||
}
|
||||
}
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chain handlers
|
||||
validator := MonadChainLeft(MonadChainLeft(failingValidator, handler1), handler2)
|
||||
res := validator("input")(nil)
|
||||
|
||||
// Should recover because error1 is present
|
||||
assert.Equal(t, validation.Of(100), res)
|
||||
})
|
||||
|
||||
t.Run("does not call handler on success", func(t *testing.T) {
|
||||
successValidator := Of[string, int](42)
|
||||
handlerCalled := false
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
handlerCalled = true
|
||||
return Of[string, int](0)
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(successValidator, handler)
|
||||
res := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), res)
|
||||
assert.False(t, handlerCalled, "Handler should not be called on success")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAlt tests the MonadAlt function
|
||||
func TestMonadAlt(t *testing.T) {
|
||||
t.Run("returns first validator when it succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator2 := func() Validate[string, int] {
|
||||
return Of[string, int](100)
|
||||
}
|
||||
|
||||
result := MonadAlt(validator1, validator2)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("returns second validator when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "first failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
result := MonadAlt(failing, fallback)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := MonadAlt(failing1, failing2)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1", "Should contain error from first validator")
|
||||
assert.Contains(t, messages, "error 2", "Should contain error from second validator")
|
||||
})
|
||||
|
||||
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
evaluated := false
|
||||
validator2 := func() Validate[string, int] {
|
||||
evaluated = true
|
||||
return Of[string, int](100)
|
||||
}
|
||||
|
||||
result := MonadAlt(validator1, validator2)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
assert.False(t, evaluated, "Second validator should not be evaluated")
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, string] {
|
||||
return Of[string, string]("fallback")
|
||||
}
|
||||
|
||||
result := MonadAlt(failing, fallback)("input")(nil)
|
||||
assert.Equal(t, validation.Of("fallback"), result)
|
||||
})
|
||||
|
||||
t.Run("chains multiple alternatives", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
succeeding := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
// Chain: try failing1, then failing2, then succeeding
|
||||
result := MonadAlt(MonadAlt(failing1, failing2), succeeding)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("works with complex input types", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
failing := func(cfg Config) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: cfg.Port, Messsage: "invalid port"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[Config, string] {
|
||||
return Of[Config, string]("default")
|
||||
}
|
||||
|
||||
result := MonadAlt(failing, fallback)(Config{Port: 9999})(nil)
|
||||
assert.Equal(t, validation.Of("default"), result)
|
||||
})
|
||||
|
||||
t.Run("preserves error context", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Value: input,
|
||||
Messsage: "parse error",
|
||||
Context: validation.Context{{Key: "field", Type: "int"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Value: input,
|
||||
Messsage: "validation error",
|
||||
Context: validation.Context{{Key: "value", Type: "int"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := MonadAlt(failing1, failing2)("abc")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should have errors from both validators")
|
||||
|
||||
// Verify that errors with context are present
|
||||
hasParseError := false
|
||||
hasValidationError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "parse error" {
|
||||
hasParseError = true
|
||||
assert.NotNil(t, err.Context)
|
||||
}
|
||||
if err.Messsage == "validation error" {
|
||||
hasValidationError = true
|
||||
assert.NotNil(t, err.Context)
|
||||
}
|
||||
}
|
||||
assert.True(t, hasParseError, "Should have parse error")
|
||||
assert.True(t, hasValidationError, "Should have validation error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlt tests the Alt function
|
||||
func TestAlt(t *testing.T) {
|
||||
t.Run("returns first validator when it succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator2 := func() Validate[string, int] {
|
||||
return Of[string, int](100)
|
||||
}
|
||||
|
||||
withAlt := Alt(validator2)
|
||||
result := withAlt(validator1)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("returns second validator when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "first failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
withAlt := Alt(fallback)
|
||||
result := withAlt(failing)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withAlt := Alt(failing2)
|
||||
result := withAlt(failing1)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
|
||||
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
evaluated := false
|
||||
validator2 := func() Validate[string, int] {
|
||||
evaluated = true
|
||||
return Of[string, int](100)
|
||||
}
|
||||
|
||||
withAlt := Alt(validator2)
|
||||
result := withAlt(validator1)("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
assert.False(t, evaluated, "Second validator should not be evaluated")
|
||||
})
|
||||
|
||||
t.Run("can be used in pipelines", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
succeeding := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
// Use F.Pipe to chain alternatives
|
||||
validator := F.Pipe2(
|
||||
failing1,
|
||||
Alt(failing2),
|
||||
Alt(succeeding),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("is equivalent to MonadAlt", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
// Alt - curried for pipelines
|
||||
result1 := Alt(fallback)(failing)("input")(nil)
|
||||
|
||||
// MonadAlt - direct application
|
||||
result2 := MonadAlt(failing, fallback)("input")(nil)
|
||||
|
||||
assert.Equal(t, result1, result2, "Alt and MonadAlt should produce identical results")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAltAndAltEquivalence tests that MonadAlt and Alt are equivalent
|
||||
func TestMonadAltAndAltEquivalence(t *testing.T) {
|
||||
t.Run("both produce same results for success", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator2 := func() Validate[string, int] {
|
||||
return Of[string, int](100)
|
||||
}
|
||||
|
||||
resultMonadAlt := MonadAlt(validator1, validator2)("input")(nil)
|
||||
resultAlt := Alt(validator2)(validator1)("input")(nil)
|
||||
|
||||
assert.Equal(t, resultMonadAlt, resultAlt)
|
||||
})
|
||||
|
||||
t.Run("both produce same results for fallback", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
}
|
||||
|
||||
resultMonadAlt := MonadAlt(failing, fallback)("input")(nil)
|
||||
resultAlt := Alt(fallback)(failing)("input")(nil)
|
||||
|
||||
assert.Equal(t, resultMonadAlt, resultAlt)
|
||||
})
|
||||
|
||||
t.Run("both produce same results for error aggregation", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resultMonadAlt := MonadAlt(failing1, failing2)("input")(nil)
|
||||
resultAlt := Alt(failing2)(failing1)("input")(nil)
|
||||
|
||||
// Both should fail
|
||||
assert.True(t, either.IsLeft(resultMonadAlt))
|
||||
assert.True(t, either.IsLeft(resultAlt))
|
||||
|
||||
// Both should have same errors
|
||||
errorsMonadAlt := either.MonadFold(resultMonadAlt,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
errorsAlt := either.MonadFold(resultAlt,
|
||||
reader.Ask[Errors](),
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
|
||||
assert.Equal(t, len(errorsMonadAlt), len(errorsAlt))
|
||||
})
|
||||
}
|
||||
@@ -122,3 +122,268 @@ func ApplicativeMonoid[I, A any](m Monoid[A]) Monoid[Validate[I, A]] {
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a Monoid instance for Validate[I, A] that combines both
|
||||
// applicative and alternative semantics.
|
||||
//
|
||||
// This function creates a monoid that:
|
||||
// 1. When both validators succeed: Combines their results using the provided monoid operation
|
||||
// 2. When one validator fails: Uses the successful validator's result (alternative behavior)
|
||||
// 3. When both validators fail: Aggregates all errors from both validators
|
||||
//
|
||||
// This is a hybrid approach that combines:
|
||||
// - ApplicativeMonoid: Combines successful results using the monoid operation
|
||||
// - AltMonoid: Provides fallback behavior when validators fail
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type that validators accept
|
||||
// - A: The output type that validators produce (must have a Monoid instance)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: A Monoid[A] that defines how to combine values of type A
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Monoid[Validate[I, A]] that combines validators using both applicative and alternative semantics.
|
||||
//
|
||||
// # Behavior Details
|
||||
//
|
||||
// The AlternativeMonoid differs from ApplicativeMonoid in how it handles mixed success/failure:
|
||||
//
|
||||
// - **Both succeed**: Results are combined using the monoid operation (like ApplicativeMonoid)
|
||||
// - **First succeeds, second fails**: Returns the first result (alternative fallback)
|
||||
// - **First fails, second succeeds**: Returns the second result (alternative fallback)
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
//
|
||||
// # Example: String Concatenation with Fallback
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// m := validate.AlternativeMonoid[string, string](S.Monoid)
|
||||
//
|
||||
// // Both succeed - results are concatenated
|
||||
// validator1 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success("Hello")
|
||||
// }
|
||||
// }
|
||||
// validator2 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success(" World")
|
||||
// }
|
||||
// }
|
||||
// combined := m.Concat(validator1, validator2)
|
||||
// result := combined("input")(nil)
|
||||
// // result is validation.Success("Hello World")
|
||||
//
|
||||
// # Example: Fallback Behavior
|
||||
//
|
||||
// // First fails, second succeeds - uses second result
|
||||
// failing := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.FailureWithMessage[string](input, "first failed")(ctx)
|
||||
// }
|
||||
// }
|
||||
// succeeding := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success("fallback")
|
||||
// }
|
||||
// }
|
||||
// combined := m.Concat(failing, succeeding)
|
||||
// result := combined("input")(nil)
|
||||
// // result is validation.Success("fallback")
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// // Both fail - errors are aggregated
|
||||
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.FailureWithMessage[string](input, "error 1")(ctx)
|
||||
// }
|
||||
// }
|
||||
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.FailureWithMessage[string](input, "error 2")(ctx)
|
||||
// }
|
||||
// }
|
||||
// combined := m.Concat(failing1, failing2)
|
||||
// result := combined("input")(nil)
|
||||
// // result contains both "error 1" and "error 2"
|
||||
//
|
||||
// # Comparison with Other Monoids
|
||||
//
|
||||
// - **ApplicativeMonoid**: Always combines results when both succeed, fails if either fails
|
||||
// - **AlternativeMonoid**: Combines results when both succeed, provides fallback when one fails
|
||||
// - **AltMonoid**: Always uses first success, never combines results
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Validation with fallback strategies and result combination
|
||||
// - Building validators that accumulate results but provide alternatives
|
||||
// - Configuration loading with multiple sources and merging
|
||||
// - Data aggregation with error recovery
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Both validators receive the same input value I
|
||||
// - The empty element of the monoid serves as the identity for the Concat operation
|
||||
// - Error aggregation ensures no validation failures are lost
|
||||
// - This follows both applicative and alternative functor laws
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ApplicativeMonoid: For pure applicative combination without fallback
|
||||
// - AltMonoid: For pure alternative behavior without result combination
|
||||
// - MonadAlt: The underlying alternative operation
|
||||
func AlternativeMonoid[I, A any](m Monoid[A]) Monoid[Validate[I, A]] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[I, A],
|
||||
MonadMap[I, A, func(A) A],
|
||||
MonadAp[A, I, A],
|
||||
MonadAlt[I, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a Monoid instance for Validate[I, A] using alternative semantics
|
||||
// with a provided zero/default validator.
|
||||
//
|
||||
// This function creates a monoid where:
|
||||
// 1. The first successful validator wins (no result combination)
|
||||
// 2. If the first fails, the second is tried as a fallback
|
||||
// 3. If both fail, errors are aggregated
|
||||
// 4. The provided zero validator serves as the identity element
|
||||
//
|
||||
// Unlike AlternativeMonoid, AltMonoid does NOT combine successful results - it always
|
||||
// returns the first success. This makes it ideal for fallback chains and default values.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type that validators accept
|
||||
// - A: The output type that validators produce
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - zero: A lazy Validate[I, A] that serves as the identity element. This is typically
|
||||
// a validator that always succeeds with a default value, but can also be a failing
|
||||
// validator if no default is appropriate.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Monoid[Validate[I, A]] that combines validators using alternative semantics where
|
||||
// the first success wins.
|
||||
//
|
||||
// # Behavior Details
|
||||
//
|
||||
// The AltMonoid implements a "first success wins" strategy:
|
||||
//
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
// - **Concat with Empty**: The zero validator is used as fallback
|
||||
//
|
||||
// # Example: Default Value Fallback
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// )
|
||||
//
|
||||
// // Create a monoid with a default value of 0
|
||||
// m := validate.AltMonoid(func() validate.Validate[string, int] {
|
||||
// return validate.Of[string, int](0)
|
||||
// })
|
||||
//
|
||||
// // First validator succeeds - returns 42, second is not evaluated
|
||||
// validator1 := validate.Of[string, int](42)
|
||||
// validator2 := validate.Of[string, int](100)
|
||||
// combined := m.Concat(validator1, validator2)
|
||||
// result := combined("input")(nil)
|
||||
// // result is validation.Success(42)
|
||||
//
|
||||
// # Example: Fallback Chain
|
||||
//
|
||||
// // Try primary, then fallback, then default
|
||||
// m := validate.AltMonoid(func() validate.Validate[string, string] {
|
||||
// return validate.Of[string, string]("default")
|
||||
// })
|
||||
//
|
||||
// primary := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.FailureWithMessage[string](input, "primary failed")(ctx)
|
||||
// }
|
||||
// }
|
||||
// secondary := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success("secondary value")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Chain: try primary, then secondary, then default
|
||||
// combined := m.Concat(m.Concat(primary, secondary), m.Empty())
|
||||
// result := combined("input")(nil)
|
||||
// // result is validation.Success("secondary value")
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// // Both fail - errors are aggregated
|
||||
// m := validate.AltMonoid(func() validate.Validate[string, int] {
|
||||
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](input, "no default")(ctx)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](input, "error 1")(ctx)
|
||||
// }
|
||||
// }
|
||||
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](input, "error 2")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// combined := m.Concat(failing1, failing2)
|
||||
// result := combined("input")(nil)
|
||||
// // result contains both "error 1" and "error 2"
|
||||
//
|
||||
// # Comparison with Other Monoids
|
||||
//
|
||||
// - **ApplicativeMonoid**: Combines results when both succeed using monoid operation
|
||||
// - **AlternativeMonoid**: Combines results when both succeed, provides fallback when one fails
|
||||
// - **AltMonoid**: First success wins, never combines results (pure alternative)
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Configuration loading with fallback sources (try file, then env, then default)
|
||||
// - Validation with default values
|
||||
// - Parser combinators with alternative branches
|
||||
// - Error recovery with multiple strategies
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The zero validator is lazily evaluated, only when needed
|
||||
// - First success short-circuits evaluation (second validator not called)
|
||||
// - Error aggregation ensures all validation failures are reported
|
||||
// - This follows the alternative functor laws
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - AlternativeMonoid: For combining results when both succeed
|
||||
// - ApplicativeMonoid: For pure applicative combination
|
||||
// - MonadAlt: The underlying alternative operation
|
||||
// - Alt: The curried version for pipeline composition
|
||||
func AltMonoid[I, A any](zero Lazy[Validate[I, A]]) Monoid[Validate[I, A]] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[I, A],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,475 +1,397 @@
|
||||
// Copyright (c) 2023 - 2025 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 validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
MO "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
intAddMonoid = N.MonoidSum[int]()
|
||||
strMonoid = S.Monoid
|
||||
)
|
||||
// TestAlternativeMonoid tests the AlternativeMonoid function
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
t.Run("with string monoid", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string, string](S.Monoid)
|
||||
|
||||
// Helper function to create a successful validator
|
||||
func successValidator[I, A any](value A) Validate[I, A] {
|
||||
return func(input I) Reader[validation.Context, validation.Validation[A]] {
|
||||
return func(ctx validation.Context) validation.Validation[A] {
|
||||
return validation.Success(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Run("empty returns validator that succeeds with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")(nil)
|
||||
|
||||
// Helper function to create a failing validator
|
||||
func failureValidator[I, A any](message string) Validate[I, A] {
|
||||
return func(input I) Reader[validation.Context, validation.Validation[A]] {
|
||||
return validation.FailureWithMessage[A](input, message)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
|
||||
// Helper function to create a validator that uses the input
|
||||
func inputDependentValidator[A any](f func(A) A) Validate[A, A] {
|
||||
return func(input A) Reader[validation.Context, validation.Validation[A]] {
|
||||
return func(ctx validation.Context) validation.Validation[A] {
|
||||
return validation.Success(f(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Run("concat combines successful validators using monoid", func(t *testing.T) {
|
||||
validator1 := Of[string, string]("Hello")
|
||||
validator2 := Of[string, string](" World")
|
||||
|
||||
// TestApplicativeMonoid_EmptyElement tests the empty element of the monoid
|
||||
func TestApplicativeMonoid_EmptyElement(t *testing.T) {
|
||||
t.Run("int addition monoid", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
empty := m.Empty()
|
||||
combined := m.Concat(validator1, validator2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
result := empty("test")(nil)
|
||||
assert.Equal(t, validation.Of("Hello World"), result)
|
||||
})
|
||||
|
||||
assert.Equal(t, validation.Of(0), result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation monoid", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[int](strMonoid)
|
||||
empty := m.Empty()
|
||||
|
||||
result := empty(42)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ConcatSuccesses tests concatenating two successful validators
|
||||
func TestApplicativeMonoid_ConcatSuccesses(t *testing.T) {
|
||||
t.Run("int addition", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](5)
|
||||
v2 := successValidator[string](3)
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(8), result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[int](strMonoid)
|
||||
|
||||
v1 := successValidator[int]("Hello")
|
||||
v2 := successValidator[int](" World")
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined(42)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("Hello World"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ConcatWithFailure tests concatenating validators where one fails
|
||||
func TestApplicativeMonoid_ConcatWithFailure(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
t.Run("left failure", func(t *testing.T) {
|
||||
v1 := failureValidator[string, int]("left error")
|
||||
v2 := successValidator[string](5)
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "left error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("right failure", func(t *testing.T) {
|
||||
v1 := successValidator[string](5)
|
||||
v2 := failureValidator[string, int]("right error")
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "right error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("both failures", func(t *testing.T) {
|
||||
v1 := failureValidator[string, int]("left error")
|
||||
v2 := failureValidator[string, int]("right error")
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
// Note: The current implementation returns the first error encountered
|
||||
assert.GreaterOrEqual(t, len(errors), 1)
|
||||
// At least one of the errors should be present
|
||||
hasError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "left error" || err.Messsage == "right error" {
|
||||
hasError = true
|
||||
break
|
||||
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "first failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, hasError, "Should contain at least one validation error")
|
||||
})
|
||||
}
|
||||
succeeding := Of[string, string]("fallback")
|
||||
|
||||
// TestApplicativeMonoid_LeftIdentity tests the left identity law
|
||||
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")(nil)
|
||||
|
||||
v := successValidator[string](42)
|
||||
assert.Equal(t, validation.Of("fallback"), result)
|
||||
})
|
||||
|
||||
// empty <> v == v
|
||||
combined := m.Concat(m.Empty(), v)
|
||||
|
||||
resultCombined := combined("test")(nil)
|
||||
resultOriginal := v("test")(nil)
|
||||
|
||||
assert.Equal(t, resultOriginal, resultCombined)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RightIdentity tests the right identity law
|
||||
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v := successValidator[string](42)
|
||||
|
||||
// v <> empty == v
|
||||
combined := m.Concat(v, m.Empty())
|
||||
|
||||
resultCombined := combined("test")(nil)
|
||||
resultOriginal := v("test")(nil)
|
||||
|
||||
assert.Equal(t, resultOriginal, resultCombined)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Associativity tests the associativity law
|
||||
func TestApplicativeMonoid_Associativity(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](1)
|
||||
v2 := successValidator[string](2)
|
||||
v3 := successValidator[string](3)
|
||||
|
||||
// (v1 <> v2) <> v3 == v1 <> (v2 <> v3)
|
||||
left := m.Concat(m.Concat(v1, v2), v3)
|
||||
right := m.Concat(v1, m.Concat(v2, v3))
|
||||
|
||||
resultLeft := left("test")(nil)
|
||||
resultRight := right("test")(nil)
|
||||
|
||||
assert.Equal(t, resultRight, resultLeft)
|
||||
|
||||
// Both should equal 6
|
||||
assert.Equal(t, validation.Of(6), resultLeft)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_AssociativityWithFailures tests associativity with failures
|
||||
func TestApplicativeMonoid_AssociativityWithFailures(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](1)
|
||||
v2 := failureValidator[string, int]("error 2")
|
||||
v3 := successValidator[string](3)
|
||||
|
||||
// (v1 <> v2) <> v3 == v1 <> (v2 <> v3)
|
||||
left := m.Concat(m.Concat(v1, v2), v3)
|
||||
right := m.Concat(v1, m.Concat(v2, v3))
|
||||
|
||||
resultLeft := left("test")(nil)
|
||||
resultRight := right("test")(nil)
|
||||
|
||||
// Both should fail with the same error
|
||||
assert.True(t, E.IsLeft(resultLeft))
|
||||
assert.True(t, E.IsLeft(resultRight))
|
||||
|
||||
_, errorsLeft := E.Unwrap(resultLeft)
|
||||
_, errorsRight := E.Unwrap(resultRight)
|
||||
|
||||
assert.Len(t, errorsLeft, 1)
|
||||
assert.Len(t, errorsRight, 1)
|
||||
assert.Equal(t, "error 2", errorsLeft[0].Messsage)
|
||||
assert.Equal(t, "error 2", errorsRight[0].Messsage)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_MultipleValidators tests combining multiple validators
|
||||
func TestApplicativeMonoid_MultipleValidators(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](10)
|
||||
v2 := successValidator[string](20)
|
||||
v3 := successValidator[string](30)
|
||||
v4 := successValidator[string](40)
|
||||
|
||||
// Chain multiple concat operations
|
||||
combined := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(v1, v2),
|
||||
v3,
|
||||
),
|
||||
v4,
|
||||
)
|
||||
|
||||
result := combined("test")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(100), result)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_InputDependent tests validators that depend on input
|
||||
func TestApplicativeMonoid_InputDependent(t *testing.T) {
|
||||
m := ApplicativeMonoid[int](intAddMonoid)
|
||||
|
||||
// Validator that doubles the input
|
||||
v1 := inputDependentValidator(N.Mul(2))
|
||||
// Validator that adds 10 to the input
|
||||
v2 := inputDependentValidator(N.Add(10))
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined(5)(nil)
|
||||
|
||||
// (5 * 2) + (5 + 10) = 10 + 15 = 25
|
||||
assert.Equal(t, validation.Of(25), result)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ContextPropagation tests that context is properly propagated
|
||||
func TestApplicativeMonoid_ContextPropagation(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
// Create a validator that captures the context
|
||||
var capturedContext validation.Context
|
||||
v1 := func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
capturedContext = ctx
|
||||
return validation.Success(5)
|
||||
}
|
||||
}
|
||||
|
||||
v2 := successValidator[string](3)
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
|
||||
// Create a context with some entries
|
||||
ctx := validation.Context{
|
||||
{Key: "field1", Type: "int"},
|
||||
{Key: "field2", Type: "string"},
|
||||
}
|
||||
|
||||
result := combined("test")(ctx)
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
assert.Equal(t, ctx, capturedContext)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ErrorAccumulation tests that errors are accumulated
|
||||
func TestApplicativeMonoid_ErrorAccumulation(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := failureValidator[string, int]("error 1")
|
||||
v2 := failureValidator[string, int]("error 2")
|
||||
v3 := failureValidator[string, int]("error 3")
|
||||
|
||||
combined := m.Concat(m.Concat(v1, v2), v3)
|
||||
result := combined("test")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
|
||||
// Note: The current implementation returns the first error encountered
|
||||
// At least one error should be present
|
||||
assert.GreaterOrEqual(t, len(errors), 1)
|
||||
hasError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "error 1" || err.Messsage == "error 2" || err.Messsage == "error 3" {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasError, "Should contain at least one validation error")
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_MixedSuccessFailure tests mixing successes and failures
|
||||
func TestApplicativeMonoid_MixedSuccessFailure(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](10)
|
||||
v2 := failureValidator[string, int]("error in v2")
|
||||
v3 := successValidator[string](20)
|
||||
v4 := failureValidator[string, int]("error in v4")
|
||||
|
||||
combined := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(v1, v2),
|
||||
v3,
|
||||
),
|
||||
v4,
|
||||
)
|
||||
|
||||
result := combined("test")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
|
||||
// Note: The current implementation returns the first error encountered
|
||||
// At least one error should be present
|
||||
assert.GreaterOrEqual(t, len(errors), 1)
|
||||
hasError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "error in v2" || err.Messsage == "error in v4" {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasError, "Should contain at least one validation error")
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_DifferentInputTypes tests with different input types
|
||||
func TestApplicativeMonoid_DifferentInputTypes(t *testing.T) {
|
||||
t.Run("struct input", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
Timeout int
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid[Config](intAddMonoid)
|
||||
|
||||
v1 := func(cfg Config) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.Success(cfg.Port)
|
||||
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v2 := func(cfg Config) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.Success(cfg.Timeout)
|
||||
failing2 := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined(Config{Port: 8080, Timeout: 30})(nil)
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(8110), result) // 8080 + 30
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves validator", func(t *testing.T) {
|
||||
validator := Of[string, string]("test")
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(validator, empty)("input")(nil)
|
||||
result2 := m.Concat(empty, validator)("input")(nil)
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "test", val1)
|
||||
assert.Equal(t, "test", val2)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_StringConcatenation tests string concatenation scenarios
|
||||
func TestApplicativeMonoid_StringConcatenation(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](strMonoid)
|
||||
|
||||
t.Run("build sentence", func(t *testing.T) {
|
||||
v1 := successValidator[string]("The")
|
||||
v2 := successValidator[string](" quick")
|
||||
v3 := successValidator[string](" brown")
|
||||
v4 := successValidator[string](" fox")
|
||||
|
||||
combined := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(v1, v2),
|
||||
v3,
|
||||
),
|
||||
v4,
|
||||
t.Run("with int addition monoid", func(t *testing.T) {
|
||||
intMonoid := MO.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := AlternativeMonoid[string, int](intMonoid)
|
||||
|
||||
result := combined("input")(nil)
|
||||
t.Run("empty returns validator with zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("The quick brown fox"), result)
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
|
||||
validator1 := Of[string, int](10)
|
||||
validator2 := Of[string, int](32)
|
||||
|
||||
combined := m.Concat(validator1, validator2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("concat uses fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
succeeding := Of[string, int](42)
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")(nil)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
validator1 := Of[string, int](1)
|
||||
validator2 := Of[string, int](2)
|
||||
validator3 := Of[string, int](3)
|
||||
validator4 := Of[string, int](4)
|
||||
|
||||
combined := m.Concat(m.Concat(m.Concat(validator1, validator2), validator3), validator4)
|
||||
result := combined("input")(nil)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with empty strings", func(t *testing.T) {
|
||||
v1 := successValidator[string]("Hello")
|
||||
v2 := successValidator[string]("")
|
||||
v3 := successValidator[string]("World")
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string, string](S.Monoid)
|
||||
|
||||
combined := m.Concat(m.Concat(v1, v2), v3)
|
||||
result := combined("input")(nil)
|
||||
validator1 := Of[string, string]("a")
|
||||
validator2 := Of[string, string]("b")
|
||||
validator3 := Of[string, string]("c")
|
||||
|
||||
assert.Equal(t, validation.Of("HelloWorld"), result)
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
result := m.Concat(m.Empty(), validator1)("input")(nil)
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
result := m.Concat(validator1, m.Empty())("input")(nil)
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
left := m.Concat(m.Concat(validator1, validator2), validator3)("input")(nil)
|
||||
right := m.Concat(validator1, m.Concat(validator2, validator3))("input")(nil)
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "abc", leftVal)
|
||||
assert.Equal(t, "abc", rightVal)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkApplicativeMonoid_ConcatSuccesses(b *testing.B) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
v1 := successValidator[string](5)
|
||||
v2 := successValidator[string](3)
|
||||
combined := m.Concat(v1, v2)
|
||||
// TestAltMonoid tests the AltMonoid function
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
t.Run("with default value as zero", func(t *testing.T) {
|
||||
m := AltMonoid(func() Validate[string, int] {
|
||||
return Of[string, int](0)
|
||||
})
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
_ = combined("test")(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApplicativeMonoid_ConcatFailures(b *testing.B) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
v1 := failureValidator[string, int]("error 1")
|
||||
v2 := failureValidator[string, int]("error 2")
|
||||
combined := m.Concat(v1, v2)
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
_ = combined("test")(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApplicativeMonoid_MultipleConcat(b *testing.B) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
validators := make([]Validate[string, int], 10)
|
||||
for i := range validators {
|
||||
validators[i] = successValidator[string](i)
|
||||
}
|
||||
|
||||
// Chain all validators
|
||||
combined := validators[0]
|
||||
for i := 1; i < len(validators); i++ {
|
||||
combined = m.Concat(combined, validators[i])
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
_ = combined("test")(nil)
|
||||
}
|
||||
t.Run("empty returns the provided zero validator", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(0), result)
|
||||
})
|
||||
|
||||
t.Run("concat returns first validator when it succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator2 := Of[string, int](100)
|
||||
|
||||
combined := m.Concat(validator1, validator2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
|
||||
failing := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
succeeding := Of[string, int](42)
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "error 1")
|
||||
assert.Contains(t, messages, "error 2")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with failing zero", func(t *testing.T) {
|
||||
m := AltMonoid(func() Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "no default available"},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty returns the failing zero validator", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
result := empty("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("concat with all failures aggregates errors", func(t *testing.T) {
|
||||
failing1 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 1"},
|
||||
})
|
||||
}
|
||||
}
|
||||
failing2 := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error 2"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
combined := m.Concat(failing1, failing2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("chaining multiple fallbacks", func(t *testing.T) {
|
||||
m := AltMonoid(func() Validate[string, string] {
|
||||
return Of[string, string]("default")
|
||||
})
|
||||
|
||||
primary := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "primary failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
secondary := func(input string) Reader[Context, Validation[string]] {
|
||||
return func(ctx Context) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "secondary failed"},
|
||||
})
|
||||
}
|
||||
}
|
||||
tertiary := Of[string, string]("tertiary value")
|
||||
|
||||
combined := m.Concat(m.Concat(primary, secondary), tertiary)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("tertiary value"), result)
|
||||
})
|
||||
|
||||
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
|
||||
// AltMonoid - first success wins
|
||||
altM := AltMonoid(func() Validate[string, int] {
|
||||
return Of[string, int](0)
|
||||
})
|
||||
|
||||
// AlternativeMonoid - combines successes
|
||||
altMonoid := AlternativeMonoid[string, int](N.MonoidSum[int]())
|
||||
|
||||
validator1 := Of[string, int](10)
|
||||
validator2 := Of[string, int](32)
|
||||
|
||||
// AltMonoid: returns first success (10)
|
||||
result1 := altM.Concat(validator1, validator2)("input")(nil)
|
||||
value1 := either.MonadFold(result1,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value1, "AltMonoid returns first success")
|
||||
|
||||
// AlternativeMonoid: combines both successes (10 + 32 = 42)
|
||||
result2 := altMonoid.Concat(validator1, validator2)("input")(nil)
|
||||
value2 := either.MonadFold(result2,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value2, "AlternativeMonoid combines successes")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/decode"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
@@ -90,25 +92,72 @@ type (
|
||||
// "at user.address.zipCode: expected string, got number"
|
||||
Context = validation.Context
|
||||
|
||||
Decode[I, A any] = decode.Decode[I, A]
|
||||
|
||||
// Validate is a function that validates input I to produce type A with full context tracking.
|
||||
// Decode represents a decoding operation that transforms input I into output A
|
||||
// within a validation context.
|
||||
//
|
||||
// Type structure:
|
||||
// Validate[I, A] = Reader[I, Decode[Context, A]]
|
||||
// Decode[I, A] = Reader[Context, Validation[A]]
|
||||
//
|
||||
// This means:
|
||||
// 1. Takes an input of type I
|
||||
// 2. Returns a Reader that depends on validation Context
|
||||
// 3. That Reader produces a Validation[A] (Either[Errors, A])
|
||||
// 1. Takes a validation Context (path through nested structures)
|
||||
// 2. Returns a Validation[A] (Either[Errors, A])
|
||||
//
|
||||
// The layered structure enables:
|
||||
// - Access to the input value being validated
|
||||
// - Context tracking through nested structures
|
||||
// - Error accumulation with detailed paths
|
||||
// - Composition with other validators
|
||||
// Decode is used as the foundation for validation operations, providing:
|
||||
// - Context-aware error reporting with detailed paths
|
||||
// - Error accumulation across multiple validations
|
||||
// - Composable validation logic
|
||||
//
|
||||
// The Decode type is typically not used directly but through the Validate type,
|
||||
// which adds an additional Reader layer for accessing the input value.
|
||||
//
|
||||
// Example:
|
||||
// decoder := func(ctx Context) Validation[int] {
|
||||
// // Perform validation and return result
|
||||
// return validation.Success(42)
|
||||
// }
|
||||
// // decoder is a Decode[any, int]
|
||||
Decode[I, A any] = decode.Decode[I, A]
|
||||
|
||||
// Validate represents a composable validator that transforms input I to output A
|
||||
// with comprehensive error tracking and context propagation.
|
||||
//
|
||||
// # Type Structure
|
||||
//
|
||||
// Validate[I, A] = Reader[I, Decode[Context, A]]
|
||||
// = Reader[I, Reader[Context, Validation[A]]]
|
||||
// = func(I) func(Context) Either[Errors, A]
|
||||
//
|
||||
// This three-layer structure provides:
|
||||
// 1. Input access: The outer Reader[I, ...] gives access to the input value I
|
||||
// 2. Context tracking: The middle Reader[Context, ...] tracks the validation path
|
||||
// 3. Error handling: The inner Validation[A] accumulates errors or produces value A
|
||||
//
|
||||
// # Purpose
|
||||
//
|
||||
// Validate is the core type for building type-safe, composable validators that:
|
||||
// - Transform and validate data from one type to another
|
||||
// - Track the path through nested structures for detailed error messages
|
||||
// - Accumulate multiple validation errors instead of failing fast
|
||||
// - Compose with other validators using functional patterns
|
||||
//
|
||||
// # Key Features
|
||||
//
|
||||
// - Context-aware: Automatically tracks validation path (e.g., "user.address.zipCode")
|
||||
// - Error accumulation: Collects all validation errors, not just the first one
|
||||
// - Type-safe: Leverages Go's type system to ensure correctness
|
||||
// - Composable: Validators can be combined using Map, Chain, Ap, and other operators
|
||||
//
|
||||
// # Algebraic Structure
|
||||
//
|
||||
// Validate forms several algebraic structures:
|
||||
// - Functor: Transform successful results with Map
|
||||
// - Applicative: Combine independent validators in parallel with Ap
|
||||
// - Monad: Chain dependent validators sequentially with Chain
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// Basic validator:
|
||||
//
|
||||
// Example usage:
|
||||
// validatePositive := func(n int) Reader[Context, Validation[int]] {
|
||||
// return func(ctx Context) Validation[int] {
|
||||
// if n > 0 {
|
||||
@@ -119,10 +168,33 @@ type (
|
||||
// }
|
||||
// // validatePositive is a Validate[int, int]
|
||||
//
|
||||
// The Validate type forms:
|
||||
// - A Functor: Can map over successful results
|
||||
// - An Applicative: Can combine validators in parallel
|
||||
// - A Monad: Can chain dependent validations
|
||||
// Composing validators:
|
||||
//
|
||||
// // Transform the result of a validator
|
||||
// doubled := Map[int, int, int](func(x int) int { return x * 2 })(validatePositive)
|
||||
//
|
||||
// // Chain dependent validations
|
||||
// validateRange := func(n int) Validate[int, int] {
|
||||
// return func(input int) Reader[Context, Validation[int]] {
|
||||
// return func(ctx Context) Validation[int] {
|
||||
// if n <= 100 {
|
||||
// return validation.Success(n)
|
||||
// }
|
||||
// return validation.FailureWithMessage[int](n, "must be <= 100")(ctx)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// combined := Chain(validateRange)(validatePositive)
|
||||
//
|
||||
// # Integration
|
||||
//
|
||||
// Validate integrates with the broader optics/codec ecosystem:
|
||||
// - Works with Decode for decoding operations
|
||||
// - Uses Validation for error handling
|
||||
// - Leverages Context for detailed error reporting
|
||||
// - Composes with other codec types for complete encode/decode pipelines
|
||||
//
|
||||
// See the package documentation for more examples and patterns.
|
||||
Validate[I, A any] = Reader[I, Decode[Context, A]]
|
||||
|
||||
// Errors is a collection of validation errors that occurred during validation.
|
||||
@@ -174,4 +246,32 @@ type (
|
||||
// // toUpper is an Operator[string, string, string]
|
||||
// // It can be applied to any string validator to uppercase the result
|
||||
Operator[I, A, B any] = Kleisli[I, Validate[I, A], B]
|
||||
|
||||
// Endomorphism represents a function from a type to itself.
|
||||
//
|
||||
// Type: Endomorphism[A] = func(A) A
|
||||
//
|
||||
// An endomorphism is a morphism (structure-preserving map) where the source
|
||||
// and target are the same type. In simpler terms, it's a function that takes
|
||||
// a value of type A and returns a value of the same type A.
|
||||
//
|
||||
// Endomorphisms are useful for:
|
||||
// - Transformations that preserve type (e.g., string normalization)
|
||||
// - Composable updates and modifications
|
||||
// - Building pipelines of same-type transformations
|
||||
// - Implementing the Monoid pattern (composition as binary operation)
|
||||
//
|
||||
// Endomorphisms form a Monoid under function composition:
|
||||
// - Identity: func(a A) A { return a }
|
||||
// - Concat: func(f, g Endomorphism[A]) Endomorphism[A] {
|
||||
// return func(a A) A { return f(g(a)) }
|
||||
// }
|
||||
//
|
||||
// Example:
|
||||
// trim := strings.TrimSpace // Endomorphism[string]
|
||||
// lower := strings.ToLower // Endomorphism[string]
|
||||
// normalize := compose(trim, lower) // Endomorphism[string]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
)
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/decode"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -309,6 +310,364 @@ func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// ChainLeft sequences a computation on the failure (Left) channel of a validation.
|
||||
//
|
||||
// This function operates on the error path of validation, allowing you to transform,
|
||||
// enrich, or recover from validation failures. It's the dual of Chain - while Chain
|
||||
// operates on success values, ChainLeft operates on error values.
|
||||
//
|
||||
// # Key Behavior
|
||||
//
|
||||
// **Critical difference from standard Either operations**: This validation-specific
|
||||
// implementation **aggregates errors** using the Errors monoid. When the transformation
|
||||
// function returns a failure, both the original errors AND the new errors are combined,
|
||||
// ensuring comprehensive error reporting.
|
||||
//
|
||||
// 1. **Success Pass-Through**: If validation succeeds, the handler is never called and
|
||||
// the success value passes through unchanged.
|
||||
//
|
||||
// 2. **Error Recovery**: The handler can recover from failures by returning a successful
|
||||
// validation, converting Left to Right.
|
||||
//
|
||||
// 3. **Error Aggregation**: When the handler also returns a failure, both the original
|
||||
// errors and the new errors are combined using the Errors monoid.
|
||||
//
|
||||
// 4. **Input Access**: The handler returns a Validate[I, A] function, giving it access
|
||||
// to the original input value I for context-aware error handling.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
|
||||
// is called only when validation fails, receiving the accumulated errors.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[I, A, A] that transforms validators by handling their error cases.
|
||||
//
|
||||
// # Example: Error Recovery
|
||||
//
|
||||
// // Validator that may fail
|
||||
// validatePositive := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// if n > 0 {
|
||||
// return validation.Success(n)
|
||||
// }
|
||||
// return validation.FailureWithMessage[int](n, "must be positive")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Recover from specific errors with a default value
|
||||
// withDefault := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "must be positive" {
|
||||
// return Of[int](0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return either.Left[int](errs)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// validator := withDefault(validatePositive)
|
||||
// result := validator(-5)(nil)
|
||||
// // Result: Success(0) - recovered from failure
|
||||
//
|
||||
// # Example: Error Context Addition
|
||||
//
|
||||
// // Add contextual information to errors
|
||||
// addContext := ChainLeft(func(errs Errors) Validate[string, int] {
|
||||
// return func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {
|
||||
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to validate user age",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// validator := addContext(someValidator)
|
||||
// // Errors will include both original error and context
|
||||
//
|
||||
// # Example: Input-Dependent Recovery
|
||||
//
|
||||
// // Recover with different defaults based on input
|
||||
// smartDefault := ChainLeft(func(errs Errors) Validate[string, int] {
|
||||
// return func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// // Use input to determine appropriate default
|
||||
// if strings.Contains(input, "http") {
|
||||
// return validation.Of(80)
|
||||
// }
|
||||
// if strings.Contains(input, "https") {
|
||||
// return validation.Of(443)
|
||||
// }
|
||||
// return validation.Of(8080)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Errors are accumulated, not replaced - this ensures no validation failures are lost
|
||||
// - The handler has access to both the errors and the original input
|
||||
// - Success values bypass the handler completely
|
||||
// - This enables sophisticated error handling strategies including recovery, enrichment, and transformation
|
||||
// - Use OrElse as a semantic alias when emphasizing fallback/alternative logic
|
||||
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return readert.Chain[Validate[I, A]](
|
||||
decode.ChainLeft,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainLeft sequences a computation on the failure (Left) channel of a validation.
|
||||
//
|
||||
// This is the direct application version of ChainLeft. It operates on the error path
|
||||
// of validation, allowing you to transform, enrich, or recover from validation failures.
|
||||
// It's the dual of Chain - while Chain operates on success values, MonadChainLeft
|
||||
// operates on error values.
|
||||
//
|
||||
// # Key Behavior
|
||||
//
|
||||
// **Critical difference from standard Either operations**: This validation-specific
|
||||
// implementation **aggregates errors** using the Errors monoid. When the transformation
|
||||
// function returns a failure, both the original errors AND the new errors are combined,
|
||||
// ensuring comprehensive error reporting.
|
||||
//
|
||||
// 1. **Success Pass-Through**: If validation succeeds, the handler is never called and
|
||||
// the success value passes through unchanged.
|
||||
//
|
||||
// 2. **Error Recovery**: The handler can recover from failures by returning a successful
|
||||
// validation, converting Left to Right.
|
||||
//
|
||||
// 3. **Error Aggregation**: When the handler also returns a failure, both the original
|
||||
// errors and the new errors are combined using the Errors monoid.
|
||||
//
|
||||
// 4. **Input Access**: The handler returns a Validate[I, A] function, giving it access
|
||||
// to the original input value I for context-aware error handling.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fa: The Validate[I, A] to transform
|
||||
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
|
||||
// is called only when validation fails, receiving the accumulated errors.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, A] that handles error cases according to the provided function.
|
||||
//
|
||||
// # Example: Error Recovery
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// )
|
||||
//
|
||||
// // Validator that may fail
|
||||
// validatePositive := func(n int) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// if n > 0 {
|
||||
// return validation.Success(n)
|
||||
// }
|
||||
// return validation.FailureWithMessage[int](n, "must be positive")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Recover from specific errors with a default value
|
||||
// withDefault := func(errs validation.Errors) validate.Validate[int, int] {
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "must be positive" {
|
||||
// return validate.Of[int](0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// // Propagate other errors
|
||||
// return func(input int) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return either.Left[int](errs)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// validator := validate.MonadChainLeft(validatePositive, withDefault)
|
||||
// result := validator(-5)(nil)
|
||||
// // Result: Success(0) - recovered from failure
|
||||
//
|
||||
// # Example: Error Context Addition
|
||||
//
|
||||
// // Add contextual information to errors
|
||||
// addContext := func(errs validation.Errors) validate.Validate[string, int] {
|
||||
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// // Add context error (will be aggregated with original)
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {
|
||||
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to validate user age",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// validator := validate.MonadChainLeft(someValidator, addContext)
|
||||
// // Errors will include both original error and context
|
||||
//
|
||||
// # Example: Input-Dependent Recovery
|
||||
//
|
||||
// // Recover with different defaults based on input
|
||||
// smartDefault := func(errs validation.Errors) validate.Validate[string, int] {
|
||||
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// // Use input to determine appropriate default
|
||||
// if strings.Contains(input, "http:") {
|
||||
// return validation.Success(80)
|
||||
// }
|
||||
// if strings.Contains(input, "https:") {
|
||||
// return validation.Success(443)
|
||||
// }
|
||||
// return validation.Success(8080)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// validator := validate.MonadChainLeft(parsePort, smartDefault)
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Errors are accumulated, not replaced - this ensures no validation failures are lost
|
||||
// - The handler has access to both the errors and the original input
|
||||
// - Success values bypass the handler completely
|
||||
// - This is the direct application version of ChainLeft
|
||||
// - This enables sophisticated error handling strategies including recovery, enrichment, and transformation
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ChainLeft: The curried, point-free version
|
||||
// - OrElse: Semantic alias for ChainLeft emphasizing fallback logic
|
||||
// - MonadAlt: Simplified alternative that ignores error details
|
||||
// - Alt: Curried version of MonadAlt
|
||||
func MonadChainLeft[I, A any](fa Validate[I, A], f Kleisli[I, Errors, A]) Validate[I, A] {
|
||||
return readert.MonadChain(
|
||||
decode.MonadChainLeft,
|
||||
fa,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// OrElse provides an alternative validation when the primary validation fails.
|
||||
//
|
||||
// This is a semantic alias for ChainLeft with identical behavior. The name "OrElse"
|
||||
// emphasizes the intent of providing fallback or alternative validation logic, making
|
||||
// code more readable when that's the primary use case.
|
||||
//
|
||||
// # Relationship to ChainLeft
|
||||
//
|
||||
// **OrElse and ChainLeft are functionally identical** - they produce exactly the same
|
||||
// results for all inputs. The choice between them is purely about code readability:
|
||||
//
|
||||
// - Use **OrElse** when emphasizing fallback/alternative validation logic
|
||||
// - Use **ChainLeft** when emphasizing technical error channel transformation
|
||||
//
|
||||
// Both maintain the critical property of **error aggregation**, ensuring all validation
|
||||
// failures are preserved and reported together.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
|
||||
// is called only when validation fails, receiving the accumulated errors.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[I, A, A] that transforms validators by providing alternative validation.
|
||||
//
|
||||
// # Example: Fallback Validation
|
||||
//
|
||||
// // Primary validator that may fail
|
||||
// validateFromConfig := func(key string) Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// // Try to get value from config
|
||||
// if value, ok := config[key]; ok {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return validation.FailureWithMessage[string](key, "not found in config")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use OrElse for semantic clarity - "try config, or else use environment"
|
||||
// withEnvFallback := OrElse(func(errs Errors) Validate[string, string] {
|
||||
// return func(key string) Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if value := os.Getenv(key); value != "" {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return either.Left[string](errs) // propagate original errors
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// validator := withEnvFallback(validateFromConfig)
|
||||
// result := validator("DATABASE_URL")(nil)
|
||||
// // Tries config first, falls back to environment variable
|
||||
//
|
||||
// # Example: Default Value on Failure
|
||||
//
|
||||
// // Provide a default value when validation fails
|
||||
// withDefault := OrElse(func(errs Errors) Validate[int, int] {
|
||||
// return Of[int](0) // default to 0 on any failure
|
||||
// })
|
||||
//
|
||||
// validator := withDefault(someValidator)
|
||||
// result := validator(input)(nil)
|
||||
// // Always succeeds, using default value if validation fails
|
||||
//
|
||||
// # Example: Pipeline with Multiple Fallbacks
|
||||
//
|
||||
// // Build a validation pipeline with multiple fallback strategies
|
||||
// validator := F.Pipe2(
|
||||
// validateFromDatabase,
|
||||
// OrElse(func(errs Errors) Validate[string, Config] {
|
||||
// // Try cache as first fallback
|
||||
// return validateFromCache
|
||||
// }),
|
||||
// OrElse(func(errs Errors) Validate[string, Config] {
|
||||
// // Use default config as final fallback
|
||||
// return Of[string](defaultConfig)
|
||||
// }),
|
||||
// )
|
||||
// // Tries database, then cache, then default
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Identical behavior to ChainLeft - they are aliases
|
||||
// - Errors are accumulated when transformations fail
|
||||
// - Success values pass through unchanged
|
||||
// - The handler has access to both errors and original input
|
||||
// - Choose OrElse for better readability when providing alternatives
|
||||
// - See ChainLeft documentation for detailed behavior and additional examples
|
||||
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
|
||||
// MonadAp applies a validator containing a function to a validator containing a value.
|
||||
//
|
||||
// This is the applicative apply operation for Validate. It allows you to apply
|
||||
@@ -409,3 +768,218 @@ func Ap[B, I, A any](fa Validate[I, A]) Operator[I, func(A) B, B] {
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// Alt provides an alternative validator when the primary validator fails.
|
||||
//
|
||||
// This is the curried, point-free version of MonadAlt. It creates an operator that
|
||||
// transforms a validator by adding a fallback alternative. When the first validator
|
||||
// fails, the second (lazily evaluated) validator is tried. If both fail, errors are
|
||||
// aggregated.
|
||||
//
|
||||
// Alt implements the Alternative typeclass pattern, providing a way to express
|
||||
// "try this, or else try that" logic in a composable way.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - second: A lazy Validate[I, A] that serves as the fallback. It's only evaluated
|
||||
// if the first validator fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[I, A, A] that transforms validators by adding alternative fallback logic.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
//
|
||||
// # Example: Fallback Validation
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// )
|
||||
//
|
||||
// // Primary validator that may fail
|
||||
// validateFromConfig := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// // Try to get value from config
|
||||
// if value, ok := config[key]; ok {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return validation.FailureWithMessage[string](key, "not in config")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Fallback to environment variable
|
||||
// validateFromEnv := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if value := os.Getenv(key); value != "" {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return validation.FailureWithMessage[string](key, "not in env")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use Alt to add fallback - point-free style
|
||||
// withFallback := validate.Alt(func() validate.Validate[string, string] {
|
||||
// return validateFromEnv
|
||||
// })
|
||||
//
|
||||
// validator := withFallback(validateFromConfig)
|
||||
// result := validator("DATABASE_URL")(nil)
|
||||
// // Tries config first, falls back to environment variable
|
||||
//
|
||||
// # Example: Pipeline with Multiple Alternatives
|
||||
//
|
||||
// // Chain multiple alternatives using function composition
|
||||
// validator := F.Pipe2(
|
||||
// validateFromDatabase,
|
||||
// validate.Alt(func() validate.Validate[string, Config] {
|
||||
// return validateFromCache
|
||||
// }),
|
||||
// validate.Alt(func() validate.Validate[string, Config] {
|
||||
// return validate.Of[string](defaultConfig)
|
||||
// }),
|
||||
// )
|
||||
// // Tries database, then cache, then default
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The second validator is lazily evaluated for efficiency
|
||||
// - First success short-circuits evaluation
|
||||
// - Errors are aggregated when both fail
|
||||
// - This is the point-free version of MonadAlt
|
||||
// - Useful for building validation pipelines with F.Pipe
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - MonadAlt: The direct application version
|
||||
// - ChainLeft: The more general error transformation operator
|
||||
// - OrElse: Semantic alias for ChainLeft
|
||||
// - AltMonoid: For combining multiple alternatives with monoid structure
|
||||
func Alt[I, A any](second Lazy[Validate[I, A]]) Operator[I, A, A] {
|
||||
return ChainLeft(function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
// MonadAlt provides an alternative validator when the primary validator fails.
|
||||
//
|
||||
// This is the direct application version of Alt. It takes two validators and returns
|
||||
// a new validator that tries the first, and if it fails, tries the second. If both
|
||||
// fail, errors from both are aggregated.
|
||||
//
|
||||
// MonadAlt implements the Alternative typeclass pattern, enabling "try this, or else
|
||||
// try that" logic with comprehensive error reporting.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - first: The primary Validate[I, A] to try first
|
||||
// - second: A lazy Validate[I, A] that serves as the fallback. It's only evaluated
|
||||
// if the first validator fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, A] that tries the first validator, falling back to the second if needed.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
//
|
||||
// # Example: Configuration with Fallback
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// )
|
||||
//
|
||||
// // Primary validator
|
||||
// validateFromConfig := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if value, ok := config[key]; ok {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return validation.FailureWithMessage[string](key, "not in config")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Fallback validator
|
||||
// validateFromEnv := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if value := os.Getenv(key); value != "" {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return validation.FailureWithMessage[string](key, "not in env")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Combine with MonadAlt
|
||||
// validator := validate.MonadAlt(
|
||||
// validateFromConfig,
|
||||
// func() validate.Validate[string, string] { return validateFromEnv },
|
||||
// )
|
||||
// result := validator("DATABASE_URL")(nil)
|
||||
// // Tries config first, falls back to environment variable
|
||||
//
|
||||
// # Example: Multiple Fallbacks
|
||||
//
|
||||
// // Chain multiple alternatives
|
||||
// validator := validate.MonadAlt(
|
||||
// validate.MonadAlt(
|
||||
// validateFromDatabase,
|
||||
// func() validate.Validate[string, Config] { return validateFromCache },
|
||||
// ),
|
||||
// func() validate.Validate[string, Config] { return validate.Of[string](defaultConfig) },
|
||||
// )
|
||||
// // Tries database, then cache, then default
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](input, "error 1")(ctx)
|
||||
// }
|
||||
// }
|
||||
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](input, "error 2")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// validator := validate.MonadAlt(
|
||||
// failing1,
|
||||
// func() validate.Validate[string, int] { return failing2 },
|
||||
// )
|
||||
// result := validator("input")(nil)
|
||||
// // result contains both "error 1" and "error 2"
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The second validator is lazily evaluated for efficiency
|
||||
// - First success short-circuits evaluation (second not called)
|
||||
// - Errors are aggregated when both fail
|
||||
// - This is equivalent to Alt but with direct application
|
||||
// - Both validators receive the same input value
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Alt: The curried, point-free version
|
||||
// - MonadChainLeft: The underlying error transformation operation
|
||||
// - OrElse: Semantic alias for ChainLeft
|
||||
// - AltMonoid: For combining multiple alternatives with monoid structure
|
||||
func MonadAlt[I, A any](first Validate[I, A], second Lazy[Validate[I, A]]) Validate[I, A] {
|
||||
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
@@ -849,3 +849,428 @@ func TestFunctorLaws(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainLeft tests the ChainLeft function
|
||||
func TestChainLeft(t *testing.T) {
|
||||
t.Run("transforms failures while preserving successes", func(t *testing.T) {
|
||||
// Create a failing validator
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "validation failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler that recovers from specific errors
|
||||
handler := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "validation failed" {
|
||||
return Of[int](0) // recover with default
|
||||
}
|
||||
}
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return E.Left[int](errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler(failingValidator)
|
||||
result := validator(-5)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(0), result, "Should recover from failure")
|
||||
})
|
||||
|
||||
t.Run("preserves success values unchanged", func(t *testing.T) {
|
||||
successValidator := Of[int](42)
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](input, "should not be called")(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler(successValidator)
|
||||
result := validator(100)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result, "Success should pass through unchanged")
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when transformation also fails", func(t *testing.T) {
|
||||
failingValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](s, "original error")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Validate[string, string] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](input, "additional error")(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler(failingValidator)
|
||||
result := validator("test")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 2, "Should aggregate both errors")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "original error")
|
||||
assert.Contains(t, messages, "additional error")
|
||||
})
|
||||
|
||||
t.Run("adds context to errors", func(t *testing.T) {
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "invalid value")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
addContext := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return E.Left[int](validation.Errors{
|
||||
{
|
||||
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
Messsage: "failed to validate user age",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := addContext(failingValidator)
|
||||
result := validator(150)(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 2, "Should have both original and context errors")
|
||||
})
|
||||
|
||||
t.Run("can be composed in pipeline", func(t *testing.T) {
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "error1")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
handler1 := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](input, "error2")(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
handler2 := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](input, "error3")(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler2(handler1(failingValidator))
|
||||
result := validator(42)(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should accumulate errors through pipeline")
|
||||
})
|
||||
|
||||
t.Run("provides access to original input", func(t *testing.T) {
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler uses input to determine recovery strategy
|
||||
handler := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
// Use input value to decide on recovery
|
||||
if input < 0 {
|
||||
return validation.Of(0)
|
||||
}
|
||||
if input > 100 {
|
||||
return validation.Of(100)
|
||||
}
|
||||
return E.Left[int](errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler(failingValidator)
|
||||
|
||||
result1 := validator(-10)(nil)
|
||||
assert.Equal(t, validation.Of(0), result1, "Should recover negative to 0")
|
||||
|
||||
result2 := validator(150)(nil)
|
||||
assert.Equal(t, validation.Of(100), result2, "Should recover large to 100")
|
||||
})
|
||||
|
||||
t.Run("works with different input and output types", func(t *testing.T) {
|
||||
// Validator that converts string to int
|
||||
parseValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "parse failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler that provides default based on input string
|
||||
handler := ChainLeft(func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if input == "default" {
|
||||
return validation.Of(42)
|
||||
}
|
||||
return E.Left[int](errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler(parseValidator)
|
||||
result := validator("default")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOrElse tests the OrElse function
|
||||
func TestOrElse(t *testing.T) {
|
||||
t.Run("provides fallback for failing validation", func(t *testing.T) {
|
||||
// Primary validator that fails
|
||||
primaryValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](s, "not found")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Use OrElse to provide fallback
|
||||
withFallback := OrElse(func(errs Errors) Validate[string, string] {
|
||||
return Of[string]("default value")
|
||||
})
|
||||
|
||||
validator := withFallback(primaryValidator)
|
||||
result := validator("missing")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("default value"), result)
|
||||
})
|
||||
|
||||
t.Run("preserves success values unchanged", func(t *testing.T) {
|
||||
successValidator := Of[string]("success")
|
||||
|
||||
withFallback := OrElse(func(errs Errors) Validate[string, string] {
|
||||
return Of[string]("fallback")
|
||||
})
|
||||
|
||||
validator := withFallback(successValidator)
|
||||
result := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("success"), result, "Should not use fallback for success")
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when fallback also fails", func(t *testing.T) {
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "primary failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
withFallback := OrElse(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](input, "fallback failed")(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := withFallback(failingValidator)
|
||||
result := validator(42)(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 2, "Should aggregate both errors")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "primary failed")
|
||||
assert.Contains(t, messages, "fallback failed")
|
||||
})
|
||||
|
||||
t.Run("supports multiple fallback strategies", func(t *testing.T) {
|
||||
failingValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](s, "not in database")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// First fallback: try cache
|
||||
tryCache := OrElse(func(errs Errors) Validate[string, string] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
if input == "cached" {
|
||||
return validation.Of("from cache")
|
||||
}
|
||||
return E.Left[string](errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Second fallback: use default
|
||||
useDefault := OrElse(func(errs Errors) Validate[string, string] {
|
||||
return Of[string]("default")
|
||||
})
|
||||
|
||||
// Compose fallbacks
|
||||
validator := useDefault(tryCache(failingValidator))
|
||||
|
||||
// Test with cached value
|
||||
result1 := validator("cached")(nil)
|
||||
assert.Equal(t, validation.Of("from cache"), result1)
|
||||
|
||||
// Test with non-cached value (should use default)
|
||||
result2 := validator("other")(nil)
|
||||
assert.Equal(t, validation.Of("default"), result2)
|
||||
})
|
||||
|
||||
t.Run("provides input-dependent fallback", func(t *testing.T) {
|
||||
failingValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "parse failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback with different defaults based on input
|
||||
smartFallback := OrElse(func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
// Provide context-aware defaults
|
||||
if input == "http" {
|
||||
return validation.Of(80)
|
||||
}
|
||||
if input == "https" {
|
||||
return validation.Of(443)
|
||||
}
|
||||
return validation.Of(8080)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := smartFallback(failingValidator)
|
||||
|
||||
result1 := validator("http")(nil)
|
||||
assert.Equal(t, validation.Of(80), result1)
|
||||
|
||||
result2 := validator("https")(nil)
|
||||
assert.Equal(t, validation.Of(443), result2)
|
||||
|
||||
result3 := validator("other")(nil)
|
||||
assert.Equal(t, validation.Of(8080), result3)
|
||||
})
|
||||
|
||||
t.Run("is equivalent to ChainLeft", func(t *testing.T) {
|
||||
// Create identical handlers
|
||||
handler := func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if input < 0 {
|
||||
return validation.Of(0)
|
||||
}
|
||||
return E.Left[int](errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply with ChainLeft
|
||||
withChainLeft := ChainLeft(handler)(failingValidator)
|
||||
|
||||
// Apply with OrElse
|
||||
withOrElse := OrElse(handler)(failingValidator)
|
||||
|
||||
// Test with same inputs
|
||||
inputs := []int{-10, 0, 10, -5, 100}
|
||||
for _, input := range inputs {
|
||||
result1 := withChainLeft(input)(nil)
|
||||
result2 := withOrElse(input)(nil)
|
||||
|
||||
// Results should be identical
|
||||
assert.Equal(t, E.IsLeft(result1), E.IsLeft(result2))
|
||||
if E.IsRight(result1) {
|
||||
val1, _ := E.Unwrap(result1)
|
||||
val2, _ := E.Unwrap(result2)
|
||||
assert.Equal(t, val1, val2, "OrElse and ChainLeft should produce identical results")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("works in complex validation pipeline", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
Host string
|
||||
}
|
||||
|
||||
// Validator that tries to parse config
|
||||
parseConfig := func(s string) Reader[validation.Context, validation.Validation[Config]] {
|
||||
return func(ctx validation.Context) validation.Validation[Config] {
|
||||
return validation.FailureWithMessage[Config](s, "invalid config")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to environment variables
|
||||
tryEnv := OrElse(func(errs Errors) Validate[string, Config] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[Config]] {
|
||||
return func(ctx validation.Context) validation.Validation[Config] {
|
||||
// Simulate env var lookup
|
||||
if input == "from_env" {
|
||||
return validation.Of(Config{Port: 8080, Host: "localhost"})
|
||||
}
|
||||
return E.Left[Config](errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Final fallback to defaults
|
||||
useDefaults := OrElse(func(errs Errors) Validate[string, Config] {
|
||||
return Of[string](Config{Port: 3000, Host: "0.0.0.0"})
|
||||
})
|
||||
|
||||
// Build pipeline
|
||||
validator := useDefaults(tryEnv(parseConfig))
|
||||
|
||||
// Test with env fallback
|
||||
result1 := validator("from_env")(nil)
|
||||
assert.True(t, E.IsRight(result1))
|
||||
if E.IsRight(result1) {
|
||||
cfg, _ := E.Unwrap(result1)
|
||||
assert.Equal(t, 8080, cfg.Port)
|
||||
assert.Equal(t, "localhost", cfg.Host)
|
||||
}
|
||||
|
||||
// Test with default fallback
|
||||
result2 := validator("other")(nil)
|
||||
assert.True(t, E.IsRight(result2))
|
||||
if E.IsRight(result2) {
|
||||
cfg, _ := E.Unwrap(result2)
|
||||
assert.Equal(t, 3000, cfg.Port)
|
||||
assert.Equal(t, "0.0.0.0", cfg.Host)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
162
v2/optics/codec/validation/OrElse_explanation.md
Normal file
162
v2/optics/codec/validation/OrElse_explanation.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# OrElse is Equivalent to ChainLeft
|
||||
|
||||
## Overview
|
||||
|
||||
In [`optics/codec/validation/monad.go`](monad.go:474-476), the [`OrElse`](monad.go:474) function is defined as a simple alias for [`ChainLeft`](monad.go:304):
|
||||
|
||||
```go
|
||||
//go:inline
|
||||
func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
```
|
||||
|
||||
This means **`OrElse` and `ChainLeft` are functionally identical** - they produce exactly the same results for all inputs.
|
||||
|
||||
## Why Have Both?
|
||||
|
||||
While they are technically the same, they serve different **semantic purposes**:
|
||||
|
||||
### ChainLeft - Technical Perspective
|
||||
[`ChainLeft`](monad.go:304-309) emphasizes the **technical operation**: it chains a computation on the Left (failure) channel of the Either/Validation monad. This name comes from category theory and functional programming terminology.
|
||||
|
||||
### OrElse - Semantic Perspective
|
||||
[`OrElse`](monad.go:474-476) emphasizes the **intent**: it provides an alternative or fallback when validation fails. The name reads naturally in code: "try this validation, **or else** try this alternative."
|
||||
|
||||
## Key Behavior
|
||||
|
||||
Both functions share the same critical behavior that distinguishes them from standard Either operations:
|
||||
|
||||
### Error Aggregation
|
||||
When the transformation function returns a failure, **both the original errors AND the new errors are combined** using the Errors monoid. This ensures no validation errors are lost.
|
||||
|
||||
```go
|
||||
// Example: Error aggregation
|
||||
result := OrElse(func(errs Errors) Validation[string] {
|
||||
return Failures[string](Errors{
|
||||
&ValidationError{Messsage: "additional error"},
|
||||
})
|
||||
})(Failures[string](Errors{
|
||||
&ValidationError{Messsage: "original error"},
|
||||
}))
|
||||
|
||||
// Result contains BOTH errors: ["original error", "additional error"]
|
||||
```
|
||||
|
||||
### Success Pass-Through
|
||||
Success values pass through unchanged - the function is never called:
|
||||
|
||||
```go
|
||||
result := OrElse(func(errs Errors) Validation[int] {
|
||||
return Failures[int](Errors{
|
||||
&ValidationError{Messsage: "never called"},
|
||||
})
|
||||
})(Success(42))
|
||||
|
||||
// Result: Success(42) - unchanged
|
||||
```
|
||||
|
||||
### Error Recovery
|
||||
The function can recover from failures by returning a Success:
|
||||
|
||||
```go
|
||||
recoverFromNotFound := OrElse(func(errs Errors) Validation[int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "not found" {
|
||||
return Success(0) // recover with default
|
||||
}
|
||||
}
|
||||
return Failures[int](errs)
|
||||
})
|
||||
|
||||
result := recoverFromNotFound(Failures[int](Errors{
|
||||
&ValidationError{Messsage: "not found"},
|
||||
}))
|
||||
|
||||
// Result: Success(0) - recovered from failure
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Fallback Validation (OrElse reads better)
|
||||
```go
|
||||
validatePositive := func(x int) Validation[int] {
|
||||
if x > 0 {
|
||||
return Success(x)
|
||||
}
|
||||
return Failures[int](Errors{
|
||||
&ValidationError{Messsage: "must be positive"},
|
||||
})
|
||||
}
|
||||
|
||||
// Use OrElse for semantic clarity
|
||||
withDefault := OrElse(func(errs Errors) Validation[int] {
|
||||
return Success(1) // default to 1 if validation fails
|
||||
})
|
||||
|
||||
result := F.Pipe1(validatePositive(-5), withDefault)
|
||||
// Result: Success(1)
|
||||
```
|
||||
|
||||
### 2. Error Context Addition (ChainLeft reads better)
|
||||
```go
|
||||
addContext := ChainLeft(func(errs Errors) Validation[string] {
|
||||
return Failures[string](Errors{
|
||||
&ValidationError{
|
||||
Messsage: "validation failed in user.email field",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
Failures[string](Errors{
|
||||
&ValidationError{Messsage: "invalid format"},
|
||||
}),
|
||||
addContext,
|
||||
)
|
||||
// Result contains: ["invalid format", "validation failed in user.email field"]
|
||||
```
|
||||
|
||||
### 3. Pipeline Composition
|
||||
Both can be used in pipelines, with errors accumulating at each step:
|
||||
|
||||
```go
|
||||
result := F.Pipe2(
|
||||
Failures[int](Errors{
|
||||
&ValidationError{Messsage: "database error"},
|
||||
}),
|
||||
OrElse(func(errs Errors) Validation[int] {
|
||||
return Failures[int](Errors{
|
||||
&ValidationError{Messsage: "context added"},
|
||||
})
|
||||
}),
|
||||
OrElse(func(errs Errors) Validation[int] {
|
||||
return Failures[int](errs) // propagate
|
||||
}),
|
||||
)
|
||||
// Errors accumulate at each step in the pipeline
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
The test suite in [`monad_test.go`](monad_test.go:1698) includes comprehensive tests proving that `OrElse` and `ChainLeft` are equivalent:
|
||||
|
||||
- ✅ Identical behavior for Success values
|
||||
- ✅ Identical behavior for error recovery
|
||||
- ✅ Identical behavior for error aggregation
|
||||
- ✅ Identical behavior in pipeline composition
|
||||
- ✅ Identical behavior for multiple error scenarios
|
||||
|
||||
Run the tests:
|
||||
```bash
|
||||
go test -v -run TestOrElse ./optics/codec/validation
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
**`OrElse` is exactly the same as `ChainLeft`** - they are aliases with identical implementations and behavior. The choice between them is purely about **code readability and semantic intent**:
|
||||
|
||||
- Use **`OrElse`** when emphasizing fallback/alternative validation logic
|
||||
- Use **`ChainLeft`** when emphasizing technical error channel transformation
|
||||
|
||||
Both maintain the critical validation property of **error aggregation**, ensuring all validation failures are preserved and reported together.
|
||||
318
v2/optics/codec/validation/bind.go
Normal file
318
v2/optics/codec/validation/bind.go
Normal file
@@ -0,0 +1,318 @@
|
||||
// Copyright (c) 2025 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 validation
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type S to be used with the Bind operation.
|
||||
// This is the starting point for building up a context using do-notation style.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Result struct {
|
||||
// x int
|
||||
// y string
|
||||
// }
|
||||
// result := Do(Result{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) Validation[S] {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context S1 to produce a context S2.
|
||||
// This is used in do-notation style to sequentially build up a context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{}),
|
||||
// Bind(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, func(s State) Validation[int] { return Success(42) }),
|
||||
// )
|
||||
func Bind[S1, S2, A any](
|
||||
setter func(A) func(S1) S2,
|
||||
f Kleisli[S1, A],
|
||||
) Operator[S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[A, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
|
||||
// Unlike Bind, the computation function returns a plain value, not an Option.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; computed int }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{x: 5}),
|
||||
// Let(func(c int) func(State) State {
|
||||
// return func(s State) State { s.computed = c; return s }
|
||||
// }, func(s State) int { return s.x * 2 }),
|
||||
// )
|
||||
func Let[S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
f func(S1) B,
|
||||
) Operator[S1, S2] {
|
||||
return F.Let(
|
||||
Map[S1, S2],
|
||||
key,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context S1 to produce a context S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; name string }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{x: 5}),
|
||||
// LetTo(func(n string) func(State) State {
|
||||
// return func(s State) State { s.name = n; return s }
|
||||
// }, "example"),
|
||||
// )
|
||||
func LetTo[S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
b B,
|
||||
) Operator[S1, S2] {
|
||||
return F.LetTo(
|
||||
Map[S1, S2],
|
||||
key,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state S1 from a value T.
|
||||
// This is typically used as the first operation after creating a Validation value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { value int }
|
||||
// result := F.Pipe1(
|
||||
// Success(42),
|
||||
// BindTo(func(x int) State { return State{value: x} }),
|
||||
// )
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[T, S1] {
|
||||
return C.BindTo(
|
||||
Map[T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
|
||||
// This uses the applicative functor pattern, allowing parallel composition.
|
||||
//
|
||||
// IMPORTANT: Unlike Bind which fails fast, ApS aggregates ALL validation errors from both the context
|
||||
// and the value. If both validations fail, all errors are collected and returned together.
|
||||
// This is useful for validating multiple independent fields and reporting all errors at once.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{}),
|
||||
// ApS(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, Success(42)),
|
||||
// )
|
||||
//
|
||||
// Error aggregation example:
|
||||
//
|
||||
// stateFailure := Failures[State](Errors{&ValidationError{Messsage: "state error"}})
|
||||
// valueFailure := Failures[int](Errors{&ValidationError{Messsage: "value error"}})
|
||||
// result := ApS(setter, valueFailure)(stateFailure)
|
||||
// // Result contains BOTH errors: ["state error", "value error"]
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Validation[T],
|
||||
) Operator[S1, S2] {
|
||||
return A.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// IMPORTANT: Like ApS, this function aggregates ALL validation errors. If both the context
|
||||
// and the value fail validation, all errors are collected and returned together.
|
||||
// This enables comprehensive error reporting for complex nested structures.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Address struct {
|
||||
// Street string
|
||||
// City string
|
||||
// }
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Address Address
|
||||
// }
|
||||
//
|
||||
// // Create a lens for the Address field
|
||||
// addressLens := lens.MakeLens(
|
||||
// func(p Person) Address { return p.Address },
|
||||
// func(p Person, a Address) Person { p.Address = a; return p },
|
||||
// )
|
||||
//
|
||||
// // Use ApSL to update the address
|
||||
// result := F.Pipe2(
|
||||
// Success(Person{Name: "Alice"}),
|
||||
// ApSL(
|
||||
// addressLens,
|
||||
// Success(Address{Street: "Main St", City: "NYC"}),
|
||||
// ),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Validation[T],
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// a Validation that produces the new value.
|
||||
//
|
||||
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
|
||||
// the current value of the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but fail if it would exceed 100
|
||||
// increment := func(v int) Validation[int] {
|
||||
// if v >= 100 {
|
||||
// return Failures[int](Errors{&ValidationError{Messsage: "exceeds limit"}})
|
||||
// }
|
||||
// return Success(v + 1)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Success(Counter{Value: 42}),
|
||||
// BindL(valueLens, increment),
|
||||
// ) // Success(Counter{Value: 43})
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return Bind(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in Validation).
|
||||
//
|
||||
// This is useful for pure transformations that cannot fail, such as mathematical operations,
|
||||
// string manipulations, or other deterministic updates.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Success(Counter{Value: 21}),
|
||||
// LetL(valueLens, double),
|
||||
// ) // Success(Counter{Value: 42})
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[S, S] {
|
||||
return Let(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// This is useful for resetting fields, initializing values, or setting fields to
|
||||
// predetermined constants.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Success(Config{Debug: true, Timeout: 30}),
|
||||
// LetToL(debugLens, false),
|
||||
// ) // Success(Config{Debug: false, Timeout: 30})
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return LetTo(lens.Set, b)
|
||||
}
|
||||
540
v2/optics/codec/validation/bind_test.go
Normal file
540
v2/optics/codec/validation/bind_test.go
Normal file
@@ -0,0 +1,540 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
t.Run("creates successful validation with empty state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
result := Do(State{})
|
||||
|
||||
assert.Equal(t, Of(State{}), result)
|
||||
})
|
||||
|
||||
t.Run("creates successful validation with initialized state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
initial := State{x: 42, y: "hello"}
|
||||
result := Do(initial)
|
||||
|
||||
assert.Equal(t, Of(initial), result)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
intResult := Do(0)
|
||||
assert.Equal(t, Of(0), intResult)
|
||||
|
||||
strResult := Do("")
|
||||
assert.Equal(t, Of(""), strResult)
|
||||
|
||||
type Custom struct{ Value int }
|
||||
customResult := Do(Custom{Value: 100})
|
||||
assert.Equal(t, Of(Custom{Value: 100}), customResult)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("binds successful validation to state", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validation[int] { return Success(42) }),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validation[int] { return Success(10) }),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 42, y: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("propagates failure", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validation[int] { return Success(42) }),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validation[int] {
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "y failed"}})
|
||||
}),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "y failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("can access previous state values", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validation[int] { return Success(10) }),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validation[int] {
|
||||
// y depends on x
|
||||
return Success(s.x * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, Success(State{x: 10, y: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
computed int
|
||||
}
|
||||
|
||||
t.Run("attaches pure computation result to state", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Do(State{x: 5}),
|
||||
Let(func(c int) func(State) State {
|
||||
return func(s State) State { s.computed = c; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 5, computed: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := Failures[State](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := Let(func(c int) func(State) State {
|
||||
return func(s State) State { s.computed = c; return s }
|
||||
}, func(s State) int { return s.x * 2 })(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("chains multiple Let operations", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
z int
|
||||
}
|
||||
result := F.Pipe3(
|
||||
Do(State{x: 5}),
|
||||
Let(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
Let(func(z int) func(State) State {
|
||||
return func(s State) State { s.z = z; return s }
|
||||
}, func(s State) int { return s.y + 10 }),
|
||||
Let(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) int { return s.z * 3 }),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 60, y: 10, z: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
name string
|
||||
}
|
||||
|
||||
t.Run("attaches constant value to state", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Do(State{x: 5}),
|
||||
LetTo(func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "example"),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 5, name: "example"}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := Failures[State](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := LetTo(func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "example")(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("sets multiple constant values", func(t *testing.T) {
|
||||
type State struct {
|
||||
name string
|
||||
version int
|
||||
active bool
|
||||
}
|
||||
result := F.Pipe3(
|
||||
Do(State{}),
|
||||
LetTo(func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "app"),
|
||||
LetTo(func(v int) func(State) State {
|
||||
return func(s State) State { s.version = v; return s }
|
||||
}, 2),
|
||||
LetTo(func(a bool) func(State) State {
|
||||
return func(s State) State { s.active = a; return s }
|
||||
}, true),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{name: "app", version: 2, active: true}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
}
|
||||
|
||||
t.Run("initializes state from value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Success(42),
|
||||
BindTo(func(x int) State { return State{value: x} }),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{value: 42}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := Failures[int](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := BindTo(func(x int) State { return State{value: x} })(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
type StringState struct {
|
||||
text string
|
||||
}
|
||||
result := F.Pipe1(
|
||||
Success("hello"),
|
||||
BindTo(func(s string) StringState { return StringState{text: s} }),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(StringState{text: "hello"}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("attaches value using applicative pattern", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Do(State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Success(42)),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 42}), result)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors from both validations", func(t *testing.T) {
|
||||
stateFailure := Failures[State](Errors{&ValidationError{Messsage: "state error"}})
|
||||
valueFailure := Failures[int](Errors{&ValidationError{Messsage: "value error"}})
|
||||
|
||||
result := ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, valueFailure)(stateFailure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "state error")
|
||||
assert.Contains(t, messages, "value error")
|
||||
})
|
||||
|
||||
t.Run("combines multiple ApS operations", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Success(10)),
|
||||
ApS(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, Success(20)),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 10, y: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
t.Run("updates nested structure using lens", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
result := F.Pipe1(
|
||||
Success(Person{Name: "Alice"}),
|
||||
ApSL(
|
||||
addressLens,
|
||||
Success(Address{Street: "Main St", City: "NYC"}),
|
||||
),
|
||||
)
|
||||
|
||||
expected := Person{
|
||||
Name: "Alice",
|
||||
Address: Address{Street: "Main St", City: "NYC"},
|
||||
}
|
||||
assert.Equal(t, Of(expected), result)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
personFailure := Failures[Person](Errors{&ValidationError{Messsage: "person error"}})
|
||||
addressFailure := Failures[Address](Errors{&ValidationError{Messsage: "address error"}})
|
||||
|
||||
result := ApSL(addressLens, addressFailure)(personFailure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(Person) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("updates field based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Validation[int] {
|
||||
return Success(v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Success(Counter{Value: 42}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(Counter{Value: 43}), result)
|
||||
})
|
||||
|
||||
t.Run("fails validation based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Validation[int] {
|
||||
if v >= 100 {
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "exceeds limit"}})
|
||||
}
|
||||
return Success(v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Success(Counter{Value: 100}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(Counter) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "exceeds limit", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
increment := func(v int) Validation[int] {
|
||||
return Success(v + 1)
|
||||
}
|
||||
|
||||
failure := Failures[Counter](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := BindL(valueLens, increment)(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("transforms field with pure function", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe1(
|
||||
Success(Counter{Value: 21}),
|
||||
LetL(valueLens, double),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(Counter{Value: 42}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
failure := Failures[Counter](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := LetL(valueLens, double)(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("chains multiple transformations", func(t *testing.T) {
|
||||
add10 := func(v int) int { return v + 10 }
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe2(
|
||||
Success(Counter{Value: 5}),
|
||||
LetL(valueLens, add10),
|
||||
LetL(valueLens, double),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(Counter{Value: 30}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Timeout int
|
||||
}
|
||||
|
||||
debugLens := L.MakeLens(
|
||||
func(c Config) bool { return c.Debug },
|
||||
func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
)
|
||||
|
||||
t.Run("sets field to constant value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Success(Config{Debug: true, Timeout: 30}),
|
||||
LetToL(debugLens, false),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(Config{Debug: false, Timeout: 30}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := Failures[Config](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := LetToL(debugLens, false)(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("sets multiple fields", func(t *testing.T) {
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) int { return c.Timeout },
|
||||
func(c Config, t int) Config { c.Timeout = t; return c },
|
||||
)
|
||||
|
||||
result := F.Pipe2(
|
||||
Success(Config{Debug: true, Timeout: 30}),
|
||||
LetToL(debugLens, false),
|
||||
LetToL(timeoutLens, 60),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(Config{Debug: false, Timeout: 60}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindOperationsComposition(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
t.Run("combines Do, Bind, Let, and LetTo", func(t *testing.T) {
|
||||
result := F.Pipe4(
|
||||
Do(User{}),
|
||||
LetTo(func(n string) func(User) User {
|
||||
return func(u User) User { u.Name = n; return u }
|
||||
}, "Alice"),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Validation[int] {
|
||||
// Age validation
|
||||
if len(u.Name) > 0 {
|
||||
return Success(25)
|
||||
}
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "name required"}})
|
||||
}),
|
||||
Let(func(e string) func(User) User {
|
||||
return func(u User) User { u.Email = e; return u }
|
||||
}, func(u User) string {
|
||||
// Derive email from name
|
||||
return u.Name + "@example.com"
|
||||
}),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Validation[int] {
|
||||
// Validate age is positive
|
||||
if u.Age > 0 {
|
||||
return Success(u.Age)
|
||||
}
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "age must be positive"}})
|
||||
}),
|
||||
)
|
||||
|
||||
expected := User{Name: "Alice", Age: 25, Email: "Alice@example.com"}
|
||||
assert.Equal(t, Of(expected), result)
|
||||
})
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/applicative"
|
||||
)
|
||||
|
||||
var errorsMonoid = ErrorsMonoid()
|
||||
|
||||
// Of creates a successful validation result containing the given value.
|
||||
// This is the pure/return operation for the Validation monad.
|
||||
//
|
||||
@@ -28,37 +32,376 @@ func Of[A any](a A) Validation[A] {
|
||||
// return func(age int) User { return User{name, age} }
|
||||
// }))(validateName))(validateAge)
|
||||
func Ap[B, A any](fa Validation[A]) Operator[func(A) B, B] {
|
||||
return either.ApV[B, A](ErrorsMonoid())(fa)
|
||||
return either.ApV[B, A](errorsMonoid)(fa)
|
||||
}
|
||||
|
||||
// MonadAp applies a validation containing a function to a validation containing a value.
|
||||
// This is the applicative apply operation that **accumulates errors** from both validations.
|
||||
//
|
||||
// **Key behavior**: Unlike Either's MonadAp which fails fast (returns first error),
|
||||
// this validation-specific implementation **accumulates all errors** using the Errors monoid.
|
||||
// When both the function validation and value validation fail, all errors from both are combined.
|
||||
//
|
||||
// This error accumulation is the defining characteristic of the Validation applicative,
|
||||
// making it ideal for scenarios where you want to collect all validation failures at once
|
||||
// rather than stopping at the first error.
|
||||
//
|
||||
// Behavior:
|
||||
// - Both succeed: applies the function to the value → Success(result)
|
||||
// - Function fails, value succeeds: returns function's errors → Failure(func errors)
|
||||
// - Function succeeds, value fails: returns value's errors → Failure(value errors)
|
||||
// - Both fail: **combines all errors** → Failure(func errors + value errors)
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Form validation: collect all field errors at once
|
||||
// - Configuration validation: report all invalid settings together
|
||||
// - Data validation: accumulate all constraint violations
|
||||
// - Multi-field validation: validate independent fields in parallel
|
||||
//
|
||||
// Example - Both succeed:
|
||||
//
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// result := MonadAp(Of(double), Of(21))
|
||||
// // Result: Success(42)
|
||||
//
|
||||
// Example - Error accumulation (key feature):
|
||||
//
|
||||
// funcValidation := Failures[func(int) int](Errors{
|
||||
// &ValidationError{Messsage: "function error"},
|
||||
// })
|
||||
// valueValidation := Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "value error"},
|
||||
// })
|
||||
// result := MonadAp(funcValidation, valueValidation)
|
||||
// // Result: Failure with BOTH errors: ["function error", "value error"]
|
||||
//
|
||||
// Example - Validating multiple fields:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// makeUser := func(name string) func(int) User {
|
||||
// return func(age int) User { return User{name, age} }
|
||||
// }
|
||||
//
|
||||
// nameValidation := validateName("ab") // Fails: too short
|
||||
// ageValidation := validateAge(16) // Fails: too young
|
||||
//
|
||||
// // First apply name
|
||||
// step1 := MonadAp(Of(makeUser), nameValidation)
|
||||
// // Then apply age
|
||||
// result := MonadAp(step1, ageValidation)
|
||||
// // Result contains ALL validation errors from both fields
|
||||
func MonadAp[B, A any](fab Validation[func(A) B], fa Validation[A]) Validation[B] {
|
||||
return either.MonadApV[B, A](ErrorsMonoid())(fab, fa)
|
||||
return either.MonadApV[B, A](errorsMonoid)(fab, fa)
|
||||
}
|
||||
|
||||
// Map transforms the value inside a successful validation using the provided function.
|
||||
// If the validation is a failure, the errors are preserved unchanged.
|
||||
// This is the functor map operation for Validation.
|
||||
//
|
||||
// Example:
|
||||
// Map is used for transforming successful values without changing the validation context.
|
||||
// It's the most basic operation for working with validated values and forms the foundation
|
||||
// for more complex validation pipelines.
|
||||
//
|
||||
// Behavior:
|
||||
// - Success: applies function to value → Success(f(value))
|
||||
// - Failure: preserves errors unchanged → Failure(same errors)
|
||||
//
|
||||
// This is useful for:
|
||||
// - Type transformations: converting validated values to different types
|
||||
// - Value transformations: normalizing, formatting, or computing derived values
|
||||
// - Pipeline composition: chaining multiple transformations
|
||||
// - Preserving validation context: errors pass through unchanged
|
||||
//
|
||||
// Example - Transform successful value:
|
||||
//
|
||||
// doubled := Map(func(x int) int { return x * 2 })(Of(21))
|
||||
// // Result: Success(42)
|
||||
//
|
||||
// Example - Failure preserved:
|
||||
//
|
||||
// result := Map(func(x int) int { return x * 2 })(
|
||||
// Failures[int](Errors{&ValidationError{Messsage: "invalid"}}),
|
||||
// )
|
||||
// // Result: Failure with same error: ["invalid"]
|
||||
//
|
||||
// Example - Type transformation:
|
||||
//
|
||||
// toString := Map(func(x int) string { return fmt.Sprintf("%d", x) })
|
||||
// result := toString(Of(42))
|
||||
// // Result: Success("42")
|
||||
//
|
||||
// Example - Chaining transformations:
|
||||
//
|
||||
// result := F.Pipe3(
|
||||
// Of(5),
|
||||
// Map(func(x int) int { return x + 10 }), // 15
|
||||
// Map(func(x int) int { return x * 2 }), // 30
|
||||
// Map(func(x int) string { return fmt.Sprintf("%d", x) }), // "30"
|
||||
// )
|
||||
// // Result: Success("30")
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return either.Map[Errors](f)
|
||||
}
|
||||
|
||||
// MonadMap transforms the value inside a successful validation using the provided function.
|
||||
// If the validation is a failure, the errors are preserved unchanged.
|
||||
// This is the non-curried version of [Map].
|
||||
//
|
||||
// MonadMap is useful when you have both the validation and the transformation function
|
||||
// available at the same time, rather than needing to create a reusable operator.
|
||||
//
|
||||
// Behavior:
|
||||
// - Success: applies function to value → Success(f(value))
|
||||
// - Failure: preserves errors unchanged → Failure(same errors)
|
||||
//
|
||||
// Example - Transform successful value:
|
||||
//
|
||||
// result := MonadMap(Of(21), func(x int) int { return x * 2 })
|
||||
// // Result: Success(42)
|
||||
//
|
||||
// Example - Failure preserved:
|
||||
//
|
||||
// result := MonadMap(
|
||||
// Failures[int](Errors{&ValidationError{Messsage: "invalid"}}),
|
||||
// func(x int) int { return x * 2 },
|
||||
// )
|
||||
// // Result: Failure with same error: ["invalid"]
|
||||
//
|
||||
// Example - Type transformation:
|
||||
//
|
||||
// result := MonadMap(Of(42), func(x int) string {
|
||||
// return fmt.Sprintf("Value: %d", x)
|
||||
// })
|
||||
// // Result: Success("Value: 42")
|
||||
//
|
||||
// Example - Computing derived values:
|
||||
//
|
||||
// type User struct { FirstName, LastName string }
|
||||
// result := MonadMap(
|
||||
// Of(User{"John", "Doe"}),
|
||||
// func(u User) string { return u.FirstName + " " + u.LastName },
|
||||
// )
|
||||
// // Result: Success("John Doe")
|
||||
func MonadMap[A, B any](fa Validation[A], f func(A) B) Validation[B] {
|
||||
return either.MonadMap(fa, f)
|
||||
}
|
||||
|
||||
// Chain is the curried version of [MonadChain].
|
||||
// Sequences two validation computations where the second depends on the first.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validatePositive := func(x int) Validation[int] {
|
||||
// if x > 0 { return Success(x) }
|
||||
// return Failure("must be positive")
|
||||
// }
|
||||
// result := Chain(validatePositive)(Success(42)) // Success(42)
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return either.Chain(f)
|
||||
}
|
||||
|
||||
// MonadChain sequences two validation computations where the second depends on the first.
|
||||
// If the first validation fails, returns the failure without executing the second.
|
||||
// This is the monadic bind operation for Validation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := MonadChain(
|
||||
// Success(42),
|
||||
// func(x int) Validation[string] {
|
||||
// return Success(fmt.Sprintf("Value: %d", x))
|
||||
// },
|
||||
// ) // Success("Value: 42")
|
||||
func MonadChain[A, B any](fa Validation[A], f Kleisli[A, B]) Validation[B] {
|
||||
return either.MonadChain(fa, f)
|
||||
}
|
||||
|
||||
// chainErrors is an internal helper that chains error transformations while accumulating errors.
|
||||
// When the transformation function f returns a failure, it concatenates the original errors (e1)
|
||||
// with the new errors (e2) using the Errors monoid, ensuring all validation errors are preserved.
|
||||
func chainErrors[A any](f Kleisli[Errors, A]) func(Errors) Validation[A] {
|
||||
return func(e1 Errors) Validation[A] {
|
||||
return either.MonadFold(
|
||||
f(e1),
|
||||
function.Flow2(array.Concat(e1), either.Left[A]),
|
||||
Of[A],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// Returns a function that transforms validation failures while preserving successes.
|
||||
//
|
||||
// Unlike the standard Either ChainLeft which replaces errors, this validation-specific
|
||||
// implementation **aggregates errors** using the Errors monoid. When the transformation
|
||||
// function returns a failure, both the original errors and the new errors are combined,
|
||||
// ensuring no validation errors are lost.
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Error recovery with fallback validation
|
||||
// - Adding contextual information to existing errors
|
||||
// - Transforming error types while preserving all error details
|
||||
// - Building error handling pipelines that accumulate failures
|
||||
//
|
||||
// Key behavior:
|
||||
// - Success values pass through unchanged
|
||||
// - When transforming failures, if the transformation also fails, **all errors are aggregated**
|
||||
// - If the transformation succeeds, it recovers from the original failure
|
||||
//
|
||||
// Example - Error recovery with aggregation:
|
||||
//
|
||||
// recoverFromNotFound := ChainLeft(func(errs Errors) Validation[int] {
|
||||
// // Check if this is a "not found" error
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "not found" {
|
||||
// return Success(0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// // Add context to existing errors
|
||||
// return Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "recovery failed"},
|
||||
// })
|
||||
// // Result will contain BOTH original errors AND "recovery failed"
|
||||
// })
|
||||
//
|
||||
// result := recoverFromNotFound(Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "database error"},
|
||||
// }))
|
||||
// // Result contains: ["database error", "recovery failed"]
|
||||
//
|
||||
// Example - Adding context to errors:
|
||||
//
|
||||
// addContext := ChainLeft(func(errs Errors) Validation[string] {
|
||||
// // Add contextual information
|
||||
// return Failures[string](Errors{
|
||||
// &ValidationError{
|
||||
// Messsage: "validation failed in user.email field",
|
||||
// },
|
||||
// })
|
||||
// // Original errors are preserved and new context is added
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Failures[string](Errors{
|
||||
// &ValidationError{Messsage: "invalid format"},
|
||||
// }),
|
||||
// addContext,
|
||||
// )
|
||||
// // Result contains: ["invalid format", "validation failed in user.email field"]
|
||||
//
|
||||
// Example - Success values pass through:
|
||||
//
|
||||
// handler := ChainLeft(func(errs Errors) Validation[int] {
|
||||
// return Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "never called"},
|
||||
// })
|
||||
// })
|
||||
// result := handler(Success(42)) // Success(42) - unchanged
|
||||
func ChainLeft[A any](f Kleisli[Errors, A]) Operator[A, A] {
|
||||
return either.Fold(
|
||||
chainErrors(f),
|
||||
Of[A],
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainLeft sequences a computation on the failure (Left) channel of a Validation.
|
||||
// If the Validation is a failure, applies the function to transform or recover from the errors.
|
||||
// If the Validation is a success, returns the success value unchanged.
|
||||
//
|
||||
// **Critical difference from Either.MonadChainLeft**: This validation-specific implementation
|
||||
// **aggregates errors** using the Errors monoid. When the transformation function returns a
|
||||
// failure, both the original errors and the new errors are combined, ensuring comprehensive
|
||||
// error reporting.
|
||||
//
|
||||
// This is the dual of [MonadChain] - while Chain operates on success values, ChainLeft
|
||||
// operates on failure values. It's particularly useful for:
|
||||
// - Error recovery: converting specific errors into successful values
|
||||
// - Error enrichment: adding context or transforming error messages
|
||||
// - Fallback logic: providing alternative validations when the first fails
|
||||
// - Error aggregation: combining multiple validation failures
|
||||
//
|
||||
// The function parameter receives the collection of validation errors and must return
|
||||
// a new Validation[A]. This allows you to:
|
||||
// - Recover by returning Success(value)
|
||||
// - Transform errors by returning Failures(newErrors) - **original errors are preserved**
|
||||
// - Implement conditional error handling based on error content
|
||||
//
|
||||
// Example - Error recovery:
|
||||
//
|
||||
// result := MonadChainLeft(
|
||||
// Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "not found"},
|
||||
// }),
|
||||
// func(errs Errors) Validation[int] {
|
||||
// // Check if we can recover
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "not found" {
|
||||
// return Success(0) // recover with default value
|
||||
// }
|
||||
// }
|
||||
// return Failures[int](errs) // propagate errors
|
||||
// },
|
||||
// ) // Success(0)
|
||||
//
|
||||
// Example - Error aggregation (key feature):
|
||||
//
|
||||
// result := MonadChainLeft(
|
||||
// Failures[string](Errors{
|
||||
// &ValidationError{Messsage: "error 1"},
|
||||
// &ValidationError{Messsage: "error 2"},
|
||||
// }),
|
||||
// func(errs Errors) Validation[string] {
|
||||
// // Transformation also fails
|
||||
// return Failures[string](Errors{
|
||||
// &ValidationError{Messsage: "error 3"},
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
// // Result contains ALL errors: ["error 1", "error 2", "error 3"]
|
||||
// // This is different from Either.MonadChainLeft which would only keep "error 3"
|
||||
//
|
||||
// Example - Adding context to errors:
|
||||
//
|
||||
// result := MonadChainLeft(
|
||||
// Failures[int](Errors{
|
||||
// &ValidationError{Value: "abc", Messsage: "invalid number"},
|
||||
// }),
|
||||
// func(errs Errors) Validation[int] {
|
||||
// // Add contextual information
|
||||
// contextErrors := Errors{
|
||||
// &ValidationError{
|
||||
// Context: []ContextEntry{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to parse user age",
|
||||
// },
|
||||
// }
|
||||
// return Failures[int](contextErrors)
|
||||
// },
|
||||
// )
|
||||
// // Result contains both original error and context:
|
||||
// // ["invalid number", "failed to parse user age"]
|
||||
//
|
||||
// Example - Success values pass through:
|
||||
//
|
||||
// result := MonadChainLeft(
|
||||
// Success(42),
|
||||
// func(errs Errors) Validation[int] {
|
||||
// return Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "never called"},
|
||||
// })
|
||||
// },
|
||||
// ) // Success(42) - unchanged
|
||||
func MonadChainLeft[A any](fa Validation[A], f Kleisli[Errors, A]) Validation[A] {
|
||||
return either.MonadFold(
|
||||
fa,
|
||||
chainErrors(f),
|
||||
Of[A],
|
||||
)
|
||||
}
|
||||
|
||||
// Applicative creates an Applicative instance for Validation with error accumulation.
|
||||
//
|
||||
// This returns a lawful Applicative that accumulates validation errors using the Errors monoid.
|
||||
@@ -123,6 +466,175 @@ func MonadChain[A, B any](fa Validation[A], f Kleisli[A, B]) Validation[B] {
|
||||
// An Applicative instance with Of, Map, and Ap operations that accumulate errors
|
||||
func Applicative[A, B any]() applicative.Applicative[A, B, Validation[A], Validation[B], Validation[func(A) B]] {
|
||||
return either.ApplicativeV[Errors, A, B](
|
||||
ErrorsMonoid(),
|
||||
errorsMonoid,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
|
||||
// MonadAlt implements the Alternative operation for Validation, providing fallback behavior.
|
||||
// If the first validation fails, it evaluates and returns the second validation as an alternative.
|
||||
// If the first validation succeeds, it returns the first validation without evaluating the second.
|
||||
//
|
||||
// This is the fundamental operation for the Alt typeclass, enabling "try first, fallback to second"
|
||||
// semantics. It's particularly useful for:
|
||||
// - Providing default values when validation fails
|
||||
// - Trying multiple validation strategies in sequence
|
||||
// - Building validation pipelines with fallback logic
|
||||
// - Implementing optional validation with defaults
|
||||
//
|
||||
// **Key behavior**: Unlike error accumulation in [MonadAp], MonadAlt does NOT accumulate errors.
|
||||
// When falling back to the second validation, the first validation's errors are discarded.
|
||||
// This is the standard Alt behavior - it's about choosing alternatives, not combining errors.
|
||||
//
|
||||
// The second parameter is lazy (Lazy[Validation[A]]) to avoid unnecessary computation when
|
||||
// the first validation succeeds. The second validation is only evaluated if needed.
|
||||
//
|
||||
// Behavior:
|
||||
// - First succeeds: returns first validation (second is not evaluated)
|
||||
// - First fails: evaluates and returns second validation (first errors discarded)
|
||||
//
|
||||
// This is useful for:
|
||||
// - Fallback values: provide defaults when primary validation fails
|
||||
// - Alternative strategies: try different validation approaches
|
||||
// - Optional validation: make validation optional with a default
|
||||
// - Chaining attempts: try multiple sources until one succeeds
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the successful value
|
||||
//
|
||||
// Parameters:
|
||||
// - first: The primary validation to try
|
||||
// - second: A lazy computation producing the fallback validation (only evaluated if first fails)
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// The first validation if it succeeds, otherwise the second validation
|
||||
//
|
||||
// Example - Fallback to default:
|
||||
//
|
||||
// primary := parseConfig("config.json") // Fails
|
||||
// fallback := func() Validation[Config] {
|
||||
// return Success(defaultConfig)
|
||||
// }
|
||||
// result := MonadAlt(primary, fallback)
|
||||
// // Result: Success(defaultConfig)
|
||||
//
|
||||
// Example - First succeeds (second not evaluated):
|
||||
//
|
||||
// primary := Success(42)
|
||||
// fallback := func() Validation[int] {
|
||||
// panic("never called") // This won't execute
|
||||
// }
|
||||
// result := MonadAlt(primary, fallback)
|
||||
// // Result: Success(42)
|
||||
//
|
||||
// Example - Chaining multiple alternatives:
|
||||
//
|
||||
// result := MonadAlt(
|
||||
// parseFromEnv("API_KEY"),
|
||||
// func() Validation[string] {
|
||||
// return MonadAlt(
|
||||
// parseFromFile(".env"),
|
||||
// func() Validation[string] {
|
||||
// return Success("default-key")
|
||||
// },
|
||||
// )
|
||||
// },
|
||||
// )
|
||||
// // Tries: env var → file → default (uses first that succeeds)
|
||||
//
|
||||
// Example - No error accumulation:
|
||||
//
|
||||
// v1 := Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "error 1"},
|
||||
// &ValidationError{Messsage: "error 2"},
|
||||
// })
|
||||
// v2 := func() Validation[int] {
|
||||
// return Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "error 3"},
|
||||
// })
|
||||
// }
|
||||
// result := MonadAlt(v1, v2)
|
||||
// // Result: Failures with only ["error 3"]
|
||||
// // The errors from v1 are discarded (not accumulated)
|
||||
func MonadAlt[A any](first Validation[A], second Lazy[Validation[A]]) Validation[A] {
|
||||
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
// Alt is the curried version of [MonadAlt].
|
||||
// Returns a function that provides fallback behavior for a Validation.
|
||||
//
|
||||
// This is useful for creating reusable fallback operators that can be applied
|
||||
// to multiple validations, or for use in function composition pipelines.
|
||||
//
|
||||
// The returned function takes a validation and returns either that validation
|
||||
// (if successful) or the provided alternative (if the validation fails).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the successful value
|
||||
//
|
||||
// Parameters:
|
||||
// - second: A lazy computation producing the fallback validation
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A function that takes a Validation[A] and returns a Validation[A] with fallback behavior
|
||||
//
|
||||
// Example - Creating a reusable fallback operator:
|
||||
//
|
||||
// withDefault := Alt(func() Validation[int] {
|
||||
// return Success(0)
|
||||
// })
|
||||
//
|
||||
// result1 := withDefault(parseNumber("42")) // Success(42)
|
||||
// result2 := withDefault(parseNumber("abc")) // Success(0) - fallback
|
||||
// result3 := withDefault(parseNumber("123")) // Success(123)
|
||||
//
|
||||
// Example - Using in a pipeline:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// parseFromEnv("CONFIG_PATH"),
|
||||
// Alt(func() Validation[string] {
|
||||
// return parseFromFile("config.json")
|
||||
// }),
|
||||
// Alt(func() Validation[string] {
|
||||
// return Success("./default-config.json")
|
||||
// }),
|
||||
// )
|
||||
// // Tries: env var → file → default path
|
||||
//
|
||||
// Example - Combining with Map:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// validatePositive(-5), // Fails
|
||||
// Alt(func() Validation[int] { return Success(1) }),
|
||||
// Map(func(x int) int { return x * 2 }),
|
||||
// )
|
||||
// // Result: Success(2) - uses fallback value 1, then doubles it
|
||||
//
|
||||
// Example - Multiple fallback layers:
|
||||
//
|
||||
// primaryFallback := Alt(func() Validation[Config] {
|
||||
// return loadFromFile("backup.json")
|
||||
// })
|
||||
// secondaryFallback := Alt(func() Validation[Config] {
|
||||
// return Success(defaultConfig)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// loadFromFile("config.json"),
|
||||
// primaryFallback,
|
||||
// secondaryFallback,
|
||||
// )
|
||||
// // Tries: config.json → backup.json → default
|
||||
func Alt[A any](second Lazy[Validation[A]]) Operator[A, A] {
|
||||
return ChainLeft(function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,3 +52,177 @@ func ApplicativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a Monoid instance for Validation[A] using the Alternative pattern.
|
||||
// This combines the applicative error-accumulation behavior with the alternative fallback behavior,
|
||||
// allowing you to both accumulate errors and provide fallback alternatives.
|
||||
//
|
||||
// The Alternative pattern provides two key operations:
|
||||
// - Applicative operations (Of, Map, Ap): accumulate errors when combining validations
|
||||
// - Alternative operation (Alt): provide fallback when a validation fails
|
||||
//
|
||||
// This monoid is particularly useful when you want to:
|
||||
// - Try multiple validation strategies and fall back to alternatives
|
||||
// - Combine successful values using the provided monoid
|
||||
// - Accumulate all errors from failed attempts
|
||||
// - Build validation pipelines with fallback logic
|
||||
//
|
||||
// The resulting monoid:
|
||||
// - Empty: Returns a successful validation with the empty value from the inner monoid
|
||||
// - Concat: Combines two validations using both applicative and alternative semantics:
|
||||
// - If first succeeds and second succeeds: combines values using inner monoid
|
||||
// - If first fails: tries second as fallback (alternative behavior)
|
||||
// - If both fail: accumulates all errors
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the successful value
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The monoid for combining successful values of type A
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Monoid[Validation[A]] that combines applicative and alternative behaviors
|
||||
//
|
||||
// Example - Combining successful validations:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// m := AlternativeMonoid(string.Monoid)
|
||||
// v1 := Success("Hello")
|
||||
// v2 := Success(" World")
|
||||
// result := m.Concat(v1, v2)
|
||||
// // Result: Success("Hello World")
|
||||
//
|
||||
// Example - Fallback behavior:
|
||||
//
|
||||
// m := AlternativeMonoid(string.Monoid)
|
||||
// v1 := Failures[string](Errors{&ValidationError{Messsage: "first failed"}})
|
||||
// v2 := Success("fallback value")
|
||||
// result := m.Concat(v1, v2)
|
||||
// // Result: Success("fallback value") - second validation used as fallback
|
||||
//
|
||||
// Example - Error accumulation when both fail:
|
||||
//
|
||||
// m := AlternativeMonoid(string.Monoid)
|
||||
// v1 := Failures[string](Errors{&ValidationError{Messsage: "error 1"}})
|
||||
// v2 := Failures[string](Errors{&ValidationError{Messsage: "error 2"}})
|
||||
// result := m.Concat(v1, v2)
|
||||
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
|
||||
//
|
||||
// Example - Building validation with fallbacks:
|
||||
//
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// m := AlternativeMonoid(N.MonoidSum[int]())
|
||||
//
|
||||
// // Try to parse from different sources
|
||||
// fromEnv := parseFromEnv() // Fails
|
||||
// fromConfig := parseFromConfig() // Succeeds with 42
|
||||
// fromDefault := Success(0) // Default fallback
|
||||
//
|
||||
// result := m.Concat(m.Concat(fromEnv, fromConfig), fromDefault)
|
||||
// // Result: Success(42) - uses first successful validation
|
||||
func AlternativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
|
||||
return M.AlternativeMonoid(
|
||||
Of[A],
|
||||
MonadMap[A, func(A) A],
|
||||
MonadAp[A, A],
|
||||
MonadAlt[A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a Monoid instance for Validation[A] using the Alt (alternative) operation.
|
||||
// This monoid provides a way to combine validations with fallback behavior, where the second
|
||||
// validation is used as an alternative if the first one fails.
|
||||
//
|
||||
// The Alt operation implements the "try first, fallback to second" pattern, which is useful
|
||||
// for validation scenarios where you want to attempt multiple validation strategies in sequence
|
||||
// and use the first one that succeeds.
|
||||
//
|
||||
// The resulting monoid:
|
||||
// - Empty: Returns the provided zero value (a lazy computation that produces a Validation[A])
|
||||
// - Concat: Combines two validations using Alt semantics:
|
||||
// - If first succeeds: returns the first validation (ignores second)
|
||||
// - If first fails: returns the second validation as fallback
|
||||
//
|
||||
// This is different from [AlternativeMonoid] in that:
|
||||
// - AltMonoid uses a custom zero value (provided by the user)
|
||||
// - AlternativeMonoid derives the zero from an inner monoid
|
||||
// - AltMonoid is simpler and only provides fallback behavior
|
||||
// - AlternativeMonoid combines applicative and alternative behaviors
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the successful value
|
||||
//
|
||||
// Parameters:
|
||||
// - zero: A lazy computation that produces the identity/empty Validation[A].
|
||||
// This is typically a successful validation with a default value, or could be
|
||||
// a failure representing "no validation attempted"
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A Monoid[Validation[A]] that combines validations with fallback behavior
|
||||
//
|
||||
// Example - Using default value as zero:
|
||||
//
|
||||
// m := AltMonoid(func() Validation[int] { return Success(0) })
|
||||
//
|
||||
// v1 := Failures[int](Errors{&ValidationError{Messsage: "failed"}})
|
||||
// v2 := Success(42)
|
||||
//
|
||||
// result := m.Concat(v1, v2)
|
||||
// // Result: Success(42) - falls back to second validation
|
||||
//
|
||||
// empty := m.Empty()
|
||||
// // Result: Success(0) - the provided zero value
|
||||
//
|
||||
// Example - Chaining multiple fallbacks:
|
||||
//
|
||||
// m := AltMonoid(func() Validation[string] {
|
||||
// return Success("default")
|
||||
// })
|
||||
//
|
||||
// primary := parseFromPrimarySource() // Fails
|
||||
// secondary := parseFromSecondary() // Fails
|
||||
// tertiary := parseFromTertiary() // Succeeds with "value"
|
||||
//
|
||||
// result := m.Concat(m.Concat(primary, secondary), tertiary)
|
||||
// // Result: Success("value") - uses first successful validation
|
||||
//
|
||||
// Example - All validations fail:
|
||||
//
|
||||
// m := AltMonoid(func() Validation[int] {
|
||||
// return Failures[int](Errors{&ValidationError{Messsage: "no default"}})
|
||||
// })
|
||||
//
|
||||
// v1 := Failures[int](Errors{&ValidationError{Messsage: "error 1"}})
|
||||
// v2 := Failures[int](Errors{&ValidationError{Messsage: "error 2"}})
|
||||
//
|
||||
// result := m.Concat(v1, v2)
|
||||
// // Result: Failures with errors from v2: ["error 2"]
|
||||
// // Note: Unlike AlternativeMonoid, errors are NOT accumulated
|
||||
//
|
||||
// Example - Building a validation pipeline with fallbacks:
|
||||
//
|
||||
// m := AltMonoid(func() Validation[Config] {
|
||||
// return Success(defaultConfig)
|
||||
// })
|
||||
//
|
||||
// // Try multiple configuration sources in order
|
||||
// configs := []Validation[Config]{
|
||||
// loadFromFile("config.json"), // Try file first
|
||||
// loadFromEnv(), // Then environment
|
||||
// loadFromRemote("api.example.com"), // Then remote API
|
||||
// }
|
||||
//
|
||||
// // Fold using the monoid to get first successful config
|
||||
// result := A.MonoidFold(m)(configs)
|
||||
// // Result: First successful config, or defaultConfig if all fail
|
||||
func AltMonoid[A any](zero Lazy[Validation[A]]) Monoid[Validation[A]] {
|
||||
return M.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[A],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,12 +74,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
t.Run("empty returns successful validation with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
|
||||
assert.True(t, either.IsRight(empty))
|
||||
value := either.MonadFold(empty,
|
||||
func(Errors) string { return "ERROR" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, Success(""), empty)
|
||||
})
|
||||
|
||||
t.Run("concat combines successful validations", func(t *testing.T) {
|
||||
@@ -88,12 +83,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "Hello World", value)
|
||||
assert.Equal(t, Success("Hello World"), result)
|
||||
})
|
||||
|
||||
t.Run("concat with failure returns failure", func(t *testing.T) {
|
||||
@@ -141,17 +131,8 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
result1 := m.Concat(v, empty)
|
||||
result2 := m.Concat(empty, v)
|
||||
|
||||
val1 := either.MonadFold(result1,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
val2 := either.MonadFold(result2,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "test", val1)
|
||||
assert.Equal(t, "test", val2)
|
||||
assert.Equal(t, Of("test"), result1)
|
||||
assert.Equal(t, Of("test"), result2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -166,11 +147,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
t.Run("empty returns zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
|
||||
value := either.MonadFold(empty,
|
||||
func(Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
assert.Equal(t, Of(0), empty)
|
||||
})
|
||||
|
||||
t.Run("concat adds values", func(t *testing.T) {
|
||||
@@ -179,11 +156,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
@@ -194,11 +167,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
|
||||
result := m.Concat(m.Concat(m.Concat(v1, v2), v3), v4)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 10, value)
|
||||
assert.Equal(t, Of(10), result)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -245,21 +214,13 @@ func TestMonoidLaws(t *testing.T) {
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// empty + a = a
|
||||
result := m.Concat(m.Empty(), v1)
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
assert.Equal(t, Of("a"), result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// a + empty = a
|
||||
result := m.Concat(v1, m.Empty())
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "a", value)
|
||||
assert.Equal(t, Of("a"), result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
@@ -268,17 +229,8 @@ func TestMonoidLaws(t *testing.T) {
|
||||
left := m.Concat(m.Concat(v1, v2), v3)
|
||||
right := m.Concat(v1, m.Concat(v2, v3))
|
||||
|
||||
leftVal := either.MonadFold(left,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
rightVal := either.MonadFold(right,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
assert.Equal(t, "abc", leftVal)
|
||||
assert.Equal(t, "abc", rightVal)
|
||||
assert.Equal(t, Of("abc"), left)
|
||||
assert.Equal(t, Of("abc"), right)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -332,11 +284,7 @@ func TestApplicativeMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, 15, value.Count)
|
||||
assert.Equal(t, Of(Counter{Count: 15}), result)
|
||||
})
|
||||
|
||||
t.Run("empty concat empty", func(t *testing.T) {
|
||||
@@ -344,10 +292,6 @@ func TestApplicativeMonoidEdgeCases(t *testing.T) {
|
||||
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "ERROR" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, Of(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
// Copyright (c) 2025 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 validation
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -9,13 +26,35 @@ import (
|
||||
|
||||
type (
|
||||
// Result represents a computation that may succeed with a value of type A or fail with an error.
|
||||
// This is an alias for result.Result[A], which is Either[error, A].
|
||||
//
|
||||
// Used for converting validation results to standard Go error handling patterns.
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Either represents a value that can be one of two types: Left (error) or Right (success).
|
||||
// This is an alias for either.Either[E, A], a disjoint union type.
|
||||
//
|
||||
// In the validation context:
|
||||
// - Left[E]: Contains error information of type E
|
||||
// - Right[A]: Contains a successfully validated value of type A
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// ContextEntry represents a single entry in the validation context path.
|
||||
// It tracks the location and type information during nested validation.
|
||||
// It tracks the location and type information during nested validation,
|
||||
// enabling precise error reporting with full path information.
|
||||
//
|
||||
// Fields:
|
||||
// - Key: The key or field name (e.g., "email", "address", "items[0]")
|
||||
// - Type: The expected type name (e.g., "string", "int", "User")
|
||||
// - Actual: The actual value being validated (for error reporting)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// entry := ContextEntry{
|
||||
// Key: "user.email",
|
||||
// Type: "string",
|
||||
// Actual: 12345,
|
||||
// }
|
||||
ContextEntry struct {
|
||||
Key string // The key or field name (for objects/maps)
|
||||
Type string // The expected type name
|
||||
@@ -23,42 +62,202 @@ type (
|
||||
}
|
||||
|
||||
// Context is a stack of ContextEntry values representing the path through
|
||||
// nested structures during validation. Used to provide detailed error messages.
|
||||
// nested structures during validation. Used to provide detailed error messages
|
||||
// that show exactly where in a nested structure a validation failure occurred.
|
||||
//
|
||||
// The context builds up as validation descends into nested structures:
|
||||
// - [] - root level
|
||||
// - [{Key: "user"}] - inside user object
|
||||
// - [{Key: "user"}, {Key: "address"}] - inside user.address
|
||||
// - [{Key: "user"}, {Key: "address"}, {Key: "zipCode"}] - at user.address.zipCode
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ctx := Context{
|
||||
// {Key: "user", Type: "User"},
|
||||
// {Key: "address", Type: "Address"},
|
||||
// {Key: "zipCode", Type: "string"},
|
||||
// }
|
||||
// // Represents path: user.address.zipCode
|
||||
Context = []ContextEntry
|
||||
|
||||
// ValidationError represents a single validation failure with context.
|
||||
// ValidationError represents a single validation failure with full context information.
|
||||
// It implements the error interface and provides detailed information about what failed,
|
||||
// where it failed, and why it failed.
|
||||
//
|
||||
// Fields:
|
||||
// - Value: The actual value that failed validation
|
||||
// - Context: The path to the value in nested structures (e.g., user.address.zipCode)
|
||||
// - Messsage: Human-readable error description
|
||||
// - Cause: Optional underlying error that caused the validation failure
|
||||
//
|
||||
// The ValidationError type implements:
|
||||
// - error interface: For standard Go error handling
|
||||
// - fmt.Formatter: For custom formatting with %v, %+v
|
||||
// - slog.LogValuer: For structured logging with slog
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// err := &ValidationError{
|
||||
// Value: "not-an-email",
|
||||
// Context: []ContextEntry{{Key: "user"}, {Key: "email"}},
|
||||
// Messsage: "invalid email format",
|
||||
// Cause: nil,
|
||||
// }
|
||||
// fmt.Printf("%v", err) // at user.email: invalid email format
|
||||
// fmt.Printf("%+v", err) // at user.email: invalid email format
|
||||
// // value: "not-an-email"
|
||||
ValidationError struct {
|
||||
Value any // The value that failed validation
|
||||
Context Context // The path to the value in nested structures
|
||||
Messsage string // Human-readable error message
|
||||
Cause error
|
||||
Cause error // Optional underlying error cause
|
||||
}
|
||||
|
||||
// Errors is a collection of validation errors.
|
||||
// This type is used to accumulate multiple validation failures,
|
||||
// allowing all errors to be reported at once rather than failing fast.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// errors := Errors{
|
||||
// &ValidationError{Value: "", Messsage: "name is required"},
|
||||
// &ValidationError{Value: "invalid", Messsage: "invalid email"},
|
||||
// &ValidationError{Value: -1, Messsage: "age must be positive"},
|
||||
// }
|
||||
Errors = []*ValidationError
|
||||
|
||||
// validationErrors wraps a collection of validation errors with an optional root cause.
|
||||
// It provides structured error information for validation failures.
|
||||
// It provides structured error information for validation failures and implements
|
||||
// the error interface for integration with standard Go error handling.
|
||||
//
|
||||
// This type is internal and created via MakeValidationErrors.
|
||||
// It implements:
|
||||
// - error interface: For standard Go error handling
|
||||
// - fmt.Formatter: For custom formatting with %v, %+v
|
||||
// - slog.LogValuer: For structured logging with slog
|
||||
//
|
||||
// Fields:
|
||||
// - errors: The collection of individual validation errors
|
||||
// - cause: Optional root cause error
|
||||
validationErrors struct {
|
||||
errors Errors
|
||||
cause error
|
||||
}
|
||||
|
||||
// Validation represents the result of a validation operation.
|
||||
// Left contains validation errors, Right contains the successfully validated value.
|
||||
// It's an Either type where:
|
||||
// - Left(Errors): Validation failed with one or more errors
|
||||
// - Right(A): Successfully validated value of type A
|
||||
//
|
||||
// This type supports applicative operations, allowing multiple validations
|
||||
// to be combined while accumulating all errors rather than failing fast.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Success case
|
||||
// valid := Success(42) // Right(42)
|
||||
//
|
||||
// // Failure case
|
||||
// invalid := Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "must be positive"},
|
||||
// }) // Left([...])
|
||||
//
|
||||
// // Combining validations (accumulates all errors)
|
||||
// result := Ap(Ap(Of(func(x int) func(y int) int {
|
||||
// return func(y int) int { return x + y }
|
||||
// }))(validateX))(validateY)
|
||||
Validation[A any] = Either[Errors, A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
// This is an alias for reader.Reader[R, A], which is func(R) A.
|
||||
//
|
||||
// In the validation context, Reader is used for context-dependent validation operations
|
||||
// where the validation logic needs access to the current validation context path.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validateWithContext := func(ctx Context) Validation[int] {
|
||||
// // Use ctx to provide detailed error messages
|
||||
// return Success(42)
|
||||
// }
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// Kleisli represents a function from A to a validated B.
|
||||
// It's a Reader that takes an input A and produces a Validation[B].
|
||||
// This is the fundamental building block for composable validation operations.
|
||||
//
|
||||
// Type: func(A) Validation[B]
|
||||
//
|
||||
// Kleisli arrows can be composed using Chain/Bind operations to build
|
||||
// complex validation pipelines from simple validation functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validatePositive := func(x int) Validation[int] {
|
||||
// if x > 0 {
|
||||
// return Success(x)
|
||||
// }
|
||||
// return Failures[int](/* error */)
|
||||
// }
|
||||
//
|
||||
// validateEven := func(x int) Validation[int] {
|
||||
// if x%2 == 0 {
|
||||
// return Success(x)
|
||||
// }
|
||||
// return Failures[int](/* error */)
|
||||
// }
|
||||
//
|
||||
// // Compose validations
|
||||
// validatePositiveEven := Chain(validateEven)(Success(42))
|
||||
Kleisli[A, B any] = Reader[A, Validation[B]]
|
||||
|
||||
// Operator represents a validation transformation that takes a validated A and produces a validated B.
|
||||
// It's a specialized Kleisli arrow for composing validation operations.
|
||||
// It's a specialized Kleisli arrow for composing validation operations where the input
|
||||
// is already a Validation[A].
|
||||
//
|
||||
// Type: func(Validation[A]) Validation[B]
|
||||
//
|
||||
// Operators are used to transform and compose validation results, enabling
|
||||
// functional composition of validation pipelines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Transform a validated int to a validated string
|
||||
// intToString := Map(func(x int) string {
|
||||
// return strconv.Itoa(x)
|
||||
// }) // Operator[int, string]
|
||||
//
|
||||
// result := intToString(Success(42)) // Success("42")
|
||||
Operator[A, B any] = Kleisli[Validation[A], B]
|
||||
|
||||
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
|
||||
// This is an alias for monoid.Monoid[A].
|
||||
//
|
||||
// In the validation context, monoids are used to combine validation results:
|
||||
// - ApplicativeMonoid: Combines successful validations using the monoid operation
|
||||
// - AlternativeMonoid: Provides fallback behavior for failed validations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// intAdd := N.MonoidSum[int]()
|
||||
// m := ApplicativeMonoid(intAdd)
|
||||
// result := m.Concat(Success(5), Success(3)) // Success(8)
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
|
||||
// Endomorphism represents a function from a type to itself: func(A) A.
|
||||
// This is an alias for endomorphism.Endomorphism[A].
|
||||
//
|
||||
// In the validation context, endomorphisms are used with LetL to transform
|
||||
// values within a validation context using pure functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := func(x int) int { return x * 2 } // Endomorphism[int]
|
||||
// result := LetL(lens, double)(Success(21)) // Success(42)
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
)
|
||||
|
||||
@@ -186,12 +186,7 @@ func TestSuccess(t *testing.T) {
|
||||
t.Run("creates right either with value", func(t *testing.T) {
|
||||
result := Success(42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
@@ -327,7 +322,7 @@ func TestValidationIntegration(t *testing.T) {
|
||||
&ValidationError{Value: "bad", Messsage: "error"},
|
||||
})
|
||||
|
||||
assert.True(t, either.IsRight(success))
|
||||
assert.Equal(t, Success(42), success)
|
||||
assert.True(t, either.IsLeft(failure))
|
||||
})
|
||||
|
||||
@@ -512,12 +507,7 @@ func TestToResult(t *testing.T) {
|
||||
|
||||
result := ToResult(validation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(error) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, either.Of[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("converts failed validation to result with error", func(t *testing.T) {
|
||||
@@ -569,23 +559,18 @@ func TestToResult(t *testing.T) {
|
||||
// String type
|
||||
strValidation := Success("hello")
|
||||
strResult := ToResult(strValidation)
|
||||
assert.True(t, either.IsRight(strResult))
|
||||
assert.Equal(t, either.Of[error]("hello"), strResult)
|
||||
|
||||
// Bool type
|
||||
boolValidation := Success(true)
|
||||
boolResult := ToResult(boolValidation)
|
||||
assert.True(t, either.IsRight(boolResult))
|
||||
assert.Equal(t, either.Of[error](true), boolResult)
|
||||
|
||||
// Struct type
|
||||
type User struct{ Name string }
|
||||
userValidation := Success(User{Name: "Alice"})
|
||||
userResult := ToResult(userValidation)
|
||||
assert.True(t, either.IsRight(userResult))
|
||||
user := either.MonadFold(userResult,
|
||||
func(error) User { return User{} },
|
||||
F.Identity[User],
|
||||
)
|
||||
assert.Equal(t, "Alice", user.Name)
|
||||
assert.Equal(t, either.Of[error](User{Name: "Alice"}), userResult)
|
||||
})
|
||||
|
||||
t.Run("preserves error context in result", func(t *testing.T) {
|
||||
|
||||
@@ -302,7 +302,7 @@ func SetOption[S, A any](a A) func(Optional[S, A]) O.Kleisli[S, S] {
|
||||
return ModifyOption[S](F.Constant1[A](a))
|
||||
}
|
||||
|
||||
func ichain[S, A, B any](sa Optional[S, A], ab func(A) O.Option[B], ba func(B) O.Option[A]) Optional[S, B] {
|
||||
func ichain[S, A, B any](sa Optional[S, A], ab O.Kleisli[A, B], ba O.Kleisli[B, A]) Optional[S, B] {
|
||||
return MakeOptional(
|
||||
F.Flow2(sa.GetOption, O.Chain(ab)),
|
||||
func(s S, b B) S {
|
||||
@@ -312,7 +312,7 @@ func ichain[S, A, B any](sa Optional[S, A], ab func(A) O.Option[B], ba func(B) O
|
||||
}
|
||||
|
||||
// IChain implements a bidirectional mapping of the transform if the transform can produce optionals (e.g. in case of type mappings)
|
||||
func IChain[S, A, B any](ab func(A) O.Option[B], ba func(B) O.Option[A]) Operator[S, A, B] {
|
||||
func IChain[S, A, B any](ab O.Kleisli[A, B], ba O.Kleisli[B, A]) Operator[S, A, B] {
|
||||
return func(sa Optional[S, A]) Optional[S, B] {
|
||||
return ichain(sa, ab, ba)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,96 @@ func Curry1[R, T1, A any](f func(R, T1) A) Kleisli[R, T1, A] {
|
||||
return G.Curry1[Reader[R, A]](f)
|
||||
}
|
||||
|
||||
// Curry is an alias for Curry1, converting a function with context as first parameter
|
||||
// into a curried function returning a Reader.
|
||||
//
|
||||
// # Currying Direction
|
||||
//
|
||||
// The Curry functions in this package follow a specific direction that bridges Go conventions
|
||||
// with functional programming conventions:
|
||||
//
|
||||
// **Input (Go Convention)**: Functions with context as the FIRST parameter
|
||||
// - func(Context, T1, T2, ...) Result
|
||||
// - This follows Go's standard practice (https://pkg.go.dev/context)
|
||||
//
|
||||
// **Output (FP Convention)**: Curried functions with context as the LAST parameter (Reader position)
|
||||
// - func(T1) func(T2) ... Reader[Context, Result]
|
||||
// - This follows the Reader monad convention where context is the final parameter
|
||||
//
|
||||
// # Transformation Process
|
||||
//
|
||||
// The currying process transforms parameters in this order:
|
||||
//
|
||||
// 1. Original function: func(Context, T1, T2, T3) Result
|
||||
// 2. After Curry3: func(T1) func(T2) func(T3) Reader[Context, Result]
|
||||
// 3. When applied: T1 -> T2 -> T3 -> (Context -> Result)
|
||||
//
|
||||
// The context parameter moves from FIRST position to LAST position (inside the Reader).
|
||||
//
|
||||
// # Why This Direction?
|
||||
//
|
||||
// This direction allows you to:
|
||||
// - Write functions following Go's context-first convention
|
||||
// - Use them in functional pipelines where context is provided at the end
|
||||
// - Compose functions before providing the context
|
||||
// - Delay context injection until the final execution
|
||||
//
|
||||
// # Example - Direction Visualization
|
||||
//
|
||||
// // Original Go-style function (context first)
|
||||
// func processData(ctx Context, id int, name string) string {
|
||||
// return fmt.Sprintf("%s: %s-%d", ctx.Prefix, name, id)
|
||||
// }
|
||||
//
|
||||
// // After currying (context last, inside Reader)
|
||||
// curried := reader.Curry2(processData)
|
||||
// // Type: func(int) func(string) Reader[Context, string]
|
||||
//
|
||||
// // Apply parameters left-to-right
|
||||
// step1 := curried(42) // Provide id
|
||||
// step2 := step1("example") // Provide name
|
||||
// // step2 is now: Reader[Context, string]
|
||||
//
|
||||
// // Finally provide context (last)
|
||||
// result := step2(Context{Prefix: "Item"})
|
||||
// // Result: "Item: example-42"
|
||||
//
|
||||
// # Comparison with Standard Currying
|
||||
//
|
||||
// Standard currying (left-to-right):
|
||||
// - func(A, B, C) R → func(A) func(B) func(C) R
|
||||
// - Parameters stay in the same order
|
||||
//
|
||||
// Reader currying (context moves to end):
|
||||
// - func(Context, A, B) R → func(A) func(B) Reader[Context, R]
|
||||
// - Context moves from first to last position
|
||||
// - This is sometimes called "flipping" or "context rotation"
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// 1. **Dependency Injection**: Provide dependencies (context) at the end
|
||||
// 2. **Configuration**: Build operations first, configure later
|
||||
// 3. **Testing**: Create testable functions that receive mocked context last
|
||||
// 4. **Composition**: Compose operations before providing shared context
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - Curry0-Curry4: Convert functions with 0-4 additional parameters
|
||||
// - Uncurry0-Uncurry4: Reverse the transformation (Reader back to Go-style)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Prefix string }
|
||||
// addPrefix := func(c Config, s string) string { return c.Prefix + s }
|
||||
// curried := reader.Curry(addPrefix)
|
||||
// r := curried("hello")
|
||||
// result := r(Config{Prefix: ">> "}) // ">> hello"
|
||||
//
|
||||
//go:inline
|
||||
func Curry[R, T1, A any](f func(R, T1) A) Kleisli[R, T1, A] {
|
||||
return Curry1(f)
|
||||
}
|
||||
|
||||
// Curry2 converts a function with context as first parameter and 2 other parameters
|
||||
// into a curried function returning a Reader.
|
||||
//
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/readerioeither/bind.go:26.27,28.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/bind.go:34.26,36.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/bind.go:42.26,44.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/bind.go:50.26,52.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/bind.go:57.25,59.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/bind.go:65.26,67.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/bracket.go:31.27,33.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/eq.go:25.92,27.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/eq.go:30.88,32.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:14.92,16.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:20.90,22.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:26.92,28.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:32.102,34.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:38.100,40.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:44.102,46.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:50.114,52.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:56.112,58.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:62.114,64.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:68.126,70.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:74.124,76.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:80.126,82.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:86.138,88.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:92.136,94.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:98.138,100.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:104.150,106.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:110.148,112.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:116.150,118.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:122.162,124.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:128.160,130.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:134.162,136.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:140.174,142.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:146.172,148.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:152.174,154.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:158.186,160.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:164.184,166.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:170.186,172.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:176.198,178.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:182.196,184.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:188.198,190.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:194.211,196.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:200.209,202.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/gen.go:206.211,208.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/monad.go:26.73,28.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/monad.go:31.104,33.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/monad.go:36.131,38.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:40.92,46.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:50.90,52.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:55.76,60.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:63.75,68.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:73.96,75.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:79.60,81.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:85.90,87.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:91.54,93.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:98.120,104.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:108.125,114.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:118.123,125.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:129.87,135.2 1 0
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:139.128,147.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:151.92,158.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:162.116,169.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:173.80,179.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:183.124,190.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:194.88,200.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:204.108,211.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:215.72,221.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:225.113,233.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:237.77,244.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:248.99,254.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:258.119,265.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:268.122,275.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:278.122,285.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:289.119,291.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:295.84,300.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:304.89,309.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:312.54,314.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:317.53,319.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:323.59,325.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:329.51,331.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:335.102,337.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:341.77,343.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:346.72,348.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:351.71,353.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:357.71,359.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:362.64,364.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:367.63,369.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:373.63,375.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:379.79,381.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:385.89,387.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:391.46,393.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:397.64,399.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:403.89,405.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:409.103,411.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:415.135,417.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:421.105,423.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:427.129,429.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:433.120,440.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:444.120,449.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:453.117,455.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:459.77,461.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:465.86,467.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:471.104,472.34 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:472.34,474.3 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:479.123,487.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:491.84,498.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:503.68,505.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:509.98,511.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:515.94,517.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:520.106,522.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:526.103,528.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/reader.go:533.101,535.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/resource.go:21.181,22.73 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/resource.go:22.73,23.44 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/resource.go:23.44,27.41 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/resource.go:27.41,29.6 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/resource.go:30.40,32.5 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/sequence.go:25.91,30.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/sequence.go:32.124,38.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/sequence.go:40.157,47.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/sequence.go:49.190,57.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/sync.go:26.81,28.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/traverse.go:23.107,25.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/traverse.go:28.121,30.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/traverse.go:33.89,35.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/traverse.go:38.134,40.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/traverse.go:43.146,45.2 1 1
|
||||
github.com/IBM/fp-go/v2/readerioeither/traverse.go:48.116,50.2 1 1
|
||||
@@ -1104,6 +1104,8 @@ func After[R, E, A any](timestamp time.Time) Operator[R, E, A, A] {
|
||||
// If the ReaderIOEither is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderIOEither that replaces the original.
|
||||
//
|
||||
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative IO computations
|
||||
// that need access to configuration or dependencies. The error type can be widened from E1 to E2.
|
||||
//
|
||||
|
||||
@@ -531,3 +531,205 @@ func TestReadIO(t *testing.T) {
|
||||
assert.Equal(t, E.Right[error](25), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainLeftIdenticalToOrElse proves that ChainLeft and OrElse are identical functions.
|
||||
// This test verifies that both functions produce the same results for all scenarios with reader context:
|
||||
// - Left values with error recovery using reader context
|
||||
// - Left values with error transformation
|
||||
// - Right values passing through unchanged
|
||||
// - Error type widening
|
||||
func TestChainLeftIdenticalToOrElse(t *testing.T) {
|
||||
type Config struct {
|
||||
fallbackValue int
|
||||
retryEnabled bool
|
||||
}
|
||||
|
||||
// Test 1: Left value with error recovery using reader context
|
||||
t.Run("Left value recovery with reader - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
recoveryFn := func(e string) ReaderIOEither[Config, string, int] {
|
||||
if e == "recoverable" {
|
||||
return func(cfg Config) IOE.IOEither[string, int] {
|
||||
return IOE.Right[string](cfg.fallbackValue)
|
||||
}
|
||||
}
|
||||
return Left[Config, int](e)
|
||||
}
|
||||
|
||||
input := Left[Config, int]("recoverable")
|
||||
cfg := Config{fallbackValue: 42, retryEnabled: true}
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(recoveryFn)(input)(cfg)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(recoveryFn)(input)(cfg)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](42), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 2: Left value with error transformation
|
||||
t.Run("Left value transformation - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
transformFn := func(e string) ReaderIOEither[Config, string, int] {
|
||||
return Left[Config, int]("transformed: " + e)
|
||||
}
|
||||
|
||||
input := Left[Config, int]("original error")
|
||||
cfg := Config{fallbackValue: 0, retryEnabled: false}
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(transformFn)(input)(cfg)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(transformFn)(input)(cfg)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Left[int]("transformed: original error"), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 3: Right value - both should pass through unchanged
|
||||
t.Run("Right value passthrough - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
handlerFn := func(e string) ReaderIOEither[Config, string, int] {
|
||||
return Left[Config, int]("should not be called")
|
||||
}
|
||||
|
||||
input := Right[Config, string](100)
|
||||
cfg := Config{fallbackValue: 0, retryEnabled: false}
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(handlerFn)(input)(cfg)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(handlerFn)(input)(cfg)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](100), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 4: Error type widening
|
||||
t.Run("Error type widening - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
widenFn := func(e string) ReaderIOEither[Config, int, int] {
|
||||
return Left[Config, int](404)
|
||||
}
|
||||
|
||||
input := Left[Config, int]("not found")
|
||||
cfg := Config{fallbackValue: 0, retryEnabled: false}
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(widenFn)(input)(cfg)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(widenFn)(input)(cfg)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Left[int](404), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 5: Composition in pipeline with reader context
|
||||
t.Run("Pipeline composition with reader - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
recoveryFn := func(e string) ReaderIOEither[Config, string, int] {
|
||||
if e == "network error" {
|
||||
return func(cfg Config) IOE.IOEither[string, int] {
|
||||
if cfg.retryEnabled {
|
||||
return IOE.Right[string](cfg.fallbackValue)
|
||||
}
|
||||
return IOE.Left[int]("retry disabled")
|
||||
}
|
||||
}
|
||||
return Left[Config, int](e)
|
||||
}
|
||||
|
||||
input := Left[Config, int]("network error")
|
||||
cfg := Config{fallbackValue: 99, retryEnabled: true}
|
||||
|
||||
// Using ChainLeft in pipeline
|
||||
resultChainLeft := F.Pipe1(input, ChainLeft(recoveryFn))(cfg)()
|
||||
|
||||
// Using OrElse in pipeline
|
||||
resultOrElse := F.Pipe1(input, OrElse(recoveryFn))(cfg)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](99), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 6: Multiple chained operations with reader context
|
||||
t.Run("Multiple operations with reader - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
handler1 := func(e string) ReaderIOEither[Config, string, int] {
|
||||
if e == "error1" {
|
||||
return Right[Config, string](1)
|
||||
}
|
||||
return Left[Config, int](e)
|
||||
}
|
||||
|
||||
handler2 := func(e string) ReaderIOEither[Config, string, int] {
|
||||
if e == "error2" {
|
||||
return func(cfg Config) IOE.IOEither[string, int] {
|
||||
return IOE.Right[string](cfg.fallbackValue)
|
||||
}
|
||||
}
|
||||
return Left[Config, int](e)
|
||||
}
|
||||
|
||||
input := Left[Config, int]("error2")
|
||||
cfg := Config{fallbackValue: 2, retryEnabled: false}
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := F.Pipe2(
|
||||
input,
|
||||
ChainLeft(handler1),
|
||||
ChainLeft(handler2),
|
||||
)(cfg)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := F.Pipe2(
|
||||
input,
|
||||
OrElse(handler1),
|
||||
OrElse(handler2),
|
||||
)(cfg)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](2), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 7: Reader context is properly threaded through both functions
|
||||
t.Run("Reader context threading - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
var chainLeftCfg, orElseCfg *Config
|
||||
|
||||
recoveryFn := func(e string) ReaderIOEither[Config, string, int] {
|
||||
return func(cfg Config) IOE.IOEither[string, int] {
|
||||
// Capture the config to verify it's passed correctly
|
||||
if chainLeftCfg == nil {
|
||||
chainLeftCfg = &cfg
|
||||
} else {
|
||||
orElseCfg = &cfg
|
||||
}
|
||||
return IOE.Right[string](cfg.fallbackValue)
|
||||
}
|
||||
}
|
||||
|
||||
input := Left[Config, int]("error")
|
||||
cfg := Config{fallbackValue: 123, retryEnabled: true}
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(recoveryFn)(input)(cfg)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(recoveryFn)(input)(cfg)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](123), resultChainLeft)
|
||||
|
||||
// Verify both received the same config
|
||||
assert.NotNil(t, chainLeftCfg)
|
||||
assert.NotNil(t, orElseCfg)
|
||||
assert.Equal(t, *chainLeftCfg, *orElseCfg)
|
||||
assert.Equal(t, cfg, *chainLeftCfg)
|
||||
})
|
||||
}
|
||||
|
||||
72
v2/readeriooption/array.go
Normal file
72
v2/readeriooption/array.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
RA "github.com/IBM/fp-go/v2/internal/array"
|
||||
)
|
||||
|
||||
// TraverseArray transforms an array by applying a function that returns a ReaderIOOption to each element.
|
||||
// If any element results in None, the entire result is None.
|
||||
// Otherwise, returns Some containing an array of all the unwrapped values.
|
||||
//
|
||||
// This is useful for performing a sequence of operations that may fail on each element of an array,
|
||||
// where you want all operations to succeed or the entire computation to fail.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type DB struct { ... }
|
||||
//
|
||||
// findUser := func(id int) readeroption.ReaderIOOption[DB, User] { ... }
|
||||
//
|
||||
// userIDs := []int{1, 2, 3}
|
||||
// result := F.Pipe1(
|
||||
// readeroption.Of[DB](userIDs),
|
||||
// readeroption.Chain(readeroption.TraverseArray[DB](findUser)),
|
||||
// )
|
||||
// // result will be Some([]User) if all users are found, None otherwise
|
||||
func TraverseArray[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, []A, []B] {
|
||||
return RA.Traverse[[]A, []B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex is like TraverseArray but the function also receives the index of each element.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type DB struct { ... }
|
||||
//
|
||||
// processWithIndex := func(idx int, value string) readeroption.ReaderIOOption[DB, Result] {
|
||||
// // Use idx in processing
|
||||
// return readeroption.Asks(func(db DB) option.Option[Result] { ... })
|
||||
// }
|
||||
//
|
||||
// values := []string{"a", "b", "c"}
|
||||
// result := readeroption.TraverseArrayWithIndex[DB](processWithIndex)(values)
|
||||
func TraverseArrayWithIndex[E, A, B any](f func(int, A) ReaderIOOption[E, B]) func([]A) ReaderIOOption[E, []B] {
|
||||
return RA.TraverseWithIndex[[]A, []B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
258
v2/readeriooption/array_test.go
Normal file
258
v2/readeriooption/array_test.go
Normal file
@@ -0,0 +1,258 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTraverseArray_AllSuccess(t *testing.T) {
|
||||
// Test traversing an array where all operations succeed
|
||||
double := func(x int) ReaderIOOption[context.Context, int] {
|
||||
return Of[context.Context](x * 2)
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := TraverseArray[context.Context](double)(input)
|
||||
|
||||
expected := O.Of([]int{2, 4, 6, 8, 10})
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestTraverseArray_OneFailure(t *testing.T) {
|
||||
// Test traversing an array where one operation fails
|
||||
failOnThree := func(x int) ReaderIOOption[context.Context, int] {
|
||||
if x == 3 {
|
||||
return None[context.Context, int]()
|
||||
}
|
||||
return Of[context.Context](x * 2)
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := TraverseArray[context.Context](failOnThree)(input)
|
||||
|
||||
expected := O.None[[]int]()
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestTraverseArray_EmptyArray(t *testing.T) {
|
||||
// Test traversing an empty array
|
||||
double := func(x int) ReaderIOOption[context.Context, int] {
|
||||
return Of[context.Context](x * 2)
|
||||
}
|
||||
|
||||
input := []int{}
|
||||
result := TraverseArray[context.Context](double)(input)
|
||||
|
||||
expected := O.Of([]int{})
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestTraverseArray_WithEnvironment(t *testing.T) {
|
||||
// Test that the environment is properly passed through
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
multiply := func(x int) ReaderIOOption[Config, int] {
|
||||
return func(cfg Config) IOOption[int] {
|
||||
return func() Option[int] {
|
||||
return O.Of(x * cfg.Multiplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3}
|
||||
result := TraverseArray[Config](multiply)(input)
|
||||
|
||||
cfg := Config{Multiplier: 10}
|
||||
expected := O.Of([]int{10, 20, 30})
|
||||
assert.Equal(t, expected, result(cfg)())
|
||||
}
|
||||
|
||||
func TestTraverseArray_ChainedOperation(t *testing.T) {
|
||||
// Test traversing as part of a chain
|
||||
type Config struct {
|
||||
Factor int
|
||||
}
|
||||
|
||||
multiplyByFactor := func(x int) ReaderIOOption[Config, int] {
|
||||
return func(cfg Config) IOOption[int] {
|
||||
return func() Option[int] {
|
||||
return O.Of(x * cfg.Factor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Of[Config]([]int{1, 2, 3, 4}),
|
||||
Chain(TraverseArray[Config](multiplyByFactor)),
|
||||
)
|
||||
|
||||
cfg := Config{Factor: 5}
|
||||
expected := O.Of([]int{5, 10, 15, 20})
|
||||
assert.Equal(t, expected, result(cfg)())
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_AllSuccess(t *testing.T) {
|
||||
// Test traversing with index where all operations succeed
|
||||
addIndex := func(idx int, x string) ReaderIOOption[context.Context, string] {
|
||||
return Of[context.Context](fmt.Sprintf("%d:%s", idx, x))
|
||||
}
|
||||
|
||||
input := []string{"a", "b", "c"}
|
||||
result := TraverseArrayWithIndex[context.Context](addIndex)(input)
|
||||
|
||||
expected := O.Of([]string{"0:a", "1:b", "2:c"})
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_OneFailure(t *testing.T) {
|
||||
// Test traversing with index where one operation fails
|
||||
failOnIndex := func(idx int, x string) ReaderIOOption[context.Context, string] {
|
||||
if idx == 1 {
|
||||
return None[context.Context, string]()
|
||||
}
|
||||
return Of[context.Context](fmt.Sprintf("%d:%s", idx, x))
|
||||
}
|
||||
|
||||
input := []string{"a", "b", "c"}
|
||||
result := TraverseArrayWithIndex[context.Context](failOnIndex)(input)
|
||||
|
||||
expected := O.None[[]string]()
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_EmptyArray(t *testing.T) {
|
||||
// Test traversing an empty array with index
|
||||
addIndex := func(idx int, x string) ReaderIOOption[context.Context, string] {
|
||||
return Of[context.Context](fmt.Sprintf("%d:%s", idx, x))
|
||||
}
|
||||
|
||||
input := []string{}
|
||||
result := TraverseArrayWithIndex[context.Context](addIndex)(input)
|
||||
|
||||
expected := O.Of([]string{})
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_WithEnvironment(t *testing.T) {
|
||||
// Test that environment is properly passed with index
|
||||
type Config struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
formatWithIndex := func(idx int, x string) ReaderIOOption[Config, string] {
|
||||
return func(cfg Config) IOOption[string] {
|
||||
return func() Option[string] {
|
||||
return O.Of(fmt.Sprintf("%s%d:%s", cfg.Prefix, idx, x))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input := []string{"a", "b", "c"}
|
||||
result := TraverseArrayWithIndex[Config](formatWithIndex)(input)
|
||||
|
||||
cfg := Config{Prefix: "item-"}
|
||||
expected := O.Of([]string{"item-0:a", "item-1:b", "item-2:c"})
|
||||
assert.Equal(t, expected, result(cfg)())
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_IndexUsedInLogic(t *testing.T) {
|
||||
// Test using index in computation logic
|
||||
multiplyByIndex := func(idx int, x int) ReaderIOOption[context.Context, int] {
|
||||
return Of[context.Context](x * idx)
|
||||
}
|
||||
|
||||
input := []int{10, 20, 30, 40}
|
||||
result := TraverseArrayWithIndex[context.Context](multiplyByIndex)(input)
|
||||
|
||||
// 10*0=0, 20*1=20, 30*2=60, 40*3=120
|
||||
expected := O.Of([]int{0, 20, 60, 120})
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestTraverseArray_ComplexType(t *testing.T) {
|
||||
// Test traversing with complex types
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
type UserProfile struct {
|
||||
UserID int
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
loadProfile := func(user User) ReaderIOOption[context.Context, UserProfile] {
|
||||
return Of[context.Context](UserProfile{
|
||||
UserID: user.ID,
|
||||
DisplayName: "Profile: " + user.Name,
|
||||
})
|
||||
}
|
||||
|
||||
users := []User{
|
||||
{ID: 1, Name: "Alice"},
|
||||
{ID: 2, Name: "Bob"},
|
||||
{ID: 3, Name: "Charlie"},
|
||||
}
|
||||
|
||||
result := TraverseArray[context.Context](loadProfile)(users)
|
||||
|
||||
expected := O.Of([]UserProfile{
|
||||
{UserID: 1, DisplayName: "Profile: Alice"},
|
||||
{UserID: 2, DisplayName: "Profile: Bob"},
|
||||
{UserID: 3, DisplayName: "Profile: Charlie"},
|
||||
})
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestTraverseArray_ConditionalFailure(t *testing.T) {
|
||||
// Test conditional failure based on environment
|
||||
type Config struct {
|
||||
MaxValue int
|
||||
}
|
||||
|
||||
validateAndDouble := func(x int) ReaderIOOption[Config, int] {
|
||||
return func(cfg Config) IOOption[int] {
|
||||
return func() Option[int] {
|
||||
if x > cfg.MaxValue {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Of(x * 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
|
||||
// With MaxValue=3, should fail on 4 and 5
|
||||
cfg1 := Config{MaxValue: 3}
|
||||
result1 := TraverseArray[Config](validateAndDouble)(input)
|
||||
assert.Equal(t, O.None[[]int](), result1(cfg1)())
|
||||
|
||||
// With MaxValue=10, all should succeed
|
||||
cfg2 := Config{MaxValue: 10}
|
||||
result2 := TraverseArray[Config](validateAndDouble)(input)
|
||||
expected := O.Of([]int{2, 4, 6, 8, 10})
|
||||
assert.Equal(t, expected, result2(cfg2)())
|
||||
}
|
||||
326
v2/readeriooption/bind.go
Normal file
326
v2/readeriooption/bind.go
Normal file
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
// result := readereither.Do[Env, error](State{})
|
||||
func Do[R, S any](
|
||||
empty S,
|
||||
) ReaderIOOption[R, S] {
|
||||
return Of[R](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access the shared environment.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderIOOption[Env, error, User] {
|
||||
// return readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// readereither.Bind(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderIOOption[Env, error, Config] {
|
||||
// // This can access s.User from the previous step
|
||||
// return readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfigForUser(s.User.ID)
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[R, S1, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return chain.Bind(
|
||||
Chain,
|
||||
Map,
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
func Let[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Operator[R, S1, S2] {
|
||||
return functor.Let(
|
||||
Map[R, S1, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
func LetTo[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) Operator[R, S1, S2] {
|
||||
return functor.LetTo(
|
||||
Map[R, S1, S2],
|
||||
setter,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[R, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[R, T, S1] {
|
||||
return chain.BindTo(
|
||||
Map[R, T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUser := readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfig()
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.ApS(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// readereither.ApS(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// getConfig,
|
||||
// ),
|
||||
// )
|
||||
func ApS[R, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderIOOption[R, T],
|
||||
) Operator[R, S1, S2] {
|
||||
return apply.ApS(
|
||||
Ap[S2],
|
||||
Map,
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
|
||||
// return env.ConfigService.GetConfig()
|
||||
// })
|
||||
// result := F.Pipe2(
|
||||
// readereither.Of[Env, error](State{}),
|
||||
// readereither.ApSL(configLens, getConfig),
|
||||
// )
|
||||
func ApSL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderIOOption[R, T],
|
||||
) Operator[R, S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a ReaderIOOption computation that produces an updated value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// type Env struct {
|
||||
// UserService UserService
|
||||
// ConfigService ConfigService
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[Env, error](State{}),
|
||||
// readereither.BindL(userLens, func(user User) readereither.ReaderIOOption[Env, error, User] {
|
||||
// return readereither.Asks(func(env Env) either.Either[error, User] {
|
||||
// return env.UserService.GetUser()
|
||||
// })
|
||||
// }),
|
||||
// )
|
||||
func BindL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[R, T, T],
|
||||
) Operator[R, S, S] {
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new value (without wrapping in a ReaderIOOption).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[any, error](State{Config: Config{Host: "localhost"}}),
|
||||
// readereither.LetL(configLens, func(cfg Config) Config {
|
||||
// cfg.Port = 8080
|
||||
// return cfg
|
||||
// }),
|
||||
// )
|
||||
func LetL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[R, S, S] {
|
||||
return Let[R](lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The value b is set directly to the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// configLens := lens.MakeLens(
|
||||
// func(s State) Config { return s.Config },
|
||||
// func(s State, c Config) State { s.Config = c; return s },
|
||||
// )
|
||||
//
|
||||
// newConfig := Config{Host: "localhost", Port: 8080}
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do[any, error](State{}),
|
||||
// readereither.LetToL(configLens, newConfig),
|
||||
// )
|
||||
func LetToL[R, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[R, S, S] {
|
||||
return LetTo[R](lens.Set, b)
|
||||
}
|
||||
99
v2/readeriooption/bind_test.go
Normal file
99
v2/readeriooption/bind_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getLastName(s utils.Initial) ReaderIOOption[context.Context, string] {
|
||||
return Of[context.Context]("Doe")
|
||||
}
|
||||
|
||||
func getGivenName(s utils.WithLastName) ReaderIOOption[context.Context, string] {
|
||||
return Of[context.Context]("John")
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[context.Context](utils.Empty),
|
||||
Bind(utils.SetLastName, getLastName),
|
||||
Bind(utils.SetGivenName, getGivenName),
|
||||
Map[context.Context](utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of("John Doe"), res(context.Background())())
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
|
||||
res := F.Pipe3(
|
||||
Do[context.Context](utils.Empty),
|
||||
ApS(utils.SetLastName, Of[context.Context]("Doe")),
|
||||
ApS(utils.SetGivenName, Of[context.Context]("John")),
|
||||
Map[context.Context](utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of("John Doe"), res(context.Background())())
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
res := F.Pipe3(
|
||||
Do[context.Context](utils.Empty),
|
||||
Let[context.Context](utils.SetLastName, func(s utils.Initial) string {
|
||||
return "Doe"
|
||||
}),
|
||||
Let[context.Context](utils.SetGivenName, func(s utils.WithLastName) string {
|
||||
return "John"
|
||||
}),
|
||||
Map[context.Context](utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of("John Doe"), res(context.Background())())
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
res := F.Pipe3(
|
||||
Do[context.Context](utils.Empty),
|
||||
LetTo[context.Context](utils.SetLastName, "Doe"),
|
||||
LetTo[context.Context](utils.SetGivenName, "John"),
|
||||
Map[context.Context](utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of("John Doe"), res(context.Background())())
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
res := F.Pipe1(
|
||||
Of[context.Context](42),
|
||||
BindTo[context.Context](func(v int) State {
|
||||
return State{Value: v}
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of(State{Value: 42}), res(context.Background())())
|
||||
}
|
||||
243
v2/readeriooption/doc.go
Normal file
243
v2/readeriooption/doc.go
Normal file
@@ -0,0 +1,243 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption provides a monad transformer that combines the Reader, IO, and Option monads.
|
||||
//
|
||||
// # Overview
|
||||
//
|
||||
// ReaderIOOption[R, A] represents a computation that:
|
||||
// - Depends on a shared environment of type R (Reader monad)
|
||||
// - Performs side effects (IO monad)
|
||||
// - May fail to produce a value of type A (Option monad)
|
||||
//
|
||||
// This is particularly useful for computations that need:
|
||||
// - Dependency injection or configuration access
|
||||
// - Side effects like I/O operations
|
||||
// - Optional results without using error types
|
||||
//
|
||||
// The ReaderIOOption monad is defined as: Reader[R, IOOption[A]]
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This package implements the following Fantasy Land algebras:
|
||||
// - Functor: https://github.com/fantasyland/fantasy-land#functor
|
||||
// - Apply: https://github.com/fantasyland/fantasy-land#apply
|
||||
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
|
||||
// - Chain: https://github.com/fantasyland/fantasy-land#chain
|
||||
// - Monad: https://github.com/fantasyland/fantasy-land#monad
|
||||
// - Alt: https://github.com/fantasyland/fantasy-land#alt
|
||||
// - Profunctor: https://github.com/fantasyland/fantasy-land#profunctor
|
||||
//
|
||||
// # Core Operations
|
||||
//
|
||||
// Creating ReaderIOOption values:
|
||||
// - Of/Some: Wraps a value in a successful ReaderIOOption
|
||||
// - None: Creates a ReaderIOOption representing no value
|
||||
// - FromOption: Lifts an Option into ReaderIOOption
|
||||
// - FromReader: Lifts a Reader into ReaderIOOption
|
||||
// - Ask/Asks: Accesses the environment
|
||||
//
|
||||
// Transforming values:
|
||||
// - Map: Transforms the value inside a ReaderIOOption
|
||||
// - Chain: Sequences ReaderIOOption computations
|
||||
// - Ap: Applies a function wrapped in ReaderIOOption
|
||||
// - Alt: Provides alternative computation on failure
|
||||
//
|
||||
// Extracting values:
|
||||
// - Fold: Extracts value by providing handlers for both cases
|
||||
// - GetOrElse: Returns value or default
|
||||
// - Read: Executes the computation with an environment
|
||||
//
|
||||
// # Basic Example
|
||||
//
|
||||
// type Config struct {
|
||||
// DatabaseURL string
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// // A computation that may or may not find a user
|
||||
// func findUser(id int) readeriooption.ReaderIOOption[Config, User] {
|
||||
// return readeriooption.Asks(func(cfg Config) iooption.IOOption[User] {
|
||||
// return func() option.Option[User] {
|
||||
// // Use cfg.DatabaseURL to query database
|
||||
// // Return Some(user) if found, None() if not found
|
||||
// user, found := queryDB(cfg.DatabaseURL, id)
|
||||
// if found {
|
||||
// return option.Some(user)
|
||||
// }
|
||||
// return option.None[User]()
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Chain multiple operations
|
||||
// result := F.Pipe2(
|
||||
// findUser(123),
|
||||
// readeriooption.Chain(func(user User) readeriooption.ReaderIOOption[Config, Profile] {
|
||||
// return loadProfile(user.ProfileID)
|
||||
// }),
|
||||
// readeriooption.Map(func(profile Profile) string {
|
||||
// return profile.DisplayName
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// // Execute with config
|
||||
// config := Config{DatabaseURL: "localhost:5432", Timeout: 30}
|
||||
// displayName := result(config)() // Returns Option[string]
|
||||
//
|
||||
// # Do-Notation Style
|
||||
//
|
||||
// The package supports do-notation style composition for building complex computations:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Profile Profile
|
||||
// Posts []Post
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe3(
|
||||
// readeriooption.Do[Config](State{}),
|
||||
// readeriooption.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readeriooption.ReaderIOOption[Config, User] {
|
||||
// return findUser(123)
|
||||
// },
|
||||
// ),
|
||||
// readeriooption.Bind(
|
||||
// func(profile Profile) func(State) State {
|
||||
// return func(s State) State { s.Profile = profile; return s }
|
||||
// },
|
||||
// func(s State) readeriooption.ReaderIOOption[Config, Profile] {
|
||||
// return loadProfile(s.User.ProfileID)
|
||||
// },
|
||||
// ),
|
||||
// readeriooption.Bind(
|
||||
// func(posts []Post) func(State) State {
|
||||
// return func(s State) State { s.Posts = posts; return s }
|
||||
// },
|
||||
// func(s State) readeriooption.ReaderIOOption[Config, []Post] {
|
||||
// return loadPosts(s.User.ID)
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// # Alternative Computations
|
||||
//
|
||||
// Use Alt to provide fallback behavior when computations fail:
|
||||
//
|
||||
// // Try cache first, fall back to database
|
||||
// result := F.Pipe1(
|
||||
// findUserInCache(123),
|
||||
// readeriooption.Alt(func() readeriooption.ReaderIOOption[Config, User] {
|
||||
// return findUserInDB(123)
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// # Array Operations
|
||||
//
|
||||
// Transform arrays where each element may fail:
|
||||
//
|
||||
// userIDs := []int{1, 2, 3, 4, 5}
|
||||
// users := F.Pipe1(
|
||||
// readeriooption.Of[Config](userIDs),
|
||||
// readeriooption.Chain(readeriooption.TraverseArray[Config](findUser)),
|
||||
// )
|
||||
// // Returns Some([]User) if all users found, None otherwise
|
||||
//
|
||||
// # Monoid Operations
|
||||
//
|
||||
// Combine multiple ReaderIOOption computations:
|
||||
//
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// // Applicative monoid - all must succeed
|
||||
// intAdd := N.MonoidSum[int]()
|
||||
// roMonoid := readeriooption.ApplicativeMonoid[Config](intAdd)
|
||||
// combined := roMonoid.Concat(
|
||||
// readeriooption.Of[Config](5),
|
||||
// readeriooption.Of[Config](3),
|
||||
// )
|
||||
// // Returns Some(8)
|
||||
//
|
||||
// // Alternative monoid - provides fallback
|
||||
// altMonoid := readeriooption.AlternativeMonoid[Config](intAdd)
|
||||
// withFallback := altMonoid.Concat(
|
||||
// readeriooption.None[Config, int](),
|
||||
// readeriooption.Of[Config](10),
|
||||
// )
|
||||
// // Returns Some(10)
|
||||
//
|
||||
// # Profunctor Operations
|
||||
//
|
||||
// Transform both input and output:
|
||||
//
|
||||
// type GlobalConfig struct {
|
||||
// DB DBConfig
|
||||
// }
|
||||
//
|
||||
// type DBConfig struct {
|
||||
// Host string
|
||||
// }
|
||||
//
|
||||
// // Adapt environment and transform result
|
||||
// adapted := F.Pipe1(
|
||||
// queryDB, // ReaderIOOption[DBConfig, User]
|
||||
// readeriooption.Promap(
|
||||
// func(g GlobalConfig) DBConfig { return g.DB },
|
||||
// func(u User) string { return u.Name },
|
||||
// ),
|
||||
// )
|
||||
// // Now: ReaderIOOption[GlobalConfig, string]
|
||||
//
|
||||
// # Tail Recursion
|
||||
//
|
||||
// For recursive computations, use TailRec to avoid stack overflow:
|
||||
//
|
||||
// func factorial(n int) readeriooption.ReaderIOOption[Config, int] {
|
||||
// return readeriooption.TailRec(func(acc int) readeriooption.ReaderIOOption[Config, tailrec.Trampoline[int, int]] {
|
||||
// if n <= 1 {
|
||||
// return readeriooption.Of[Config](tailrec.Done[int](acc))
|
||||
// }
|
||||
// return readeriooption.Of[Config](tailrec.Continue[int](acc * n))
|
||||
// })(1)
|
||||
// }
|
||||
//
|
||||
// # Relationship to Other Monads
|
||||
//
|
||||
// ReaderIOOption is related to other monads in the fp-go library:
|
||||
// - reader: ReaderIOOption adds IO and Option capabilities
|
||||
// - readerio: ReaderIOOption adds Option capability
|
||||
// - readeroption: ReaderIOOption adds IO capability
|
||||
// - iooption: ReaderIOOption adds Reader capability
|
||||
// - option: ReaderIOOption adds Reader and IO capabilities
|
||||
//
|
||||
// # Type Safety
|
||||
//
|
||||
// The type system ensures:
|
||||
// - Environment dependencies are explicit in the type signature
|
||||
// - Side effects are tracked through the IO layer
|
||||
// - Optional results are handled explicitly
|
||||
// - Composition maintains type safety
|
||||
//
|
||||
// # Performance Considerations
|
||||
//
|
||||
// ReaderIOOption computations are lazy and only execute when:
|
||||
// 1. An environment is provided (Reader layer)
|
||||
// 2. The IO action is invoked (IO layer)
|
||||
//
|
||||
// This allows for efficient composition without premature execution.
|
||||
package readeriooption
|
||||
145
v2/readeriooption/monoid.go
Normal file
145
v2/readeriooption/monoid.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import "github.com/IBM/fp-go/v2/monoid"
|
||||
|
||||
// ApplicativeMonoid creates a Monoid for ReaderIOOption based on Applicative functor composition.
|
||||
// The empty element is Of(m.Empty()), and concat combines two computations using the underlying monoid.
|
||||
// Both computations must succeed (return Some) for the result to succeed.
|
||||
//
|
||||
// This is useful for accumulating results from multiple independent computations that all need
|
||||
// to succeed. If any computation returns None, the entire result is None.
|
||||
//
|
||||
// The resulting monoid satisfies the monoid laws:
|
||||
// - Left identity: Concat(Empty(), x) = x
|
||||
// - Right identity: Concat(x, Empty()) = x
|
||||
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for combining success values of type A
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[ReaderIOOption[R, A]] that combines ReaderIOOption computations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// RO "github.com/IBM/fp-go/v2/readeroption"
|
||||
// )
|
||||
//
|
||||
// // Create a monoid for integer addition
|
||||
// intAdd := N.MonoidSum[int]()
|
||||
// roMonoid := RO.ApplicativeMonoid[Config](intAdd)
|
||||
//
|
||||
// // Combine successful computations
|
||||
// ro1 := RO.Of[Config](5)
|
||||
// ro2 := RO.Of[Config](3)
|
||||
// combined := roMonoid.Concat(ro1, ro2)
|
||||
// // combined(cfg) returns option.Some(8)
|
||||
//
|
||||
// // If either fails, the whole computation fails
|
||||
// ro3 := RO.None[Config, int]()
|
||||
// failed := roMonoid.Concat(ro1, ro3)
|
||||
// // failed(cfg) returns option.None[int]()
|
||||
//
|
||||
// // Empty element is the identity
|
||||
// withEmpty := roMonoid.Concat(ro1, roMonoid.Empty())
|
||||
// // withEmpty(cfg) returns option.Some(5)
|
||||
//
|
||||
//go:inline
|
||||
func ApplicativeMonoid[R, A any](m monoid.Monoid[A]) monoid.Monoid[ReaderIOOption[R, A]] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[R, A, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a Monoid for ReaderIOOption that combines both Alternative and Applicative behavior.
|
||||
// It uses the provided monoid for the success values and falls back to alternative computations on failure.
|
||||
//
|
||||
// The empty element is Of(m.Empty()), and concat tries the first computation, falling back to the second
|
||||
// if it fails (returns None), then combines successful values using the underlying monoid.
|
||||
//
|
||||
// This is particularly useful when you want to:
|
||||
// - Try multiple computations and accumulate their results
|
||||
// - Provide fallback behavior when computations fail
|
||||
// - Combine results from computations that may or may not succeed
|
||||
//
|
||||
// The behavior differs from ApplicativeMonoid in that it provides fallback semantics:
|
||||
// - If the first computation succeeds, use its value
|
||||
// - If the first fails but the second succeeds, use the second's value
|
||||
// - If both succeed, combine their values using the underlying monoid
|
||||
// - If both fail, the result is None
|
||||
//
|
||||
// The resulting monoid satisfies the monoid laws:
|
||||
// - Left identity: Concat(Empty(), x) = x
|
||||
// - Right identity: Concat(x, Empty()) = x
|
||||
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The underlying monoid for combining success values of type A
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[ReaderIOOption[R, A]] that combines ReaderIOOption computations with fallback
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// RO "github.com/IBM/fp-go/v2/readeroption"
|
||||
// )
|
||||
//
|
||||
// // Create a monoid for integer addition with alternative behavior
|
||||
// intAdd := N.MonoidSum[int]()
|
||||
// roMonoid := RO.AlternativeMonoid[Config](intAdd)
|
||||
//
|
||||
// // Combine successful computations
|
||||
// ro1 := RO.Of[Config](5)
|
||||
// ro2 := RO.Of[Config](3)
|
||||
// combined := roMonoid.Concat(ro1, ro2)
|
||||
// // combined(cfg) returns option.Some(8)
|
||||
//
|
||||
// // Fallback when first fails
|
||||
// ro3 := RO.None[Config, int]()
|
||||
// ro4 := RO.Of[Config](10)
|
||||
// withFallback := roMonoid.Concat(ro3, ro4)
|
||||
// // withFallback(cfg) returns option.Some(10)
|
||||
//
|
||||
// // Use first success when available
|
||||
// withFirst := roMonoid.Concat(ro1, ro3)
|
||||
// // withFirst(cfg) returns option.Some(5)
|
||||
//
|
||||
// // Accumulate multiple values with some failures
|
||||
// result := roMonoid.Concat(
|
||||
// roMonoid.Concat(ro3, ro1), // None + 5 = 5
|
||||
// ro2, // 5 + 3 = 8
|
||||
// )
|
||||
// // result(cfg) returns option.Some(8)
|
||||
//
|
||||
//go:inline
|
||||
func AlternativeMonoid[R, A any](m monoid.Monoid[A]) monoid.Monoid[ReaderIOOption[R, A]] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[R, A],
|
||||
MonadMap[R, A, func(A) A],
|
||||
MonadAp[R, A, A],
|
||||
MonadAlt[R, A],
|
||||
m,
|
||||
)
|
||||
}
|
||||
357
v2/readeriooption/monoid_test.go
Normal file
357
v2/readeriooption/monoid_test.go
Normal file
@@ -0,0 +1,357 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplicativeMonoid_BothSuccess(t *testing.T) {
|
||||
// Test combining two successful computations
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := Of[context.Context](5)
|
||||
ro2 := Of[context.Context](3)
|
||||
|
||||
result := m.Concat(ro1, ro2)
|
||||
expected := O.Of(8)
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid_FirstFailure(t *testing.T) {
|
||||
// Test when first computation fails
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := None[context.Context, int]()
|
||||
ro2 := Of[context.Context](3)
|
||||
|
||||
result := m.Concat(ro1, ro2)
|
||||
expected := O.None[int]()
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid_SecondFailure(t *testing.T) {
|
||||
// Test when second computation fails
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := Of[context.Context](5)
|
||||
ro2 := None[context.Context, int]()
|
||||
|
||||
result := m.Concat(ro1, ro2)
|
||||
expected := O.None[int]()
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid_BothFailure(t *testing.T) {
|
||||
// Test when both computations fail
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := None[context.Context, int]()
|
||||
ro2 := None[context.Context, int]()
|
||||
|
||||
result := m.Concat(ro1, ro2)
|
||||
expected := O.None[int]()
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
|
||||
// Test left identity: Concat(Empty(), x) = x
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro := Of[context.Context](5)
|
||||
result := m.Concat(m.Empty(), ro)
|
||||
|
||||
assert.Equal(t, O.Of(5), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
|
||||
// Test right identity: Concat(x, Empty()) = x
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro := Of[context.Context](5)
|
||||
result := m.Concat(ro, m.Empty())
|
||||
|
||||
assert.Equal(t, O.Of(5), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid_Associativity(t *testing.T) {
|
||||
// Test associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := Of[context.Context](2)
|
||||
ro2 := Of[context.Context](3)
|
||||
ro3 := Of[context.Context](5)
|
||||
|
||||
left := m.Concat(m.Concat(ro1, ro2), ro3)
|
||||
right := m.Concat(ro1, m.Concat(ro2, ro3))
|
||||
|
||||
assert.Equal(t, O.Of(10), left(context.Background())())
|
||||
assert.Equal(t, O.Of(10), right(context.Background())())
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid_StringConcat(t *testing.T) {
|
||||
// Test with string concatenation monoid
|
||||
strConcat := S.Monoid
|
||||
m := ApplicativeMonoid[context.Context](strConcat)
|
||||
|
||||
ro1 := Of[context.Context]("Hello")
|
||||
ro2 := Of[context.Context](" ")
|
||||
ro3 := Of[context.Context]("World")
|
||||
|
||||
result := m.Concat(m.Concat(ro1, ro2), ro3)
|
||||
expected := O.Of("Hello World")
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid_WithEnvironment(t *testing.T) {
|
||||
// Test that environment is properly passed through
|
||||
type Config struct {
|
||||
Factor int
|
||||
}
|
||||
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := ApplicativeMonoid[Config](intAdd)
|
||||
|
||||
ro1 := func(cfg Config) IOOption[int] {
|
||||
return func() Option[int] {
|
||||
return O.Of(10 * cfg.Factor)
|
||||
}
|
||||
}
|
||||
ro2 := func(cfg Config) IOOption[int] {
|
||||
return func() Option[int] {
|
||||
return O.Of(5 * cfg.Factor)
|
||||
}
|
||||
}
|
||||
|
||||
result := m.Concat(ro1, ro2)
|
||||
cfg := Config{Factor: 2}
|
||||
expected := O.Of(30) // (10*2) + (5*2) = 30
|
||||
|
||||
assert.Equal(t, expected, result(cfg)())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_BothSuccess(t *testing.T) {
|
||||
// Test combining two successful computations
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := Of[context.Context](5)
|
||||
ro2 := Of[context.Context](3)
|
||||
|
||||
result := m.Concat(ro1, ro2)
|
||||
expected := O.Of(8)
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_FirstFailure(t *testing.T) {
|
||||
// Test fallback when first computation fails
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := None[context.Context, int]()
|
||||
ro2 := Of[context.Context](10)
|
||||
|
||||
result := m.Concat(ro1, ro2)
|
||||
expected := O.Of(10)
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_SecondFailure(t *testing.T) {
|
||||
// Test using first success when second fails
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := Of[context.Context](5)
|
||||
ro2 := None[context.Context, int]()
|
||||
|
||||
result := m.Concat(ro1, ro2)
|
||||
expected := O.Of(5)
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_BothFailure(t *testing.T) {
|
||||
// Test when both computations fail
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := None[context.Context, int]()
|
||||
ro2 := None[context.Context, int]()
|
||||
|
||||
result := m.Concat(ro1, ro2)
|
||||
expected := O.None[int]()
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_LeftIdentity(t *testing.T) {
|
||||
// Test left identity: Concat(Empty(), x) = x
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro := Of[context.Context](5)
|
||||
result := m.Concat(m.Empty(), ro)
|
||||
|
||||
assert.Equal(t, O.Of(5), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_RightIdentity(t *testing.T) {
|
||||
// Test right identity: Concat(x, Empty()) = x
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro := Of[context.Context](5)
|
||||
result := m.Concat(ro, m.Empty())
|
||||
|
||||
assert.Equal(t, O.Of(5), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_Associativity(t *testing.T) {
|
||||
// Test associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := Of[context.Context](2)
|
||||
ro2 := Of[context.Context](3)
|
||||
ro3 := Of[context.Context](5)
|
||||
|
||||
left := m.Concat(m.Concat(ro1, ro2), ro3)
|
||||
right := m.Concat(ro1, m.Concat(ro2, ro3))
|
||||
|
||||
assert.Equal(t, O.Of(10), left(context.Background())())
|
||||
assert.Equal(t, O.Of(10), right(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_FallbackChain(t *testing.T) {
|
||||
// Test chaining multiple fallbacks
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := None[context.Context, int]()
|
||||
ro2 := None[context.Context, int]()
|
||||
ro3 := Of[context.Context](7)
|
||||
ro4 := Of[context.Context](3)
|
||||
|
||||
// None + None = None, then None + 7 = 7, then 7 + 3 = 10
|
||||
result := m.Concat(m.Concat(m.Concat(ro1, ro2), ro3), ro4)
|
||||
expected := O.Of(10)
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_WithEnvironment(t *testing.T) {
|
||||
// Test that environment is properly passed through with fallback
|
||||
type Config struct {
|
||||
UseCache bool
|
||||
Factor int
|
||||
}
|
||||
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[Config](intAdd)
|
||||
|
||||
cacheValue := func(cfg Config) IOOption[int] {
|
||||
return func() Option[int] {
|
||||
if cfg.UseCache {
|
||||
return O.Of(100)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
}
|
||||
|
||||
dbValue := func(cfg Config) IOOption[int] {
|
||||
return func() Option[int] {
|
||||
return O.Of(50 * cfg.Factor)
|
||||
}
|
||||
}
|
||||
|
||||
result := m.Concat(cacheValue, dbValue)
|
||||
|
||||
// With cache enabled, both succeed so values are combined: 100 + (50*2) = 200
|
||||
cfg1 := Config{UseCache: true, Factor: 2}
|
||||
assert.Equal(t, O.Of(200), result(cfg1)())
|
||||
|
||||
// With cache disabled, should fall back to DB value: 0 + (50*2) = 100
|
||||
cfg2 := Config{UseCache: false, Factor: 2}
|
||||
assert.Equal(t, O.Of(100), result(cfg2)())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_StringConcat(t *testing.T) {
|
||||
// Test with string concatenation and fallback
|
||||
strConcat := S.Monoid
|
||||
m := AlternativeMonoid[context.Context](strConcat)
|
||||
|
||||
ro1 := None[context.Context, string]()
|
||||
ro2 := Of[context.Context]("Hello")
|
||||
ro3 := Of[context.Context](" World")
|
||||
|
||||
result := m.Concat(m.Concat(ro1, ro2), ro3)
|
||||
expected := O.Of("Hello World")
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_MultipleSuccesses(t *testing.T) {
|
||||
// Test accumulating multiple successful values
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := Of[context.Context](1)
|
||||
ro2 := Of[context.Context](2)
|
||||
ro3 := Of[context.Context](3)
|
||||
ro4 := Of[context.Context](4)
|
||||
|
||||
result := m.Concat(m.Concat(m.Concat(ro1, ro2), ro3), ro4)
|
||||
expected := O.Of(10)
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid_InterspersedFailures(t *testing.T) {
|
||||
// Test with failures interspersed between successes
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[context.Context](intAdd)
|
||||
|
||||
ro1 := Of[context.Context](5)
|
||||
ro2 := None[context.Context, int]()
|
||||
ro3 := Of[context.Context](3)
|
||||
ro4 := None[context.Context, int]()
|
||||
ro5 := Of[context.Context](2)
|
||||
|
||||
result := m.Concat(m.Concat(m.Concat(m.Concat(ro1, ro2), ro3), ro4), ro5)
|
||||
expected := O.Of(10) // 5 + 3 + 2
|
||||
|
||||
assert.Equal(t, expected, result(context.Background())())
|
||||
}
|
||||
74
v2/readeriooption/profunctor.go
Normal file
74
v2/readeriooption/profunctor.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOOption.
|
||||
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Adapt the environment type before passing it to the ReaderIOOption (via f)
|
||||
// - Transform the Some value after the computation completes (via g)
|
||||
//
|
||||
// The None case remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The original environment type expected by the ReaderIOOption
|
||||
// - A: The original value type produced by the ReaderIOOption
|
||||
// - D: The new input environment type
|
||||
// - B: The new output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to R (contravariant)
|
||||
// - g: Function to transform the output Some value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOOption[R, A] and returns a ReaderIOOption[D, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderIOOption[R, A], B] {
|
||||
return reader.Promap(f, iooption.Map(g))
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderIOOption.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is useful for adapting a ReaderIOOption to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderIOOption
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOOption[R1, A] and returns a ReaderIOOption[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIOOption[R1, A], A] {
|
||||
return reader.Contramap[IOOption[A]](f)
|
||||
}
|
||||
395
v2/readeriooption/profunctor_test.go
Normal file
395
v2/readeriooption/profunctor_test.go
Normal file
@@ -0,0 +1,395 @@
|
||||
// Copyright (c) 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPromap_TransformBoth(t *testing.T) {
|
||||
// Test transforming both input environment and output value
|
||||
type GlobalConfig struct {
|
||||
Factor int
|
||||
}
|
||||
|
||||
type LocalConfig struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Original computation expects LocalConfig and returns int
|
||||
original := func(cfg LocalConfig) IOOption[int] {
|
||||
return func() Option[int] {
|
||||
return O.Of(10 * cfg.Multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
// Transform GlobalConfig to LocalConfig (contravariant)
|
||||
envTransform := func(g GlobalConfig) LocalConfig {
|
||||
return LocalConfig{Multiplier: g.Factor}
|
||||
}
|
||||
|
||||
// Transform int to string (covariant)
|
||||
valueTransform := func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
|
||||
// Apply Promap
|
||||
adapted := F.Pipe1(
|
||||
original,
|
||||
Promap(envTransform, valueTransform),
|
||||
)
|
||||
|
||||
globalCfg := GlobalConfig{Factor: 5}
|
||||
result := adapted(globalCfg)()
|
||||
|
||||
expected := O.Of("50")
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestPromap_WithNone(t *testing.T) {
|
||||
// Test that None is preserved through Promap
|
||||
type Config1 struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type Config2 struct {
|
||||
Data int
|
||||
}
|
||||
|
||||
original := None[Config1, int]()
|
||||
|
||||
envTransform := func(c2 Config2) Config1 {
|
||||
return Config1{Value: c2.Data}
|
||||
}
|
||||
|
||||
valueTransform := func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
|
||||
adapted := F.Pipe1(
|
||||
original,
|
||||
Promap(envTransform, valueTransform),
|
||||
)
|
||||
|
||||
cfg := Config2{Data: 10}
|
||||
result := adapted(cfg)()
|
||||
|
||||
expected := O.None[string]()
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestPromap_Identity(t *testing.T) {
|
||||
// Test that Promap with identity functions is identity
|
||||
original := Of[context.Context](42)
|
||||
|
||||
adapted := F.Pipe1(
|
||||
original,
|
||||
Promap(
|
||||
F.Identity[context.Context],
|
||||
F.Identity[int],
|
||||
),
|
||||
)
|
||||
|
||||
result := adapted(context.Background())()
|
||||
expected := O.Of(42)
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestPromap_Composition(t *testing.T) {
|
||||
// Test that Promap composes correctly
|
||||
type Config1 struct{ A int }
|
||||
type Config2 struct{ B int }
|
||||
type Config3 struct{ C int }
|
||||
|
||||
original := func(c1 Config1) IOOption[int] {
|
||||
return func() Option[int] {
|
||||
return O.Of(c1.A * 2)
|
||||
}
|
||||
}
|
||||
|
||||
// First transformation
|
||||
f1 := func(c2 Config2) Config1 { return Config1{A: c2.B + 1} }
|
||||
g1 := func(n int) int { return n * 3 }
|
||||
|
||||
// Second transformation
|
||||
f2 := func(c3 Config3) Config2 { return Config2{B: c3.C + 2} }
|
||||
g2 := func(n int) string { return fmt.Sprintf("%d", n) }
|
||||
|
||||
// Apply transformations separately
|
||||
step1 := F.Pipe1(original, Promap(f1, g1))
|
||||
step2 := F.Pipe1(step1, Promap(f2, g2))
|
||||
|
||||
// Apply composed transformation
|
||||
composed := F.Pipe1(
|
||||
original,
|
||||
Promap(
|
||||
F.Flow2(f2, f1),
|
||||
F.Flow2(g1, g2),
|
||||
),
|
||||
)
|
||||
|
||||
cfg := Config3{C: 5}
|
||||
|
||||
result1 := step2(cfg)()
|
||||
result2 := composed(cfg)()
|
||||
|
||||
// Both should give the same result: ((5+2+1)*2)*3 = 48
|
||||
expected := O.Of("48")
|
||||
assert.Equal(t, expected, result1)
|
||||
assert.Equal(t, expected, result2)
|
||||
}
|
||||
|
||||
func TestContramap_TransformEnvironment(t *testing.T) {
|
||||
// Test transforming only the environment
|
||||
type GlobalConfig struct {
|
||||
DatabaseURL string
|
||||
Port int
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
// Original computation expects DBConfig
|
||||
original := func(cfg DBConfig) IOOption[string] {
|
||||
return func() Option[string] {
|
||||
return O.Of("Connected to: " + cfg.URL)
|
||||
}
|
||||
}
|
||||
|
||||
// Transform GlobalConfig to DBConfig
|
||||
envTransform := func(g GlobalConfig) DBConfig {
|
||||
return DBConfig{URL: g.DatabaseURL}
|
||||
}
|
||||
|
||||
// Apply Contramap
|
||||
adapted := F.Pipe1(
|
||||
original,
|
||||
Contramap[string](envTransform),
|
||||
)
|
||||
|
||||
globalCfg := GlobalConfig{
|
||||
DatabaseURL: "localhost:5432",
|
||||
Port: 8080,
|
||||
}
|
||||
result := adapted(globalCfg)()
|
||||
|
||||
expected := O.Of("Connected to: localhost:5432")
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestContramap_WithNone(t *testing.T) {
|
||||
// Test that None is preserved through Contramap
|
||||
type Config1 struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type Config2 struct {
|
||||
Data int
|
||||
}
|
||||
|
||||
original := None[Config1, string]()
|
||||
|
||||
envTransform := func(c2 Config2) Config1 {
|
||||
return Config1{Value: c2.Data}
|
||||
}
|
||||
|
||||
adapted := F.Pipe1(
|
||||
original,
|
||||
Contramap[string](envTransform),
|
||||
)
|
||||
|
||||
cfg := Config2{Data: 10}
|
||||
result := adapted(cfg)()
|
||||
|
||||
expected := O.None[string]()
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestContramap_Identity(t *testing.T) {
|
||||
// Test that Contramap with identity function is identity
|
||||
original := Of[context.Context](42)
|
||||
|
||||
adapted := F.Pipe1(
|
||||
original,
|
||||
Contramap[int](F.Identity[context.Context]),
|
||||
)
|
||||
|
||||
result := adapted(context.Background())()
|
||||
expected := O.Of(42)
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestContramap_Composition(t *testing.T) {
|
||||
// Test that Contramap composes correctly
|
||||
type Config1 struct{ A int }
|
||||
type Config2 struct{ B int }
|
||||
type Config3 struct{ C int }
|
||||
|
||||
original := func(c1 Config1) IOOption[int] {
|
||||
return func() Option[int] {
|
||||
return O.Of(c1.A * 10)
|
||||
}
|
||||
}
|
||||
|
||||
f1 := func(c2 Config2) Config1 { return Config1{A: c2.B + 1} }
|
||||
f2 := func(c3 Config3) Config2 { return Config2{B: c3.C + 2} }
|
||||
|
||||
// Apply transformations separately
|
||||
step1 := F.Pipe1(original, Contramap[int](f1))
|
||||
step2 := F.Pipe1(step1, Contramap[int](f2))
|
||||
|
||||
// Apply composed transformation
|
||||
composed := F.Pipe1(
|
||||
original,
|
||||
Contramap[int](F.Flow2(f2, f1)),
|
||||
)
|
||||
|
||||
cfg := Config3{C: 5}
|
||||
|
||||
result1 := step2(cfg)()
|
||||
result2 := composed(cfg)()
|
||||
|
||||
// Both should give the same result: (5+2+1)*10 = 80
|
||||
expected := O.Of(80)
|
||||
assert.Equal(t, expected, result1)
|
||||
assert.Equal(t, expected, result2)
|
||||
}
|
||||
|
||||
func TestPromap_RealWorldExample(t *testing.T) {
|
||||
// Real-world example: adapting a database query function
|
||||
type AppConfig struct {
|
||||
DBHost string
|
||||
DBPort int
|
||||
DBUser string
|
||||
DBPassword string
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
type DBConnection struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
type UserDTO struct {
|
||||
UserID int
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
// Original function that queries database
|
||||
queryUser := func(conn DBConnection) IOOption[User] {
|
||||
return func() Option[User] {
|
||||
// Simulate database query
|
||||
if conn.ConnectionString != "" {
|
||||
return O.Of(User{ID: 1, Name: "Alice"})
|
||||
}
|
||||
return O.None[User]()
|
||||
}
|
||||
}
|
||||
|
||||
// Adapt to work with AppConfig and return UserDTO
|
||||
adaptedQuery := F.Pipe1(
|
||||
queryUser,
|
||||
Promap(
|
||||
// Extract DB connection from app config
|
||||
func(cfg AppConfig) DBConnection {
|
||||
return DBConnection{
|
||||
ConnectionString: cfg.DBUser + "@" + cfg.DBHost,
|
||||
}
|
||||
},
|
||||
// Convert User to UserDTO
|
||||
func(u User) UserDTO {
|
||||
return UserDTO{
|
||||
UserID: u.ID,
|
||||
DisplayName: "User: " + u.Name,
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
appCfg := AppConfig{
|
||||
DBHost: "localhost",
|
||||
DBPort: 5432,
|
||||
DBUser: "admin",
|
||||
DBPassword: "secret",
|
||||
LogLevel: "info",
|
||||
}
|
||||
|
||||
result := adaptedQuery(appCfg)()
|
||||
expected := O.Of(UserDTO{UserID: 1, DisplayName: "User: Alice"})
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestContramap_RealWorldExample(t *testing.T) {
|
||||
// Real-world example: adapting a service that needs specific config
|
||||
type GlobalConfig struct {
|
||||
ServiceURL string
|
||||
APIKey string
|
||||
Timeout int
|
||||
RetryCount int
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
Endpoint string
|
||||
Auth string
|
||||
}
|
||||
|
||||
// Service function that needs ServiceConfig
|
||||
callService := func(cfg ServiceConfig) IOOption[string] {
|
||||
return func() Option[string] {
|
||||
if cfg.Endpoint != "" && cfg.Auth != "" {
|
||||
return O.Of("Response from " + cfg.Endpoint)
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
}
|
||||
|
||||
// Adapt to work with GlobalConfig
|
||||
adaptedService := F.Pipe1(
|
||||
callService,
|
||||
Contramap[string](func(g GlobalConfig) ServiceConfig {
|
||||
return ServiceConfig{
|
||||
Endpoint: g.ServiceURL,
|
||||
Auth: "Bearer " + g.APIKey,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
globalCfg := GlobalConfig{
|
||||
ServiceURL: "https://api.example.com",
|
||||
APIKey: "secret-key",
|
||||
Timeout: 30,
|
||||
RetryCount: 3,
|
||||
}
|
||||
|
||||
result := adaptedService(globalCfg)()
|
||||
expected := O.Of("Response from https://api.example.com")
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
434
v2/readeriooption/reader.go
Normal file
434
v2/readeriooption/reader.go
Normal file
@@ -0,0 +1,434 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/fromoption"
|
||||
"github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/optiont"
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// FromOption lifts an Option[A] into a ReaderIOOption[R, A].
|
||||
// The resulting computation ignores the environment and returns the given option.
|
||||
//
|
||||
//go:inline
|
||||
func FromOption[R, A any](t Option[A]) ReaderIOOption[R, A] {
|
||||
return readerio.Of[R](t)
|
||||
}
|
||||
|
||||
// Some wraps a value in a ReaderIOOption, representing a successful computation.
|
||||
// This is equivalent to Of but more explicit about the Option semantics.
|
||||
//
|
||||
//go:inline
|
||||
func Some[R, A any](r A) ReaderIOOption[R, A] {
|
||||
return optiont.Of(readerio.Of[R, Option[A]], r)
|
||||
}
|
||||
|
||||
// FromReader lifts a Reader[R, A] into a ReaderIOOption[R, A].
|
||||
// The resulting computation always succeeds (returns Some).
|
||||
//
|
||||
//go:inline
|
||||
func FromReader[R, A any](r Reader[R, A]) ReaderIOOption[R, A] {
|
||||
return SomeReader(r)
|
||||
}
|
||||
|
||||
// SomeReader lifts a Reader[R, A] into a ReaderIOOption[R, A].
|
||||
// The resulting computation always succeeds (returns Some).
|
||||
//
|
||||
//go:inline
|
||||
func SomeReader[R, A any](r Reader[R, A]) ReaderIOOption[R, A] {
|
||||
return function.Flow2(r, iooption.Some[A])
|
||||
}
|
||||
|
||||
// MonadMap applies a function to the value inside a ReaderIOOption.
|
||||
// If the ReaderIOOption contains None, the function is not applied.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ro := readeroption.Of[Config](42)
|
||||
// doubled := readeroption.MonadMap(ro, N.Mul(2))
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[R, A, B any](fa ReaderIOOption[R, A], f func(A) B) ReaderIOOption[R, B] {
|
||||
return optiont.MonadMap(readerio.MonadMap[R, Option[A], Option[B]], fa, f)
|
||||
}
|
||||
|
||||
// Map returns a function that applies a transformation to the value inside a ReaderIOOption.
|
||||
// This is the curried version of MonadMap, useful for composition with F.Pipe.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// doubled := F.Pipe1(
|
||||
// readeroption.Of[Config](42),
|
||||
// readeroption.Map[Config](N.Mul(2)),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
|
||||
return optiont.Map(readerio.Map[R, Option[A], Option[B]], f)
|
||||
}
|
||||
|
||||
// MonadChain sequences two ReaderIOOption computations, where the second depends on the result of the first.
|
||||
// If the first computation returns None, the second is not executed.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// findUser := func(id int) readeroption.ReaderIOOption[DB, User] { ... }
|
||||
// loadProfile := func(user User) readeroption.ReaderIOOption[DB, Profile] { ... }
|
||||
// result := readeroption.MonadChain(findUser(123), loadProfile)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[R, A, B any](ma ReaderIOOption[R, A], f Kleisli[R, A, B]) ReaderIOOption[R, B] {
|
||||
return optiont.MonadChain(
|
||||
readerio.MonadChain[R, Option[A], Option[B]],
|
||||
readerio.Of[R, Option[B]],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Chain returns a function that sequences ReaderIOOption computations.
|
||||
// This is the curried version of MonadChain, useful for composition with F.Pipe.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// findUser(123),
|
||||
// readeroption.Chain(loadProfile),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return optiont.Chain(
|
||||
readerio.Chain[R, Option[A], Option[B]],
|
||||
readerio.Of[R, Option[B]],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Of wraps a value in a ReaderIOOption, representing a successful computation.
|
||||
// The resulting computation ignores the environment and returns Some(a).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ro := readeroption.Of[Config](42)
|
||||
// result := ro(config) // Returns option.Some(42)
|
||||
//
|
||||
//go:inline
|
||||
func Of[R, A any](a A) ReaderIOOption[R, A] {
|
||||
return Some[R](a)
|
||||
}
|
||||
|
||||
// None creates a ReaderIOOption representing a failed computation.
|
||||
// The resulting computation ignores the environment and returns None.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ro := readeroption.None[Config, int]()
|
||||
// result := ro(config) // Returns option.None[int]()
|
||||
//
|
||||
//go:inline
|
||||
func None[R, A any]() ReaderIOOption[R, A] {
|
||||
return readerio.Of[R](O.None[A]())
|
||||
}
|
||||
|
||||
// MonadAp applies a function wrapped in a ReaderIOOption to a value wrapped in a ReaderIOOption.
|
||||
// Both computations are executed with the same environment.
|
||||
// If either computation returns None, the result is None.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAp[R, A, B any](fab ReaderIOOption[R, func(A) B], fa ReaderIOOption[R, A]) ReaderIOOption[R, B] {
|
||||
return optiont.MonadAp(
|
||||
readerio.MonadAp[Option[B], R, Option[A]],
|
||||
readerio.MonadMap[R, Option[func(A) B], func(Option[A]) Option[B]],
|
||||
fab,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// Ap returns a function that applies a function wrapped in a ReaderIOOption to a value.
|
||||
// This is the curried version of MonadAp.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, R, A any](fa ReaderIOOption[R, A]) Operator[R, func(A) B, B] {
|
||||
return optiont.Ap(
|
||||
readerio.Ap[Option[B], R, Option[A]],
|
||||
readerio.Map[R, Option[func(A) B], func(Option[A]) Option[B]],
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// FromPredicate creates a Kleisli arrow that filters a value based on a predicate.
|
||||
// If the predicate returns true, the value is wrapped in Some; otherwise, None is returned.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isPositive := readeroption.FromPredicate[Config](N.MoreThan(0))
|
||||
// result := F.Pipe1(
|
||||
// readeroption.Of[Config](42),
|
||||
// readeroption.Chain(isPositive),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func FromPredicate[R, A any](pred Predicate[A]) Kleisli[R, A, A] {
|
||||
return fromoption.FromPredicate(FromOption[R, A], pred)
|
||||
}
|
||||
|
||||
// Fold extracts the value from a ReaderIOOption by providing handlers for both cases.
|
||||
// The onNone handler is called if the computation returns None.
|
||||
// The onRight handler is called if the computation returns Some(a).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := readeroption.Fold(
|
||||
// func() reader.Reader[Config, string] { return reader.Of[Config]("not found") },
|
||||
// func(user User) reader.Reader[Config, string] { return reader.Of[Config](user.Name) },
|
||||
// )(findUser(123))
|
||||
//
|
||||
//go:inline
|
||||
func Fold[R, A, B any](onNone Reader[R, B], onRight reader.Kleisli[R, A, B]) reader.Operator[R, Option[A], B] {
|
||||
return optiont.MatchE(reader.Chain[R, Option[A], B], lazy.Of(onNone), onRight)
|
||||
}
|
||||
|
||||
// MonadFold extracts the value from a ReaderIOOption by providing handlers for both cases.
|
||||
// This is the non-curried version of Fold.
|
||||
// The onNone handler is called if the computation returns None.
|
||||
// The onRight handler is called if the computation returns Some(a).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := readeroption.MonadFold(
|
||||
// findUser(123),
|
||||
// reader.Of[Config]("not found"),
|
||||
// func(user User) reader.Reader[Config, string] { return reader.Of[Config](user.Name) },
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func MonadFold[R, A, B any](fa ReaderIOOption[R, A], onNone ReaderIO[R, B], onRight readerio.Kleisli[R, A, B]) ReaderIO[R, B] {
|
||||
return optiont.MonadMatchE(fa, readerio.MonadChain[R, Option[A], B], lazy.Of(onNone), onRight)
|
||||
}
|
||||
|
||||
// GetOrElse returns the value from a ReaderIOOption, or a default value if it's None.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := readeroption.GetOrElse(
|
||||
// func() reader.Reader[Config, User] { return reader.Of[Config](defaultUser) },
|
||||
// )(findUser(123))
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[R, A any](onNone Reader[R, A]) reader.Operator[R, Option[A], A] {
|
||||
return optiont.GetOrElse(reader.Chain[R, Option[A], A], lazy.Of(onNone), reader.Of[R, A])
|
||||
}
|
||||
|
||||
// Ask retrieves the current environment as a ReaderIOOption.
|
||||
// This always succeeds and returns Some(environment).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getConfig := readeroption.Ask[Config]()
|
||||
// result := getConfig(myConfig) // Returns option.Some(myConfig)
|
||||
//
|
||||
//go:inline
|
||||
func Ask[R any]() ReaderIOOption[R, R] {
|
||||
return fromreader.Ask(FromReader[R, R])()
|
||||
}
|
||||
|
||||
// Asks creates a ReaderIOOption that applies a function to the environment.
|
||||
// This always succeeds and returns Some(f(environment)).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getTimeout := readeroption.Asks(func(cfg Config) int { return cfg.Timeout })
|
||||
// result := getTimeout(myConfig) // Returns option.Some(myConfig.Timeout)
|
||||
//
|
||||
//go:inline
|
||||
func Asks[R, A any](r Reader[R, A]) ReaderIOOption[R, A] {
|
||||
return fromreader.Asks(FromReader[R, A])(r)
|
||||
}
|
||||
|
||||
// MonadChainOptionK chains a ReaderIOOption with a function that returns an Option.
|
||||
// This is useful for integrating functions that return Option directly.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseAge := func(s string) option.Option[int] { ... }
|
||||
// result := readeroption.MonadChainOptionK(
|
||||
// readeroption.Of[Config]("25"),
|
||||
// parseAge,
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainOptionK[R, A, B any](ma ReaderIOOption[R, A], f O.Kleisli[A, B]) ReaderIOOption[R, B] {
|
||||
return fromoption.MonadChainOptionK(
|
||||
MonadChain[R, A, B],
|
||||
FromOption[R, B],
|
||||
ma,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainOptionK returns a function that chains a ReaderIOOption with a function returning an Option.
|
||||
// This is the curried version of MonadChainOptionK.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parseAge := func(s string) option.Option[int] { ... }
|
||||
// result := F.Pipe1(
|
||||
// readeroption.Of[Config]("25"),
|
||||
// readeroption.ChainOptionK[Config](parseAge),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[R, A, B any](f O.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return fromoption.ChainOptionK(
|
||||
Chain[R, A, B],
|
||||
FromOption[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Flatten removes one level of nesting from a ReaderIOOption.
|
||||
// Converts ReaderIOOption[R, ReaderIOOption[R, A]] to ReaderIOOption[R, A].
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nested := readeroption.Of[Config](readeroption.Of[Config](42))
|
||||
// flattened := readeroption.Flatten(nested)
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[R, A any](mma ReaderIOOption[R, ReaderIOOption[R, A]]) ReaderIOOption[R, A] {
|
||||
return MonadChain(mma, function.Identity[ReaderIOOption[R, A]])
|
||||
}
|
||||
|
||||
// Local changes the value of the local context during the execution of the action `ma` (similar to `Contravariant`'s
|
||||
// `contramap`).
|
||||
//
|
||||
// This allows you to transform the environment before passing it to a computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type GlobalConfig struct { DB DBConfig }
|
||||
// type DBConfig struct { Host string }
|
||||
//
|
||||
// // A computation that needs DBConfig
|
||||
// query := func(cfg DBConfig) option.Option[User] { ... }
|
||||
//
|
||||
// // Transform GlobalConfig to DBConfig
|
||||
// result := readeroption.Local(func(g GlobalConfig) DBConfig { return g.DB })(
|
||||
// readeroption.Asks(query),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderIOOption[R1, A]) ReaderIOOption[R2, A] {
|
||||
return reader.Local[IOOption[A]](f)
|
||||
}
|
||||
|
||||
// Read applies a context to a reader to obtain its value.
|
||||
// This executes the ReaderIOOption computation with the given environment.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ro := readeroption.Of[Config](42)
|
||||
// result := readeroption.Read[int](myConfig)(ro) // Returns option.Some(42)
|
||||
//
|
||||
//go:inline
|
||||
func Read[A, R any](e R) func(ReaderIOOption[R, A]) IOOption[A] {
|
||||
return reader.Read[IOOption[A]](e)
|
||||
}
|
||||
|
||||
// ReadOption executes a ReaderIOOption with an optional environment.
|
||||
// If the environment is None, the result is None.
|
||||
// If the environment is Some(e), the ReaderIOOption is executed with e.
|
||||
//
|
||||
// This is useful when the environment itself might not be available.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ro := readeroption.Of[Config](42)
|
||||
// result1 := readeroption.ReadOption[int](option.Some(myConfig))(ro) // Returns option.Some(42)
|
||||
// result2 := readeroption.ReadOption[int](option.None[Config]())(ro) // Returns option.None[int]()
|
||||
//
|
||||
//go:inline
|
||||
// TOGGLE
|
||||
// func ReadOption[A, R any](e Option[R]) func(ReaderIOOption[R, A]) IOOption[A] {
|
||||
// return function.Flow2(
|
||||
// optiont.Chain,
|
||||
// Read[A](e),
|
||||
// )
|
||||
// }
|
||||
|
||||
// MonadFlap applies a value to a function wrapped in a ReaderIOOption.
|
||||
// This is the reverse of MonadAp.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[R, A, B any](fab ReaderIOOption[R, func(A) B], a A) ReaderIOOption[R, B] {
|
||||
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
|
||||
}
|
||||
|
||||
// Flap returns a function that applies a value to a function wrapped in a ReaderIOOption.
|
||||
// This is the curried version of MonadFlap.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
return functor.Flap(Map[R, func(A) B, B], a)
|
||||
}
|
||||
|
||||
// MonadAlt provides an alternative ReaderIOOption if the first one returns None.
|
||||
// If fa returns Some(a), that value is returned; otherwise, the alternative computation is executed.
|
||||
// This is useful for providing fallback behavior.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// primary := findUserInCache(123)
|
||||
// fallback := findUserInDB(123)
|
||||
// result := readeroption.MonadAlt(primary, fallback)
|
||||
//
|
||||
//go:inline
|
||||
func MonadAlt[R, A any](first ReaderIOOption[R, A], second Lazy[ReaderIOOption[R, A]]) ReaderIOOption[R, A] {
|
||||
return optiont.MonadAlt(
|
||||
readerio.Of[R, Option[A]],
|
||||
readerio.MonadChain[R, Option[A], Option[A]],
|
||||
first,
|
||||
second,
|
||||
)
|
||||
}
|
||||
|
||||
// Alt returns a function that provides an alternative ReaderIOOption if the first one returns None.
|
||||
// This is the curried version of MonadAlt, useful for composition with F.Pipe.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// findUserInCache(123),
|
||||
// readeroption.Alt(findUserInDB(123)),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Alt[R, A any](second Lazy[ReaderIOOption[R, A]]) Operator[R, A, A] {
|
||||
return optiont.Alt(
|
||||
readerio.Of[R, Option[A]],
|
||||
readerio.Chain[R, Option[A], Option[A]],
|
||||
second,
|
||||
)
|
||||
}
|
||||
462
v2/readeriooption/reader_test.go
Normal file
462
v2/readeriooption/reader_test.go
Normal file
@@ -0,0 +1,462 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
ro := Of[context.Context](42)
|
||||
result := ro(context.Background())()
|
||||
assert.Equal(t, O.Of(42), result)
|
||||
}
|
||||
|
||||
func TestSome(t *testing.T) {
|
||||
ro := Some[context.Context](42)
|
||||
result := ro(context.Background())()
|
||||
assert.Equal(t, O.Of(42), result)
|
||||
}
|
||||
|
||||
func TestNone(t *testing.T) {
|
||||
ro := None[context.Context, int]()
|
||||
result := ro(context.Background())()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
}
|
||||
|
||||
func TestFromOption_Some(t *testing.T) {
|
||||
opt := O.Of(42)
|
||||
ro := FromOption[context.Context](opt)
|
||||
result := ro(context.Background())()
|
||||
assert.Equal(t, O.Of(42), result)
|
||||
}
|
||||
|
||||
func TestFromOption_None(t *testing.T) {
|
||||
opt := O.None[int]()
|
||||
ro := FromOption[context.Context](opt)
|
||||
result := ro(context.Background())()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
r := func(cfg Config) int {
|
||||
return cfg.Value * 2
|
||||
}
|
||||
|
||||
ro := FromReader[Config](r)
|
||||
cfg := Config{Value: 21}
|
||||
result := ro(cfg)()
|
||||
|
||||
assert.Equal(t, O.Of(42), result)
|
||||
}
|
||||
|
||||
func TestSomeReader(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
r := func(cfg Config) int {
|
||||
return cfg.Value * 2
|
||||
}
|
||||
|
||||
ro := SomeReader[Config](r)
|
||||
cfg := Config{Value: 21}
|
||||
result := ro(cfg)()
|
||||
|
||||
assert.Equal(t, O.Of(42), result)
|
||||
}
|
||||
|
||||
func TestMonadMap_Some(t *testing.T) {
|
||||
ro := Of[context.Context](21)
|
||||
result := MonadMap(ro, func(x int) int { return x * 2 })
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadMap_None(t *testing.T) {
|
||||
ro := None[context.Context, int]()
|
||||
result := MonadMap(ro, func(x int) int { return x * 2 })
|
||||
assert.Equal(t, O.None[int](), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMap_Some(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[context.Context](21),
|
||||
Map[context.Context](func(x int) int { return x * 2 }),
|
||||
)
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMap_None(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
None[context.Context, int](),
|
||||
Map[context.Context](func(x int) int { return x * 2 }),
|
||||
)
|
||||
assert.Equal(t, O.None[int](), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChain_BothSome(t *testing.T) {
|
||||
ro1 := Of[context.Context](21)
|
||||
ro2 := func(x int) ReaderIOOption[context.Context, int] {
|
||||
return Of[context.Context](x * 2)
|
||||
}
|
||||
|
||||
result := MonadChain(ro1, ro2)
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChain_FirstNone(t *testing.T) {
|
||||
ro1 := None[context.Context, int]()
|
||||
ro2 := func(x int) ReaderIOOption[context.Context, int] {
|
||||
return Of[context.Context](x * 2)
|
||||
}
|
||||
|
||||
result := MonadChain(ro1, ro2)
|
||||
assert.Equal(t, O.None[int](), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChain_SecondNone(t *testing.T) {
|
||||
ro1 := Of[context.Context](21)
|
||||
ro2 := func(x int) ReaderIOOption[context.Context, int] {
|
||||
return None[context.Context, int]()
|
||||
}
|
||||
|
||||
result := MonadChain(ro1, ro2)
|
||||
assert.Equal(t, O.None[int](), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[context.Context](21),
|
||||
Chain(func(x int) ReaderIOOption[context.Context, int] {
|
||||
return Of[context.Context](x * 2)
|
||||
}),
|
||||
)
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadAp_BothSome(t *testing.T) {
|
||||
fab := Of[context.Context](func(x int) int { return x * 2 })
|
||||
fa := Of[context.Context](21)
|
||||
|
||||
result := MonadAp(fab, fa)
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadAp_FunctionNone(t *testing.T) {
|
||||
fab := None[context.Context, func(int) int]()
|
||||
fa := Of[context.Context](21)
|
||||
|
||||
result := MonadAp(fab, fa)
|
||||
assert.Equal(t, O.None[int](), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadAp_ValueNone(t *testing.T) {
|
||||
fab := Of[context.Context](func(x int) int { return x * 2 })
|
||||
fa := None[context.Context, int]()
|
||||
|
||||
result := MonadAp(fab, fa)
|
||||
assert.Equal(t, O.None[int](), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
fa := Of[context.Context](21)
|
||||
result := F.Pipe1(
|
||||
Of[context.Context](func(x int) int { return x * 2 }),
|
||||
Ap[int](fa),
|
||||
)
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestFromPredicate_True(t *testing.T) {
|
||||
isPositive := FromPredicate[context.Context](func(x int) bool { return x > 0 })
|
||||
|
||||
result := F.Pipe1(
|
||||
Of[context.Context](42),
|
||||
Chain(isPositive),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestFromPredicate_False(t *testing.T) {
|
||||
isPositive := FromPredicate[context.Context](func(x int) bool { return x > 0 })
|
||||
|
||||
result := F.Pipe1(
|
||||
Of[context.Context](-42),
|
||||
Chain(isPositive),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.None[int](), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
ro := Ask[Config]()
|
||||
cfg := Config{Value: 42}
|
||||
result := ro(cfg)()
|
||||
|
||||
assert.Equal(t, O.Of(cfg), result)
|
||||
}
|
||||
|
||||
func TestAsks(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
ro := Asks(func(cfg Config) int {
|
||||
return cfg.Value * 2
|
||||
})
|
||||
|
||||
cfg := Config{Value: 21}
|
||||
result := ro(cfg)()
|
||||
|
||||
assert.Equal(t, O.Of(42), result)
|
||||
}
|
||||
|
||||
func TestMonadChainOptionK_Some(t *testing.T) {
|
||||
parsePositive := func(x int) O.Option[int] {
|
||||
if x > 0 {
|
||||
return O.Of(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
result := MonadChainOptionK(
|
||||
Of[context.Context](42),
|
||||
parsePositive,
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadChainOptionK_None(t *testing.T) {
|
||||
parsePositive := func(x int) O.Option[int] {
|
||||
if x > 0 {
|
||||
return O.Of(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
result := MonadChainOptionK(
|
||||
Of[context.Context](-42),
|
||||
parsePositive,
|
||||
)
|
||||
|
||||
assert.Equal(t, O.None[int](), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
parsePositive := func(x int) O.Option[int] {
|
||||
if x > 0 {
|
||||
return O.Of(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Of[context.Context](42),
|
||||
ChainOptionK[context.Context](parsePositive),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
nested := Of[context.Context](Of[context.Context](42))
|
||||
result := Flatten(nested)
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
type GlobalConfig struct {
|
||||
Factor int
|
||||
}
|
||||
|
||||
type LocalConfig struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Computation that needs LocalConfig
|
||||
computation := func(cfg LocalConfig) IOOption[int] {
|
||||
return func() O.Option[int] {
|
||||
return O.Of(10 * cfg.Multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
// Adapt to work with GlobalConfig
|
||||
adapted := Local[int](func(g GlobalConfig) LocalConfig {
|
||||
return LocalConfig{Multiplier: g.Factor}
|
||||
})(computation)
|
||||
|
||||
globalCfg := GlobalConfig{Factor: 5}
|
||||
result := adapted(globalCfg)()
|
||||
|
||||
assert.Equal(t, O.Of(50), result)
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
ro := func(cfg Config) IOOption[int] {
|
||||
return func() O.Option[int] {
|
||||
return O.Of(cfg.Value * 2)
|
||||
}
|
||||
}
|
||||
|
||||
cfg := Config{Value: 21}
|
||||
result := Read[int](cfg)(ro)()
|
||||
|
||||
assert.Equal(t, O.Of(42), result)
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fab := Of[context.Context](func(x int) int { return x * 2 })
|
||||
result := MonadFlap(fab, 21)
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[context.Context](func(x int) int { return x * 2 }),
|
||||
Flap[context.Context, int](21),
|
||||
)
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadAlt_FirstSome(t *testing.T) {
|
||||
first := Of[context.Context](42)
|
||||
second := func() ReaderIOOption[context.Context, int] {
|
||||
return Of[context.Context](100)
|
||||
}
|
||||
|
||||
result := MonadAlt(first, second)
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadAlt_FirstNone(t *testing.T) {
|
||||
first := None[context.Context, int]()
|
||||
second := func() ReaderIOOption[context.Context, int] {
|
||||
return Of[context.Context](100)
|
||||
}
|
||||
|
||||
result := MonadAlt(first, second)
|
||||
assert.Equal(t, O.Of(100), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestMonadAlt_BothNone(t *testing.T) {
|
||||
first := None[context.Context, int]()
|
||||
second := func() ReaderIOOption[context.Context, int] {
|
||||
return None[context.Context, int]()
|
||||
}
|
||||
|
||||
result := MonadAlt(first, second)
|
||||
assert.Equal(t, O.None[int](), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestAlt(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
None[context.Context, int](),
|
||||
Alt(func() ReaderIOOption[context.Context, int] {
|
||||
return Of[context.Context](42)
|
||||
}),
|
||||
)
|
||||
assert.Equal(t, O.Of(42), result(context.Background())())
|
||||
}
|
||||
|
||||
func TestGetOrElse_Some(t *testing.T) {
|
||||
ro := Of[context.Context](42)
|
||||
result := MonadFold(ro, RIO.Of[context.Context](100), func(x int) RIO.ReaderIO[context.Context, int] {
|
||||
return RIO.Of[context.Context](x)
|
||||
})(context.Background())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestGetOrElse_None(t *testing.T) {
|
||||
ro := None[context.Context, int]()
|
||||
result := MonadFold(ro, RIO.Of[context.Context](100), func(x int) RIO.ReaderIO[context.Context, int] {
|
||||
return RIO.Of[context.Context](x)
|
||||
})(context.Background())()
|
||||
assert.Equal(t, 100, result)
|
||||
}
|
||||
|
||||
func TestMonadFold_Some(t *testing.T) {
|
||||
ro := Of[context.Context](42)
|
||||
result := MonadFold(
|
||||
ro,
|
||||
RIO.Of[context.Context]("none"),
|
||||
func(x int) RIO.ReaderIO[context.Context, string] {
|
||||
return RIO.Of[context.Context]("value: " + fmt.Sprintf("%d", x))
|
||||
},
|
||||
)(context.Background())()
|
||||
assert.Equal(t, "value: 42", result)
|
||||
}
|
||||
|
||||
func TestMonadFold_None(t *testing.T) {
|
||||
ro := None[context.Context, int]()
|
||||
result := MonadFold(
|
||||
ro,
|
||||
RIO.Of[context.Context]("none"),
|
||||
func(x int) RIO.ReaderIO[context.Context, string] {
|
||||
return RIO.Of[context.Context]("value: " + fmt.Sprintf("%d", x))
|
||||
},
|
||||
)(context.Background())()
|
||||
assert.Equal(t, "none", result)
|
||||
}
|
||||
|
||||
func TestComplexChain(t *testing.T) {
|
||||
// Test a complex chain of operations
|
||||
type Config struct {
|
||||
Factor int
|
||||
}
|
||||
|
||||
result := F.Pipe3(
|
||||
Of[Config](10),
|
||||
Map[Config](func(x int) int { return x * 2 }), // 20
|
||||
Chain(func(x int) ReaderIOOption[Config, int] {
|
||||
return Asks(func(cfg Config) int {
|
||||
return x * cfg.Factor
|
||||
})
|
||||
}),
|
||||
Chain(func(x int) ReaderIOOption[Config, int] {
|
||||
if x > 50 {
|
||||
return Of[Config](x)
|
||||
}
|
||||
return None[Config, int]()
|
||||
}),
|
||||
)
|
||||
|
||||
cfg := Config{Factor: 5}
|
||||
assert.Equal(t, O.Of(100), result(cfg)())
|
||||
|
||||
cfg2 := Config{Factor: 2}
|
||||
assert.Equal(t, O.None[int](), result(cfg2)())
|
||||
}
|
||||
44
v2/readeriooption/rec.go
Normal file
44
v2/readeriooption/rec.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, tailrec.Trampoline[A, B]]) Kleisli[R, A, B] {
|
||||
return func(a A) ReaderIOOption[R, B] {
|
||||
initialReader := f(a)
|
||||
return func(r R) IOOption[B] {
|
||||
initialB := initialReader(r)
|
||||
return func() Option[B] {
|
||||
current := initialB()
|
||||
for {
|
||||
rec, ok := option.Unwrap(current)
|
||||
if !ok {
|
||||
return option.None[B]()
|
||||
}
|
||||
if rec.Landed {
|
||||
return option.Of(rec.Land)
|
||||
}
|
||||
current = f(rec.Bounce)(r)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
v2/readeriooption/sequence.go
Normal file
133
v2/readeriooption/sequence.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
)
|
||||
|
||||
// SequenceT functions convert multiple ReaderIOOption values into a single ReaderIOOption containing a tuple.
|
||||
// If any input is None, the entire result is None.
|
||||
// Otherwise, returns Some containing a tuple of all the unwrapped values.
|
||||
//
|
||||
// These functions are useful for combining multiple independent ReaderIOOption computations
|
||||
// where you need to preserve the individual types of each result.
|
||||
|
||||
// SequenceT1 converts a single ReaderIOOption into a ReaderIOOption of a 1-tuple.
|
||||
// This is mainly useful for consistency with the other SequenceT functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { ... }
|
||||
//
|
||||
// user := readeroption.Of[Config](User{Name: "Alice"})
|
||||
// result := readeroption.SequenceT1(user)
|
||||
// // result(config) returns option.Some(tuple.MakeTuple1(User{Name: "Alice"}))
|
||||
func SequenceT1[R, A any](a ReaderIOOption[R, A]) ReaderIOOption[R, T.Tuple1[A]] {
|
||||
return apply.SequenceT1(
|
||||
Map,
|
||||
a,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceT2 combines two ReaderIOOption values into a ReaderIOOption of a 2-tuple.
|
||||
// If either input is None, the result is None.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { ... }
|
||||
//
|
||||
// user := readeroption.Of[Config](User{Name: "Alice"})
|
||||
// count := readeroption.Of[Config](42)
|
||||
//
|
||||
// result := readeroption.SequenceT2(user, count)
|
||||
// // result(config) returns option.Some(tuple.MakeTuple2(User{Name: "Alice"}, 42))
|
||||
//
|
||||
// noneUser := readeroption.None[Config, User]()
|
||||
// result2 := readeroption.SequenceT2(noneUser, count)
|
||||
// // result2(config) returns option.None[tuple.Tuple2[User, int]]()
|
||||
func SequenceT2[R, A, B any](
|
||||
a ReaderIOOption[R, A],
|
||||
b ReaderIOOption[R, B],
|
||||
) ReaderIOOption[R, T.Tuple2[A, B]] {
|
||||
return apply.SequenceT2(
|
||||
Map,
|
||||
Ap,
|
||||
a,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceT3 combines three ReaderIOOption values into a ReaderIOOption of a 3-tuple.
|
||||
// If any input is None, the result is None.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { ... }
|
||||
//
|
||||
// user := readeroption.Of[Config](User{Name: "Alice"})
|
||||
// count := readeroption.Of[Config](42)
|
||||
// active := readeroption.Of[Config](true)
|
||||
//
|
||||
// result := readeroption.SequenceT3(user, count, active)
|
||||
// // result(config) returns option.Some(tuple.MakeTuple3(User{Name: "Alice"}, 42, true))
|
||||
func SequenceT3[R, A, B, C any](
|
||||
a ReaderIOOption[R, A],
|
||||
b ReaderIOOption[R, B],
|
||||
c ReaderIOOption[R, C],
|
||||
) ReaderIOOption[R, T.Tuple3[A, B, C]] {
|
||||
return apply.SequenceT3(
|
||||
Map,
|
||||
Ap,
|
||||
Ap,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceT4 combines four ReaderIOOption values into a ReaderIOOption of a 4-tuple.
|
||||
// If any input is None, the result is None.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { ... }
|
||||
//
|
||||
// user := readeroption.Of[Config](User{Name: "Alice"})
|
||||
// count := readeroption.Of[Config](42)
|
||||
// active := readeroption.Of[Config](true)
|
||||
// score := readeroption.Of[Config](95.5)
|
||||
//
|
||||
// result := readeroption.SequenceT4(user, count, active, score)
|
||||
// // result(config) returns option.Some(tuple.MakeTuple4(User{Name: "Alice"}, 42, true, 95.5))
|
||||
func SequenceT4[R, A, B, C, D any](
|
||||
a ReaderIOOption[R, A],
|
||||
b ReaderIOOption[R, B],
|
||||
c ReaderIOOption[R, C],
|
||||
d ReaderIOOption[R, D],
|
||||
) ReaderIOOption[R, T.Tuple4[A, B, C, D]] {
|
||||
return apply.SequenceT4(
|
||||
Map,
|
||||
Ap,
|
||||
Ap,
|
||||
Ap,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
)
|
||||
}
|
||||
91
v2/readeriooption/sequence_test.go
Normal file
91
v2/readeriooption/sequence_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type MyContext string
|
||||
|
||||
const defaultContext MyContext = "default"
|
||||
|
||||
func TestSequenceT1(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
e1 := None[MyContext, string]()
|
||||
|
||||
res1 := SequenceT1(t1)
|
||||
assert.Equal(t, O.Of(T.MakeTuple1("s1")), res1(defaultContext)())
|
||||
|
||||
res2 := SequenceT1(e1)
|
||||
assert.Equal(t, O.None[T.Tuple1[string]](), res2(defaultContext)())
|
||||
}
|
||||
|
||||
func TestSequenceT2(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
e1 := None[MyContext, string]()
|
||||
t2 := Of[MyContext](2)
|
||||
e2 := None[MyContext, int]()
|
||||
|
||||
res1 := SequenceT2(t1, t2)
|
||||
assert.Equal(t, O.Of(T.MakeTuple2("s1", 2)), res1(defaultContext)())
|
||||
|
||||
res2 := SequenceT2(e1, t2)
|
||||
assert.Equal(t, O.None[T.Tuple2[string, int]](), res2(defaultContext)())
|
||||
|
||||
res3 := SequenceT2(t1, e2)
|
||||
assert.Equal(t, O.None[T.Tuple2[string, int]](), res3(defaultContext)())
|
||||
}
|
||||
|
||||
func TestSequenceT3(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
e1 := None[MyContext, string]()
|
||||
t2 := Of[MyContext](2)
|
||||
e2 := None[MyContext, int]()
|
||||
t3 := Of[MyContext](true)
|
||||
e3 := None[MyContext, bool]()
|
||||
|
||||
res1 := SequenceT3(t1, t2, t3)
|
||||
assert.Equal(t, O.Of(T.MakeTuple3("s1", 2, true)), res1(defaultContext)())
|
||||
|
||||
res2 := SequenceT3(e1, t2, t3)
|
||||
assert.Equal(t, O.None[T.Tuple3[string, int, bool]](), res2(defaultContext)())
|
||||
|
||||
res3 := SequenceT3(t1, e2, t3)
|
||||
assert.Equal(t, O.None[T.Tuple3[string, int, bool]](), res3(defaultContext)())
|
||||
|
||||
res4 := SequenceT3(t1, t2, e3)
|
||||
assert.Equal(t, O.None[T.Tuple3[string, int, bool]](), res4(defaultContext)())
|
||||
}
|
||||
|
||||
func TestSequenceT4(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
t2 := Of[MyContext](2)
|
||||
t3 := Of[MyContext](true)
|
||||
t4 := Of[MyContext](1.0)
|
||||
|
||||
res := SequenceT4(t1, t2, t3, t4)
|
||||
|
||||
assert.Equal(t, O.Of(T.MakeTuple4("s1", 2, true, 1.0)), res(defaultContext)())
|
||||
}
|
||||
128
v2/readeriooption/types.go
Normal file
128
v2/readeriooption/types.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2023 - 2025 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 readeriooption provides a monad transformer that combines the Reader and Option monads.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This is a monad transformer combining:
|
||||
// - Reader monad: https://github.com/fantasyland/fantasy-land
|
||||
// - Maybe (Option) monad: https://github.com/fantasyland/fantasy-land#maybe
|
||||
//
|
||||
// Implemented Fantasy Land algebras:
|
||||
// - Functor: https://github.com/fantasyland/fantasy-land#functor
|
||||
// - Apply: https://github.com/fantasyland/fantasy-land#apply
|
||||
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
|
||||
// - Chain: https://github.com/fantasyland/fantasy-land#chain
|
||||
// - Monad: https://github.com/fantasyland/fantasy-land#monad
|
||||
// - Alt: https://github.com/fantasyland/fantasy-land#alt
|
||||
//
|
||||
// ReaderIOOption[R, A] represents a computation that:
|
||||
// - Depends on a shared environment of type R (Reader monad)
|
||||
// - May fail to produce a value of type A (Option monad)
|
||||
//
|
||||
// This is useful for computations that need access to configuration, context, or dependencies
|
||||
// while also being able to represent the absence of a value without using errors.
|
||||
//
|
||||
// The ReaderIOOption monad is defined as: Reader[R, Option[A]]
|
||||
//
|
||||
// Key operations:
|
||||
// - Of: Wraps a value in a ReaderIOOption
|
||||
// - None: Creates a ReaderIOOption representing no value
|
||||
// - Map: Transforms the value inside a ReaderIOOption
|
||||
// - Chain: Sequences ReaderIOOption computations
|
||||
// - Ask/Asks: Accesses the environment
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// DatabaseURL string
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// // A computation that may or may not find a user
|
||||
// func findUser(id int) readeriooption.ReaderIOOption[Config, User] {
|
||||
// return readeriooption.Asks(func(cfg Config) option.Option[User] {
|
||||
// // Use cfg.DatabaseURL to query database
|
||||
// // Return Some(user) if found, None() if not found
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Chain multiple operations
|
||||
// result := F.Pipe2(
|
||||
// findUser(123),
|
||||
// readeriooption.Chain(func(user User) readeriooption.ReaderIOOption[Config, Profile] {
|
||||
// return loadProfile(user.ProfileID)
|
||||
// }),
|
||||
// readeriooption.Map(func(profile Profile) string {
|
||||
// return profile.DisplayName
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// // Execute with config
|
||||
// config := Config{DatabaseURL: "localhost:5432", Timeout: 30}
|
||||
// displayName := result(config) // Returns Option[string]
|
||||
package readeriooption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
type (
|
||||
// Lazy represents a deferred computation that produces a value of type A.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Predicate represents a function that tests a value of type A and returns a boolean.
|
||||
// It's commonly used for filtering and conditional operations.
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// IOOption represents an IO computation that may produce a value of type A.
|
||||
// It combines IO effects with the Option monad for optional values.
|
||||
IOOption[A any] = iooption.IOOption[A]
|
||||
|
||||
// Either represents a value of one of two possible types (a disjoint union).
|
||||
// An instance of Either is either Left (representing an error) or Right (representing a success).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// ReaderIO represents a computation that depends on an environment R and performs IO to produce a value A.
|
||||
// It combines the Reader monad (for dependency injection) with IO effects.
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
|
||||
// ReaderIOOption represents a computation that depends on an environment R and may produce a value A.
|
||||
// It combines the Reader monad (for dependency injection) with IO effects and the Option monad (for optional values).
|
||||
// This is the main type of this package, defined as Reader[R, IOOption[A]].
|
||||
ReaderIOOption[R, A any] = Reader[R, IOOption[A]]
|
||||
|
||||
// Kleisli represents a function that takes a value A and returns a ReaderIOOption[R, B].
|
||||
// This is the type of functions used with Chain/Bind operations, enabling monadic composition.
|
||||
Kleisli[R, A, B any] = Reader[A, ReaderIOOption[R, B]]
|
||||
|
||||
// Operator represents a function that transforms one ReaderIOOption into another.
|
||||
// It takes a ReaderIOOption[R, A] and produces a ReaderIOOption[R, B].
|
||||
// This is commonly used for lifting functions into the ReaderIOOption context.
|
||||
Operator[R, A, B any] = Reader[ReaderIOOption[R, A], ReaderIOOption[R, B]]
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -475,6 +475,8 @@ func Alt[A any](that Lazy[Result[A]]) Operator[A, A] {
|
||||
// If the Result is Left, it applies the provided function to the error value,
|
||||
// which returns a new Result that replaces the original.
|
||||
//
|
||||
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations.
|
||||
//
|
||||
// Example:
|
||||
@@ -656,3 +658,118 @@ func InstanceOf[A any](a any) Result[A] {
|
||||
}
|
||||
return Left[A](fmt.Errorf("expected %T, got %T", res, a))
|
||||
}
|
||||
|
||||
// MonadChainLeft sequences a computation on the Left (error) channel.
|
||||
// If the Result is Left, applies the function to transform or recover from the error.
|
||||
// If the Result is Right, returns the Right value unchanged.
|
||||
//
|
||||
// This is the dual of [MonadChain] - while Chain operates on Right values,
|
||||
// ChainLeft operates on Left (error) values. It's particularly useful for:
|
||||
// - Error recovery: converting specific errors into successful values
|
||||
// - Error transformation: changing error types or adding context
|
||||
// - Fallback logic: providing alternative computations when errors occur
|
||||
//
|
||||
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// The function parameter receives the error value and must return a new Result[A].
|
||||
// This allows you to:
|
||||
// - Recover by returning Right[error](value)
|
||||
// - Transform the error by returning Left[A](newError)
|
||||
// - Implement conditional error handling based on error content
|
||||
//
|
||||
// Example - Error recovery:
|
||||
//
|
||||
// result := result.MonadChainLeft(
|
||||
// result.Left[int](errors.New("not found")),
|
||||
// func(err error) result.Result[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return result.Right(0) // recover with default value
|
||||
// }
|
||||
// return result.Left[int](err) // propagate other errors
|
||||
// },
|
||||
// ) // Right(0)
|
||||
//
|
||||
// Example - Error type transformation:
|
||||
//
|
||||
// result := result.MonadChainLeft(
|
||||
// result.Left[string](errors.New("database error")),
|
||||
// func(err error) result.Result[string] {
|
||||
// return result.Left[string](fmt.Errorf("wrapped: %w", err))
|
||||
// },
|
||||
// ) // Left(wrapped error)
|
||||
//
|
||||
// Example - Right values pass through:
|
||||
//
|
||||
// result := result.MonadChainLeft(
|
||||
// result.Right(42),
|
||||
// func(err error) result.Result[int] {
|
||||
// return result.Right(0) // never called
|
||||
// },
|
||||
// ) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[A any](fa Result[A], f Kleisli[error, A]) Result[A] {
|
||||
return either.MonadChainLeft(fa, f)
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// Returns a function that transforms Left (error) values while preserving Right values.
|
||||
//
|
||||
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This curried form is particularly useful in functional pipelines and for creating
|
||||
// reusable error handlers that can be composed with other operations.
|
||||
//
|
||||
// The returned function can be used with [F.Pipe1], [F.Pipe2], etc., to build
|
||||
// complex error handling pipelines in a point-free style.
|
||||
//
|
||||
// Example - Creating reusable error handlers:
|
||||
//
|
||||
// // Handler that recovers from "not found" errors
|
||||
// recoverNotFound := result.ChainLeft(func(err error) result.Result[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return result.Right(0)
|
||||
// }
|
||||
// return result.Left[int](err)
|
||||
// })
|
||||
//
|
||||
// result1 := recoverNotFound(result.Left[int](errors.New("not found"))) // Right(0)
|
||||
// result2 := recoverNotFound(result.Right(42)) // Right(42)
|
||||
//
|
||||
// Example - Using in pipelines:
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// result.Left[int](errors.New("timeout")),
|
||||
// result.ChainLeft(func(err error) result.Result[int] {
|
||||
// if err.Error() == "timeout" {
|
||||
// return result.Right(999) // fallback value
|
||||
// }
|
||||
// return result.Left[int](err)
|
||||
// }),
|
||||
// result.Map(func(n int) string {
|
||||
// return fmt.Sprintf("Value: %d", n)
|
||||
// }),
|
||||
// ) // Right("Value: 999")
|
||||
//
|
||||
// Example - Composing multiple error handlers:
|
||||
//
|
||||
// // First handler: convert error to string
|
||||
// toStringError := result.ChainLeft(func(err error) result.Result[int] {
|
||||
// return result.Left[int](errors.New(err.Error()))
|
||||
// })
|
||||
//
|
||||
// // Second handler: add prefix
|
||||
// addPrefix := result.ChainLeft(func(err error) result.Result[int] {
|
||||
// return result.Left[int](fmt.Errorf("Error: %w", err))
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// result.Left[int](errors.New("failed")),
|
||||
// toStringError,
|
||||
// addPrefix,
|
||||
// ) // Left(Error: failed)
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
|
||||
return either.ChainLeft(f)
|
||||
}
|
||||
|
||||
@@ -356,3 +356,296 @@ func TestInstanceOf(t *testing.T) {
|
||||
assert.Equal(t, 2, v["b"])
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadChainLeft tests the MonadChainLeft function with various scenarios
|
||||
func TestMonadChainLeft(t *testing.T) {
|
||||
t.Run("Left value is transformed by function", func(t *testing.T) {
|
||||
// Transform error to success
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("not found")),
|
||||
func(err error) Result[int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right(0) // default value
|
||||
}
|
||||
return Left[int](err)
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of(0), result)
|
||||
})
|
||||
|
||||
t.Run("Left value error is transformed", func(t *testing.T) {
|
||||
// Transform error with additional context
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("database error")),
|
||||
func(err error) Result[int] {
|
||||
return Left[int](fmt.Errorf("wrapped: %w", err))
|
||||
},
|
||||
)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := UnwrapError(result)
|
||||
assert.Contains(t, err.Error(), "wrapped:")
|
||||
assert.Contains(t, err.Error(), "database error")
|
||||
})
|
||||
|
||||
t.Run("Right value passes through unchanged", func(t *testing.T) {
|
||||
// Right value should not be affected
|
||||
result := MonadChainLeft(
|
||||
Right(42),
|
||||
func(err error) Result[int] {
|
||||
return Left[int](errors.New("should not be called"))
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("Chain multiple error transformations", func(t *testing.T) {
|
||||
// First transformation
|
||||
step1 := MonadChainLeft(
|
||||
Left[int](errors.New("error1")),
|
||||
func(err error) Result[int] {
|
||||
return Left[int](errors.New("error2"))
|
||||
},
|
||||
)
|
||||
// Second transformation
|
||||
step2 := MonadChainLeft(
|
||||
step1,
|
||||
func(err error) Result[int] {
|
||||
return Left[int](fmt.Errorf("final: %s", err.Error()))
|
||||
},
|
||||
)
|
||||
assert.True(t, IsLeft(step2))
|
||||
_, err := UnwrapError(step2)
|
||||
assert.Equal(t, "final: error2", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Error recovery with fallback", func(t *testing.T) {
|
||||
// Recover from specific errors
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("timeout")),
|
||||
func(err error) Result[int] {
|
||||
if err.Error() == "timeout" {
|
||||
return Right(999) // fallback value
|
||||
}
|
||||
return Left[int](err)
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of(999), result)
|
||||
})
|
||||
|
||||
t.Run("Conditional error handling", func(t *testing.T) {
|
||||
// Handle different error types differently
|
||||
handleError := func(err error) Result[string] {
|
||||
switch err.Error() {
|
||||
case "not found":
|
||||
return Right("default")
|
||||
case "timeout":
|
||||
return Right("retry")
|
||||
default:
|
||||
return Left[string](err)
|
||||
}
|
||||
}
|
||||
|
||||
result1 := MonadChainLeft(Left[string](errors.New("not found")), handleError)
|
||||
assert.Equal(t, Of("default"), result1)
|
||||
|
||||
result2 := MonadChainLeft(Left[string](errors.New("timeout")), handleError)
|
||||
assert.Equal(t, Of("retry"), result2)
|
||||
|
||||
result3 := MonadChainLeft(Left[string](errors.New("other")), handleError)
|
||||
assert.True(t, IsLeft(result3))
|
||||
})
|
||||
|
||||
t.Run("Type preservation", func(t *testing.T) {
|
||||
// Ensure type is preserved through transformation
|
||||
result := MonadChainLeft(
|
||||
Left[string](errors.New("error")),
|
||||
func(err error) Result[string] {
|
||||
return Right("recovered")
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of("recovered"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainLeft tests the curried ChainLeft function
|
||||
func TestChainLeft(t *testing.T) {
|
||||
t.Run("Curried function transforms Left value", func(t *testing.T) {
|
||||
// Create a reusable error handler
|
||||
handleNotFound := ChainLeft(func(err error) Result[int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right(0)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
|
||||
result := handleNotFound(Left[int](errors.New("not found")))
|
||||
assert.Equal(t, Of(0), result)
|
||||
})
|
||||
|
||||
t.Run("Curried function with Right value", func(t *testing.T) {
|
||||
handler := ChainLeft(func(err error) Result[int] {
|
||||
return Left[int](errors.New("should not be called"))
|
||||
})
|
||||
|
||||
result := handler(Right(42))
|
||||
assert.Equal(t, Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("Use in pipeline with Pipe", func(t *testing.T) {
|
||||
// Create error transformer
|
||||
wrapError := ChainLeft(func(err error) Result[string] {
|
||||
return Left[string](fmt.Errorf("Error: %w", err))
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[string](errors.New("failed")),
|
||||
wrapError,
|
||||
)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := UnwrapError(result)
|
||||
assert.Contains(t, err.Error(), "Error:")
|
||||
assert.Contains(t, err.Error(), "failed")
|
||||
})
|
||||
|
||||
t.Run("Compose multiple ChainLeft operations", func(t *testing.T) {
|
||||
// First handler: convert error to string representation
|
||||
handler1 := ChainLeft(func(err error) Result[int] {
|
||||
return Left[int](errors.New(err.Error()))
|
||||
})
|
||||
|
||||
// Second handler: add prefix to error
|
||||
handler2 := ChainLeft(func(err error) Result[int] {
|
||||
return Left[int](fmt.Errorf("Handled: %w", err))
|
||||
})
|
||||
|
||||
result := F.Pipe2(
|
||||
Left[int](errors.New("original")),
|
||||
handler1,
|
||||
handler2,
|
||||
)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := UnwrapError(result)
|
||||
assert.Contains(t, err.Error(), "Handled:")
|
||||
assert.Contains(t, err.Error(), "original")
|
||||
})
|
||||
|
||||
t.Run("Error recovery in pipeline", func(t *testing.T) {
|
||||
// Handler that recovers from specific errors
|
||||
recoverFromTimeout := ChainLeft(func(err error) Result[int] {
|
||||
if err.Error() == "timeout" {
|
||||
return Right(0) // recovered value
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
|
||||
// Test with timeout error
|
||||
result1 := F.Pipe1(
|
||||
Left[int](errors.New("timeout")),
|
||||
recoverFromTimeout,
|
||||
)
|
||||
assert.Equal(t, Of(0), result1)
|
||||
|
||||
// Test with other error
|
||||
result2 := F.Pipe1(
|
||||
Left[int](errors.New("other error")),
|
||||
recoverFromTimeout,
|
||||
)
|
||||
assert.True(t, IsLeft(result2))
|
||||
})
|
||||
|
||||
t.Run("ChainLeft with Map combination", func(t *testing.T) {
|
||||
// Combine ChainLeft with Map to handle both channels
|
||||
errorHandler := ChainLeft(func(err error) Result[int] {
|
||||
return Left[int](fmt.Errorf("Error: %w", err))
|
||||
})
|
||||
|
||||
valueMapper := Map(func(n int) string {
|
||||
return fmt.Sprintf("Value: %d", n)
|
||||
})
|
||||
|
||||
// Test with Left
|
||||
result1 := F.Pipe2(
|
||||
Left[int](errors.New("fail")),
|
||||
errorHandler,
|
||||
valueMapper,
|
||||
)
|
||||
assert.True(t, IsLeft(result1))
|
||||
|
||||
// Test with Right
|
||||
result2 := F.Pipe2(
|
||||
Right(42),
|
||||
errorHandler,
|
||||
valueMapper,
|
||||
)
|
||||
assert.Equal(t, Of("Value: 42"), result2)
|
||||
})
|
||||
|
||||
t.Run("Reusable error handlers", func(t *testing.T) {
|
||||
// Create a library of reusable error handlers
|
||||
recoverNotFound := ChainLeft(func(err error) Result[string] {
|
||||
if err.Error() == "not found" {
|
||||
return Right("default")
|
||||
}
|
||||
return Left[string](err)
|
||||
})
|
||||
|
||||
recoverTimeout := ChainLeft(func(err error) Result[string] {
|
||||
if err.Error() == "timeout" {
|
||||
return Right("retry")
|
||||
}
|
||||
return Left[string](err)
|
||||
})
|
||||
|
||||
// Apply handlers in sequence
|
||||
result := F.Pipe2(
|
||||
Left[string](errors.New("not found")),
|
||||
recoverNotFound,
|
||||
recoverTimeout,
|
||||
)
|
||||
assert.Equal(t, Of("default"), result)
|
||||
})
|
||||
|
||||
t.Run("Error transformation pipeline", func(t *testing.T) {
|
||||
// Build a pipeline that transforms errors step by step
|
||||
addContext := ChainLeft(func(err error) Result[int] {
|
||||
return Left[int](fmt.Errorf("context: %w", err))
|
||||
})
|
||||
|
||||
addTimestamp := ChainLeft(func(err error) Result[int] {
|
||||
return Left[int](fmt.Errorf("[2024-01-01] %w", err))
|
||||
})
|
||||
|
||||
result := F.Pipe2(
|
||||
Left[int](errors.New("base error")),
|
||||
addContext,
|
||||
addTimestamp,
|
||||
)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := UnwrapError(result)
|
||||
assert.Contains(t, err.Error(), "[2024-01-01]")
|
||||
assert.Contains(t, err.Error(), "context:")
|
||||
assert.Contains(t, err.Error(), "base error")
|
||||
})
|
||||
|
||||
t.Run("Conditional recovery based on error content", func(t *testing.T) {
|
||||
// Recover from errors matching specific patterns
|
||||
smartRecover := ChainLeft(func(err error) Result[int] {
|
||||
msg := err.Error()
|
||||
if msg == "not found" {
|
||||
return Right(0)
|
||||
}
|
||||
if msg == "timeout" {
|
||||
return Right(-1)
|
||||
}
|
||||
if msg == "unauthorized" {
|
||||
return Right(-2)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
|
||||
assert.Equal(t, Of(0), smartRecover(Left[int](errors.New("not found"))))
|
||||
assert.Equal(t, Of(-1), smartRecover(Left[int](errors.New("timeout"))))
|
||||
assert.Equal(t, Of(-2), smartRecover(Left[int](errors.New("unauthorized"))))
|
||||
assert.True(t, IsLeft(smartRecover(Left[int](errors.New("unknown")))))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/identity"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -36,15 +36,6 @@ var (
|
||||
// Allows combining multiple builder operations.
|
||||
monoidPartialPerson = endomorphism.Monoid[*PartialPerson]()
|
||||
|
||||
// monoidPerson is a monoid for composing endomorphisms on Person.
|
||||
monoidPerson = endomorphism.Monoid[*Person]()
|
||||
|
||||
// allOfPartialPerson combines multiple PartialPerson endomorphisms into one.
|
||||
allOfPartialPerson = monoid.ConcatAll(monoidPartialPerson)
|
||||
|
||||
// foldPartialPersons folds an array of PartialPerson operations into a single ReaderOption.
|
||||
foldPartialPersons = array.Fold(readeroption.ApplicativeMonoid[*PartialPerson](monoidPerson))
|
||||
|
||||
// foldPersons folds an array of Person operations into a single Reader.
|
||||
foldPersons = array.Fold(reader.ApplicativeMonoid[*Person](monoidPartialPerson))
|
||||
|
||||
@@ -135,12 +126,10 @@ var (
|
||||
// partial := builder(&PartialPerson{})
|
||||
// // partial now has Name="Alice" and Age=25
|
||||
func MakePerson(name string, age int) Endomorphism[*PartialPerson] {
|
||||
return F.Pipe1(
|
||||
A.From(
|
||||
WithName(name),
|
||||
WithAge(age),
|
||||
),
|
||||
allOfPartialPerson)
|
||||
return F.Flow2(
|
||||
identity.ApS(WithName, name),
|
||||
identity.ApS(WithAge, age),
|
||||
)
|
||||
}
|
||||
|
||||
// buildPerson constructs the forward direction of PersonPrism.
|
||||
@@ -157,27 +146,25 @@ func MakePerson(name string, age int) Endomorphism[*PartialPerson] {
|
||||
// or None if any validation fails.
|
||||
func buildPerson() ReaderOption[Endomorphism[*PartialPerson], *Person] {
|
||||
|
||||
// maybeName extracts the name from PartialPerson, validates it,
|
||||
// and creates a setter for the Person's Name field if valid
|
||||
maybeName := F.Flow3(
|
||||
maybeName := F.Flow2(
|
||||
partialPersonLenses.name.Get,
|
||||
namePrism.GetOption,
|
||||
option.Map(personLenses.Name.Set),
|
||||
)
|
||||
|
||||
// maybeAge extracts the age from PartialPerson, validates it,
|
||||
// and creates a setter for the Person's Age field if valid
|
||||
maybeAge := F.Flow3(
|
||||
maybeAge := F.Flow2(
|
||||
partialPersonLenses.age.Get,
|
||||
agePrism.GetOption,
|
||||
option.Map(personLenses.Age.Set),
|
||||
)
|
||||
|
||||
// Combine the field validators and apply them to build a Person
|
||||
return F.Pipe2(
|
||||
A.From(maybeName, maybeAge),
|
||||
foldPartialPersons,
|
||||
readeroption.Promap(reader.Read[*PartialPerson](emptyPartialPerson), reader.Read[*Person](emptyPerson)),
|
||||
makePerson := F.Pipe2(
|
||||
readeroption.Do[*PartialPerson](emptyPerson),
|
||||
readeroption.ApSL(personLenses.Name, maybeName),
|
||||
readeroption.ApSL(personLenses.Age, maybeAge),
|
||||
)
|
||||
|
||||
return F.Flow2(
|
||||
reader.Read[*PartialPerson](emptyPartialPerson),
|
||||
makePerson,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -200,7 +187,7 @@ func buildEndomorphism() Reader[*Person, Endomorphism[*PartialPerson]] {
|
||||
name := F.Flow3(
|
||||
personLenses.Name.Get,
|
||||
namePrism.ReverseGet,
|
||||
partialPersonLenses.name.Set,
|
||||
WithName,
|
||||
)
|
||||
|
||||
// age extracts the validated age, converts it to int,
|
||||
@@ -208,7 +195,7 @@ func buildEndomorphism() Reader[*Person, Endomorphism[*PartialPerson]] {
|
||||
age := F.Flow3(
|
||||
personLenses.Age.Get,
|
||||
agePrism.ReverseGet,
|
||||
partialPersonLenses.age.Set,
|
||||
WithAge,
|
||||
)
|
||||
|
||||
// Combine the field extractors into a single builder
|
||||
|
||||
Reference in New Issue
Block a user