mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-17 23:37:41 +02:00
1375 lines
35 KiB
Go
1375 lines
35 KiB
Go
// 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 readerresult
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
N "github.com/IBM/fp-go/v2/number"
|
|
S "github.com/IBM/fp-go/v2/string"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// Helper types for testing
|
|
// fp-go:Lens
|
|
type User struct {
|
|
ID int
|
|
Name string
|
|
}
|
|
|
|
// fp-go:Lens
|
|
type Config struct {
|
|
Port int
|
|
DatabaseURL string
|
|
}
|
|
|
|
func TestFromEither(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("lifts successful Result", func(t *testing.T) {
|
|
// FromEither expects a Result[A] which is Either[error, A]
|
|
// We need to create it properly using the result package
|
|
rr := Right(42)
|
|
value, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
|
|
t.Run("lifts failing Result", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
rr := Left[int](testErr)
|
|
_, err := rr(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|
|
|
|
func TestFromResult(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("creates successful ReaderResult", func(t *testing.T) {
|
|
rr := FromResult(42, nil)
|
|
value, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
|
|
t.Run("creates failing ReaderResult", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
rr := FromResult(0, testErr)
|
|
_, err := rr(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|
|
|
|
func TestLeftAndRight(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("Right creates successful value", func(t *testing.T) {
|
|
rr := Right(42)
|
|
value, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
|
|
t.Run("Left creates error", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
rr := Left[int](testErr)
|
|
_, err := rr(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
|
|
t.Run("Of is alias for Right", func(t *testing.T) {
|
|
rr := Of(42)
|
|
value, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
}
|
|
|
|
func TestFromReader(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("lifts Reader as success", func(t *testing.T) {
|
|
r := func(ctx context.Context) int {
|
|
return 42
|
|
}
|
|
rr := FromReader(r)
|
|
value, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
|
|
t.Run("uses context", func(t *testing.T) {
|
|
type key int
|
|
const testKey key = 0
|
|
ctx := context.WithValue(context.Background(), testKey, 100)
|
|
|
|
r := func(ctx context.Context) int {
|
|
return ctx.Value(testKey).(int)
|
|
}
|
|
rr := FromReader(r)
|
|
value, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 100, value)
|
|
})
|
|
}
|
|
|
|
func TestMonadMap(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("transforms success value", func(t *testing.T) {
|
|
rr := Right(42)
|
|
mapped := MonadMap(rr, S.Format[int]("Value: %d"))
|
|
value, err := mapped(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Value: 42", value)
|
|
})
|
|
|
|
t.Run("propagates error", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
rr := Left[int](testErr)
|
|
mapped := MonadMap(rr, S.Format[int]("Value: %d"))
|
|
_, err := mapped(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
|
|
t.Run("chains multiple maps", func(t *testing.T) {
|
|
rr := Right(10)
|
|
result := MonadMap(
|
|
MonadMap(rr, N.Mul(2)),
|
|
strconv.Itoa,
|
|
)
|
|
value, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "20", value)
|
|
})
|
|
}
|
|
|
|
func TestMap(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("curried version works", func(t *testing.T) {
|
|
rr := Right(42)
|
|
mapper := Map(S.Format[int]("Value: %d"))
|
|
mapped := mapper(rr)
|
|
value, err := mapped(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Value: 42", value)
|
|
})
|
|
}
|
|
|
|
func TestMonadChain(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("sequences dependent computations", func(t *testing.T) {
|
|
getUser := Right(User{ID: 1, Name: "Alice"})
|
|
getPosts := func(user User) ReaderResult[string] {
|
|
return Right(fmt.Sprintf("Posts for %s", user.Name))
|
|
}
|
|
result := MonadChain(getUser, getPosts)
|
|
value, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Posts for Alice", value)
|
|
})
|
|
|
|
t.Run("propagates first error", func(t *testing.T) {
|
|
testErr := errors.New("first error")
|
|
getUser := Left[User](testErr)
|
|
getPosts := func(user User) ReaderResult[string] {
|
|
return Right("posts")
|
|
}
|
|
result := MonadChain(getUser, getPosts)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
|
|
t.Run("propagates second error", func(t *testing.T) {
|
|
testErr := errors.New("second error")
|
|
getUser := Right(User{ID: 1, Name: "Alice"})
|
|
getPosts := func(user User) ReaderResult[string] {
|
|
return Left[string](testErr)
|
|
}
|
|
result := MonadChain(getUser, getPosts)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|
|
|
|
func TestChain(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("curried version works", func(t *testing.T) {
|
|
getUser := Right(User{ID: 1, Name: "Alice"})
|
|
chainer := Chain(func(user User) ReaderResult[string] {
|
|
return Right(fmt.Sprintf("Posts for %s", user.Name))
|
|
})
|
|
result := chainer(getUser)
|
|
value, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Posts for Alice", value)
|
|
})
|
|
}
|
|
|
|
func TestAsk(t *testing.T) {
|
|
t.Run("retrieves environment", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
rr := Ask()
|
|
retrievedCtx, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, ctx, retrievedCtx)
|
|
})
|
|
|
|
t.Run("always succeeds", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
rr := Ask()
|
|
_, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestAsks(t *testing.T) {
|
|
type key int
|
|
const userKey key = 0
|
|
|
|
t.Run("extracts value from environment", func(t *testing.T) {
|
|
user := User{ID: 1, Name: "Alice"}
|
|
ctx := context.WithValue(context.Background(), userKey, user)
|
|
|
|
getUser := Asks(func(ctx context.Context) User {
|
|
return ctx.Value(userKey).(User)
|
|
})
|
|
retrievedUser, err := getUser(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, user, retrievedUser)
|
|
})
|
|
|
|
t.Run("works with different extractors", func(t *testing.T) {
|
|
ctx := context.WithValue(context.Background(), userKey, 42)
|
|
|
|
getID := Asks(func(ctx context.Context) int {
|
|
return ctx.Value(userKey).(int)
|
|
})
|
|
id, err := getID(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, id)
|
|
})
|
|
}
|
|
|
|
func TestFlatten(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("removes one level of nesting", func(t *testing.T) {
|
|
nested := Right(Right(42))
|
|
flattened := Flatten(nested)
|
|
value, err := flattened(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
|
|
t.Run("propagates outer error", func(t *testing.T) {
|
|
testErr := errors.New("outer error")
|
|
nested := Left[ReaderResult[int]](testErr)
|
|
flattened := Flatten(nested)
|
|
_, err := flattened(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
|
|
t.Run("propagates inner error", func(t *testing.T) {
|
|
testErr := errors.New("inner error")
|
|
nested := Right(Left[int](testErr))
|
|
flattened := Flatten(nested)
|
|
_, err := flattened(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|
|
|
|
func TestRead(t *testing.T) {
|
|
t.Run("executes ReaderResult with context", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
rr := Right(42)
|
|
execute := Read[int](ctx)
|
|
value, err := execute(rr)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
}
|
|
|
|
func TestCurry0(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("converts function to ReaderResult", func(t *testing.T) {
|
|
f := func(ctx context.Context) (int, error) {
|
|
return 42, nil
|
|
}
|
|
rr := Curry0(f)
|
|
value, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
}
|
|
|
|
func TestCurry1(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("curries function with one parameter", func(t *testing.T) {
|
|
f := func(ctx context.Context, id int) (User, error) {
|
|
return User{ID: id, Name: "Alice"}, nil
|
|
}
|
|
getUserRR := Curry1(f)
|
|
rr := getUserRR(1)
|
|
user, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, User{ID: 1, Name: "Alice"}, user)
|
|
})
|
|
}
|
|
|
|
func TestCurry2(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("curries function with two parameters", func(t *testing.T) {
|
|
f := func(ctx context.Context, id int, name string) (User, error) {
|
|
return User{ID: id, Name: name}, nil
|
|
}
|
|
updateUserRR := Curry2(f)
|
|
rr := updateUserRR(1)("Bob")
|
|
user, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, User{ID: 1, Name: "Bob"}, user)
|
|
})
|
|
}
|
|
|
|
func TestFrom1(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("converts function to uncurried form", func(t *testing.T) {
|
|
f := func(ctx context.Context, id int) (User, error) {
|
|
return User{ID: id, Name: "Alice"}, nil
|
|
}
|
|
getUserRR := From1(f)
|
|
rr := getUserRR(1)
|
|
user, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, User{ID: 1, Name: "Alice"}, user)
|
|
})
|
|
}
|
|
|
|
// Note: SequenceReader and TraverseReader tests are complex due to type system interactions
|
|
// These functions are tested indirectly through their usage in other tests
|
|
|
|
func TestSequenceArray(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("sequences array of ReaderResults", func(t *testing.T) {
|
|
readers := []ReaderResult[int]{
|
|
Right(1),
|
|
Right(2),
|
|
Right(3),
|
|
}
|
|
result := SequenceArray(readers)
|
|
values, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []int{1, 2, 3}, values)
|
|
})
|
|
|
|
t.Run("fails on first error", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
readers := []ReaderResult[int]{
|
|
Right(1),
|
|
Left[int](testErr),
|
|
Right(3),
|
|
}
|
|
result := SequenceArray(readers)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|
|
|
|
func TestTraverseArray(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("applies function to each element", func(t *testing.T) {
|
|
double := func(n int) ReaderResult[int] {
|
|
return Right(n * 2)
|
|
}
|
|
numbers := []int{1, 2, 3}
|
|
result := TraverseArray(double)(numbers)
|
|
values, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []int{2, 4, 6}, values)
|
|
})
|
|
}
|
|
|
|
func TestSequenceT2(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("combines two ReaderResults", func(t *testing.T) {
|
|
rr1 := Right(42)
|
|
rr2 := Right("hello")
|
|
result := SequenceT2(rr1, rr2)
|
|
tuple, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, tuple.F1)
|
|
assert.Equal(t, "hello", tuple.F2)
|
|
})
|
|
|
|
t.Run("fails if first fails", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
rr1 := Left[int](testErr)
|
|
rr2 := Right("hello")
|
|
result := SequenceT2(rr1, rr2)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|
|
|
|
func TestDo(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("initializes do-notation", func(t *testing.T) {
|
|
type State struct {
|
|
Value int
|
|
}
|
|
result := Do(State{})
|
|
state, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, State{Value: 0}, state)
|
|
})
|
|
}
|
|
|
|
func TestBindTo(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("binds value to state", func(t *testing.T) {
|
|
type State struct {
|
|
User User
|
|
}
|
|
getUser := Right(User{ID: 1, Name: "Alice"})
|
|
result := BindTo(func(u User) State {
|
|
return State{User: u}
|
|
})(getUser)
|
|
state, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, User{ID: 1, Name: "Alice"}, state.User)
|
|
})
|
|
}
|
|
|
|
func TestMonadAp(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("applies function to value", func(t *testing.T) {
|
|
addTen := Right(N.Add(10))
|
|
value := Right(32)
|
|
result := MonadAp(addTen, value)
|
|
output, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, output)
|
|
})
|
|
|
|
t.Run("propagates function error", func(t *testing.T) {
|
|
testErr := errors.New("function error")
|
|
failedFn := Left[func(int) int](testErr)
|
|
value := Right(32)
|
|
result := MonadAp(failedFn, value)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
|
|
t.Run("propagates value error", func(t *testing.T) {
|
|
testErr := errors.New("value error")
|
|
addTen := Right(N.Add(10))
|
|
failedValue := Left[int](testErr)
|
|
result := MonadAp(addTen, failedValue)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
|
|
t.Run("handles context cancellation", func(t *testing.T) {
|
|
cancelCtx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
addTen := Right(N.Add(10))
|
|
value := Right(32)
|
|
result := MonadAp(addTen, value)
|
|
_, err := result(cancelCtx)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, context.Canceled, err)
|
|
})
|
|
|
|
t.Run("works with different types", func(t *testing.T) {
|
|
toString := Right(func(n int) string {
|
|
return fmt.Sprintf("Number: %d", n)
|
|
})
|
|
value := Right(42)
|
|
result := MonadAp(toString, value)
|
|
output, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Number: 42", output)
|
|
})
|
|
|
|
t.Run("works with complex functions", func(t *testing.T) {
|
|
multiply := Right(func(user User) int {
|
|
return user.ID * 10
|
|
})
|
|
user := Right(User{ID: 5, Name: "Bob"})
|
|
result := MonadAp(multiply, user)
|
|
output, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 50, output)
|
|
})
|
|
|
|
t.Run("executes both computations concurrently", func(t *testing.T) {
|
|
// This test verifies that both computations run concurrently
|
|
// by checking that they both complete even if one takes time
|
|
slowFn := func(ctx context.Context) (func(int) int, error) {
|
|
// Simulate some work
|
|
return N.Mul(2), nil
|
|
}
|
|
slowValue := func(ctx context.Context) (int, error) {
|
|
// Simulate some work
|
|
return 21, nil
|
|
}
|
|
|
|
result := MonadAp(slowFn, slowValue)
|
|
output, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, output)
|
|
})
|
|
}
|
|
|
|
func TestAp(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("curried version works", func(t *testing.T) {
|
|
value := Right(32)
|
|
addTen := Right(N.Add(10))
|
|
|
|
applyValue := Ap[int](value)
|
|
result := applyValue(addTen)
|
|
output, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, output)
|
|
})
|
|
|
|
t.Run("works in pipeline", func(t *testing.T) {
|
|
value := Right(32)
|
|
addTen := Right(N.Add(10))
|
|
|
|
// Using Ap in a functional pipeline style
|
|
result := Ap[int](value)(addTen)
|
|
output, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, output)
|
|
})
|
|
|
|
t.Run("propagates errors", func(t *testing.T) {
|
|
testErr := errors.New("value error")
|
|
failedValue := Left[int](testErr)
|
|
addTen := Right(N.Add(10))
|
|
|
|
result := Ap[int](failedValue)(addTen)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|
|
|
|
func TestLocal(t *testing.T) {
|
|
t.Run("transforms context with custom value", func(t *testing.T) {
|
|
type key int
|
|
const userKey key = 0
|
|
|
|
// Create a computation that reads from context
|
|
getUser := Asks(func(ctx context.Context) string {
|
|
if user := ctx.Value(userKey); user != nil {
|
|
return user.(string)
|
|
}
|
|
return "unknown"
|
|
})
|
|
|
|
// Transform context to add user value
|
|
addUser := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
newCtx := context.WithValue(ctx, userKey, "Alice")
|
|
return newCtx, func() {} // No-op cancel
|
|
})
|
|
|
|
// Apply transformation
|
|
result := addUser(getUser)
|
|
user, err := result(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Alice", user)
|
|
})
|
|
|
|
t.Run("cancel function is called", func(t *testing.T) {
|
|
cancelCalled := false
|
|
|
|
transform := Local[int](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
return ctx, func() {
|
|
cancelCalled = true
|
|
}
|
|
})
|
|
|
|
rr := Right(42)
|
|
result := transform(rr)
|
|
_, err := result(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.True(t, cancelCalled, "cancel function should be called")
|
|
})
|
|
|
|
t.Run("propagates errors", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
transform := Local[int](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
return ctx, func() {}
|
|
})
|
|
|
|
rr := Left[int](testErr)
|
|
result := transform(rr)
|
|
_, err := result(context.Background())
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
|
|
t.Run("nested transformations", func(t *testing.T) {
|
|
type key int
|
|
const key1 key = 0
|
|
const key2 key = 1
|
|
|
|
getValues := Asks(func(ctx context.Context) string {
|
|
v1 := ctx.Value(key1).(string)
|
|
v2 := ctx.Value(key2).(string)
|
|
return v1 + ":" + v2
|
|
})
|
|
|
|
addFirst := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithValue(ctx, key1, "A"), func() {}
|
|
})
|
|
|
|
addSecond := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithValue(ctx, key2, "B"), func() {}
|
|
})
|
|
|
|
result := addSecond(addFirst(getValues))
|
|
value, err := result(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "A:B", value)
|
|
})
|
|
|
|
t.Run("preserves parent context values", func(t *testing.T) {
|
|
type key int
|
|
const parentKey key = 0
|
|
const childKey key = 1
|
|
|
|
parentCtx := context.WithValue(context.Background(), parentKey, "parent")
|
|
|
|
getValues := Asks(func(ctx context.Context) string {
|
|
parent := ctx.Value(parentKey).(string)
|
|
child := ctx.Value(childKey).(string)
|
|
return parent + ":" + child
|
|
})
|
|
|
|
addChild := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithValue(ctx, childKey, "child"), func() {}
|
|
})
|
|
|
|
result := addChild(getValues)
|
|
value, err := result(parentCtx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "parent:child", value)
|
|
})
|
|
}
|
|
|
|
func TestWithTimeout(t *testing.T) {
|
|
t.Run("completes within timeout", func(t *testing.T) {
|
|
rr := Right(42)
|
|
result := WithTimeout[int](1 * time.Second)(rr)
|
|
value, err := result(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
|
|
t.Run("cancels on timeout", func(t *testing.T) {
|
|
// Create a computation that takes longer than timeout
|
|
slowComputation := func(ctx context.Context) (int, error) {
|
|
select {
|
|
case <-time.After(200 * time.Millisecond):
|
|
return 42, nil
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
}
|
|
}
|
|
|
|
result := WithTimeout[int](50 * time.Millisecond)(slowComputation)
|
|
_, err := result(context.Background())
|
|
assert.Error(t, err)
|
|
assert.Equal(t, context.DeadlineExceeded, err)
|
|
})
|
|
|
|
t.Run("propagates errors", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
rr := Left[int](testErr)
|
|
result := WithTimeout[int](1 * time.Second)(rr)
|
|
_, err := result(context.Background())
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
|
|
t.Run("respects parent context timeout", func(t *testing.T) {
|
|
// Parent has shorter timeout
|
|
parentCtx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
defer cancel()
|
|
|
|
slowComputation := func(ctx context.Context) (int, error) {
|
|
select {
|
|
case <-time.After(200 * time.Millisecond):
|
|
return 42, nil
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
}
|
|
}
|
|
|
|
// Child has longer timeout, but parent's shorter timeout should win
|
|
result := WithTimeout[int](1 * time.Second)(slowComputation)
|
|
_, err := result(parentCtx)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, context.DeadlineExceeded, err)
|
|
})
|
|
|
|
t.Run("works with context-aware operations", func(t *testing.T) {
|
|
type key int
|
|
const dataKey key = 0
|
|
|
|
ctx := context.WithValue(context.Background(), dataKey, "test-data")
|
|
|
|
getData := Asks(func(ctx context.Context) string {
|
|
return ctx.Value(dataKey).(string)
|
|
})
|
|
|
|
result := WithTimeout[string](1 * time.Second)(getData)
|
|
value, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "test-data", value)
|
|
})
|
|
|
|
t.Run("multiple timeouts compose correctly", func(t *testing.T) {
|
|
rr := Right(42)
|
|
// Apply multiple timeouts - the shortest should win
|
|
result := WithTimeout[int](100 * time.Millisecond)(
|
|
WithTimeout[int](1 * time.Second)(rr),
|
|
)
|
|
value, err := result(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
}
|
|
|
|
func TestWithDeadline(t *testing.T) {
|
|
t.Run("completes before deadline", func(t *testing.T) {
|
|
deadline := time.Now().Add(1 * time.Second)
|
|
rr := Right(42)
|
|
result := WithDeadline[int](deadline)(rr)
|
|
value, err := result(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
|
|
t.Run("cancels after deadline", func(t *testing.T) {
|
|
deadline := time.Now().Add(50 * time.Millisecond)
|
|
|
|
slowComputation := func(ctx context.Context) (int, error) {
|
|
select {
|
|
case <-time.After(200 * time.Millisecond):
|
|
return 42, nil
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
}
|
|
}
|
|
|
|
result := WithDeadline[int](deadline)(slowComputation)
|
|
_, err := result(context.Background())
|
|
assert.Error(t, err)
|
|
assert.Equal(t, context.DeadlineExceeded, err)
|
|
})
|
|
|
|
t.Run("propagates errors", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
deadline := time.Now().Add(1 * time.Second)
|
|
rr := Left[int](testErr)
|
|
result := WithDeadline[int](deadline)(rr)
|
|
_, err := result(context.Background())
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
|
|
t.Run("respects parent context deadline", func(t *testing.T) {
|
|
// Parent has earlier deadline
|
|
parentDeadline := time.Now().Add(50 * time.Millisecond)
|
|
parentCtx, cancel := context.WithDeadline(context.Background(), parentDeadline)
|
|
defer cancel()
|
|
|
|
slowComputation := func(ctx context.Context) (int, error) {
|
|
select {
|
|
case <-time.After(200 * time.Millisecond):
|
|
return 42, nil
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
}
|
|
}
|
|
|
|
// Child has later deadline, but parent's earlier deadline should win
|
|
childDeadline := time.Now().Add(1 * time.Second)
|
|
result := WithDeadline[int](childDeadline)(slowComputation)
|
|
_, err := result(parentCtx)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, context.DeadlineExceeded, err)
|
|
})
|
|
|
|
t.Run("works with absolute time", func(t *testing.T) {
|
|
// Set deadline to a specific time in the future
|
|
deadline := time.Date(2130, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
rr := Right(42)
|
|
result := WithDeadline[int](deadline)(rr)
|
|
value, err := result(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
|
|
t.Run("handles past deadline", func(t *testing.T) {
|
|
// Deadline already passed - context will be immediately cancelled
|
|
deadline := time.Now().Add(-1 * time.Second)
|
|
|
|
// Use a computation that checks context cancellation
|
|
checkCtx := func(ctx context.Context) (int, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
return 42, nil
|
|
}
|
|
|
|
result := WithDeadline[int](deadline)(checkCtx)
|
|
_, err := result(context.Background())
|
|
assert.Error(t, err)
|
|
assert.Equal(t, context.DeadlineExceeded, err)
|
|
})
|
|
|
|
t.Run("works with context values", func(t *testing.T) {
|
|
type key int
|
|
const configKey key = 0
|
|
|
|
ctx := context.WithValue(context.Background(), configKey, Config{Port: 8080})
|
|
deadline := time.Now().Add(1 * time.Second)
|
|
|
|
getConfig := Asks(func(ctx context.Context) Config {
|
|
return ctx.Value(configKey).(Config)
|
|
})
|
|
|
|
result := WithDeadline[Config](deadline)(getConfig)
|
|
config, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, Config{Port: 8080}, config)
|
|
})
|
|
}
|
|
|
|
func TestLocalWithTimeoutAndDeadline(t *testing.T) {
|
|
t.Run("combines Local with WithTimeout", func(t *testing.T) {
|
|
type key int
|
|
const userKey key = 0
|
|
|
|
addUser := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithValue(ctx, userKey, "Alice"), func() {}
|
|
})
|
|
|
|
getUser := Asks(func(ctx context.Context) string {
|
|
return ctx.Value(userKey).(string)
|
|
})
|
|
|
|
result := WithTimeout[string](1 * time.Second)(addUser(getUser))
|
|
user, err := result(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Alice", user)
|
|
})
|
|
|
|
t.Run("combines Local with WithDeadline", func(t *testing.T) {
|
|
type key int
|
|
const dataKey key = 0
|
|
|
|
addData := Local[int](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithValue(ctx, dataKey, 42), func() {}
|
|
})
|
|
|
|
getData := Asks(func(ctx context.Context) int {
|
|
return ctx.Value(dataKey).(int)
|
|
})
|
|
|
|
deadline := time.Now().Add(1 * time.Second)
|
|
result := WithDeadline[int](deadline)(addData(getData))
|
|
value, err := result(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
|
|
t.Run("complex composition", func(t *testing.T) {
|
|
type key int
|
|
const key1 key = 0
|
|
const key2 key = 1
|
|
|
|
// Add first value
|
|
addFirst := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithValue(ctx, key1, "A"), func() {}
|
|
})
|
|
|
|
// Add second value
|
|
addSecond := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithValue(ctx, key2, "B"), func() {}
|
|
})
|
|
|
|
// Read both values
|
|
getValues := Asks(func(ctx context.Context) string {
|
|
v1 := ctx.Value(key1).(string)
|
|
v2 := ctx.Value(key2).(string)
|
|
return v1 + ":" + v2
|
|
})
|
|
|
|
// Compose with timeout
|
|
result := WithTimeout[string](1 * time.Second)(
|
|
addSecond(addFirst(getValues)),
|
|
)
|
|
|
|
value, err := result(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "A:B", value)
|
|
})
|
|
}
|
|
|
|
func TestMonadTraverseArray(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("applies function to each element", func(t *testing.T) {
|
|
double := func(n int) ReaderResult[int] {
|
|
return Right(n * 2)
|
|
}
|
|
numbers := []int{1, 2, 3}
|
|
result := MonadTraverseArray(numbers, double)
|
|
values, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []int{2, 4, 6}, values)
|
|
})
|
|
|
|
t.Run("fails on first error", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
failOnTwo := func(n int) ReaderResult[int] {
|
|
if n == 2 {
|
|
return Left[int](testErr)
|
|
}
|
|
return Right(n * 2)
|
|
}
|
|
numbers := []int{1, 2, 3}
|
|
result := MonadTraverseArray(numbers, failOnTwo)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|
|
|
|
func TestTraverseArrayWithIndex(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("applies function with index", func(t *testing.T) {
|
|
addIndex := func(idx int, s string) ReaderResult[string] {
|
|
return Right(fmt.Sprintf("%d:%s", idx, s))
|
|
}
|
|
items := []string{"a", "b", "c"}
|
|
result := TraverseArrayWithIndex(addIndex)(items)
|
|
values, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"0:a", "1:b", "2:c"}, values)
|
|
})
|
|
|
|
t.Run("fails on error", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
failOnIndex := func(idx int, s string) ReaderResult[string] {
|
|
if idx == 1 {
|
|
return Left[string](testErr)
|
|
}
|
|
return Right(s)
|
|
}
|
|
items := []string{"a", "b", "c"}
|
|
result := TraverseArrayWithIndex(failOnIndex)(items)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|
|
|
|
func TestBracket(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("ensures resource cleanup on success", func(t *testing.T) {
|
|
released := false
|
|
result := Bracket(
|
|
func() ReaderResult[int] {
|
|
return Right(42)
|
|
},
|
|
func(resource int) ReaderResult[string] {
|
|
return Right(fmt.Sprintf("Resource: %d", resource))
|
|
},
|
|
func(resource int, value string, err error) ReaderResult[any] {
|
|
released = true
|
|
return Right[any](nil)
|
|
},
|
|
)
|
|
value, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Resource: 42", value)
|
|
assert.True(t, released)
|
|
})
|
|
|
|
t.Run("ensures resource cleanup on failure", func(t *testing.T) {
|
|
released := false
|
|
testErr := errors.New("use failed")
|
|
result := Bracket(
|
|
func() ReaderResult[int] {
|
|
return Right(42)
|
|
},
|
|
func(resource int) ReaderResult[string] {
|
|
return Left[string](testErr)
|
|
},
|
|
func(resource int, value string, err error) ReaderResult[any] {
|
|
released = true
|
|
assert.Equal(t, testErr, err)
|
|
return Right[any](nil)
|
|
},
|
|
)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
assert.True(t, released)
|
|
})
|
|
|
|
t.Run("does not release if acquire fails", func(t *testing.T) {
|
|
released := false
|
|
testErr := errors.New("acquire failed")
|
|
result := Bracket(
|
|
func() ReaderResult[int] {
|
|
return Left[int](testErr)
|
|
},
|
|
func(resource int) ReaderResult[string] {
|
|
return Right("should not execute")
|
|
},
|
|
func(resource int, value string, err error) ReaderResult[any] {
|
|
released = true
|
|
return Right[any](nil)
|
|
},
|
|
)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
assert.False(t, released)
|
|
})
|
|
}
|
|
|
|
func TestWithContextCancellation(t *testing.T) {
|
|
t.Run("returns error on cancelled context", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
rr := WithContext(Right(42))
|
|
_, err := rr(ctx)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("executes on valid context", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
rr := WithContext(Right(42))
|
|
value, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
}
|
|
|
|
func TestWithContextK(t *testing.T) {
|
|
t.Run("wraps Kleisli with cancellation check", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
kleisli := func(n int) ReaderResult[string] {
|
|
return Right(fmt.Sprintf("Value: %d", n))
|
|
}
|
|
|
|
wrapped := WithContextK(kleisli)
|
|
result := wrapped(42)
|
|
_, err := result(ctx)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("executes on valid context", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
kleisli := func(n int) ReaderResult[string] {
|
|
return Right(fmt.Sprintf("Value: %d", n))
|
|
}
|
|
|
|
wrapped := WithContextK(kleisli)
|
|
result := wrapped(42)
|
|
value, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Value: 42", value)
|
|
})
|
|
}
|
|
|
|
func TestUncurry1(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("converts curried to uncurried", func(t *testing.T) {
|
|
curried := func(id int) ReaderResult[User] {
|
|
return Right(User{ID: id, Name: "Alice"})
|
|
}
|
|
uncurried := Uncurry1(curried)
|
|
user, err := uncurried(ctx, 42)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, User{ID: 42, Name: "Alice"}, user)
|
|
})
|
|
}
|
|
|
|
func TestUncurry2(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("converts curried to uncurried", func(t *testing.T) {
|
|
curried := func(id int) func(name string) ReaderResult[User] {
|
|
return func(name string) ReaderResult[User] {
|
|
return Right(User{ID: id, Name: name})
|
|
}
|
|
}
|
|
uncurried := Uncurry2(curried)
|
|
user, err := uncurried(ctx, 42, "Bob")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, User{ID: 42, Name: "Bob"}, user)
|
|
})
|
|
}
|
|
|
|
func TestUncurry3(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("converts curried to uncurried", func(t *testing.T) {
|
|
curried := func(a int) func(b int) func(c int) ReaderResult[int] {
|
|
return func(b int) func(c int) ReaderResult[int] {
|
|
return func(c int) ReaderResult[int] {
|
|
return Right(a + b + c)
|
|
}
|
|
}
|
|
}
|
|
uncurried := Uncurry3(curried)
|
|
result, err := uncurried(ctx, 1, 2, 3)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 6, result)
|
|
})
|
|
}
|
|
|
|
func TestFrom0(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("creates lazy ReaderResult", func(t *testing.T) {
|
|
f := func(ctx context.Context) (int, error) {
|
|
return 42, nil
|
|
}
|
|
thunk := From0(f)
|
|
rr := thunk()
|
|
value, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
}
|
|
|
|
func TestFrom2(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("converts function to uncurried form", func(t *testing.T) {
|
|
f := func(ctx context.Context, id int, name string) (User, error) {
|
|
return User{ID: id, Name: name}, nil
|
|
}
|
|
updateUserRR := From2(f)
|
|
rr := updateUserRR(42, "Charlie")
|
|
user, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, User{ID: 42, Name: "Charlie"}, user)
|
|
})
|
|
}
|
|
|
|
func TestFrom3(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("converts function to uncurried form", func(t *testing.T) {
|
|
f := func(ctx context.Context, a, b, c int) (int, error) {
|
|
return a + b + c, nil
|
|
}
|
|
sumRR := From3(f)
|
|
rr := sumRR(1, 2, 3)
|
|
result, err := rr(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 6, result)
|
|
})
|
|
}
|
|
|
|
func TestAlternativeMonoid(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("combines successful values", func(t *testing.T) {
|
|
intMonoid := N.MonoidSum[int]()
|
|
rrMonoid := AlternativeMonoid(intMonoid)
|
|
|
|
rr1 := Right(10)
|
|
rr2 := Right(20)
|
|
combined := rrMonoid.Concat(rr1, rr2)
|
|
value, err := combined(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 30, value)
|
|
})
|
|
|
|
t.Run("uses second on first failure", func(t *testing.T) {
|
|
intMonoid := N.MonoidSum[int]()
|
|
rrMonoid := AlternativeMonoid(intMonoid)
|
|
|
|
testErr := errors.New("first failed")
|
|
rr1 := Left[int](testErr)
|
|
rr2 := Right(42)
|
|
combined := rrMonoid.Concat(rr1, rr2)
|
|
value, err := combined(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
}
|
|
|
|
func TestAltMonoid(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("uses custom zero", func(t *testing.T) {
|
|
zero := func() ReaderResult[int] {
|
|
return Right(0)
|
|
}
|
|
rrMonoid := AltMonoid(zero)
|
|
|
|
empty := rrMonoid.Empty()
|
|
value, err := empty(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 0, value)
|
|
})
|
|
|
|
t.Run("tries alternatives", func(t *testing.T) {
|
|
zero := func() ReaderResult[int] {
|
|
return Left[int](errors.New("empty"))
|
|
}
|
|
rrMonoid := AltMonoid(zero)
|
|
|
|
testErr := errors.New("first failed")
|
|
rr1 := Left[int](testErr)
|
|
rr2 := Right(42)
|
|
combined := rrMonoid.Concat(rr1, rr2)
|
|
value, err := combined(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, value)
|
|
})
|
|
}
|
|
|
|
func TestApplicativeMonoid(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("combines both computations", func(t *testing.T) {
|
|
intMonoid := N.MonoidSum[int]()
|
|
rrMonoid := ApplicativeMonoid(intMonoid)
|
|
|
|
rr1 := Right(10)
|
|
rr2 := Right(20)
|
|
combined := rrMonoid.Concat(rr1, rr2)
|
|
value, err := combined(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 30, value)
|
|
})
|
|
|
|
t.Run("fails if either fails", func(t *testing.T) {
|
|
intMonoid := N.MonoidSum[int]()
|
|
rrMonoid := ApplicativeMonoid(intMonoid)
|
|
|
|
testErr := errors.New("failed")
|
|
rr1 := Left[int](testErr)
|
|
rr2 := Right(42)
|
|
combined := rrMonoid.Concat(rr1, rr2)
|
|
_, err := combined(ctx)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestSequenceT1(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("wraps single value in tuple", func(t *testing.T) {
|
|
rr := Right(42)
|
|
result := SequenceT1(rr)
|
|
tuple, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 42, tuple.F1)
|
|
})
|
|
}
|
|
|
|
func TestSequenceT3(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("combines three ReaderResults", func(t *testing.T) {
|
|
rr1 := Right(1)
|
|
rr2 := Right("two")
|
|
rr3 := Right(3.0)
|
|
result := SequenceT3(rr1, rr2, rr3)
|
|
tuple, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 1, tuple.F1)
|
|
assert.Equal(t, "two", tuple.F2)
|
|
assert.Equal(t, 3.0, tuple.F3)
|
|
})
|
|
|
|
t.Run("fails if any fails", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
rr1 := Right(1)
|
|
rr2 := Left[string](testErr)
|
|
rr3 := Right(3.0)
|
|
result := SequenceT3(rr1, rr2, rr3)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|
|
|
|
func TestSequenceT4(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("combines four ReaderResults", func(t *testing.T) {
|
|
rr1 := Right(1)
|
|
rr2 := Right("two")
|
|
rr3 := Right(3.0)
|
|
rr4 := Right(true)
|
|
result := SequenceT4(rr1, rr2, rr3, rr4)
|
|
tuple, err := result(ctx)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 1, tuple.F1)
|
|
assert.Equal(t, "two", tuple.F2)
|
|
assert.Equal(t, 3.0, tuple.F3)
|
|
assert.Equal(t, true, tuple.F4)
|
|
})
|
|
|
|
t.Run("fails if any fails", func(t *testing.T) {
|
|
testErr := errors.New("test error")
|
|
rr1 := Right(1)
|
|
rr2 := Right("two")
|
|
rr3 := Left[float64](testErr)
|
|
rr4 := Right(true)
|
|
result := SequenceT4(rr1, rr2, rr3, rr4)
|
|
_, err := result(ctx)
|
|
assert.Equal(t, testErr, err)
|
|
})
|
|
}
|