mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-24 12:57:26 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3fdb03df4 | ||
|
|
47727fd514 | ||
|
|
ece7d088ea | ||
|
|
13d25eca32 | ||
|
|
a68e32308d |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x']
|
||||
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x']
|
||||
fail-fast: false # Continue with other versions if one fails
|
||||
steps:
|
||||
# full checkout for semantic-release
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ['1.24.x', '1.25.x']
|
||||
go-version: ['1.24.x', '1.25.x', '1.26.x']
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// From constructs an array from a set of variadic arguments
|
||||
@@ -163,11 +163,11 @@ func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
|
||||
return G.FilterMapWithIndex[[]A, []B](f)
|
||||
}
|
||||
|
||||
// FilterChain maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
|
||||
// ChainOptionK maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
|
||||
//
|
||||
//go:inline
|
||||
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
|
||||
return G.FilterChain[[]A](f)
|
||||
func ChainOptionK[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
|
||||
return G.ChainOptionK[[]A](f)
|
||||
}
|
||||
|
||||
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
|
||||
@@ -453,7 +453,7 @@ func Size[A any](as []A) int {
|
||||
// the second contains elements for which it returns true.
|
||||
//
|
||||
//go:inline
|
||||
func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
|
||||
func MonadPartition[A any](as []A, pred func(A) bool) pair.Pair[[]A, []A] {
|
||||
return G.MonadPartition(as, pred)
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
|
||||
// for which the predicate returns false, the right one those for which the predicate returns true
|
||||
//
|
||||
//go:inline
|
||||
func Partition[A any](pred func(A) bool) func([]A) tuple.Tuple2[[]A, []A] {
|
||||
func Partition[A any](pred func(A) bool) func([]A) pair.Pair[[]A, []A] {
|
||||
return G.Partition[[]A](pred)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -163,11 +163,11 @@ func TestPartition(t *testing.T) {
|
||||
return n > 2
|
||||
}
|
||||
|
||||
assert.Equal(t, T.MakeTuple2(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
|
||||
assert.Equal(t, T.MakeTuple2(From(1), From(3)), Partition(pred)(From(1, 3)))
|
||||
assert.Equal(t, pair.MakePair(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
|
||||
assert.Equal(t, pair.MakePair(From(1), From(3)), Partition(pred)(From(1, 3)))
|
||||
}
|
||||
|
||||
func TestFilterChain(t *testing.T) {
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
src := From(1, 2, 3)
|
||||
|
||||
f := func(i int) O.Option[[]string] {
|
||||
@@ -177,7 +177,7 @@ func TestFilterChain(t *testing.T) {
|
||||
return O.None[[]string]()
|
||||
}
|
||||
|
||||
res := FilterChain(f)(src)
|
||||
res := ChainOptionK(f)(src)
|
||||
|
||||
assert.Equal(t, From("a1", "b1", "a3", "b3"), res)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
FC "github.com/IBM/fp-go/v2/internal/functor"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Of constructs a single element array
|
||||
@@ -215,7 +215,7 @@ func Filter[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) AS {
|
||||
return FilterWithIndex[AS](F.Ignore1of2[int](pred))
|
||||
}
|
||||
|
||||
func FilterChain[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
|
||||
func ChainOptionK[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
|
||||
return F.Flow2(
|
||||
FilterMap[GA, []GB](f),
|
||||
Flatten[[]GB],
|
||||
@@ -234,7 +234,7 @@ func FilterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) O.Option[B])
|
||||
return F.Bind2nd(MonadFilterMapWithIndex[GA, GB, A, B], f)
|
||||
}
|
||||
|
||||
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, GA] {
|
||||
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) pair.Pair[GA, GA] {
|
||||
left := Empty[GA]()
|
||||
right := Empty[GA]()
|
||||
array.Reduce(as, func(c bool, a A) bool {
|
||||
@@ -246,10 +246,10 @@ func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, G
|
||||
return c
|
||||
}, true)
|
||||
// returns the partition
|
||||
return tuple.MakeTuple2(left, right)
|
||||
return pair.MakePair(left, right)
|
||||
}
|
||||
|
||||
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) tuple.Tuple2[GA, GA] {
|
||||
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) pair.Pair[GA, GA] {
|
||||
return F.Bind2nd(MonadPartition[GA, A], pred)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ package generic
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// ZipWith applies a function to pairs of elements at the same index in two arrays, collecting the results in a new array. If one
|
||||
@@ -34,19 +34,19 @@ func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, f
|
||||
|
||||
// Zip takes two arrays and returns an array of corresponding pairs. If one input array is short, excess elements of the
|
||||
// longer array are discarded
|
||||
func Zip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](fb BS) func(AS) CS {
|
||||
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) T.Tuple2[A, B]])(fb, T.MakeTuple2[A, B])
|
||||
func Zip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](fb BS) func(AS) CS {
|
||||
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) pair.Pair[A, B]])(fb, pair.MakePair[A, B])
|
||||
}
|
||||
|
||||
// Unzip is the function is reverse of [Zip]. Takes an array of pairs and return two corresponding arrays
|
||||
func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS, BS] {
|
||||
func Unzip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](cs CS) pair.Pair[AS, BS] {
|
||||
l := len(cs)
|
||||
as := make(AS, l)
|
||||
bs := make(BS, l)
|
||||
for i := range l {
|
||||
t := cs[i]
|
||||
as[i] = t.F1
|
||||
bs[i] = t.F2
|
||||
as[i] = pair.Head(t)
|
||||
bs[i] = pair.Tail(t)
|
||||
}
|
||||
return T.MakeTuple2(as, bs)
|
||||
return pair.MakePair(as, bs)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package array
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/array/generic"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// ZipWith applies a function to pairs of elements at the same index in two arrays,
|
||||
@@ -55,8 +55,8 @@ func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
|
||||
// // Result: [(a, 1), (b, 2)]
|
||||
//
|
||||
//go:inline
|
||||
func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
|
||||
return G.Zip[[]A, []B, []T.Tuple2[A, B]](fb)
|
||||
func Zip[A, B any](fb []B) func([]A) []pair.Pair[A, B] {
|
||||
return G.Zip[[]A, []B, []pair.Pair[A, B]](fb)
|
||||
}
|
||||
|
||||
// Unzip is the reverse of Zip. It takes an array of pairs (tuples) and returns
|
||||
@@ -78,6 +78,6 @@ func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
|
||||
// ages := result.Tail // [30, 25, 35]
|
||||
//
|
||||
//go:inline
|
||||
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
|
||||
func Unzip[A, B any](cs []pair.Pair[A, B]) pair.Pair[[]A, []B] {
|
||||
return G.Unzip[[]A, []B](cs)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestZip(t *testing.T) {
|
||||
|
||||
res := Zip[string](left)(right)
|
||||
|
||||
assert.Equal(t, From(T.MakeTuple2("a", 1), T.MakeTuple2("b", 2), T.MakeTuple2("c", 3)), res)
|
||||
assert.Equal(t, From(pair.MakePair("a", 1), pair.MakePair("b", 2), pair.MakePair("c", 3)), res)
|
||||
}
|
||||
|
||||
func TestUnzip(t *testing.T) {
|
||||
@@ -51,6 +51,6 @@ func TestUnzip(t *testing.T) {
|
||||
|
||||
unzipped := Unzip(zipped)
|
||||
|
||||
assert.Equal(t, right, unzipped.F1)
|
||||
assert.Equal(t, left, unzipped.F2)
|
||||
assert.Equal(t, right, pair.Head(unzipped))
|
||||
assert.Equal(t, left, pair.Tail(unzipped))
|
||||
}
|
||||
|
||||
@@ -194,6 +194,25 @@ func ArrayNotEmpty[T any](arr []T) Reader {
|
||||
}
|
||||
}
|
||||
|
||||
// ArrayEmpty checks if an array is empty.
|
||||
//
|
||||
// This is the complement of ArrayNotEmpty, asserting that a slice has no elements.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayEmpty(t *testing.T) {
|
||||
// empty := []int{}
|
||||
// assert.ArrayEmpty(empty)(t) // Passes
|
||||
//
|
||||
// numbers := []int{1, 2, 3}
|
||||
// assert.ArrayEmpty(numbers)(t) // Fails
|
||||
// }
|
||||
func ArrayEmpty[T any](arr []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Empty(t, arr)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordNotEmpty checks if a map is not empty.
|
||||
//
|
||||
// Example:
|
||||
@@ -211,6 +230,25 @@ func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
}
|
||||
}
|
||||
|
||||
// RecordEmpty checks if a map is empty.
|
||||
//
|
||||
// This is the complement of RecordNotEmpty, asserting that a map has no key-value pairs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestRecordEmpty(t *testing.T) {
|
||||
// empty := map[string]int{}
|
||||
// assert.RecordEmpty(empty)(t) // Passes
|
||||
//
|
||||
// config := map[string]int{"timeout": 30}
|
||||
// assert.RecordEmpty(config)(t) // Fails
|
||||
// }
|
||||
func RecordEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Empty(t, mp)
|
||||
}
|
||||
}
|
||||
|
||||
// StringNotEmpty checks if a string is not empty.
|
||||
//
|
||||
// Example:
|
||||
@@ -504,15 +542,7 @@ func AllOf(readers []Reader) Reader {
|
||||
//
|
||||
//go:inline
|
||||
func RunAll(testcases map[string]Reader) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
current := true
|
||||
for k, r := range testcases {
|
||||
current = current && t.Run(k, func(t1 *testing.T) {
|
||||
r(t1)
|
||||
})
|
||||
}
|
||||
return current
|
||||
}
|
||||
return SequenceRecord(testcases)
|
||||
}
|
||||
|
||||
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
|
||||
|
||||
@@ -85,6 +85,33 @@ func TestArrayNotEmpty(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayEmpty(t *testing.T) {
|
||||
t.Run("should pass for empty array", func(t *testing.T) {
|
||||
arr := []int{}
|
||||
result := ArrayEmpty(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayEmpty to pass for empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for non-empty array", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayEmpty(arr)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ArrayEmpty to fail for non-empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with different types", func(t *testing.T) {
|
||||
strArr := []string{}
|
||||
result := ArrayEmpty(strArr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayEmpty to pass for empty string array")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty map", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
@@ -131,6 +158,33 @@ func TestArrayLength(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordEmpty(t *testing.T) {
|
||||
t.Run("should pass for empty map", func(t *testing.T) {
|
||||
mp := map[string]int{}
|
||||
result := RecordEmpty(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordEmpty to pass for empty map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for non-empty map", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := RecordEmpty(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected RecordEmpty to fail for non-empty map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with different key-value types", func(t *testing.T) {
|
||||
mp := map[int]string{}
|
||||
result := RecordEmpty(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordEmpty to pass for empty map with int keys")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordLength(t *testing.T) {
|
||||
t.Run("should pass when map length matches", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
@@ -150,6 +204,33 @@ func TestRecordLength(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty string", func(t *testing.T) {
|
||||
str := "Hello, World!"
|
||||
result := StringNotEmpty(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringNotEmpty to pass for non-empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for empty string", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
str := ""
|
||||
result := StringNotEmpty(str)(mockT)
|
||||
if result {
|
||||
t.Error("Expected StringNotEmpty to fail for empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass for string with whitespace", func(t *testing.T) {
|
||||
str := " "
|
||||
result := StringNotEmpty(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringNotEmpty to pass for string with whitespace")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringLength(t *testing.T) {
|
||||
t.Run("should pass when string length matches", func(t *testing.T) {
|
||||
str := "hello"
|
||||
|
||||
122
v2/assert/from.go
Normal file
122
v2/assert/from.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// FromReaderIOResult converts a ReaderIOResult[Reader] into a Reader.
|
||||
//
|
||||
// This function bridges the gap between context-aware, IO-based computations that may fail
|
||||
// (ReaderIOResult) and the simpler Reader type used for test assertions. It executes the
|
||||
// ReaderIOResult computation using the test's context, handles any potential errors by
|
||||
// converting them to test failures via NoError, and returns the resulting Reader.
|
||||
//
|
||||
// The conversion process:
|
||||
// 1. Executes the ReaderIOResult with the test context (t.Context())
|
||||
// 2. Runs the resulting IO operation ()
|
||||
// 3. Extracts the Result, converting errors to test failures using NoError
|
||||
// 4. Returns a Reader that can be applied to *testing.T
|
||||
//
|
||||
// This is particularly useful when you have test assertions that need to:
|
||||
// - Access context for cancellation or deadlines
|
||||
// - Perform IO operations (file access, network calls, etc.)
|
||||
// - Handle potential errors gracefully in tests
|
||||
//
|
||||
// Parameters:
|
||||
// - ri: A ReaderIOResult that produces a Reader when given a context and executed
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that can be directly applied to *testing.T for assertion
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithContext(t *testing.T) {
|
||||
// // Create a ReaderIOResult that performs an IO operation
|
||||
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
|
||||
// return func() result.Result[assert.Reader] {
|
||||
// // Simulate database check
|
||||
// if err := db.PingContext(ctx); err != nil {
|
||||
// return result.Error[assert.Reader](err)
|
||||
// }
|
||||
// return result.Of[assert.Reader](assert.NoError(nil))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIOResult(checkDatabase)
|
||||
// assertion(t)
|
||||
// }
|
||||
func FromReaderIOResult(ri ReaderIOResult[Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return F.Pipe1(
|
||||
ri(t.Context())(),
|
||||
result.GetOrElse(NoError),
|
||||
)(t)
|
||||
}
|
||||
}
|
||||
|
||||
// FromReaderIO converts a ReaderIO[Reader] into a Reader.
|
||||
//
|
||||
// This function bridges the gap between context-aware, IO-based computations (ReaderIO)
|
||||
// and the simpler Reader type used for test assertions. It executes the ReaderIO
|
||||
// computation using the test's context and returns the resulting Reader.
|
||||
//
|
||||
// Unlike FromReaderIOResult, this function does not handle errors explicitly - it assumes
|
||||
// the IO operation will succeed or that any errors are handled within the ReaderIO itself.
|
||||
//
|
||||
// The conversion process:
|
||||
// 1. Executes the ReaderIO with the test context (t.Context())
|
||||
// 2. Runs the resulting IO operation ()
|
||||
// 3. Returns a Reader that can be applied to *testing.T
|
||||
//
|
||||
// This is particularly useful when you have test assertions that need to:
|
||||
// - Access context for cancellation or deadlines
|
||||
// - Perform IO operations that don't fail (or handle failures internally)
|
||||
// - Integrate with context-aware testing utilities
|
||||
//
|
||||
// Parameters:
|
||||
// - ri: A ReaderIO that produces a Reader when given a context and executed
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that can be directly applied to *testing.T for assertion
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithIO(t *testing.T) {
|
||||
// // Create a ReaderIO that performs an IO operation
|
||||
// logAndCheck := func(ctx context.Context) func() assert.Reader {
|
||||
// return func() assert.Reader {
|
||||
// // Log something using context
|
||||
// logger.InfoContext(ctx, "Running test")
|
||||
// // Return an assertion
|
||||
// return assert.Equal(42)(computeValue())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIO(logAndCheck)
|
||||
// assertion(t)
|
||||
// }
|
||||
func FromReaderIO(ri ReaderIO[Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return ri(t.Context())()(t)
|
||||
}
|
||||
}
|
||||
383
v2/assert/from_test.go
Normal file
383
v2/assert/from_test.go
Normal file
@@ -0,0 +1,383 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func TestFromReaderIOResult(t *testing.T) {
|
||||
t.Run("should pass when ReaderIOResult returns success with passing assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns a successful Reader
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
// Return a Reader that always passes
|
||||
return result.Of[Reader](func(t *testing.T) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass when ReaderIOResult returns success")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass when ReaderIOResult returns success with Equal assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns a successful Equal assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](Equal(42)(42))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with Equal assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIOResult returns error", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIOResult that returns an error
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Left[Reader](errors.New("test error"))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIOResult to fail when ReaderIOResult returns error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIOResult returns success but assertion fails", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIOResult that returns a failing assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](Equal(42)(43))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIOResult to fail when assertion fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should use test context", func(t *testing.T) {
|
||||
contextUsed := false
|
||||
|
||||
// Create a ReaderIOResult that checks if context is provided
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
if ctx != nil {
|
||||
contextUsed = true
|
||||
}
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](func(t *testing.T) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
reader(t)
|
||||
|
||||
if !contextUsed {
|
||||
t.Error("Expected FromReaderIOResult to use test context")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with NoError assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns NoError assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](NoError(nil))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with NoError assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with complex assertions", func(t *testing.T) {
|
||||
// Create a ReaderIOResult with multiple composed assertions
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
arr := []int{1, 2, 3}
|
||||
assertions := AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](3)(arr),
|
||||
ArrayContains(2)(arr),
|
||||
})
|
||||
return result.Of[Reader](assertions)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with complex assertions")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromReaderIO(t *testing.T) {
|
||||
t.Run("should pass when ReaderIO returns passing assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns a Reader that always passes
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass when ReaderIO returns passing assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass when ReaderIO returns Equal assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns an Equal assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Equal(42)(42)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Equal assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIO returns failing assertion", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIO that returns a failing assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Equal(42)(43)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIO to fail when assertion fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should use test context", func(t *testing.T) {
|
||||
contextUsed := false
|
||||
|
||||
// Create a ReaderIO that checks if context is provided
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
if ctx != nil {
|
||||
contextUsed = true
|
||||
}
|
||||
return func() Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
reader(t)
|
||||
|
||||
if !contextUsed {
|
||||
t.Error("Expected FromReaderIO to use test context")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with NoError assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns NoError assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return NoError(nil)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with NoError assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Error assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns Error assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Error(errors.New("expected error"))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Error assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with complex assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with multiple composed assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
return AllOf([]Reader{
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
ContainsKey[int]("a")(mp),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with complex assertions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with string assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with string assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
str := "hello world"
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(str),
|
||||
StringLength[any, any](11)(str),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with string assertions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Result assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with Result assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
successResult := result.Of[int](42)
|
||||
return Success(successResult)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Success assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Failure assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO with Failure assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
failureResult := result.Left[int](errors.New("test error"))
|
||||
return Failure(failureResult)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Failure assertion")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromReaderIOResultIntegration tests integration scenarios
|
||||
func TestFromReaderIOResultIntegration(t *testing.T) {
|
||||
t.Run("should work in a realistic scenario with context cancellation", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that uses the context
|
||||
ri := func(testCtx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
// Check if context is valid
|
||||
if testCtx == nil {
|
||||
return result.Left[Reader](errors.New("context is nil"))
|
||||
}
|
||||
|
||||
// Return a successful assertion
|
||||
return result.Of[Reader](Equal("test")("test"))
|
||||
}
|
||||
}
|
||||
|
||||
// Use the actual testing.T from the subtest
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected integration test to pass")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromReaderIOIntegration tests integration scenarios
|
||||
func TestFromReaderIOIntegration(t *testing.T) {
|
||||
t.Run("should work in a realistic scenario with logging", func(t *testing.T) {
|
||||
logCalled := false
|
||||
|
||||
// Create a ReaderIO that simulates logging
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
// Simulate logging with context
|
||||
if ctx != nil {
|
||||
logCalled = true
|
||||
}
|
||||
|
||||
// Return an assertion
|
||||
return Equal(100)(100)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
|
||||
if !res {
|
||||
t.Error("Expected integration test to pass")
|
||||
}
|
||||
|
||||
if !logCalled {
|
||||
t.Error("Expected logging to be called")
|
||||
}
|
||||
})
|
||||
}
|
||||
207
v2/assert/logger.go
Normal file
207
v2/assert/logger.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// Logf creates a logging function that outputs formatted test messages using Go's testing.T.Logf.
|
||||
//
|
||||
// This function provides a functional programming approach to test logging, returning a
|
||||
// [ReaderIO] that can be composed with other test operations. It's particularly useful
|
||||
// for debugging tests, tracing execution flow, or documenting test behavior without
|
||||
// affecting test outcomes.
|
||||
//
|
||||
// The function uses a curried design pattern:
|
||||
// 1. First, you provide a format string (prefix) with format verbs (like %v, %d, %s)
|
||||
// 2. This returns a function that takes a value of type T
|
||||
// 3. That function returns a ReaderIO that performs the logging when executed
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - prefix: A format string compatible with fmt.Printf (e.g., "Value: %v", "Count: %d")
|
||||
// The format string should contain exactly one format verb that matches type T
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A function that takes a value of type T and returns a [ReaderIO][*testing.T, Void]
|
||||
// When executed, this ReaderIO logs the formatted message to the test output
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type of value to be logged. Can be any type that can be formatted by fmt
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Debugging test execution by logging intermediate values
|
||||
// - Tracing the flow of complex test scenarios
|
||||
// - Documenting test behavior in the test output
|
||||
// - Logging values in functional pipelines without breaking the chain
|
||||
// - Creating reusable logging operations for specific types
|
||||
//
|
||||
// # Example - Basic Logging
|
||||
//
|
||||
// func TestBasicLogging(t *testing.T) {
|
||||
// // Create a logger for integers
|
||||
// logInt := assert.Logf[int]("Processing value: %d")
|
||||
//
|
||||
// // Use it to log a value
|
||||
// value := 42
|
||||
// logInt(value)(t)() // Outputs: "Processing value: 42"
|
||||
// }
|
||||
//
|
||||
// # Example - Logging in Test Pipeline
|
||||
//
|
||||
// func TestPipelineWithLogging(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Alice", Age: 30}
|
||||
//
|
||||
// // Create a logger for User
|
||||
// logUser := assert.Logf[User]("Testing user: %+v")
|
||||
//
|
||||
// // Log the user being tested
|
||||
// logUser(user)(t)()
|
||||
//
|
||||
// // Continue with assertions
|
||||
// assert.StringNotEmpty(user.Name)(t)
|
||||
// assert.That(func(age int) bool { return age > 0 })(user.Age)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Multiple Loggers for Different Types
|
||||
//
|
||||
// func TestMultipleLoggers(t *testing.T) {
|
||||
// // Create type-specific loggers
|
||||
// logString := assert.Logf[string]("String value: %s")
|
||||
// logInt := assert.Logf[int]("Integer value: %d")
|
||||
// logFloat := assert.Logf[float64]("Float value: %.2f")
|
||||
//
|
||||
// // Use them throughout the test
|
||||
// logString("hello")(t)() // Outputs: "String value: hello"
|
||||
// logInt(42)(t)() // Outputs: "Integer value: 42"
|
||||
// logFloat(3.14159)(t)() // Outputs: "Float value: 3.14"
|
||||
// }
|
||||
//
|
||||
// # Example - Logging Complex Structures
|
||||
//
|
||||
// func TestComplexStructureLogging(t *testing.T) {
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// config := Config{Host: "localhost", Port: 8080, Timeout: 30}
|
||||
//
|
||||
// // Use %+v to include field names
|
||||
// logConfig := assert.Logf[Config]("Configuration: %+v")
|
||||
// logConfig(config)(t)()
|
||||
// // Outputs: "Configuration: {Host:localhost Port:8080 Timeout:30}"
|
||||
//
|
||||
// // Or use %#v for Go-syntax representation
|
||||
// logConfigGo := assert.Logf[Config]("Config (Go syntax): %#v")
|
||||
// logConfigGo(config)(t)()
|
||||
// // Outputs: "Config (Go syntax): assert.Config{Host:"localhost", Port:8080, Timeout:30}"
|
||||
// }
|
||||
//
|
||||
// # Example - Debugging Test Failures
|
||||
//
|
||||
// func TestWithDebugLogging(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// logSlice := assert.Logf[[]int]("Testing slice: %v")
|
||||
//
|
||||
// // Log the input data
|
||||
// logSlice(numbers)(t)()
|
||||
//
|
||||
// // Perform assertions
|
||||
// assert.ArrayNotEmpty(numbers)(t)
|
||||
// assert.ArrayLength[int](5)(numbers)(t)
|
||||
//
|
||||
// // Log intermediate results
|
||||
// sum := 0
|
||||
// for _, n := range numbers {
|
||||
// sum += n
|
||||
// }
|
||||
// logInt := assert.Logf[int]("Sum: %d")
|
||||
// logInt(sum)(t)()
|
||||
//
|
||||
// assert.Equal(15)(sum)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Conditional Logging
|
||||
//
|
||||
// func TestConditionalLogging(t *testing.T) {
|
||||
// logDebug := assert.Logf[string]("DEBUG: %s")
|
||||
//
|
||||
// values := []int{1, 2, 3, 4, 5}
|
||||
// for _, v := range values {
|
||||
// if v%2 == 0 {
|
||||
// logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
|
||||
// }
|
||||
// }
|
||||
// // Outputs:
|
||||
// // DEBUG: Found even number: 2
|
||||
// // DEBUG: Found even number: 4
|
||||
// }
|
||||
//
|
||||
// # Format Verbs
|
||||
//
|
||||
// Common format verbs you can use in the prefix string:
|
||||
// - %v: Default format
|
||||
// - %+v: Default format with field names for structs
|
||||
// - %#v: Go-syntax representation
|
||||
// - %T: Type of the value
|
||||
// - %d: Integer in base 10
|
||||
// - %s: String
|
||||
// - %f: Floating point number
|
||||
// - %t: Boolean (true/false)
|
||||
// - %p: Pointer address
|
||||
//
|
||||
// See the fmt package documentation for a complete list of format verbs.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Logging does not affect test pass/fail status
|
||||
// - Log output appears in test results when running with -v flag or when tests fail
|
||||
// - The function returns Void, indicating it's used for side effects only
|
||||
// - The ReaderIO pattern allows logging to be composed with other operations
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [FromReaderIO]: Converts ReaderIO operations into test assertions
|
||||
// - testing.T.Logf: The underlying Go testing log function
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go testing package: https://pkg.go.dev/testing
|
||||
// - fmt package format verbs: https://pkg.go.dev/fmt
|
||||
// - ReaderIO pattern: Combines Reader (context dependency) with IO (side effects)
|
||||
func Logf[T any](prefix string) func(T) readerio.ReaderIO[*testing.T, Void] {
|
||||
return func(a T) readerio.ReaderIO[*testing.T, Void] {
|
||||
return func(t *testing.T) IO[Void] {
|
||||
return io.FromImpure(func() {
|
||||
t.Logf(prefix, a)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
406
v2/assert/logger_test.go
Normal file
406
v2/assert/logger_test.go
Normal file
@@ -0,0 +1,406 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLogf_BasicInteger tests basic integer logging
|
||||
func TestLogf_BasicInteger(t *testing.T) {
|
||||
logInt := Logf[int]("Processing value: %d")
|
||||
|
||||
// This should not panic and should log the value
|
||||
logInt(42)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicString tests basic string logging
|
||||
func TestLogf_BasicString(t *testing.T) {
|
||||
logString := Logf[string]("String value: %s")
|
||||
|
||||
logString("hello world")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicFloat tests basic float logging
|
||||
func TestLogf_BasicFloat(t *testing.T) {
|
||||
logFloat := Logf[float64]("Float value: %.2f")
|
||||
|
||||
logFloat(3.14159)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicBoolean tests basic boolean logging
|
||||
func TestLogf_BasicBoolean(t *testing.T) {
|
||||
logBool := Logf[bool]("Boolean value: %t")
|
||||
|
||||
logBool(true)(t)()
|
||||
logBool(false)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ComplexStruct tests logging of complex structures
|
||||
func TestLogf_ComplexStruct(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
logUser := Logf[User]("User: %+v")
|
||||
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
logUser(user)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Slice tests logging of slices
|
||||
func TestLogf_Slice(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Slice: %v")
|
||||
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
logSlice(numbers)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Map tests logging of maps
|
||||
func TestLogf_Map(t *testing.T) {
|
||||
logMap := Logf[map[string]int]("Map: %v")
|
||||
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
logMap(data)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Pointer tests logging of pointers
|
||||
func TestLogf_Pointer(t *testing.T) {
|
||||
logPtr := Logf[*int]("Pointer: %p")
|
||||
|
||||
value := 42
|
||||
logPtr(&value)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_NilPointer tests logging of nil pointers
|
||||
func TestLogf_NilPointer(t *testing.T) {
|
||||
logPtr := Logf[*int]("Pointer: %v")
|
||||
|
||||
var nilPtr *int
|
||||
logPtr(nilPtr)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptyString tests logging of empty strings
|
||||
func TestLogf_EmptyString(t *testing.T) {
|
||||
logString := Logf[string]("String: '%s'")
|
||||
|
||||
logString("")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptySlice tests logging of empty slices
|
||||
func TestLogf_EmptySlice(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Slice: %v")
|
||||
|
||||
logSlice([]int{})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptyMap tests logging of empty maps
|
||||
func TestLogf_EmptyMap(t *testing.T) {
|
||||
logMap := Logf[map[string]int]("Map: %v")
|
||||
|
||||
logMap(map[string]int{})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_MultipleTypes tests using multiple loggers for different types
|
||||
func TestLogf_MultipleTypes(t *testing.T) {
|
||||
logString := Logf[string]("String: %s")
|
||||
logInt := Logf[int]("Integer: %d")
|
||||
logFloat := Logf[float64]("Float: %.2f")
|
||||
|
||||
logString("test")(t)()
|
||||
logInt(42)(t)()
|
||||
logFloat(3.14)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_WithinTestPipeline tests logging within a test pipeline
|
||||
func TestLogf_WithinTestPipeline(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
config := Config{Host: "localhost", Port: 8080}
|
||||
|
||||
logConfig := Logf[Config]("Testing config: %+v")
|
||||
logConfig(config)(t)()
|
||||
|
||||
// Continue with assertions
|
||||
StringNotEmpty(config.Host)(t)
|
||||
That(func(port int) bool { return port > 0 })(config.Port)(t)
|
||||
|
||||
// Test passes if no panic occurs and assertions pass
|
||||
}
|
||||
|
||||
// TestLogf_NestedStructures tests logging of nested structures
|
||||
func TestLogf_NestedStructures(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
logPerson := Logf[Person]("Person: %+v")
|
||||
|
||||
person := Person{
|
||||
Name: "Bob",
|
||||
Address: Address{
|
||||
Street: "123 Main St",
|
||||
City: "Springfield",
|
||||
},
|
||||
}
|
||||
|
||||
logPerson(person)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Interface tests logging of interface values
|
||||
func TestLogf_Interface(t *testing.T) {
|
||||
logAny := Logf[any]("Value: %v")
|
||||
|
||||
logAny(42)(t)()
|
||||
logAny("string")(t)()
|
||||
logAny([]int{1, 2, 3})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_GoSyntaxFormat tests logging with Go-syntax format
|
||||
func TestLogf_GoSyntaxFormat(t *testing.T) {
|
||||
type Point struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
logPoint := Logf[Point]("Point: %#v")
|
||||
|
||||
point := Point{X: 10, Y: 20}
|
||||
logPoint(point)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_TypeFormat tests logging with type format
|
||||
func TestLogf_TypeFormat(t *testing.T) {
|
||||
logType := Logf[any]("Type: %T, Value: %v")
|
||||
|
||||
logType(42)(t)()
|
||||
logType("string")(t)()
|
||||
logType(3.14)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_LargeNumbers tests logging of large numbers
|
||||
func TestLogf_LargeNumbers(t *testing.T) {
|
||||
logInt := Logf[int64]("Large number: %d")
|
||||
|
||||
logInt(9223372036854775807)(t)() // Max int64
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_NegativeNumbers tests logging of negative numbers
|
||||
func TestLogf_NegativeNumbers(t *testing.T) {
|
||||
logInt := Logf[int]("Number: %d")
|
||||
|
||||
logInt(-42)(t)()
|
||||
logInt(-100)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_SpecialFloats tests logging of special float values
|
||||
func TestLogf_SpecialFloats(t *testing.T) {
|
||||
logFloat := Logf[float64]("Float: %v")
|
||||
|
||||
logFloat(0.0)(t)()
|
||||
logFloat(-0.0)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_UnicodeStrings tests logging of unicode strings
|
||||
func TestLogf_UnicodeStrings(t *testing.T) {
|
||||
logString := Logf[string]("Unicode: %s")
|
||||
|
||||
logString("Hello, 世界")(t)()
|
||||
logString("Emoji: 🎉🎊")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_MultilineStrings tests logging of multiline strings
|
||||
func TestLogf_MultilineStrings(t *testing.T) {
|
||||
logString := Logf[string]("Multiline:\n%s")
|
||||
|
||||
multiline := `Line 1
|
||||
Line 2
|
||||
Line 3`
|
||||
|
||||
logString(multiline)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ReuseLogger tests reusing the same logger multiple times
|
||||
func TestLogf_ReuseLogger(t *testing.T) {
|
||||
logInt := Logf[int]("Value: %d")
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
logInt(i)(t)()
|
||||
}
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ConditionalLogging tests conditional logging based on values
|
||||
func TestLogf_ConditionalLogging(t *testing.T) {
|
||||
logDebug := Logf[string]("DEBUG: %s")
|
||||
|
||||
values := []int{1, 2, 3, 4, 5}
|
||||
for _, v := range values {
|
||||
if v%2 == 0 {
|
||||
logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
|
||||
}
|
||||
}
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_WithAssertions tests combining logging with assertions
|
||||
func TestLogf_WithAssertions(t *testing.T) {
|
||||
logInt := Logf[int]("Testing value: %d")
|
||||
|
||||
value := 42
|
||||
logInt(value)(t)()
|
||||
|
||||
// Perform assertion after logging
|
||||
Equal(42)(value)(t)
|
||||
|
||||
// Test passes if assertion passes
|
||||
}
|
||||
|
||||
// TestLogf_DebuggingFailures demonstrates using logging to debug test failures
|
||||
func TestLogf_DebuggingFailures(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Input slice: %v")
|
||||
logInt := Logf[int]("Computed sum: %d")
|
||||
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
logSlice(numbers)(t)()
|
||||
|
||||
sum := 0
|
||||
for _, n := range numbers {
|
||||
sum += n
|
||||
}
|
||||
logInt(sum)(t)()
|
||||
|
||||
Equal(15)(sum)(t)
|
||||
|
||||
// Test passes if assertion passes
|
||||
}
|
||||
|
||||
// TestLogf_ComplexDataStructures tests logging of complex nested data
|
||||
func TestLogf_ComplexDataStructures(t *testing.T) {
|
||||
type Metadata struct {
|
||||
Version string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
ID int
|
||||
Title string
|
||||
Metadata Metadata
|
||||
}
|
||||
|
||||
logDoc := Logf[Document]("Document: %+v")
|
||||
|
||||
doc := Document{
|
||||
ID: 1,
|
||||
Title: "Test Document",
|
||||
Metadata: Metadata{
|
||||
Version: "1.0",
|
||||
Tags: []string{"test", "example"},
|
||||
},
|
||||
}
|
||||
|
||||
logDoc(doc)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ArrayTypes tests logging of array types
|
||||
func TestLogf_ArrayTypes(t *testing.T) {
|
||||
logArray := Logf[[5]int]("Array: %v")
|
||||
|
||||
arr := [5]int{1, 2, 3, 4, 5}
|
||||
logArray(arr)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ChannelTypes tests logging of channel types
|
||||
func TestLogf_ChannelTypes(t *testing.T) {
|
||||
logChan := Logf[chan int]("Channel: %v")
|
||||
|
||||
ch := make(chan int, 1)
|
||||
logChan(ch)(t)()
|
||||
close(ch)
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_FunctionTypes tests logging of function types
|
||||
func TestLogf_FunctionTypes(t *testing.T) {
|
||||
logFunc := Logf[func() int]("Function: %v")
|
||||
|
||||
fn := func() int { return 42 }
|
||||
logFunc(fn)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
152
v2/assert/monoid.go
Normal file
152
v2/assert/monoid.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/boolean"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// ApplicativeMonoid returns a [monoid.Monoid] for combining test assertion [Reader]s.
|
||||
//
|
||||
// This monoid combines multiple test assertions using logical AND (conjunction) semantics,
|
||||
// meaning all assertions must pass for the combined assertion to pass. It leverages the
|
||||
// applicative structure of Reader to execute multiple assertions with the same testing.T
|
||||
// context and combines their boolean results using boolean.MonoidAll (logical AND).
|
||||
//
|
||||
// The monoid provides:
|
||||
// - Concat: Combines two assertions such that both must pass (logical AND)
|
||||
// - Empty: Returns an assertion that always passes (identity element)
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Composing multiple test assertions into a single assertion
|
||||
// - Building complex test conditions from simpler ones
|
||||
// - Creating reusable assertion combinators
|
||||
// - Implementing test assertion DSLs
|
||||
//
|
||||
// # Monoid Laws
|
||||
//
|
||||
// The returned monoid satisfies the standard monoid laws:
|
||||
//
|
||||
// 1. Associativity:
|
||||
// Concat(Concat(a1, a2), a3) ≡ Concat(a1, Concat(a2, a3))
|
||||
//
|
||||
// 2. Left Identity:
|
||||
// Concat(Empty(), a) ≡ a
|
||||
//
|
||||
// 3. Right Identity:
|
||||
// Concat(a, Empty()) ≡ a
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [monoid.Monoid][Reader] that combines assertions using logical AND
|
||||
//
|
||||
// # Example - Basic Usage
|
||||
//
|
||||
// func TestUserValidation(t *testing.T) {
|
||||
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
// m := assert.ApplicativeMonoid()
|
||||
//
|
||||
// // Combine multiple assertions
|
||||
// assertion := m.Concat(
|
||||
// assert.Equal("Alice")(user.Name),
|
||||
// m.Concat(
|
||||
// assert.Equal(30)(user.Age),
|
||||
// assert.StringNotEmpty(user.Email),
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// // Execute combined assertion
|
||||
// assertion(t) // All three assertions must pass
|
||||
// }
|
||||
//
|
||||
// # Example - Building Reusable Validators
|
||||
//
|
||||
// func TestWithReusableValidators(t *testing.T) {
|
||||
// m := assert.ApplicativeMonoid()
|
||||
//
|
||||
// // Create a reusable validator
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return m.Concat(
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// m.Concat(
|
||||
// assert.True(u.Age > 0),
|
||||
// assert.StringContains("@")(u.Email),
|
||||
// ),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Bob", Age: 25, Email: "bob@test.com"}
|
||||
// validateUser(user)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Using Empty for Identity
|
||||
//
|
||||
// func TestEmptyIdentity(t *testing.T) {
|
||||
// m := assert.ApplicativeMonoid()
|
||||
// assertion := assert.Equal(42)(42)
|
||||
//
|
||||
// // Empty is the identity - these are equivalent
|
||||
// result1 := m.Concat(m.Empty(), assertion)(t)
|
||||
// result2 := m.Concat(assertion, m.Empty())(t)
|
||||
// result3 := assertion(t)
|
||||
// // All three produce the same result
|
||||
// }
|
||||
//
|
||||
// # Example - Combining with AllOf
|
||||
//
|
||||
// func TestCombiningWithAllOf(t *testing.T) {
|
||||
// // ApplicativeMonoid provides the underlying mechanism for AllOf
|
||||
// arr := []int{1, 2, 3, 4, 5}
|
||||
//
|
||||
// // These are conceptually equivalent:
|
||||
// m := assert.ApplicativeMonoid()
|
||||
// manual := m.Concat(
|
||||
// assert.ArrayNotEmpty(arr),
|
||||
// m.Concat(
|
||||
// assert.ArrayLength[int](5)(arr),
|
||||
// assert.ArrayContains(3)(arr),
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// // AllOf uses ApplicativeMonoid internally
|
||||
// convenient := assert.AllOf([]assert.Reader{
|
||||
// assert.ArrayNotEmpty(arr),
|
||||
// assert.ArrayLength[int](5)(arr),
|
||||
// assert.ArrayContains(3)(arr),
|
||||
// })
|
||||
//
|
||||
// manual(t)
|
||||
// convenient(t)
|
||||
// }
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [AllOf]: Convenient wrapper for combining multiple assertions using this monoid
|
||||
// - [boolean.MonoidAll]: The underlying boolean monoid (logical AND with true as identity)
|
||||
// - [reader.ApplicativeMonoid]: Generic applicative monoid for Reader types
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
|
||||
// - Applicative Functors: https://hackage.haskell.org/package/base/docs/Control-Applicative.html
|
||||
// - Boolean Monoid (All): https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:All
|
||||
func ApplicativeMonoid() monoid.Monoid[Reader] {
|
||||
return reader.ApplicativeMonoid[*testing.T](boolean.MonoidAll)
|
||||
}
|
||||
454
v2/assert/monoid_test.go
Normal file
454
v2/assert/monoid_test.go
Normal file
@@ -0,0 +1,454 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestApplicativeMonoid_Empty tests that Empty returns an assertion that always passes
|
||||
func TestApplicativeMonoid_Empty(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
empty := m.Empty()
|
||||
|
||||
result := empty(t)
|
||||
if !result {
|
||||
t.Error("Expected Empty() to return an assertion that always passes")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_BothPass tests that Concat returns true when both assertions pass
|
||||
func TestApplicativeMonoid_Concat_BothPass(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(42)
|
||||
assertion2 := Equal("hello")("hello")
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected Concat to pass when both assertions pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_FirstFails tests that Concat returns false when first assertion fails
|
||||
func TestApplicativeMonoid_Concat_FirstFails(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(43) // This will fail
|
||||
assertion2 := Equal("hello")("hello")
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when first assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_SecondFails tests that Concat returns false when second assertion fails
|
||||
func TestApplicativeMonoid_Concat_SecondFails(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(42)
|
||||
assertion2 := Equal("hello")("world") // This will fail
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when second assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_BothFail tests that Concat returns false when both assertions fail
|
||||
func TestApplicativeMonoid_Concat_BothFail(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(43) // This will fail
|
||||
assertion2 := Equal("hello")("world") // This will fail
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when both assertions fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_LeftIdentity tests the left identity law: Concat(Empty(), a) = a
|
||||
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Concat(Empty(), assertion) should behave the same as assertion
|
||||
combined := m.Concat(m.Empty(), assertion)
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Left identity law violated: Concat(Empty(), a) should equal a")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RightIdentity tests the right identity law: Concat(a, Empty()) = a
|
||||
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Concat(assertion, Empty()) should behave the same as assertion
|
||||
combined := m.Concat(assertion, m.Empty())
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Right identity law violated: Concat(a, Empty()) should equal a")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Associativity tests the associativity law: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
|
||||
func TestApplicativeMonoid_Associativity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(2)
|
||||
a3 := Equal(3)(3)
|
||||
|
||||
// Concat(Concat(a1, a2), a3)
|
||||
left := m.Concat(m.Concat(a1, a2), a3)
|
||||
|
||||
// Concat(a1, Concat(a2, a3))
|
||||
right := m.Concat(a1, m.Concat(a2, a3))
|
||||
|
||||
result1 := left(t)
|
||||
result2 := right(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Associativity law violated: Concat(Concat(a, b), c) should equal Concat(a, Concat(b, c))")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_AssociativityWithFailure tests associativity when assertions fail
|
||||
func TestApplicativeMonoid_AssociativityWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(3) // This will fail
|
||||
a3 := Equal(3)(3)
|
||||
|
||||
// Concat(Concat(a1, a2), a3)
|
||||
left := m.Concat(m.Concat(a1, a2), a3)
|
||||
|
||||
// Concat(a1, Concat(a2, a3))
|
||||
right := m.Concat(a1, m.Concat(a2, a3))
|
||||
|
||||
result1 := left(mockT)
|
||||
result2 := right(mockT)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Associativity law violated even with failures")
|
||||
}
|
||||
|
||||
if result1 || result2 {
|
||||
t.Error("Expected both to fail when one assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ComplexAssertions tests combining complex assertions
|
||||
func TestApplicativeMonoid_ComplexAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
|
||||
arrayAssertions := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
mapAssertions := m.Concat(
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
)
|
||||
|
||||
combined := m.Concat(arrayAssertions, mapAssertions)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected complex combined assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ComplexAssertionsWithFailure tests complex assertions when one fails
|
||||
func TestApplicativeMonoid_ComplexAssertionsWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
arr := []int{1, 2, 3}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
|
||||
arrayAssertions := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr), // This will fail - array has 3 elements, not 5
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
mapAssertions := m.Concat(
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
)
|
||||
|
||||
combined := m.Concat(arrayAssertions, mapAssertions)
|
||||
|
||||
result := combined(mockT)
|
||||
if result {
|
||||
t.Error("Expected complex combined assertions to fail when one assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_MultipleConcat tests chaining multiple Concat operations
|
||||
func TestApplicativeMonoid_MultipleConcat(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(2)
|
||||
a3 := Equal(3)(3)
|
||||
a4 := Equal(4)(4)
|
||||
|
||||
combined := m.Concat(
|
||||
m.Concat(a1, a2),
|
||||
m.Concat(a3, a4),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected multiple Concat operations to pass when all assertions pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithStringAssertions tests combining string assertions
|
||||
func TestApplicativeMonoid_WithStringAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
str := "hello world"
|
||||
|
||||
combined := m.Concat(
|
||||
StringNotEmpty(str),
|
||||
StringLength[any, any](11)(str),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected string assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithBooleanAssertions tests combining boolean assertions
|
||||
func TestApplicativeMonoid_WithBooleanAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
combined := m.Concat(
|
||||
Equal(true)(true),
|
||||
m.Concat(
|
||||
Equal(false)(false),
|
||||
Equal(true)(true),
|
||||
),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected boolean assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithErrorAssertions tests combining error assertions
|
||||
func TestApplicativeMonoid_WithErrorAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
combined := m.Concat(
|
||||
NoError(nil),
|
||||
Equal("test")("test"),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected error assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_EmptyWithMultipleConcat tests Empty with multiple Concat operations
|
||||
func TestApplicativeMonoid_EmptyWithMultipleConcat(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Multiple Empty values should still act as identity
|
||||
combined := m.Concat(
|
||||
m.Empty(),
|
||||
m.Concat(
|
||||
assertion,
|
||||
m.Empty(),
|
||||
),
|
||||
)
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Multiple Empty values should still act as identity")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_OnlyEmpty tests using only Empty values
|
||||
func TestApplicativeMonoid_OnlyEmpty(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
// Concat of Empty values should still be Empty (identity)
|
||||
combined := m.Concat(m.Empty(), m.Empty())
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected Concat of Empty values to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RealWorldExample tests a realistic use case
|
||||
func TestApplicativeMonoid_RealWorldExample(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
validateUser := func(u User) Reader {
|
||||
return m.Concat(
|
||||
StringNotEmpty(u.Name),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age < 150 })(u.Age),
|
||||
That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(u.Email),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
validUser := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
result := validateUser(validUser)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected valid user to pass all validations")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RealWorldExampleWithFailure tests a realistic use case with failure
|
||||
func TestApplicativeMonoid_RealWorldExampleWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
validateUser := func(u User) Reader {
|
||||
return m.Concat(
|
||||
StringNotEmpty(u.Name),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age < 150 })(u.Age),
|
||||
That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(u.Email),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
invalidUser := User{Name: "Bob", Age: 200, Email: "bob@test.com"} // Age > 150
|
||||
result := validateUser(invalidUser)(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected invalid user to fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_IntegrationWithAllOf demonstrates relationship with AllOf
|
||||
func TestApplicativeMonoid_IntegrationWithAllOf(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
// Using ApplicativeMonoid directly
|
||||
manualCombination := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
// Using AllOf (which uses ApplicativeMonoid internally)
|
||||
allOfCombination := AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
})
|
||||
|
||||
result1 := manualCombination(t)
|
||||
result2 := allOfCombination(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Expected manual combination and AllOf to produce same result")
|
||||
}
|
||||
|
||||
if !result1 || !result2 {
|
||||
t.Error("Expected both combinations to pass")
|
||||
}
|
||||
}
|
||||
650
v2/assert/traverse.go
Normal file
650
v2/assert/traverse.go
Normal file
@@ -0,0 +1,650 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// TraverseArray transforms an array of values into a test suite by applying a function
|
||||
// that generates named test cases for each element.
|
||||
//
|
||||
// This function enables data-driven testing where you have a collection of test inputs
|
||||
// and want to run a named subtest for each one. It follows the functional programming
|
||||
// pattern of "traverse" - transforming a collection while preserving structure and
|
||||
// accumulating effects (in this case, test execution).
|
||||
//
|
||||
// The function takes each element of the array, applies the provided function to generate
|
||||
// a [Pair] of (test name, test assertion), and runs each as a separate subtest using
|
||||
// Go's t.Run. All subtests must pass for the overall test to pass.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes a value of type T and returns a [Pair] containing:
|
||||
// - Head: The test name (string) for the subtest
|
||||
// - Tail: The test assertion ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Kleisli] function that takes an array of T and returns a [Reader] that:
|
||||
// - Executes each element as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation and reporting via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Data-driven testing with multiple test cases
|
||||
// - Parameterized tests where each parameter gets its own subtest
|
||||
// - Testing collections where each element needs validation
|
||||
// - Property-based testing with generated test data
|
||||
//
|
||||
// # Example - Basic Data-Driven Testing
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Input int
|
||||
// Expected int
|
||||
// }
|
||||
//
|
||||
// testCases := []TestCase{
|
||||
// {Input: 2, Expected: 4},
|
||||
// {Input: 3, Expected: 9},
|
||||
// {Input: 4, Expected: 16},
|
||||
// }
|
||||
//
|
||||
// square := func(n int) int { return n * n }
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
|
||||
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
|
||||
// assertion := assert.Equal(tc.Expected)(square(tc.Input))
|
||||
// return pair.MakePair(name, assertion)
|
||||
// })
|
||||
//
|
||||
// traverse(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - String Validation
|
||||
//
|
||||
// func TestStringValidation(t *testing.T) {
|
||||
// inputs := []string{"hello", "world", "test"}
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(s string) assert.Pair[string, assert.Reader] {
|
||||
// return pair.MakePair(
|
||||
// fmt.Sprintf("validate_%s", s),
|
||||
// assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(s),
|
||||
// assert.That(func(str string) bool { return len(str) > 0 })(s),
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// traverse(inputs)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Complex Object Testing
|
||||
//
|
||||
// func TestUsers(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// users := []User{
|
||||
// {Name: "Alice", Age: 30, Email: "alice@example.com"},
|
||||
// {Name: "Bob", Age: 25, Email: "bob@example.com"},
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(u User) assert.Pair[string, assert.Reader] {
|
||||
// return pair.MakePair(
|
||||
// fmt.Sprintf("user_%s", u.Name),
|
||||
// assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// assert.That(func(age int) bool { return age > 0 })(u.Age),
|
||||
// assert.That(func(email string) bool {
|
||||
// return len(email) > 0 && strings.Contains(email, "@")
|
||||
// })(u.Email),
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// traverse(users)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with RunAll
|
||||
//
|
||||
// TraverseArray and [RunAll] serve similar purposes but differ in their approach:
|
||||
//
|
||||
// - TraverseArray: Generates test cases from an array of data
|
||||
//
|
||||
// - Input: Array of values + function to generate test cases
|
||||
//
|
||||
// - Use when: You have test data and need to generate test cases from it
|
||||
//
|
||||
// - RunAll: Executes pre-defined named test cases
|
||||
//
|
||||
// - Input: Map of test names to assertions
|
||||
//
|
||||
// - Use when: You have already defined test cases with names
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [SequenceSeq2]: Similar but works with Go iterators (Seq2) instead of arrays
|
||||
// - [RunAll]: Executes a map of named test cases
|
||||
// - [AllOf]: Combines multiple assertions without subtests
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
func TraverseArray[T any](f func(T) Pair[string, Reader]) Kleisli[[]T] {
|
||||
return func(ts []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for _, src := range ts {
|
||||
test := f(src)
|
||||
res := t.Run(pair.Head(test), func(t *testing.T) {
|
||||
pair.Tail(test)(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceSeq2 executes a sequence of named test cases provided as a Go iterator.
|
||||
//
|
||||
// This function takes a [Seq2] iterator that yields (name, assertion) pairs and
|
||||
// executes each as a separate subtest using Go's t.Run. It's similar to [TraverseArray]
|
||||
// but works directly with Go's iterator protocol (introduced in Go 1.23) rather than
|
||||
// requiring an array.
|
||||
//
|
||||
// The function iterates through all test cases, running each as a named subtest.
|
||||
// All subtests must pass for the overall test to pass. This provides proper test
|
||||
// isolation and clear reporting of which specific test cases fail.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - s: A [Seq2] iterator that yields pairs of:
|
||||
// - Key: Test name (string) for the subtest
|
||||
// - Value: Test assertion ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Reader] that:
|
||||
// - Executes each test case as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Working with iterator-based test data
|
||||
// - Lazy evaluation of test cases
|
||||
// - Integration with Go 1.23+ iterator patterns
|
||||
// - Memory-efficient testing of large test suites
|
||||
//
|
||||
// # Example - Basic Usage with Iterator
|
||||
//
|
||||
// func TestWithIterator(t *testing.T) {
|
||||
// // Create an iterator of test cases
|
||||
// testCases := func(yield func(string, assert.Reader) bool) {
|
||||
// if !yield("test_addition", assert.Equal(4)(2+2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_subtraction", assert.Equal(1)(3-2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Generated Test Cases
|
||||
//
|
||||
// func TestGeneratedCases(t *testing.T) {
|
||||
// // Generate test cases on the fly
|
||||
// generateTests := func(yield func(string, assert.Reader) bool) {
|
||||
// for i := 1; i <= 5; i++ {
|
||||
// name := fmt.Sprintf("test_%d", i)
|
||||
// assertion := assert.Equal(i*i)(i * i)
|
||||
// if !yield(name, assertion) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(generateTests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Filtering Test Cases
|
||||
//
|
||||
// func TestFilteredCases(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Name string
|
||||
// Input int
|
||||
// Expected int
|
||||
// Skip bool
|
||||
// }
|
||||
//
|
||||
// allCases := []TestCase{
|
||||
// {Name: "test1", Input: 2, Expected: 4, Skip: false},
|
||||
// {Name: "test2", Input: 3, Expected: 9, Skip: true},
|
||||
// {Name: "test3", Input: 4, Expected: 16, Skip: false},
|
||||
// }
|
||||
//
|
||||
// // Create iterator that filters out skipped tests
|
||||
// activeTests := func(yield func(string, assert.Reader) bool) {
|
||||
// for _, tc := range allCases {
|
||||
// if !tc.Skip {
|
||||
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
// if !yield(tc.Name, assertion) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(activeTests)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseArray
|
||||
//
|
||||
// SequenceSeq2 and [TraverseArray] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - SequenceSeq2: Works with iterators (Seq2)
|
||||
//
|
||||
// - Input: Iterator yielding (name, assertion) pairs
|
||||
//
|
||||
// - Use when: Working with Go 1.23+ iterators or lazy evaluation
|
||||
//
|
||||
// - Memory: More efficient for large test suites (lazy evaluation)
|
||||
//
|
||||
// - TraverseArray: Works with arrays
|
||||
//
|
||||
// - Input: Array of values + transformation function
|
||||
//
|
||||
// - Use when: You have an array of test data
|
||||
//
|
||||
// - Memory: All test data must be in memory
|
||||
//
|
||||
// # Comparison with RunAll
|
||||
//
|
||||
// SequenceSeq2 and [RunAll] are very similar:
|
||||
//
|
||||
// - SequenceSeq2: Takes an iterator (Seq2)
|
||||
// - RunAll: Takes a map[string]Reader
|
||||
//
|
||||
// Both execute named test cases as subtests. Choose based on your data structure:
|
||||
// use SequenceSeq2 for iterators, RunAll for maps.
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [TraverseArray]: Similar but works with arrays instead of iterators
|
||||
// - [RunAll]: Executes a map of named test cases
|
||||
// - [AllOf]: Combines multiple assertions without subtests
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go iterators: https://go.dev/blog/range-functions
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
|
||||
func SequenceSeq2[T any](s Seq2[string, Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for name, test := range s {
|
||||
res := t.Run(name, func(t *testing.T) {
|
||||
test(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseRecord transforms a map of values into a test suite by applying a function
|
||||
// that generates test assertions for each map entry.
|
||||
//
|
||||
// This function enables data-driven testing where you have a map of test data and want
|
||||
// to run a named subtest for each entry. The map keys become test names, and the function
|
||||
// transforms each value into a test assertion. It follows the functional programming
|
||||
// pattern of "traverse" - transforming a collection while preserving structure and
|
||||
// accumulating effects (in this case, test execution).
|
||||
//
|
||||
// The function takes each key-value pair from the map, applies the provided function to
|
||||
// generate a [Reader] assertion, and runs each as a separate subtest using Go's t.Run.
|
||||
// All subtests must pass for the overall test to pass.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A [Kleisli] function that takes a value of type T and returns a [Reader] assertion
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Kleisli] function that takes a map[string]T and returns a [Reader] that:
|
||||
// - Executes each map entry as a named subtest (using the key as the test name)
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation and reporting via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Data-driven testing with named test cases in a map
|
||||
// - Testing configuration maps where keys are meaningful names
|
||||
// - Validating collections where natural keys exist
|
||||
// - Property-based testing with named scenarios
|
||||
//
|
||||
// # Example - Basic Configuration Testing
|
||||
//
|
||||
// func TestConfigurations(t *testing.T) {
|
||||
// configs := map[string]int{
|
||||
// "timeout": 30,
|
||||
// "maxRetries": 3,
|
||||
// "bufferSize": 1024,
|
||||
// }
|
||||
//
|
||||
// validatePositive := assert.That(func(n int) bool { return n > 0 })
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validatePositive)
|
||||
// traverse(configs)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - User Validation
|
||||
//
|
||||
// func TestUserMap(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// users := map[string]User{
|
||||
// "alice": {Name: "Alice", Age: 30},
|
||||
// "bob": {Name: "Bob", Age: 25},
|
||||
// "carol": {Name: "Carol", Age: 35},
|
||||
// }
|
||||
//
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// assert.That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validateUser)
|
||||
// traverse(users)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - API Endpoint Testing
|
||||
//
|
||||
// func TestEndpoints(t *testing.T) {
|
||||
// type Endpoint struct {
|
||||
// Path string
|
||||
// Method string
|
||||
// }
|
||||
//
|
||||
// endpoints := map[string]Endpoint{
|
||||
// "get_users": {Path: "/api/users", Method: "GET"},
|
||||
// "create_user": {Path: "/api/users", Method: "POST"},
|
||||
// "delete_user": {Path: "/api/users/:id", Method: "DELETE"},
|
||||
// }
|
||||
//
|
||||
// validateEndpoint := func(e Endpoint) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(e.Path),
|
||||
// assert.That(func(path string) bool {
|
||||
// return strings.HasPrefix(path, "/api/")
|
||||
// })(e.Path),
|
||||
// assert.That(func(method string) bool {
|
||||
// return method == "GET" || method == "POST" ||
|
||||
// method == "PUT" || method == "DELETE"
|
||||
// })(e.Method),
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validateEndpoint)
|
||||
// traverse(endpoints)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseArray
|
||||
//
|
||||
// TraverseRecord and [TraverseArray] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - TraverseRecord: Works with maps (records)
|
||||
//
|
||||
// - Input: Map with string keys + transformation function
|
||||
//
|
||||
// - Use when: You have named test data in a map
|
||||
//
|
||||
// - Test names: Derived from map keys
|
||||
//
|
||||
// - TraverseArray: Works with arrays
|
||||
//
|
||||
// - Input: Array of values + function that generates names and assertions
|
||||
//
|
||||
// - Use when: You have sequential test data
|
||||
//
|
||||
// - Test names: Generated by the transformation function
|
||||
//
|
||||
// # Comparison with SequenceRecord
|
||||
//
|
||||
// TraverseRecord and [SequenceRecord] are closely related:
|
||||
//
|
||||
// - TraverseRecord: Transforms values into assertions
|
||||
//
|
||||
// - Input: map[string]T + function T -> Reader
|
||||
//
|
||||
// - Use when: You need to transform data before asserting
|
||||
//
|
||||
// - SequenceRecord: Executes pre-defined assertions
|
||||
//
|
||||
// - Input: map[string]Reader
|
||||
//
|
||||
// - Use when: Assertions are already defined
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [SequenceRecord]: Similar but takes pre-defined assertions
|
||||
// - [TraverseArray]: Similar but works with arrays
|
||||
// - [RunAll]: Alias for SequenceRecord
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
func TraverseRecord[T any](f Kleisli[T]) Kleisli[map[string]T] {
|
||||
return func(m map[string]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for name, src := range m {
|
||||
res := t.Run(name, func(t *testing.T) {
|
||||
f(src)(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceRecord executes a map of named test cases as subtests.
|
||||
//
|
||||
// This function takes a map where keys are test names and values are test assertions
|
||||
// ([Reader]), and executes each as a separate subtest using Go's t.Run. It's the
|
||||
// record (map) equivalent of [SequenceSeq2] and is actually aliased as [RunAll] for
|
||||
// convenience.
|
||||
//
|
||||
// The function iterates through all map entries, running each as a named subtest.
|
||||
// All subtests must pass for the overall test to pass. This provides proper test
|
||||
// isolation and clear reporting of which specific test cases fail.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: A map[string]Reader where:
|
||||
// - Keys: Test names (strings) for the subtests
|
||||
// - Values: Test assertions ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Reader] that:
|
||||
// - Executes each map entry as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Executing a collection of pre-defined named test cases
|
||||
// - Organizing related tests in a map structure
|
||||
// - Running multiple assertions with descriptive names
|
||||
// - Building test suites programmatically
|
||||
//
|
||||
// # Example - Basic Named Tests
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// tests := map[string]assert.Reader{
|
||||
// "addition": assert.Equal(4)(2 + 2),
|
||||
// "subtraction": assert.Equal(1)(3 - 2),
|
||||
// "multiplication": assert.Equal(6)(2 * 3),
|
||||
// "division": assert.Equal(2)(6 / 3),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - String Validation Suite
|
||||
//
|
||||
// func TestStringValidations(t *testing.T) {
|
||||
// testString := "hello world"
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "not_empty": assert.StringNotEmpty(testString),
|
||||
// "correct_length": assert.StringLength[any, any](11)(testString),
|
||||
// "has_space": assert.That(func(s string) bool {
|
||||
// return strings.Contains(s, " ")
|
||||
// })(testString),
|
||||
// "lowercase": assert.That(func(s string) bool {
|
||||
// return s == strings.ToLower(s)
|
||||
// })(testString),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Complex Object Validation
|
||||
//
|
||||
// func TestUserValidation(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "name_not_empty": assert.StringNotEmpty(user.Name),
|
||||
// "age_positive": assert.That(func(age int) bool { return age > 0 })(user.Age),
|
||||
// "age_reasonable": assert.That(func(age int) bool { return age < 150 })(user.Age),
|
||||
// "email_valid": assert.That(func(email string) bool {
|
||||
// return strings.Contains(email, "@") && strings.Contains(email, ".")
|
||||
// })(user.Email),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Array Validation Suite
|
||||
//
|
||||
// func TestArrayValidations(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "not_empty": assert.ArrayNotEmpty(numbers),
|
||||
// "correct_length": assert.ArrayLength[int](5)(numbers),
|
||||
// "contains_three": assert.ArrayContains(3)(numbers),
|
||||
// "all_positive": assert.That(func(arr []int) bool {
|
||||
// for _, n := range arr {
|
||||
// if n <= 0 {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// })(numbers),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseRecord
|
||||
//
|
||||
// SequenceRecord and [TraverseRecord] are closely related:
|
||||
//
|
||||
// - SequenceRecord: Executes pre-defined assertions
|
||||
//
|
||||
// - Input: map[string]Reader (assertions already created)
|
||||
//
|
||||
// - Use when: You have already defined test cases with assertions
|
||||
//
|
||||
// - TraverseRecord: Transforms values into assertions
|
||||
//
|
||||
// - Input: map[string]T + function T -> Reader
|
||||
//
|
||||
// - Use when: You need to transform data before asserting
|
||||
//
|
||||
// # Comparison with SequenceSeq2
|
||||
//
|
||||
// SequenceRecord and [SequenceSeq2] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - SequenceRecord: Works with maps
|
||||
//
|
||||
// - Input: map[string]Reader
|
||||
//
|
||||
// - Use when: You have named test cases in a map
|
||||
//
|
||||
// - Iteration order: Non-deterministic (map iteration)
|
||||
//
|
||||
// - SequenceSeq2: Works with iterators
|
||||
//
|
||||
// - Input: Seq2[string, Reader]
|
||||
//
|
||||
// - Use when: You have test cases in an iterator
|
||||
//
|
||||
// - Iteration order: Deterministic (iterator order)
|
||||
//
|
||||
// # Note on Map Iteration Order
|
||||
//
|
||||
// Go maps have non-deterministic iteration order. If test execution order matters,
|
||||
// consider using [SequenceSeq2] with an iterator that provides deterministic ordering,
|
||||
// or use [TraverseArray] with a slice of test cases.
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [RunAll]: Alias for SequenceRecord
|
||||
// - [TraverseRecord]: Similar but transforms values into assertions
|
||||
// - [SequenceSeq2]: Similar but works with iterators
|
||||
// - [TraverseArray]: Similar but works with arrays
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
|
||||
func SequenceRecord(m map[string]Reader) Reader {
|
||||
return TraverseRecord(reader.Ask[Reader]())(m)
|
||||
}
|
||||
960
v2/assert/traverse_test.go
Normal file
960
v2/assert/traverse_test.go
Normal file
@@ -0,0 +1,960 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// TestTraverseArray_EmptyArray tests that TraverseArray handles empty arrays correctly
|
||||
func TestTraverseArray_EmptyArray(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(n)(n),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with empty array")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_SingleElement tests TraverseArray with a single element
|
||||
func TestTraverseArray_SingleElement(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(n*2)(n*2),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with single element")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_MultipleElements tests TraverseArray with multiple passing elements
|
||||
func TestTraverseArray_MultipleElements(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("square_%d", n),
|
||||
Equal(n*n)(n*n),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{1, 2, 3, 4, 5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with all passing elements")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_WithFailure tests that TraverseArray fails when one element fails
|
||||
func TestTraverseArray_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(10)(n), // Will fail for all except 10
|
||||
)
|
||||
})
|
||||
|
||||
// Run in a subtest - we expect the subtests to fail, so t.Run returns false
|
||||
result := traverse([]int{1, 2, 3})(t)
|
||||
|
||||
// The traverse should return false because assertions fail
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when elements don't match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_MixedResults tests TraverseArray with some passing and some failing
|
||||
func TestTraverseArray_MixedResults(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("is_even_%d", n),
|
||||
Equal(0)(n%2), // Only passes for even numbers
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{2, 3, 4})(t) // 3 is odd, should fail
|
||||
|
||||
// The traverse should return false because one assertion fails
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when some elements fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_StringData tests TraverseArray with string data
|
||||
func TestTraverseArray_StringData(t *testing.T) {
|
||||
words := []string{"hello", "world", "test"}
|
||||
|
||||
traverse := TraverseArray(func(s string) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("validate_%s", s),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(s),
|
||||
That(func(str string) bool { return len(str) > 0 })(s),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(words)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with valid strings")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_ComplexObjects tests TraverseArray with complex objects
|
||||
func TestTraverseArray_ComplexObjects(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "Bob", Age: 25},
|
||||
{Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseArray(func(u User) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("user_%s", u.Name),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with valid users")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_ComplexObjectsWithFailure tests TraverseArray with invalid complex objects
|
||||
func TestTraverseArray_ComplexObjectsWithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "", Age: 25}, // Invalid: empty name
|
||||
{Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseArray(func(u User) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("user_%s", u.Name),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
|
||||
// The traverse should return false because one user is invalid
|
||||
if result {
|
||||
t.Error("Expected traverse to return false with invalid user")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_DataDrivenTesting demonstrates data-driven testing pattern
|
||||
func TestTraverseArray_DataDrivenTesting(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testCases := []TestCase{
|
||||
{Input: 2, Expected: 4},
|
||||
{Input: 3, Expected: 9},
|
||||
{Input: 4, Expected: 16},
|
||||
{Input: 5, Expected: 25},
|
||||
}
|
||||
|
||||
square := func(n int) int { return n * n }
|
||||
|
||||
traverse := TraverseArray(func(tc TestCase) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected),
|
||||
Equal(tc.Expected)(square(tc.Input)),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(testCases)(t)
|
||||
if !result {
|
||||
t.Error("Expected all test cases to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_EmptySequence tests that SequenceSeq2 handles empty sequences correctly
|
||||
func TestSequenceSeq2_EmptySequence(t *testing.T) {
|
||||
emptySeq := func(yield func(string, Reader) bool) {
|
||||
// Empty - yields nothing
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](emptySeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with empty sequence")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_SingleTest tests SequenceSeq2 with a single test
|
||||
func TestSequenceSeq2_SingleTest(t *testing.T) {
|
||||
singleSeq := func(yield func(string, Reader) bool) {
|
||||
yield("test_one", Equal(42)(42))
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](singleSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with single test")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_MultipleTests tests SequenceSeq2 with multiple passing tests
|
||||
func TestSequenceSeq2_MultipleTests(t *testing.T) {
|
||||
multiSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_addition", Equal(4)(2+2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_subtraction", Equal(1)(3-2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_multiplication", Equal(6)(2*3)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](multiSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with all passing tests")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_WithFailure tests that SequenceSeq2 fails when one test fails
|
||||
func TestSequenceSeq2_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
failSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_pass", Equal(4)(2+2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_fail", Equal(5)(2+2)) { // This will fail
|
||||
return
|
||||
}
|
||||
if !yield("test_pass2", Equal(6)(2*3)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](failSeq)(t)
|
||||
|
||||
// The sequence should return false because one test fails
|
||||
if result {
|
||||
t.Error("Expected sequence to return false when one test fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_GeneratedTests tests SequenceSeq2 with generated test cases
|
||||
func TestSequenceSeq2_GeneratedTests(t *testing.T) {
|
||||
generateTests := func(yield func(string, Reader) bool) {
|
||||
for i := 1; i <= 5; i++ {
|
||||
name := fmt.Sprintf("test_%d", i)
|
||||
assertion := Equal(i * i)(i * i)
|
||||
if !yield(name, assertion) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](generateTests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all generated tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_StringTests tests SequenceSeq2 with string assertions
|
||||
func TestSequenceSeq2_StringTests(t *testing.T) {
|
||||
stringSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_hello", StringNotEmpty("hello")) {
|
||||
return
|
||||
}
|
||||
if !yield("test_world", StringNotEmpty("world")) {
|
||||
return
|
||||
}
|
||||
if !yield("test_length", StringLength[any, any](5)("hello")) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](stringSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all string tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_ArrayTests tests SequenceSeq2 with array assertions
|
||||
func TestSequenceSeq2_ArrayTests(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
arraySeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_not_empty", ArrayNotEmpty(arr)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_length", ArrayLength[int](5)(arr)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_contains", ArrayContains(3)(arr)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](arraySeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all array tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_ComplexAssertions tests SequenceSeq2 with complex combined assertions
|
||||
func TestSequenceSeq2_ComplexAssertions(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
|
||||
userSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_name", StringNotEmpty(user.Name)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_age", That(func(age int) bool { return age > 0 && age < 150 })(user.Age)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_email", That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(user.Email)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](userSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all user validation tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_EarlyTermination tests that SequenceSeq2 respects early termination
|
||||
func TestSequenceSeq2_EarlyTermination(t *testing.T) {
|
||||
executionCount := 0
|
||||
|
||||
earlyTermSeq := func(yield func(string, Reader) bool) {
|
||||
executionCount++
|
||||
if !yield("test_1", Equal(1)(1)) {
|
||||
return
|
||||
}
|
||||
executionCount++
|
||||
if !yield("test_2", Equal(2)(2)) {
|
||||
return
|
||||
}
|
||||
executionCount++
|
||||
// This should execute even though we don't check the return
|
||||
yield("test_3", Equal(3)(3))
|
||||
executionCount++
|
||||
}
|
||||
|
||||
SequenceSeq2[Reader](earlyTermSeq)(t)
|
||||
|
||||
// All iterations should execute since we're not terminating early
|
||||
if executionCount != 4 {
|
||||
t.Errorf("Expected 4 executions, got %d", executionCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_WithMapConversion demonstrates converting a map to Seq2
|
||||
func TestSequenceSeq2_WithMapConversion(t *testing.T) {
|
||||
testMap := map[string]Reader{
|
||||
"test_addition": Equal(4)(2 + 2),
|
||||
"test_multiplication": Equal(6)(2 * 3),
|
||||
"test_subtraction": Equal(1)(3 - 2),
|
||||
}
|
||||
|
||||
// Convert map to Seq2
|
||||
mapSeq := func(yield func(string, Reader) bool) {
|
||||
for name, assertion := range testMap {
|
||||
if !yield(name, assertion) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](mapSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all map-based tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_vs_SequenceSeq2 demonstrates the relationship between the two functions
|
||||
func TestTraverseArray_vs_SequenceSeq2(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Name string
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testCases := []TestCase{
|
||||
{Name: "test_1", Input: 2, Expected: 4},
|
||||
{Name: "test_2", Input: 3, Expected: 9},
|
||||
{Name: "test_3", Input: 4, Expected: 16},
|
||||
}
|
||||
|
||||
// Using TraverseArray
|
||||
traverseResult := TraverseArray(func(tc TestCase) Pair[string, Reader] {
|
||||
return pair.MakePair(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input))
|
||||
})(testCases)(t)
|
||||
|
||||
// Using SequenceSeq2
|
||||
seqResult := SequenceSeq2[Reader](func(yield func(string, Reader) bool) {
|
||||
for _, tc := range testCases {
|
||||
if !yield(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})(t)
|
||||
|
||||
if traverseResult != seqResult {
|
||||
t.Error("Expected TraverseArray and SequenceSeq2 to produce same result")
|
||||
}
|
||||
|
||||
if !traverseResult || !seqResult {
|
||||
t.Error("Expected both approaches to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_EmptyMap tests that TraverseRecord handles empty maps correctly
|
||||
func TestTraverseRecord_EmptyMap(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n)(n)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with empty map")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_SingleEntry tests TraverseRecord with a single map entry
|
||||
func TestTraverseRecord_SingleEntry(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n * 2)(n * 2)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{"test_5": 5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with single entry")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_MultipleEntries tests TraverseRecord with multiple passing entries
|
||||
func TestTraverseRecord_MultipleEntries(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n * n)(n * n)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"square_1": 1,
|
||||
"square_2": 2,
|
||||
"square_3": 3,
|
||||
"square_4": 4,
|
||||
"square_5": 5,
|
||||
})(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with all passing entries")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_WithFailure tests that TraverseRecord fails when one entry fails
|
||||
func TestTraverseRecord_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(10)(n) // Will fail for all except 10
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"test_1": 1,
|
||||
"test_2": 2,
|
||||
"test_3": 3,
|
||||
})(t)
|
||||
|
||||
// The traverse should return false because entries don't match
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when entries don't match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_MixedResults tests TraverseRecord with some passing and some failing
|
||||
func TestTraverseRecord_MixedResults(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(0)(n % 2) // Only passes for even numbers
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"even_2": 2,
|
||||
"odd_3": 3,
|
||||
"even_4": 4,
|
||||
})(t)
|
||||
|
||||
// The traverse should return false because some entries fail
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when some entries fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_StringData tests TraverseRecord with string data
|
||||
func TestTraverseRecord_StringData(t *testing.T) {
|
||||
words := map[string]string{
|
||||
"greeting": "hello",
|
||||
"world": "world",
|
||||
"test": "test",
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(s string) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(s),
|
||||
That(func(str string) bool { return len(str) > 0 })(s),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(words)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with valid strings")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ComplexObjects tests TraverseRecord with complex objects
|
||||
func TestTraverseRecord_ComplexObjects(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := map[string]User{
|
||||
"alice": {Name: "Alice", Age: 30},
|
||||
"bob": {Name: "Bob", Age: 25},
|
||||
"charlie": {Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(u User) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with valid users")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ComplexObjectsWithFailure tests TraverseRecord with invalid complex objects
|
||||
func TestTraverseRecord_ComplexObjectsWithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := map[string]User{
|
||||
"alice": {Name: "Alice", Age: 30},
|
||||
"invalid": {Name: "", Age: 25}, // Invalid: empty name
|
||||
"charlie": {Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(u User) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
|
||||
// The traverse should return false because one user is invalid
|
||||
if result {
|
||||
t.Error("Expected traverse to return false with invalid user")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ConfigurationTesting demonstrates configuration testing pattern
|
||||
func TestTraverseRecord_ConfigurationTesting(t *testing.T) {
|
||||
configs := map[string]int{
|
||||
"timeout": 30,
|
||||
"maxRetries": 3,
|
||||
"bufferSize": 1024,
|
||||
}
|
||||
|
||||
validatePositive := That(func(n int) bool { return n > 0 })
|
||||
|
||||
traverse := TraverseRecord(validatePositive)
|
||||
result := traverse(configs)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all configuration values to be positive")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_APIEndpointTesting demonstrates API endpoint testing pattern
|
||||
func TestTraverseRecord_APIEndpointTesting(t *testing.T) {
|
||||
type Endpoint struct {
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
|
||||
endpoints := map[string]Endpoint{
|
||||
"get_users": {Path: "/api/users", Method: "GET"},
|
||||
"create_user": {Path: "/api/users", Method: "POST"},
|
||||
"delete_user": {Path: "/api/users/:id", Method: "DELETE"},
|
||||
}
|
||||
|
||||
validateEndpoint := func(e Endpoint) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(e.Path),
|
||||
That(func(path string) bool {
|
||||
return len(path) > 0 && path[0] == '/'
|
||||
})(e.Path),
|
||||
That(func(method string) bool {
|
||||
return method == "GET" || method == "POST" ||
|
||||
method == "PUT" || method == "DELETE"
|
||||
})(e.Method),
|
||||
})
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(validateEndpoint)
|
||||
result := traverse(endpoints)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all endpoints to be valid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_EmptyMap tests that SequenceRecord handles empty maps correctly
|
||||
func TestSequenceRecord_EmptyMap(t *testing.T) {
|
||||
result := SequenceRecord(map[string]Reader{})(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with empty map")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_SingleTest tests SequenceRecord with a single test
|
||||
func TestSequenceRecord_SingleTest(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"test_one": Equal(42)(42),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with single test")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_MultipleTests tests SequenceRecord with multiple passing tests
|
||||
func TestSequenceRecord_MultipleTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"test_addition": Equal(4)(2 + 2),
|
||||
"test_subtraction": Equal(1)(3 - 2),
|
||||
"test_multiplication": Equal(6)(2 * 3),
|
||||
"test_division": Equal(2)(6 / 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with all passing tests")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_WithFailure tests that SequenceRecord fails when one test fails
|
||||
func TestSequenceRecord_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
tests := map[string]Reader{
|
||||
"test_pass": Equal(4)(2 + 2),
|
||||
"test_fail": Equal(5)(2 + 2), // This will fail
|
||||
"test_pass2": Equal(6)(2 * 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
|
||||
// The sequence should return false because one test fails
|
||||
if result {
|
||||
t.Error("Expected sequence to return false when one test fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_StringTests tests SequenceRecord with string assertions
|
||||
func TestSequenceRecord_StringTests(t *testing.T) {
|
||||
testString := "hello world"
|
||||
|
||||
tests := map[string]Reader{
|
||||
"not_empty": StringNotEmpty(testString),
|
||||
"correct_length": StringLength[any, any](11)(testString),
|
||||
"has_space": That(func(s string) bool {
|
||||
for _, ch := range s {
|
||||
if ch == ' ' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(testString),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all string tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ArrayTests tests SequenceRecord with array assertions
|
||||
func TestSequenceRecord_ArrayTests(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"not_empty": ArrayNotEmpty(arr),
|
||||
"correct_length": ArrayLength[int](5)(arr),
|
||||
"contains_three": ArrayContains(3)(arr),
|
||||
"all_positive": That(func(arr []int) bool {
|
||||
for _, n := range arr {
|
||||
if n <= 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})(arr),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all array tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ComplexAssertions tests SequenceRecord with complex combined assertions
|
||||
func TestSequenceRecord_ComplexAssertions(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"name_not_empty": StringNotEmpty(user.Name),
|
||||
"age_positive": That(func(age int) bool { return age > 0 })(user.Age),
|
||||
"age_reasonable": That(func(age int) bool { return age < 150 })(user.Age),
|
||||
"email_valid": That(func(email string) bool {
|
||||
hasAt := false
|
||||
hasDot := false
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
hasAt = true
|
||||
}
|
||||
if ch == '.' {
|
||||
hasDot = true
|
||||
}
|
||||
}
|
||||
return hasAt && hasDot
|
||||
})(user.Email),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all user validation tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_MathOperations demonstrates basic math operations testing
|
||||
func TestSequenceRecord_MathOperations(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"addition": Equal(4)(2 + 2),
|
||||
"subtraction": Equal(1)(3 - 2),
|
||||
"multiplication": Equal(6)(2 * 3),
|
||||
"division": Equal(2)(6 / 3),
|
||||
"modulo": Equal(1)(7 % 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all math operations to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_BooleanTests tests SequenceRecord with boolean assertions
|
||||
func TestSequenceRecord_BooleanTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"true_is_true": Equal(true)(true),
|
||||
"false_is_false": Equal(false)(false),
|
||||
"not_true": Equal(false)(!true),
|
||||
"not_false": Equal(true)(!false),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all boolean tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ErrorTests tests SequenceRecord with error assertions
|
||||
func TestSequenceRecord_ErrorTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"no_error": NoError(nil),
|
||||
"equal_value": Equal("test")("test"),
|
||||
"not_empty": StringNotEmpty("hello"),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all error tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_vs_SequenceRecord demonstrates the relationship between the two functions
|
||||
func TestTraverseRecord_vs_SequenceRecord(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testData := map[string]TestCase{
|
||||
"test_1": {Input: 2, Expected: 4},
|
||||
"test_2": {Input: 3, Expected: 9},
|
||||
"test_3": {Input: 4, Expected: 16},
|
||||
}
|
||||
|
||||
// Using TraverseRecord
|
||||
traverseResult := TraverseRecord(func(tc TestCase) Reader {
|
||||
return Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
})(testData)(t)
|
||||
|
||||
// Using SequenceRecord (manually creating the map)
|
||||
tests := make(map[string]Reader)
|
||||
for name, tc := range testData {
|
||||
tests[name] = Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
}
|
||||
seqResult := SequenceRecord(tests)(t)
|
||||
|
||||
if traverseResult != seqResult {
|
||||
t.Error("Expected TraverseRecord and SequenceRecord to produce same result")
|
||||
}
|
||||
|
||||
if !traverseResult || !seqResult {
|
||||
t.Error("Expected both approaches to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_WithAllOf demonstrates combining SequenceRecord with AllOf
|
||||
func TestSequenceRecord_WithAllOf(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"array_validations": AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
}),
|
||||
"element_checks": AllOf([]Reader{
|
||||
That(func(a []int) bool { return a[0] == 1 })(arr),
|
||||
That(func(a []int) bool { return a[4] == 5 })(arr),
|
||||
}),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected combined assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ConfigValidation demonstrates real-world configuration validation
|
||||
func TestTraverseRecord_ConfigValidation(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
Min int
|
||||
Max int
|
||||
}
|
||||
|
||||
configs := map[string]Config{
|
||||
"timeout": {Value: 30, Min: 1, Max: 60},
|
||||
"maxRetries": {Value: 3, Min: 1, Max: 10},
|
||||
"bufferSize": {Value: 1024, Min: 512, Max: 4096},
|
||||
}
|
||||
|
||||
validateConfig := func(c Config) Reader {
|
||||
return AllOf([]Reader{
|
||||
That(func(val int) bool { return val >= c.Min })(c.Value),
|
||||
That(func(val int) bool { return val <= c.Max })(c.Value),
|
||||
})
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(validateConfig)
|
||||
result := traverse(configs)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all configurations to be within valid ranges")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_RealWorldExample demonstrates a realistic use case
|
||||
func TestSequenceRecord_RealWorldExample(t *testing.T) {
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
response := Response{StatusCode: 200, Body: `{"status":"ok"}`}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"status_ok": Equal(200)(response.StatusCode),
|
||||
"body_not_empty": StringNotEmpty(response.Body),
|
||||
"body_is_json": That(func(s string) bool {
|
||||
return len(s) > 0 && s[0] == '{' && s[len(s)-1] == '}'
|
||||
})(response.Body),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected response validation to pass")
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,32 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerio"
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -13,23 +34,506 @@ import (
|
||||
|
||||
type (
|
||||
// Result represents a computation that may fail with an error.
|
||||
//
|
||||
// This is an alias for [result.Result][T], which encapsulates either a successful
|
||||
// value of type T or an error. It's commonly used in test assertions to represent
|
||||
// operations that might fail, allowing for functional error handling without exceptions.
|
||||
//
|
||||
// A Result can be in one of two states:
|
||||
// - Success: Contains a value of type T
|
||||
// - Failure: Contains an error
|
||||
//
|
||||
// This type is particularly useful in testing scenarios where you need to:
|
||||
// - Test functions that return results
|
||||
// - Chain operations that might fail
|
||||
// - Handle errors functionally
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestResultHandling(t *testing.T) {
|
||||
// successResult := result.Of[int](42)
|
||||
// assert.Success(successResult)(t) // Passes
|
||||
//
|
||||
// failureResult := result.Error[int](errors.New("failed"))
|
||||
// assert.Failure(failureResult)(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Success]: Asserts a Result is successful
|
||||
// - [Failure]: Asserts a Result contains an error
|
||||
// - [result.Result]: The underlying Result type
|
||||
Result[T any] = result.Result[T]
|
||||
|
||||
// Reader represents a test assertion that depends on a testing.T context and returns a boolean.
|
||||
// Reader represents a test assertion that depends on a [testing.T] context and returns a boolean.
|
||||
//
|
||||
// This is the core type for all assertions in this package. It's an alias for
|
||||
// [reader.Reader][*testing.T, bool], which is a function that takes a testing context
|
||||
// and produces a boolean result indicating whether the assertion passed.
|
||||
//
|
||||
// The Reader pattern enables:
|
||||
// - Composable assertions that can be combined using functional operators
|
||||
// - Deferred execution - assertions are defined but not executed until applied to a test
|
||||
// - Reusable assertion logic that can be applied to multiple tests
|
||||
// - Functional composition of complex test conditions
|
||||
//
|
||||
// All assertion functions in this package return a Reader, which must be applied
|
||||
// to a *testing.T to execute the assertion:
|
||||
//
|
||||
// assertion := assert.Equal(42)(result) // Creates a Reader
|
||||
// assertion(t) // Executes the assertion
|
||||
//
|
||||
// Readers can be composed using functions like [AllOf], [ApplicativeMonoid], or
|
||||
// functional operators from the reader package.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderComposition(t *testing.T) {
|
||||
// // Create individual assertions
|
||||
// assertion1 := assert.Equal(42)(42)
|
||||
// assertion2 := assert.StringNotEmpty("hello")
|
||||
//
|
||||
// // Combine them
|
||||
// combined := assert.AllOf([]assert.Reader{assertion1, assertion2})
|
||||
//
|
||||
// // Execute the combined assertion
|
||||
// combined(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Kleisli]: Function that produces a Reader from a value
|
||||
// - [AllOf]: Combines multiple Readers
|
||||
// - [ApplicativeMonoid]: Monoid for combining Readers
|
||||
Reader = reader.Reader[*testing.T, bool]
|
||||
|
||||
// Kleisli represents a function that produces a test assertion Reader from a value of type T.
|
||||
// Kleisli represents a function that produces a test assertion [Reader] from a value of type T.
|
||||
//
|
||||
// This is an alias for [reader.Reader][T, Reader], which is a function that takes a value
|
||||
// of type T and returns a Reader (test assertion). This pattern is fundamental to the
|
||||
// "data last" principle used throughout this package.
|
||||
//
|
||||
// Kleisli functions enable:
|
||||
// - Partial application of assertions - configure the expected value first, apply actual value later
|
||||
// - Reusable assertion builders that can be applied to different values
|
||||
// - Functional composition of assertion pipelines
|
||||
// - Point-free style programming with assertions
|
||||
//
|
||||
// Most assertion functions in this package return a Kleisli, which must be applied
|
||||
// to the actual value being tested, and then to a *testing.T:
|
||||
//
|
||||
// kleisli := assert.Equal(42) // Kleisli[int] - expects an int
|
||||
// reader := kleisli(result) // Reader - assertion ready to execute
|
||||
// reader(t) // Execute the assertion
|
||||
//
|
||||
// Or more concisely:
|
||||
//
|
||||
// assert.Equal(42)(result)(t)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestKleisliPattern(t *testing.T) {
|
||||
// // Create a reusable assertion for positive numbers
|
||||
// isPositive := assert.That(func(n int) bool { return n > 0 })
|
||||
//
|
||||
// // Apply it to different values
|
||||
// isPositive(42)(t) // Passes
|
||||
// isPositive(100)(t) // Passes
|
||||
// // isPositive(-5)(t) would fail
|
||||
//
|
||||
// // Can be used with Local for property testing
|
||||
// type User struct { Age int }
|
||||
// checkAge := assert.Local(func(u User) int { return u.Age })(isPositive)
|
||||
// checkAge(User{Age: 25})(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Reader]: The assertion type produced by Kleisli
|
||||
// - [Local]: Focuses a Kleisli on a property of a larger structure
|
||||
Kleisli[T any] = reader.Reader[T, Reader]
|
||||
|
||||
// Predicate represents a function that tests a value of type T and returns a boolean.
|
||||
//
|
||||
// This is an alias for [predicate.Predicate][T], which is a simple function that
|
||||
// takes a value and returns true or false based on some condition. Predicates are
|
||||
// used with the [That] function to create custom assertions.
|
||||
//
|
||||
// Predicates enable:
|
||||
// - Custom validation logic for any type
|
||||
// - Reusable test conditions
|
||||
// - Composition of complex validation rules
|
||||
// - Integration with functional programming patterns
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPredicates(t *testing.T) {
|
||||
// // Simple predicate
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// assert.That(isEven)(42)(t) // Passes
|
||||
//
|
||||
// // String predicate
|
||||
// hasPrefix := func(s string) bool { return strings.HasPrefix(s, "test") }
|
||||
// assert.That(hasPrefix)("test_file.go")(t) // Passes
|
||||
//
|
||||
// // Complex predicate
|
||||
// isValidEmail := func(s string) bool {
|
||||
// return strings.Contains(s, "@") && strings.Contains(s, ".")
|
||||
// }
|
||||
// assert.That(isValidEmail)("user@example.com")(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [That]: Creates an assertion from a Predicate
|
||||
// - [predicate.Predicate]: The underlying predicate type
|
||||
Predicate[T any] = predicate.Predicate[T]
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
//
|
||||
// This is an alias for [lens.Lens][S, T], which provides a composable way to focus
|
||||
// on a specific field within a larger structure. Lenses enable getting and setting
|
||||
// values in nested data structures in a functional, immutable way.
|
||||
//
|
||||
// In the context of testing, lenses are used with [LocalL] to focus assertions
|
||||
// on specific properties of complex objects without manually extracting those properties.
|
||||
//
|
||||
// A Lens[S, T] focuses on a value of type T within a structure of type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestLensUsage(t *testing.T) {
|
||||
// type Address struct { City string }
|
||||
// type User struct { Name string; Address Address }
|
||||
//
|
||||
// // Define lenses (typically generated)
|
||||
// addressLens := lens.Lens[User, Address]{...}
|
||||
// cityLens := lens.Lens[Address, string]{...}
|
||||
//
|
||||
// // Compose lenses to focus on nested field
|
||||
// userCityLens := lens.Compose(addressLens, cityLens)
|
||||
//
|
||||
// // Use with LocalL to assert on nested property
|
||||
// user := User{Name: "Alice", Address: Address{City: "NYC"}}
|
||||
// assert.LocalL(userCityLens)(assert.Equal("NYC"))(user)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [LocalL]: Uses a Lens to focus assertions on a property
|
||||
// - [lens.Lens]: The underlying lens type
|
||||
// - [Optional]: Similar but for values that may not exist
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Optional is an optic that focuses on a value that may or may not be present.
|
||||
//
|
||||
// This is an alias for [optional.Optional][S, T], which is similar to a [Lens] but
|
||||
// handles cases where the focused value might not exist. Optionals are useful for
|
||||
// working with nullable fields, optional properties, or values that might be absent.
|
||||
//
|
||||
// In testing, Optionals are used with [FromOptional] to create assertions that
|
||||
// verify whether an optional value is present and, if so, whether it satisfies
|
||||
// certain conditions.
|
||||
//
|
||||
// An Optional[S, T] focuses on an optional value of type T within a structure of type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestOptionalUsage(t *testing.T) {
|
||||
// type Config struct { Timeout *int }
|
||||
//
|
||||
// // Define optional (typically generated)
|
||||
// timeoutOptional := optional.Optional[Config, int]{...}
|
||||
//
|
||||
// // Test when value is present
|
||||
// config1 := Config{Timeout: ptr(30)}
|
||||
// assert.FromOptional(timeoutOptional)(
|
||||
// assert.Equal(30),
|
||||
// )(config1)(t) // Passes
|
||||
//
|
||||
// // Test when value is absent
|
||||
// config2 := Config{Timeout: nil}
|
||||
// // FromOptional would fail because value is not present
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromOptional]: Creates assertions for optional values
|
||||
// - [optional.Optional]: The underlying optional type
|
||||
// - [Lens]: Similar but for values that always exist
|
||||
Optional[S, T any] = optional.Optional[S, T]
|
||||
|
||||
// Prism is an optic that focuses on a case of a sum type.
|
||||
//
|
||||
// This is an alias for [prism.Prism][S, T], which provides a way to focus on one
|
||||
// variant of a sum type (like Result, Option, Either, etc.). Prisms enable pattern
|
||||
// matching and extraction of values from sum types in a functional way.
|
||||
//
|
||||
// In testing, Prisms are used with [FromPrism] to create assertions that verify
|
||||
// whether a value matches a specific case and, if so, whether the contained value
|
||||
// satisfies certain conditions.
|
||||
//
|
||||
// A Prism[S, T] focuses on a value of type T that may be contained within a sum type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPrismUsage(t *testing.T) {
|
||||
// // Prism for extracting success value from Result
|
||||
// successPrism := prism.Success[int]()
|
||||
//
|
||||
// // Test successful result
|
||||
// successResult := result.Of[int](42)
|
||||
// assert.FromPrism(successPrism)(
|
||||
// assert.Equal(42),
|
||||
// )(successResult)(t) // Passes
|
||||
//
|
||||
// // Prism for extracting error from Result
|
||||
// failurePrism := prism.Failure[int]()
|
||||
//
|
||||
// // Test failed result
|
||||
// failureResult := result.Error[int](errors.New("failed"))
|
||||
// assert.FromPrism(failurePrism)(
|
||||
// assert.Error,
|
||||
// )(failureResult)(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromPrism]: Creates assertions for prism-focused values
|
||||
// - [prism.Prism]: The underlying prism type
|
||||
// - [Optional]: Similar but for optional values
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
|
||||
// ReaderIOResult represents a context-aware, IO-based computation that may fail.
|
||||
//
|
||||
// This is an alias for [readerioresult.ReaderIOResult][A], which combines three
|
||||
// computational effects:
|
||||
// - Reader: Depends on a context (like context.Context)
|
||||
// - IO: Performs side effects (like file I/O, network calls)
|
||||
// - Result: May fail with an error
|
||||
//
|
||||
// In testing, ReaderIOResult is used with [FromReaderIOResult] to convert
|
||||
// context-aware, effectful computations into test assertions. This is useful
|
||||
// when your test assertions need to:
|
||||
// - Access a context for cancellation or deadlines
|
||||
// - Perform IO operations (database queries, API calls, file access)
|
||||
// - Handle potential errors gracefully
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderIOResult(t *testing.T) {
|
||||
// // Create a ReaderIOResult that performs IO and may fail
|
||||
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
|
||||
// return func() result.Result[assert.Reader] {
|
||||
// // Perform database check with context
|
||||
// if err := db.PingContext(ctx); err != nil {
|
||||
// return result.Error[assert.Reader](err)
|
||||
// }
|
||||
// return result.Of[assert.Reader](assert.NoError(nil))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIOResult(checkDatabase)
|
||||
// assertion(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromReaderIOResult]: Converts ReaderIOResult to Reader
|
||||
// - [ReaderIO]: Similar but without error handling
|
||||
// - [readerioresult.ReaderIOResult]: The underlying type
|
||||
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
|
||||
|
||||
// ReaderIO represents a context-aware, IO-based computation.
|
||||
//
|
||||
// This is an alias for [readerio.ReaderIO][A], which combines two computational effects:
|
||||
// - Reader: Depends on a context (like context.Context)
|
||||
// - IO: Performs side effects (like logging, metrics)
|
||||
//
|
||||
// In testing, ReaderIO is used with [FromReaderIO] to convert context-aware,
|
||||
// effectful computations into test assertions. This is useful when your test
|
||||
// assertions need to:
|
||||
// - Access a context for cancellation or deadlines
|
||||
// - Perform IO operations that don't fail (or handle failures internally)
|
||||
// - Integrate with context-aware utilities
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderIO(t *testing.T) {
|
||||
// // Create a ReaderIO that performs IO
|
||||
// logAndCheck := func(ctx context.Context) func() assert.Reader {
|
||||
// return func() assert.Reader {
|
||||
// // Log with context
|
||||
// logger.InfoContext(ctx, "Running test")
|
||||
// // Return assertion
|
||||
// return assert.Equal(42)(computeValue())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIO(logAndCheck)
|
||||
// assertion(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromReaderIO]: Converts ReaderIO to Reader
|
||||
// - [ReaderIOResult]: Similar but with error handling
|
||||
// - [readerio.ReaderIO]: The underlying type
|
||||
ReaderIO[A any] = readerio.ReaderIO[A]
|
||||
|
||||
// Seq2 represents a Go iterator that yields key-value pairs.
|
||||
//
|
||||
// This is an alias for [iter.Seq2][K, A], which is Go's standard iterator type
|
||||
// introduced in Go 1.23. It represents a sequence of key-value pairs that can be
|
||||
// iterated over using a for-range loop.
|
||||
//
|
||||
// In testing, Seq2 is used with [SequenceSeq2] to execute a sequence of named
|
||||
// test cases provided as an iterator. This enables:
|
||||
// - Lazy evaluation of test cases
|
||||
// - Memory-efficient testing of large test suites
|
||||
// - Integration with Go's iterator patterns
|
||||
// - Dynamic generation of test cases
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestSeq2Usage(t *testing.T) {
|
||||
// // Create an iterator of test cases
|
||||
// testCases := func(yield func(string, assert.Reader) bool) {
|
||||
// if !yield("test_addition", assert.Equal(4)(2+2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute all test cases
|
||||
// assert.SequenceSeq2[assert.Reader](testCases)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [SequenceSeq2]: Executes a Seq2 of test cases
|
||||
// - [TraverseArray]: Similar but for arrays
|
||||
// - [iter.Seq2]: The underlying iterator type
|
||||
Seq2[K, A any] = iter.Seq2[K, A]
|
||||
|
||||
// Pair represents a tuple of two values with potentially different types.
|
||||
//
|
||||
// This is an alias for [pair.Pair][L, R], which holds two values: a "head" (or "left")
|
||||
// of type L and a "tail" (or "right") of type R. Pairs are useful for grouping
|
||||
// related values together without defining a custom struct.
|
||||
//
|
||||
// In testing, Pairs are used with [TraverseArray] to associate test names with
|
||||
// their corresponding assertions. Each element in the array is transformed into
|
||||
// a Pair[string, Reader] where the string is the test name and the Reader is
|
||||
// the assertion to execute.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPairUsage(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Input int
|
||||
// Expected int
|
||||
// }
|
||||
//
|
||||
// testCases := []TestCase{
|
||||
// {Input: 2, Expected: 4},
|
||||
// {Input: 3, Expected: 9},
|
||||
// }
|
||||
//
|
||||
// // Transform each test case into a named assertion
|
||||
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
|
||||
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
|
||||
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
// return pair.MakePair(name, assertion)
|
||||
// })
|
||||
//
|
||||
// traverse(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [TraverseArray]: Uses Pairs to create named test cases
|
||||
// - [pair.Pair]: The underlying pair type
|
||||
// - [pair.MakePair]: Creates a Pair
|
||||
// - [pair.Head]: Extracts the first value
|
||||
// - [pair.Tail]: Extracts the second value
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
|
||||
// Void represents the absence of a meaningful value, similar to unit type in functional programming.
|
||||
//
|
||||
// This is an alias for [function.Void], which is used to represent operations that don't
|
||||
// return a meaningful value but may perform side effects. In the context of testing, Void
|
||||
// is used with IO operations that perform actions without producing a result.
|
||||
//
|
||||
// Void is conceptually similar to:
|
||||
// - Unit type in functional languages (Haskell's (), Scala's Unit)
|
||||
// - void in languages like C/Java (but as a value, not just a type)
|
||||
// - Empty struct{} in Go (but with clearer semantic meaning)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithSideEffect(t *testing.T) {
|
||||
// // An IO operation that logs but returns Void
|
||||
// logOperation := func() function.Void {
|
||||
// log.Println("Test executed")
|
||||
// return function.Void{}
|
||||
// }
|
||||
//
|
||||
// // Execute the operation
|
||||
// logOperation()
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [IO]: Wraps side-effecting operations
|
||||
// - [function.Void]: The underlying void type
|
||||
Void = function.Void
|
||||
|
||||
// IO represents a side-effecting computation that produces a value of type A.
|
||||
//
|
||||
// This is an alias for [io.IO][A], which encapsulates operations that perform side effects
|
||||
// (like I/O operations, logging, or state mutations) and return a value. IO is a lazy
|
||||
// computation - it describes an effect but doesn't execute it until explicitly run.
|
||||
//
|
||||
// In testing, IO is used to:
|
||||
// - Defer execution of side effects until needed
|
||||
// - Compose multiple side-effecting operations
|
||||
// - Maintain referential transparency in test setup
|
||||
// - Separate effect description from effect execution
|
||||
//
|
||||
// An IO[A] is essentially a function `func() A` that:
|
||||
// - Encapsulates a side effect
|
||||
// - Returns a value of type A when executed
|
||||
// - Can be composed with other IO operations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestIOOperation(t *testing.T) {
|
||||
// // Define an IO operation that reads a file
|
||||
// readConfig := func() io.IO[string] {
|
||||
// return func() string {
|
||||
// data, _ := os.ReadFile("config.txt")
|
||||
// return string(data)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // The IO is not executed yet - it's just a description
|
||||
// configIO := readConfig()
|
||||
//
|
||||
// // Execute the IO to get the result
|
||||
// config := configIO()
|
||||
// assert.StringNotEmpty(config)(t)
|
||||
// }
|
||||
//
|
||||
// Example with composition:
|
||||
//
|
||||
// func TestIOComposition(t *testing.T) {
|
||||
// // Chain multiple IO operations
|
||||
// pipeline := io.Map(
|
||||
// func(s string) int { return len(s) },
|
||||
// )(readFileIO)
|
||||
//
|
||||
// // Execute the composed operation
|
||||
// length := pipeline()
|
||||
// assert.That(func(n int) bool { return n > 0 })(length)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [ReaderIO]: Combines Reader and IO effects
|
||||
// - [ReaderIOResult]: Adds error handling to ReaderIO
|
||||
// - [io.IO]: The underlying IO type
|
||||
// - [Void]: Represents operations without meaningful return values
|
||||
IO[A any] = io.IO[A]
|
||||
)
|
||||
|
||||
@@ -452,7 +452,7 @@ func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
|
||||
// Returns a function that chains Option-returning functions into ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -800,7 +800,7 @@ func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
|
||||
func FromReaderOption[A any](onNone Lazy[error]) Kleisli[ReaderOption[context.Context, A], A] {
|
||||
return RIOR.FromReaderOption[context.Context, A](onNone)
|
||||
}
|
||||
|
||||
@@ -895,17 +895,17 @@ func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
func ChainReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
func ChainFirstReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
func TapReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
|
||||
@@ -87,8 +87,8 @@ var (
|
||||
// assembleProviders constructs the provider map for item and non-item providers
|
||||
assembleProviders = F.Flow3(
|
||||
A.Partition(isItemProvider),
|
||||
T.Map2(collectProviders, collectItemProviders),
|
||||
T.Tupled2(mergeProviders.Concat),
|
||||
pair.BiMap(collectProviders, collectItemProviders),
|
||||
pair.Paired(mergeProviders.Concat),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
351
v2/either/filterable.go
Normal file
351
v2/either/filterable.go
Normal file
@@ -0,0 +1,351 @@
|
||||
// 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 either provides implementations of the Either type and related operations.
|
||||
//
|
||||
// This package implements several Fantasy Land algebraic structures:
|
||||
// - Filterable: https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The Filterable specification defines operations for filtering and partitioning
|
||||
// data structures based on predicates and mapping functions.
|
||||
package either
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Partition separates an [Either] value into a [Pair] based on a predicate function.
|
||||
// It returns a function that takes an Either and produces a Pair of Either values,
|
||||
// where the first element contains values that fail the predicate and the second
|
||||
// contains values that pass the predicate.
|
||||
//
|
||||
// This function implements the Filterable specification's partition operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is Left, both elements of the resulting Pair will be the same Left value
|
||||
// - If the input is Right and the predicate returns true, the result is (Left(empty), Right(value))
|
||||
// - If the input is Right and the predicate returns false, the result is (Right(value), Left(empty))
|
||||
//
|
||||
// This function is useful for separating Either values into two categories based on
|
||||
// a condition, commonly used in filtering operations where you want to keep track of
|
||||
// both the values that pass and fail a test.
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A predicate function that tests values of type A
|
||||
// - empty: The default Left value to use when creating Left instances for partitioning
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A function that takes an Either[E, A] and returns a Pair where:
|
||||
// - First element: Either values that fail the predicate (or original Left)
|
||||
// - Second element: Either values that pass the predicate (or original Left)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// E "github.com/IBM/fp-go/v2/either"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// P "github.com/IBM/fp-go/v2/pair"
|
||||
// )
|
||||
//
|
||||
// // Partition positive and non-positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// partition := E.Partition(isPositive, "not positive")
|
||||
//
|
||||
// // Right value that passes predicate
|
||||
// result1 := partition(E.Right[string](5))
|
||||
// // result1 = Pair(Left("not positive"), Right(5))
|
||||
// left1, right1 := P.Unpack(result1)
|
||||
// // left1 = Left("not positive"), right1 = Right(5)
|
||||
//
|
||||
// // Right value that fails predicate
|
||||
// result2 := partition(E.Right[string](-3))
|
||||
// // result2 = Pair(Right(-3), Left("not positive"))
|
||||
// left2, right2 := P.Unpack(result2)
|
||||
// // left2 = Right(-3), right2 = Left("not positive")
|
||||
//
|
||||
// // Left value passes through unchanged in both positions
|
||||
// result3 := partition(E.Left[int]("error"))
|
||||
// // result3 = Pair(Left("error"), Left("error"))
|
||||
// left3, right3 := P.Unpack(result3)
|
||||
// // left3 = Left("error"), right3 = Left("error")
|
||||
func Partition[E, A any](p Predicate[A], empty E) func(Either[E, A]) Pair[Either[E, A], Either[E, A]] {
|
||||
l := Left[A](empty)
|
||||
return func(e Either[E, A]) Pair[Either[E, A], Either[E, A]] {
|
||||
if e.isLeft {
|
||||
return pair.Of(e)
|
||||
}
|
||||
if p(e.r) {
|
||||
return pair.MakePair(l, e)
|
||||
}
|
||||
return pair.MakePair(e, l)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter creates a filtering operation for [Either] values based on a predicate function.
|
||||
// It returns a function that takes an Either and produces an Either, where Right values
|
||||
// that fail the predicate are converted to Left values with the provided empty value.
|
||||
//
|
||||
// This function implements the Filterable specification's filter operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is Left, it passes through unchanged
|
||||
// - If the input is Right and the predicate returns true, the Right value passes through unchanged
|
||||
// - If the input is Right and the predicate returns false, it's converted to Left(empty)
|
||||
//
|
||||
// This function is useful for conditional validation or filtering of Either values,
|
||||
// where you want to reject Right values that don't meet certain criteria by converting
|
||||
// them to Left values with a default error.
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A predicate function that tests values of type A
|
||||
// - empty: The default Left value to use when filtering out Right values that fail the predicate
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator function that takes an Either[E, A] and returns an Either[E, A] where:
|
||||
// - Left values pass through unchanged
|
||||
// - Right values that pass the predicate remain as Right
|
||||
// - Right values that fail the predicate become Left(empty)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// E "github.com/IBM/fp-go/v2/either"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// // Filter to keep only positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// filterPositive := E.Filter(isPositive, "not positive")
|
||||
//
|
||||
// // Right value that passes predicate - remains Right
|
||||
// result1 := filterPositive(E.Right[string](5))
|
||||
// // result1 = Right(5)
|
||||
//
|
||||
// // Right value that fails predicate - becomes Left
|
||||
// result2 := filterPositive(E.Right[string](-3))
|
||||
// // result2 = Left("not positive")
|
||||
//
|
||||
// // Left value passes through unchanged
|
||||
// result3 := filterPositive(E.Left[int]("original error"))
|
||||
// // result3 = Left("original error")
|
||||
//
|
||||
// // Chaining filters
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// filterEven := E.Filter(isEven, "not even")
|
||||
//
|
||||
// // Apply multiple filters in sequence
|
||||
// result4 := filterEven(filterPositive(E.Right[string](4)))
|
||||
// // result4 = Right(4) - passes both filters
|
||||
//
|
||||
// result5 := filterEven(filterPositive(E.Right[string](3)))
|
||||
// // result5 = Left("not even") - passes first, fails second
|
||||
func Filter[E, A any](p Predicate[A], empty E) Operator[E, A, A] {
|
||||
l := Left[A](empty)
|
||||
return func(e Either[E, A]) Either[E, A] {
|
||||
if e.isLeft || p(e.r) {
|
||||
return e
|
||||
}
|
||||
return l
|
||||
}
|
||||
}
|
||||
|
||||
// FilterMap combines filtering and mapping operations for [Either] values using an [Option]-returning function.
|
||||
// It returns a function that takes an Either[E, A] and produces an Either[E, B], where Right values
|
||||
// are transformed by applying the function f. If f returns Some(B), the result is Right(B). If f returns
|
||||
// None, the result is Left(empty).
|
||||
//
|
||||
// This function implements the Filterable specification's filterMap operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is Left, it passes through with its error value preserved as Left[B]
|
||||
// - If the input is Right and f returns Some(B), the result is Right(B)
|
||||
// - If the input is Right and f returns None, the result is Left(empty)
|
||||
//
|
||||
// This function is useful for operations that combine validation/filtering with transformation,
|
||||
// such as parsing strings to numbers (where invalid strings result in None), or extracting
|
||||
// optional fields from structures.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An Option Kleisli function that transforms values of type A to Option[B]
|
||||
// - empty: The default Left value to use when f returns None
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator function that takes an Either[E, A] and returns an Either[E, B] where:
|
||||
// - Left values pass through with error preserved
|
||||
// - Right values are transformed by f: Some(B) becomes Right(B), None becomes Left(empty)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// E "github.com/IBM/fp-go/v2/either"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// "strconv"
|
||||
// )
|
||||
//
|
||||
// // Parse string to int, filtering out invalid values
|
||||
// parseInt := func(s string) O.Option[int] {
|
||||
// if n, err := strconv.Atoi(s); err == nil {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
// filterMapInt := E.FilterMap(parseInt, "invalid number")
|
||||
//
|
||||
// // Valid number string - transforms to Right(int)
|
||||
// result1 := filterMapInt(E.Right[string]("42"))
|
||||
// // result1 = Right(42)
|
||||
//
|
||||
// // Invalid number string - becomes Left
|
||||
// result2 := filterMapInt(E.Right[string]("abc"))
|
||||
// // result2 = Left("invalid number")
|
||||
//
|
||||
// // Left value passes through with error preserved
|
||||
// result3 := filterMapInt(E.Left[string]("original error"))
|
||||
// // result3 = Left("original error")
|
||||
//
|
||||
// // Extract optional field from struct
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Email O.Option[string]
|
||||
// }
|
||||
// extractEmail := func(p Person) O.Option[string] { return p.Email }
|
||||
// filterMapEmail := E.FilterMap(extractEmail, "no email")
|
||||
//
|
||||
// result4 := filterMapEmail(E.Right[string](Person{Name: "Alice", Email: O.Some("alice@example.com")}))
|
||||
// // result4 = Right("alice@example.com")
|
||||
//
|
||||
// result5 := filterMapEmail(E.Right[string](Person{Name: "Bob", Email: O.None[string]()}))
|
||||
// // result5 = Left("no email")
|
||||
func FilterMap[E, A, B any](f option.Kleisli[A, B], empty E) Operator[E, A, B] {
|
||||
l := Left[B](empty)
|
||||
return func(e Either[E, A]) Either[E, B] {
|
||||
if e.isLeft {
|
||||
return Left[B](e.l)
|
||||
}
|
||||
if b, ok := option.Unwrap(f(e.r)); ok {
|
||||
return Right[E](b)
|
||||
}
|
||||
return l
|
||||
}
|
||||
}
|
||||
|
||||
// PartitionMap separates and transforms an [Either] value into a [Pair] of Either values using a mapping function.
|
||||
// It returns a function that takes an Either[E, A] and produces a Pair of Either values, where the mapping
|
||||
// function f transforms the Right value into Either[B, C]. The result is partitioned based on whether f
|
||||
// produces a Left or Right value.
|
||||
//
|
||||
// This function implements the Filterable specification's partitionMap operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is Left, both elements of the resulting Pair will be Left with the original error
|
||||
// - If the input is Right and f returns Left(B), the result is (Right(B), Left(empty))
|
||||
// - If the input is Right and f returns Right(C), the result is (Left(empty), Right(C))
|
||||
//
|
||||
// This function is useful for operations that need to categorize and transform values simultaneously,
|
||||
// such as separating valid and invalid data while applying different transformations to each category.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that transforms values of type A to Either[B, C]
|
||||
// - empty: The default error value to use when creating Left instances for partitioning
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A function that takes an Either[E, A] and returns a Pair[Either[E, B], Either[E, C]] where:
|
||||
// - If input is Left: (Left(original_error), Left(original_error))
|
||||
// - If f returns Left(B): (Right(B), Left(empty))
|
||||
// - If f returns Right(C): (Left(empty), Right(C))
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// E "github.com/IBM/fp-go/v2/either"
|
||||
// P "github.com/IBM/fp-go/v2/pair"
|
||||
// )
|
||||
//
|
||||
// // Classify and transform numbers: negative -> error message, positive -> squared value
|
||||
// classifyNumber := func(n int) E.Either[string, int] {
|
||||
// if n < 0 {
|
||||
// return E.Left[int]("negative: " + strconv.Itoa(n))
|
||||
// }
|
||||
// return E.Right[string](n * n)
|
||||
// }
|
||||
// partitionMap := E.PartitionMap(classifyNumber, "not classified")
|
||||
//
|
||||
// // Positive number - goes to right side as squared value
|
||||
// result1 := partitionMap(E.Right[string](5))
|
||||
// // result1 = Pair(Left("not classified"), Right(25))
|
||||
// left1, right1 := P.Unpack(result1)
|
||||
// // left1 = Left("not classified"), right1 = Right(25)
|
||||
//
|
||||
// // Negative number - goes to left side with error message
|
||||
// result2 := partitionMap(E.Right[string](-3))
|
||||
// // result2 = Pair(Right("negative: -3"), Left("not classified"))
|
||||
// left2, right2 := P.Unpack(result2)
|
||||
// // left2 = Right("negative: -3"), right2 = Left("not classified")
|
||||
//
|
||||
// // Original Left value - appears in both positions
|
||||
// result3 := partitionMap(E.Left[int]("original error"))
|
||||
// // result3 = Pair(Left("original error"), Left("original error"))
|
||||
// left3, right3 := P.Unpack(result3)
|
||||
// // left3 = Left("original error"), right3 = Left("original error")
|
||||
//
|
||||
// // Validate and transform user input
|
||||
// type ValidationError struct{ Field, Message string }
|
||||
// type User struct{ Name string; Age int }
|
||||
//
|
||||
// validateUser := func(input map[string]string) E.Either[ValidationError, User] {
|
||||
// name, hasName := input["name"]
|
||||
// ageStr, hasAge := input["age"]
|
||||
// if !hasName {
|
||||
// return E.Left[User](ValidationError{"name", "missing"})
|
||||
// }
|
||||
// if !hasAge {
|
||||
// return E.Left[User](ValidationError{"age", "missing"})
|
||||
// }
|
||||
// age, err := strconv.Atoi(ageStr)
|
||||
// if err != nil {
|
||||
// return E.Left[User](ValidationError{"age", "invalid"})
|
||||
// }
|
||||
// return E.Right[ValidationError](User{name, age})
|
||||
// }
|
||||
// partitionUsers := E.PartitionMap(validateUser, ValidationError{"", "not processed"})
|
||||
//
|
||||
// validInput := map[string]string{"name": "Alice", "age": "30"}
|
||||
// result4 := partitionUsers(E.Right[string](validInput))
|
||||
// // result4 = Pair(Left(ValidationError{"", "not processed"}), Right(User{"Alice", 30}))
|
||||
//
|
||||
// invalidInput := map[string]string{"name": "Bob"}
|
||||
// result5 := partitionUsers(E.Right[string](invalidInput))
|
||||
// // result5 = Pair(Right(ValidationError{"age", "missing"}), Left(ValidationError{"", "not processed"}))
|
||||
func PartitionMap[E, A, B, C any](f Kleisli[B, A, C], empty E) func(Either[E, A]) Pair[Either[E, B], Either[E, C]] {
|
||||
return func(e Either[E, A]) Pair[Either[E, B], Either[E, C]] {
|
||||
if e.isLeft {
|
||||
return pair.MakePair(Left[B](e.l), Left[C](e.l))
|
||||
}
|
||||
res := f(e.r)
|
||||
if res.isLeft {
|
||||
return pair.MakePair(Right[E](res.l), Left[C](empty))
|
||||
}
|
||||
return pair.MakePair(Left[B](empty), Right[E](res.r))
|
||||
}
|
||||
}
|
||||
1433
v2/either/filterable_test.go
Normal file
1433
v2/either/filterable_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
@@ -53,4 +54,6 @@ type (
|
||||
// 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]
|
||||
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
)
|
||||
|
||||
@@ -466,6 +466,11 @@ func Chain[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return F.Bind2nd(MonadChain[A, B], f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
// Flatten flattens a sequence of sequences into a single sequence.
|
||||
//
|
||||
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll
|
||||
|
||||
158
v2/iterator/iter/option.go
Normal file
158
v2/iterator/iter/option.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// 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 iter
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// MonadChainOptionK chains a function that returns an Option into a sequence,
|
||||
// filtering out None values and unwrapping Some values.
|
||||
//
|
||||
// This is useful for operations that may or may not produce a value for each element
|
||||
// in the sequence. Only the successful (Some) results are included in the output sequence,
|
||||
// while None values are filtered out.
|
||||
//
|
||||
// This is the monadic form that takes the sequence as the first parameter.
|
||||
//
|
||||
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: The element type of the input sequence
|
||||
// - B: The element type of the output sequence (wrapped in Option by the function)
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input sequence to transform
|
||||
// - f: A function that takes an element and returns an Option[B]
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A new sequence containing only the unwrapped Some values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "strconv"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// I "github.com/IBM/fp-go/v2/iterator/iter"
|
||||
// )
|
||||
//
|
||||
// // Parse strings to integers, filtering out invalid ones
|
||||
// parseNum := func(s string) O.Option[int] {
|
||||
// if n, err := strconv.Atoi(s); err == nil {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// seq := I.From("1", "invalid", "2", "3", "bad")
|
||||
// result := I.MonadChainOptionK(seq, parseNum)
|
||||
// // yields: 1, 2, 3 (invalid strings are filtered out)
|
||||
func MonadChainOptionK[A, B any](as Seq[A], f option.Kleisli[A, B]) Seq[B] {
|
||||
return MonadFilterMap(as, f)
|
||||
}
|
||||
|
||||
// ChainOptionK returns an operator that chains a function returning an Option into a sequence,
|
||||
// filtering out None values and unwrapping Some values.
|
||||
//
|
||||
// This is the curried version of [MonadChainOptionK], useful for function composition
|
||||
// and creating reusable transformations.
|
||||
//
|
||||
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: The element type of the input sequence
|
||||
// - B: The element type of the output sequence (wrapped in Option by the function)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an element and returns an Option[B]
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator that transforms Seq[A] to Seq[B], filtering out None values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "strconv"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// I "github.com/IBM/fp-go/v2/iterator/iter"
|
||||
// )
|
||||
//
|
||||
// // Create a reusable parser operator
|
||||
// parsePositive := I.ChainOptionK(func(x int) O.Option[int] {
|
||||
// if x > 0 {
|
||||
// return O.Some(x)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// I.From(-1, 2, -3, 4, 5),
|
||||
// parsePositive,
|
||||
// )
|
||||
// // yields: 2, 4, 5 (negative numbers are filtered out)
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
|
||||
return FilterMap(f)
|
||||
}
|
||||
|
||||
// FlatMapOptionK is an alias for [ChainOptionK].
|
||||
//
|
||||
// This provides a more familiar name for developers coming from other functional
|
||||
// programming languages or libraries where "flatMap" is the standard terminology
|
||||
// for the monadic bind operation.
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: The element type of the input sequence
|
||||
// - B: The element type of the output sequence (wrapped in Option by the function)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an element and returns an Option[B]
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator that transforms Seq[A] to Seq[B], filtering out None values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// I "github.com/IBM/fp-go/v2/iterator/iter"
|
||||
// )
|
||||
//
|
||||
// // Validate and transform data
|
||||
// validateAge := I.FlatMapOptionK(func(age int) O.Option[string] {
|
||||
// if age >= 18 && age <= 120 {
|
||||
// return O.Some(fmt.Sprintf("Valid age: %d", age))
|
||||
// }
|
||||
// return O.None[string]()
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// I.From(15, 25, 150, 30),
|
||||
// validateAge,
|
||||
// )
|
||||
// // yields: "Valid age: 25", "Valid age: 30"
|
||||
//
|
||||
//go:inline
|
||||
func FlatMapOptionK[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
|
||||
return ChainOptionK(f)
|
||||
}
|
||||
387
v2/iterator/iter/option_test.go
Normal file
387
v2/iterator/iter/option_test.go
Normal file
@@ -0,0 +1,387 @@
|
||||
// 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 iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestMonadChainOptionK_AllSome tests MonadChainOptionK when all values produce Some
|
||||
func TestMonadChainOptionK_AllSome(t *testing.T) {
|
||||
// Function that always returns Some
|
||||
double := func(x int) O.Option[int] {
|
||||
return O.Some(x * 2)
|
||||
}
|
||||
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := MonadChainOptionK(seq, double)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(2, 4, 6, 8, 10)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_AllNone tests MonadChainOptionK when all values produce None
|
||||
func TestMonadChainOptionK_AllNone(t *testing.T) {
|
||||
// Function that always returns None
|
||||
alwaysNone := func(x int) O.Option[int] {
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := MonadChainOptionK(seq, alwaysNone)
|
||||
values := slices.Collect(result)
|
||||
|
||||
assert.Empty(t, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_MixedSomeNone tests MonadChainOptionK with mixed Some and None
|
||||
func TestMonadChainOptionK_MixedSomeNone(t *testing.T) {
|
||||
// Function that returns Some for even numbers, None for odd
|
||||
evenOnly := func(x int) O.Option[int] {
|
||||
if x%2 == 0 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
seq := From(1, 2, 3, 4, 5, 6)
|
||||
result := MonadChainOptionK(seq, evenOnly)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(2, 4, 6)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_ParseStrings tests parsing strings to integers
|
||||
func TestMonadChainOptionK_ParseStrings(t *testing.T) {
|
||||
// Parse strings to integers, returning None for invalid strings
|
||||
parseNum := func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
seq := From("1", "invalid", "2", "3", "bad", "4")
|
||||
result := MonadChainOptionK(seq, parseNum)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(1, 2, 3, 4)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_EmptySequence tests MonadChainOptionK with empty sequence
|
||||
func TestMonadChainOptionK_EmptySequence(t *testing.T) {
|
||||
double := func(x int) O.Option[int] {
|
||||
return O.Some(x * 2)
|
||||
}
|
||||
|
||||
seq := From[int]()
|
||||
result := MonadChainOptionK(seq, double)
|
||||
values := slices.Collect(result)
|
||||
|
||||
assert.Empty(t, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_TypeTransformation tests transforming types
|
||||
func TestMonadChainOptionK_TypeTransformation(t *testing.T) {
|
||||
// Convert integers to strings, only for positive numbers
|
||||
positiveToString := func(x int) O.Option[string] {
|
||||
if x > 0 {
|
||||
return O.Some(fmt.Sprintf("num_%d", x))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
|
||||
seq := From(-2, -1, 0, 1, 2, 3)
|
||||
result := MonadChainOptionK(seq, positiveToString)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From("num_1", "num_2", "num_3")
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_ComplexType tests with complex types
|
||||
func TestMonadChainOptionK_ComplexType(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
// Extract age only for adults
|
||||
getAdultAge := func(p Person) O.Option[int] {
|
||||
if p.Age >= 18 {
|
||||
return O.Some(p.Age)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
seq := From(
|
||||
Person{"Alice", 25},
|
||||
Person{"Bob", 15},
|
||||
Person{"Charlie", 30},
|
||||
Person{"David", 12},
|
||||
)
|
||||
result := MonadChainOptionK(seq, getAdultAge)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(25, 30)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestChainOptionK_BasicUsage tests ChainOptionK basic functionality
|
||||
func TestChainOptionK_BasicUsage(t *testing.T) {
|
||||
// Create a reusable operator
|
||||
parsePositive := ChainOptionK(func(x int) O.Option[int] {
|
||||
if x > 0 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
seq := From(-1, 2, -3, 4, 5, -6)
|
||||
result := parsePositive(seq)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(2, 4, 5)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestChainOptionK_WithPipe tests ChainOptionK in a pipeline
|
||||
func TestChainOptionK_WithPipe(t *testing.T) {
|
||||
// Validate and transform in a pipeline
|
||||
validateRange := ChainOptionK(func(x int) O.Option[int] {
|
||||
if x >= 0 && x <= 100 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
result := F.Pipe2(
|
||||
From(-10, 20, 150, 50, 200, 75),
|
||||
validateRange,
|
||||
Map(func(x int) int { return x * 2 }),
|
||||
)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(40, 100, 150)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestChainOptionK_Composition tests composing multiple ChainOptionK operations
|
||||
func TestChainOptionK_Composition(t *testing.T) {
|
||||
// First filter: only positive
|
||||
onlyPositive := ChainOptionK(func(x int) O.Option[int] {
|
||||
if x > 0 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
// Second filter: only even
|
||||
onlyEven := ChainOptionK(func(x int) O.Option[int] {
|
||||
if x%2 == 0 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
result := F.Pipe2(
|
||||
From(-2, -1, 0, 1, 2, 3, 4, 5, 6),
|
||||
onlyPositive,
|
||||
onlyEven,
|
||||
)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(2, 4, 6)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestChainOptionK_StringParsing tests parsing with ChainOptionK
|
||||
func TestChainOptionK_StringParsing(t *testing.T) {
|
||||
// Create a reusable string parser
|
||||
parseInt := ChainOptionK(func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
From("10", "abc", "20", "xyz", "30"),
|
||||
parseInt,
|
||||
)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(10, 20, 30)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestFlatMapOptionK_Equivalence tests that FlatMapOptionK is equivalent to ChainOptionK
|
||||
func TestFlatMapOptionK_Equivalence(t *testing.T) {
|
||||
validate := func(x int) O.Option[int] {
|
||||
if x >= 0 && x <= 10 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
seq := From(-5, 0, 5, 10, 15)
|
||||
|
||||
// Using ChainOptionK
|
||||
result1 := ChainOptionK(validate)(seq)
|
||||
values1 := slices.Collect(result1)
|
||||
|
||||
// Using FlatMapOptionK
|
||||
result2 := FlatMapOptionK(validate)(seq)
|
||||
values2 := slices.Collect(result2)
|
||||
|
||||
// Both should produce the same result
|
||||
assert.Equal(t, values1, values2)
|
||||
assert.Equal(t, A.From(0, 5, 10), values1)
|
||||
}
|
||||
|
||||
// TestFlatMapOptionK_WithMap tests FlatMapOptionK combined with Map
|
||||
func TestFlatMapOptionK_WithMap(t *testing.T) {
|
||||
// Validate age and convert to category
|
||||
validateAge := FlatMapOptionK(func(age int) O.Option[string] {
|
||||
if age >= 18 && age <= 120 {
|
||||
return O.Some(fmt.Sprintf("Valid age: %d", age))
|
||||
}
|
||||
return O.None[string]()
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
From(15, 25, 150, 30, 200),
|
||||
validateAge,
|
||||
)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From("Valid age: 25", "Valid age: 30")
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestChainOptionK_LookupOperation tests using ChainOptionK for lookup operations
|
||||
func TestChainOptionK_LookupOperation(t *testing.T) {
|
||||
// Simulate a lookup table
|
||||
lookup := map[string]int{
|
||||
"one": 1,
|
||||
"two": 2,
|
||||
"three": 3,
|
||||
}
|
||||
|
||||
lookupValue := ChainOptionK(func(key string) O.Option[int] {
|
||||
if val, ok := lookup[key]; ok {
|
||||
return O.Some(val)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
From("one", "invalid", "two", "missing", "three"),
|
||||
lookupValue,
|
||||
)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(1, 2, 3)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_EarlyTermination tests that iteration stops when yield returns false
|
||||
func TestMonadChainOptionK_EarlyTermination(t *testing.T) {
|
||||
callCount := 0
|
||||
countCalls := func(x int) O.Option[int] {
|
||||
callCount++
|
||||
return O.Some(x)
|
||||
}
|
||||
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := MonadChainOptionK(seq, countCalls)
|
||||
|
||||
// Collect only first 3 elements
|
||||
collected := make([]int, 0)
|
||||
for v := range result {
|
||||
collected = append(collected, v)
|
||||
if len(collected) >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Should have called the function only 3 times due to early termination
|
||||
assert.Equal(t, 3, callCount)
|
||||
assert.Equal(t, A.From(1, 2, 3), collected)
|
||||
}
|
||||
|
||||
// TestChainOptionK_WithReduce tests ChainOptionK with reduction
|
||||
func TestChainOptionK_WithReduce(t *testing.T) {
|
||||
// Parse and sum valid numbers
|
||||
parseInt := ChainOptionK(func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
From("10", "invalid", "20", "bad", "30"),
|
||||
parseInt,
|
||||
)
|
||||
|
||||
sum := MonadReduce(result, func(acc, x int) int {
|
||||
return acc + x
|
||||
}, 0)
|
||||
|
||||
assert.Equal(t, 60, sum)
|
||||
}
|
||||
|
||||
// TestFlatMapOptionK_NestedOptions tests FlatMapOptionK with nested option handling
|
||||
func TestFlatMapOptionK_NestedOptions(t *testing.T) {
|
||||
type Result struct {
|
||||
Value int
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// Extract value only if valid
|
||||
extractValid := FlatMapOptionK(func(r Result) O.Option[int] {
|
||||
if r.Valid {
|
||||
return O.Some(r.Value)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
seq := From(
|
||||
Result{10, true},
|
||||
Result{20, false},
|
||||
Result{30, true},
|
||||
Result{40, false},
|
||||
Result{50, true},
|
||||
)
|
||||
|
||||
result := F.Pipe1(seq, extractValid)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(10, 30, 50)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
@@ -13,6 +13,50 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package monoid provides an implementation of the Monoid algebraic structure.
|
||||
//
|
||||
// # Monoid
|
||||
//
|
||||
// A Monoid is an algebraic structure that extends [Semigroup] by adding an identity element.
|
||||
// It consists of:
|
||||
// - A type A
|
||||
// - An associative binary operation Concat: (A, A) → A
|
||||
// - An identity element Empty: () → A
|
||||
//
|
||||
// # Laws
|
||||
//
|
||||
// A Monoid must satisfy the following laws:
|
||||
//
|
||||
// 1. Associativity (from Semigroup):
|
||||
// Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
//
|
||||
// 2. Left Identity:
|
||||
// Concat(Empty(), x) = x
|
||||
//
|
||||
// 3. Right Identity:
|
||||
// Concat(x, Empty()) = x
|
||||
//
|
||||
// # Common Examples
|
||||
//
|
||||
// - Integer addition: Concat = (+), Empty = 0
|
||||
// - Integer multiplication: Concat = (*), Empty = 1
|
||||
// - String concatenation: Concat = (++), Empty = ""
|
||||
// - List concatenation: Concat = (++), Empty = []
|
||||
// - Boolean AND: Concat = (&&), Empty = true
|
||||
// - Boolean OR: Concat = (||), Empty = false
|
||||
// - Function composition: Concat = (∘), Empty = id
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Data.Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
|
||||
// - Fantasy Land Monoid: https://github.com/fantasyland/fantasy-land#monoid
|
||||
// - Semigroup: https://github.com/IBM/fp-go/v2/semigroup
|
||||
//
|
||||
// # Related Concepts
|
||||
//
|
||||
// - [Semigroup]: A Monoid without the identity element requirement
|
||||
// - Magma: A set with a binary operation (no laws required)
|
||||
// - Group: A Monoid where every element has an inverse
|
||||
package monoid
|
||||
|
||||
import (
|
||||
@@ -21,20 +65,31 @@ import (
|
||||
|
||||
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
|
||||
//
|
||||
// A Monoid extends Semigroup by adding an identity element (Empty) that satisfies:
|
||||
// A Monoid extends [Semigroup] by adding an identity element (Empty) that satisfies:
|
||||
// - Left identity: Concat(Empty(), x) = x
|
||||
// - Right identity: Concat(x, Empty()) = x
|
||||
//
|
||||
// The Monoid must also satisfy the associativity law from Semigroup:
|
||||
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
//
|
||||
// Common examples:
|
||||
// # Methods
|
||||
//
|
||||
// - Concat(x, y A) A: Inherited from Semigroup, combines two values associatively
|
||||
// - Empty() A: Returns the identity element for the monoid
|
||||
//
|
||||
// # Common Examples
|
||||
//
|
||||
// - Integer addition with 0 as identity
|
||||
// - Integer multiplication with 1 as identity
|
||||
// - String concatenation with "" as identity
|
||||
// - List concatenation with [] as identity
|
||||
// - Boolean AND with true as identity
|
||||
// - Boolean OR with false as identity
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Monoid typeclass: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Monoid
|
||||
// - Fantasy Land Monoid specification: https://github.com/fantasyland/fantasy-land#monoid
|
||||
type Monoid[A any] interface {
|
||||
S.Semigroup[A]
|
||||
Empty() A
|
||||
@@ -58,16 +113,22 @@ func (m monoid[A]) Empty() A {
|
||||
// The provided concat function must be associative, and the empty element must
|
||||
// satisfy the identity laws (left and right identity).
|
||||
//
|
||||
// Parameters:
|
||||
// - c: An associative binary operation func(A, A) A
|
||||
// - e: The identity element of type A
|
||||
// This is the primary constructor for creating custom monoid instances. It's the
|
||||
// equivalent of defining a Monoid instance in Haskell or implementing the Fantasy Land
|
||||
// Monoid specification.
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[A] instance
|
||||
// # Parameters
|
||||
//
|
||||
// Example:
|
||||
// - c: An associative binary operation func(A, A) A (equivalent to Haskell's mappend or <>)
|
||||
// - e: The identity element of type A (equivalent to Haskell's mempty)
|
||||
//
|
||||
// // Integer addition monoid
|
||||
// # Returns
|
||||
//
|
||||
// - A [Monoid][A] instance
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Integer addition monoid (Sum in Haskell)
|
||||
// addMonoid := MakeMonoid(
|
||||
// func(a, b int) int { return a + b },
|
||||
// 0, // identity element
|
||||
@@ -81,6 +142,11 @@ func (m monoid[A]) Empty() A {
|
||||
// "", // identity element
|
||||
// )
|
||||
// result := stringMonoid.Concat("Hello", " World") // "Hello World"
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Monoid instance: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Monoid
|
||||
// - Fantasy Land Monoid.empty: https://github.com/fantasyland/fantasy-land#monoid
|
||||
func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
|
||||
return monoid[A]{c: c, e: e}
|
||||
}
|
||||
@@ -91,13 +157,18 @@ func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
|
||||
// operation in the opposite order. This is useful for operations that are
|
||||
// not commutative.
|
||||
//
|
||||
// Parameters:
|
||||
// This corresponds to the Dual newtype wrapper in Haskell's Data.Monoid, which
|
||||
// provides a Monoid instance with reversed operation order.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: The monoid to reverse
|
||||
//
|
||||
// Returns:
|
||||
// - A new Monoid[A] with reversed operation order
|
||||
// # Returns
|
||||
//
|
||||
// Example:
|
||||
// - A new [Monoid][A] with reversed operation order
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Subtraction monoid (not commutative)
|
||||
// subMonoid := MakeMonoid(
|
||||
@@ -116,6 +187,10 @@ func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
|
||||
// )
|
||||
// reversed := Reverse(stringMonoid)
|
||||
// result := reversed.Concat("Hello", "World") // "WorldHello"
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Data.Monoid.Dual: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Dual
|
||||
func Reverse[A any](m Monoid[A]) Monoid[A] {
|
||||
return MakeMonoid(S.Reverse(m).Concat, m.Empty())
|
||||
}
|
||||
@@ -125,13 +200,19 @@ func Reverse[A any](m Monoid[A]) Monoid[A] {
|
||||
// This is useful when you need to use a monoid in a context that only requires
|
||||
// a semigroup (associative binary operation without identity).
|
||||
//
|
||||
// Parameters:
|
||||
// Since every Monoid is also a Semigroup (Monoid extends Semigroup), this conversion
|
||||
// is always safe. This reflects the mathematical relationship where monoids form a
|
||||
// subset of semigroups.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: The monoid to convert
|
||||
//
|
||||
// Returns:
|
||||
// - A Semigroup[A] that uses the same Concat operation
|
||||
// # Returns
|
||||
//
|
||||
// Example:
|
||||
// - A [Semigroup][A] that uses the same Concat operation
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// addMonoid := MakeMonoid(
|
||||
// func(a, b int) int { return a + b },
|
||||
@@ -139,6 +220,11 @@ func Reverse[A any](m Monoid[A]) Monoid[A] {
|
||||
// )
|
||||
// sg := ToSemigroup(addMonoid)
|
||||
// result := sg.Concat(5, 3) // 8 (identity not available)
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Semigroup: https://hackage.haskell.org/package/base/docs/Data-Semigroup.html
|
||||
// - Fantasy Land Semigroup: https://github.com/fantasyland/fantasy-land#semigroup
|
||||
func ToSemigroup[A any](m Monoid[A]) S.Semigroup[A] {
|
||||
return S.Semigroup[A](m)
|
||||
}
|
||||
|
||||
153
v2/optics/iso/prism/compose.go
Normal file
153
v2/optics/iso/prism/compose.go
Normal file
@@ -0,0 +1,153 @@
|
||||
// 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 prism
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
P "github.com/IBM/fp-go/v2/optics/prism"
|
||||
)
|
||||
|
||||
// Compose creates a Kleisli arrow that composes a prism with an isomorphism.
|
||||
//
|
||||
// This function takes a Prism[A, B] and returns a Kleisli arrow that can transform
|
||||
// any Iso[S, A] into a Prism[S, B]. The resulting prism changes the source type from
|
||||
// A to S using the bidirectional transformation provided by the isomorphism, while
|
||||
// maintaining the same focus type B.
|
||||
//
|
||||
// The composition works as follows:
|
||||
// - GetOption: First transforms S to A using the iso's Get, then extracts B from A using the prism's GetOption
|
||||
// - ReverseGet: First constructs A from B using the prism's ReverseGet, then transforms A to S using the iso's ReverseGet
|
||||
//
|
||||
// This is the dual operation of optics/prism/iso.Compose:
|
||||
// - optics/prism/iso.Compose: Transforms the focus type (A → B) while keeping source type (S) constant
|
||||
// - optics/iso/prism.Compose: Transforms the source type (A → S) while keeping focus type (B) constant
|
||||
//
|
||||
// This is particularly useful when you have a prism that works with one type but you
|
||||
// need to adapt it to work with a different source type that has a lossless bidirectional
|
||||
// transformation to the original type.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The new source type after applying the isomorphism
|
||||
// - A: The original source type of the prism
|
||||
// - B: The focus type (remains constant through composition)
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: A prism that extracts B from A
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow (function) that takes an Iso[S, A] and returns a Prism[S, B]
|
||||
//
|
||||
// Laws:
|
||||
// The composed prism must satisfy the prism laws:
|
||||
// 1. GetOption(ReverseGet(b)) == Some(b) for all b: B
|
||||
// 2. If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)
|
||||
//
|
||||
// These laws are preserved because:
|
||||
// - The isomorphism satisfies: ia.ReverseGet(ia.Get(s)) == s and ia.Get(ia.ReverseGet(a)) == a
|
||||
// - The original prism satisfies the prism laws
|
||||
//
|
||||
// Haskell Equivalent:
|
||||
// This corresponds to the (.) operator for composing optics in Haskell's lens library,
|
||||
// specifically when composing an Iso with a Prism:
|
||||
//
|
||||
// iso . prism :: Iso s a -> Prism a b -> Prism s b
|
||||
//
|
||||
// In Haskell's lens library, this is part of the general optic composition mechanism.
|
||||
// See: https://hackage.haskell.org/package/lens/docs/Control-Lens-Iso.html
|
||||
//
|
||||
// Example - Composing with Either prism:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/either"
|
||||
// "github.com/IBM/fp-go/v2/optics/iso"
|
||||
// "github.com/IBM/fp-go/v2/optics/prism"
|
||||
// IP "github.com/IBM/fp-go/v2/optics/iso/prism"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// )
|
||||
//
|
||||
// // Create a prism that extracts Right values from Either[error, string]
|
||||
// rightPrism := prism.FromEither[error, string]()
|
||||
//
|
||||
// // Create an isomorphism between []byte and string
|
||||
// bytesStringIso := iso.MakeIso(
|
||||
// func(b []byte) string { return string(b) },
|
||||
// func(s string) []byte { return []byte(s) },
|
||||
// )
|
||||
//
|
||||
// // Compose them to get a prism that works with []byte as source
|
||||
// bytesPrism := IP.Compose(rightPrism)(bytesStringIso)
|
||||
//
|
||||
// // Use the composed prism
|
||||
// // First converts []byte to string via iso, then extracts Right value
|
||||
// bytes := []byte("hello")
|
||||
// either := either.Right[error](string(bytes))
|
||||
// result := bytesPrism.GetOption(bytes) // Extracts "hello" if Right
|
||||
//
|
||||
// // Construct []byte from string
|
||||
// constructed := bytesPrism.ReverseGet("world")
|
||||
// // Returns []byte("world") wrapped in Right
|
||||
//
|
||||
// Example - Composing with custom types:
|
||||
//
|
||||
// type JSON []byte
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Isomorphism between JSON and []byte
|
||||
// jsonIso := iso.MakeIso(
|
||||
// func(j JSON) []byte { return []byte(j) },
|
||||
// func(b []byte) JSON { return JSON(b) },
|
||||
// )
|
||||
//
|
||||
// // Prism that extracts Config from []byte (via JSON parsing)
|
||||
// configPrism := prism.MakePrism(
|
||||
// func(b []byte) option.Option[Config] {
|
||||
// var cfg Config
|
||||
// if err := json.Unmarshal(b, &cfg); err != nil {
|
||||
// return option.None[Config]()
|
||||
// }
|
||||
// return option.Some(cfg)
|
||||
// },
|
||||
// func(cfg Config) []byte {
|
||||
// b, _ := json.Marshal(cfg)
|
||||
// return b
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Compose to work with JSON type instead of []byte
|
||||
// jsonConfigPrism := IP.Compose(configPrism)(jsonIso)
|
||||
//
|
||||
// jsonData := JSON(`{"host":"localhost","port":8080}`)
|
||||
// config := jsonConfigPrism.GetOption(jsonData)
|
||||
// // config is Some(Config{Host: "localhost", Port: 8080})
|
||||
//
|
||||
// See also:
|
||||
// - github.com/IBM/fp-go/v2/optics/iso for isomorphism operations
|
||||
// - github.com/IBM/fp-go/v2/optics/prism for prism operations
|
||||
// - github.com/IBM/fp-go/v2/optics/prism/iso for the dual composition (transforming focus type)
|
||||
func Compose[S, A, B any](ab Prism[A, B]) P.Kleisli[S, Iso[S, A], B] {
|
||||
return func(ia Iso[S, A]) Prism[S, B] {
|
||||
return P.MakePrismWithName(
|
||||
F.Flow2(ia.Get, ab.GetOption),
|
||||
F.Flow2(ab.ReverseGet, ia.ReverseGet),
|
||||
fmt.Sprintf("IsoCompose[%s -> %s]", ia, ab),
|
||||
)
|
||||
}
|
||||
}
|
||||
435
v2/optics/iso/prism/compose_test.go
Normal file
435
v2/optics/iso/prism/compose_test.go
Normal file
@@ -0,0 +1,435 @@
|
||||
// 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 prism
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
I "github.com/IBM/fp-go/v2/optics/iso"
|
||||
P "github.com/IBM/fp-go/v2/optics/prism"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestComposeWithEitherPrism tests composing a prism with an isomorphism using Either
|
||||
func TestComposeWithEitherPrism(t *testing.T) {
|
||||
// Create a prism that extracts Right values from Either[error, string]
|
||||
rightPrism := P.FromEither[error, string]()
|
||||
|
||||
// Create an isomorphism between []byte and Either[error, string]
|
||||
bytesEitherIso := I.MakeIso(
|
||||
func(b []byte) E.Either[error, string] {
|
||||
return E.Right[error](string(b))
|
||||
},
|
||||
func(e E.Either[error, string]) []byte {
|
||||
return []byte(E.GetOrElse(func(error) string { return "" })(e))
|
||||
},
|
||||
)
|
||||
|
||||
// Compose them: Prism[Either, string] with Iso[[]byte, Either] -> Prism[[]byte, string]
|
||||
bytesPrism := Compose[[]byte](rightPrism)(bytesEitherIso)
|
||||
|
||||
t.Run("GetOption extracts string from []byte", func(t *testing.T) {
|
||||
bytes := []byte("hello")
|
||||
result := bytesPrism.GetOption(bytes)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
str := O.GetOrElse(F.Constant(""))(result)
|
||||
assert.Equal(t, "hello", str)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet constructs []byte from string", func(t *testing.T) {
|
||||
value := "world"
|
||||
result := bytesPrism.ReverseGet(value)
|
||||
|
||||
assert.Equal(t, []byte("world"), result)
|
||||
})
|
||||
|
||||
t.Run("Round-trip through GetOption and ReverseGet", func(t *testing.T) {
|
||||
original := "test"
|
||||
|
||||
// ReverseGet to create []byte
|
||||
bytes := bytesPrism.ReverseGet(original)
|
||||
|
||||
// GetOption to extract string back
|
||||
result := bytesPrism.GetOption(bytes)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.GetOrElse(F.Constant(""))(result)
|
||||
assert.Equal(t, original, extracted)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeWithOptionPrism tests composing a prism with an isomorphism using Option
|
||||
func TestComposeWithOptionPrism(t *testing.T) {
|
||||
// Create a prism that extracts Some values from Option[int]
|
||||
somePrism := P.FromOption[int]()
|
||||
|
||||
// Create an isomorphism between string and Option[int]
|
||||
stringOptionIso := I.MakeIso(
|
||||
func(s string) O.Option[int] {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Some(i)
|
||||
},
|
||||
func(opt O.Option[int]) string {
|
||||
return strconv.Itoa(O.GetOrElse(F.Constant(0))(opt))
|
||||
},
|
||||
)
|
||||
|
||||
// Compose them: Prism[Option, int] with Iso[string, Option] -> Prism[string, int]
|
||||
stringPrism := Compose[string](somePrism)(stringOptionIso)
|
||||
|
||||
t.Run("GetOption extracts int from valid string", func(t *testing.T) {
|
||||
result := stringPrism.GetOption("42")
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
num := O.GetOrElse(F.Constant(0))(result)
|
||||
assert.Equal(t, 42, num)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None for invalid string", func(t *testing.T) {
|
||||
result := stringPrism.GetOption("invalid")
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet constructs string from int", func(t *testing.T) {
|
||||
result := stringPrism.ReverseGet(100)
|
||||
|
||||
assert.Equal(t, "100", result)
|
||||
})
|
||||
}
|
||||
|
||||
// Custom types for testing
|
||||
type Celsius float64
|
||||
type Fahrenheit float64
|
||||
|
||||
type Temperature interface {
|
||||
isTemperature()
|
||||
}
|
||||
|
||||
type CelsiusTemp struct {
|
||||
Value Celsius
|
||||
}
|
||||
|
||||
func (c CelsiusTemp) isTemperature() {}
|
||||
|
||||
type FahrenheitTemp struct {
|
||||
Value Fahrenheit
|
||||
}
|
||||
|
||||
func (f FahrenheitTemp) isTemperature() {}
|
||||
|
||||
// TestComposeWithCustomPrism tests composing with custom types
|
||||
func TestComposeWithCustomPrism(t *testing.T) {
|
||||
// Prism that extracts Celsius from Temperature
|
||||
celsiusPrism := P.MakePrism(
|
||||
func(t Temperature) O.Option[Celsius] {
|
||||
if ct, ok := t.(CelsiusTemp); ok {
|
||||
return O.Some(ct.Value)
|
||||
}
|
||||
return O.None[Celsius]()
|
||||
},
|
||||
func(c Celsius) Temperature {
|
||||
return CelsiusTemp{Value: c}
|
||||
},
|
||||
)
|
||||
|
||||
// Isomorphism between Fahrenheit and Temperature
|
||||
fahrenheitTempIso := I.MakeIso(
|
||||
func(f Fahrenheit) Temperature {
|
||||
celsius := Celsius((f - 32) * 5 / 9)
|
||||
return CelsiusTemp{Value: celsius}
|
||||
},
|
||||
func(t Temperature) Fahrenheit {
|
||||
if ct, ok := t.(CelsiusTemp); ok {
|
||||
return Fahrenheit(ct.Value*9/5 + 32)
|
||||
}
|
||||
return 0
|
||||
},
|
||||
)
|
||||
|
||||
// Compose: Prism[Temperature, Celsius] with Iso[Fahrenheit, Temperature] -> Prism[Fahrenheit, Celsius]
|
||||
fahrenheitPrism := Compose[Fahrenheit](celsiusPrism)(fahrenheitTempIso)
|
||||
|
||||
t.Run("GetOption extracts Celsius from Fahrenheit", func(t *testing.T) {
|
||||
fahrenheit := Fahrenheit(68)
|
||||
result := fahrenheitPrism.GetOption(fahrenheit)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
celsius := O.GetOrElse(F.Constant(Celsius(0)))(result)
|
||||
assert.InDelta(t, 20.0, float64(celsius), 0.01)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet constructs Fahrenheit from Celsius", func(t *testing.T) {
|
||||
celsius := Celsius(20)
|
||||
result := fahrenheitPrism.ReverseGet(celsius)
|
||||
|
||||
assert.InDelta(t, 68.0, float64(result), 0.01)
|
||||
})
|
||||
|
||||
t.Run("Round-trip preserves value", func(t *testing.T) {
|
||||
original := Celsius(25)
|
||||
|
||||
// ReverseGet to create Fahrenheit
|
||||
fahrenheit := fahrenheitPrism.ReverseGet(original)
|
||||
|
||||
// GetOption to extract Celsius back
|
||||
result := fahrenheitPrism.GetOption(fahrenheit)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.GetOrElse(F.Constant(Celsius(0)))(result)
|
||||
assert.InDelta(t, float64(original), float64(extracted), 0.01)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeIdentityIso tests composing with an identity isomorphism
|
||||
func TestComposeIdentityIso(t *testing.T) {
|
||||
// Prism that extracts Right values
|
||||
rightPrism := P.FromEither[error, string]()
|
||||
|
||||
// Identity isomorphism on Either
|
||||
idIso := I.Id[E.Either[error, string]]()
|
||||
|
||||
// Compose with identity should not change behavior
|
||||
composedPrism := Compose[E.Either[error, string]](rightPrism)(idIso)
|
||||
|
||||
t.Run("Composed prism behaves like original prism", func(t *testing.T) {
|
||||
either := E.Right[error]("test")
|
||||
|
||||
// Original prism
|
||||
originalResult := rightPrism.GetOption(either)
|
||||
|
||||
// Composed prism
|
||||
composedResult := composedPrism.GetOption(either)
|
||||
|
||||
assert.Equal(t, originalResult, composedResult)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet produces same result", func(t *testing.T) {
|
||||
value := "test"
|
||||
|
||||
// Original prism
|
||||
originalResult := rightPrism.ReverseGet(value)
|
||||
|
||||
// Composed prism
|
||||
composedResult := composedPrism.ReverseGet(value)
|
||||
|
||||
assert.Equal(t, originalResult, composedResult)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeChaining tests chaining multiple compositions
|
||||
func TestComposeChaining(t *testing.T) {
|
||||
// Prism: extracts Right values from Either[error, int]
|
||||
rightPrism := P.FromEither[error, int]()
|
||||
|
||||
// Iso 1: string to Either[error, int]
|
||||
stringEitherIso := I.MakeIso(
|
||||
func(s string) E.Either[error, int] {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return E.Left[int](err)
|
||||
}
|
||||
return E.Right[error](i)
|
||||
},
|
||||
func(e E.Either[error, int]) string {
|
||||
return strconv.Itoa(E.GetOrElse(func(error) int { return 0 })(e))
|
||||
},
|
||||
)
|
||||
|
||||
// Iso 2: []byte to string
|
||||
bytesStringIso := I.MakeIso(
|
||||
func(b []byte) string { return string(b) },
|
||||
func(s string) []byte { return []byte(s) },
|
||||
)
|
||||
|
||||
// First composition: Prism[Either, int] with Iso[string, Either] -> Prism[string, int]
|
||||
step1 := Compose[string](rightPrism)(stringEitherIso)
|
||||
|
||||
// Second composition: Prism[string, int] with Iso[[]byte, string] -> Prism[[]byte, int]
|
||||
step2 := Compose[[]byte](step1)(bytesStringIso)
|
||||
|
||||
t.Run("Chained composition extracts correctly", func(t *testing.T) {
|
||||
bytes := []byte("42")
|
||||
result := step2.GetOption(bytes)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
num := O.GetOrElse(F.Constant(0))(result)
|
||||
assert.Equal(t, 42, num)
|
||||
})
|
||||
|
||||
t.Run("Chained composition ReverseGet works correctly", func(t *testing.T) {
|
||||
num := 100
|
||||
result := step2.ReverseGet(num)
|
||||
|
||||
assert.Equal(t, []byte("100"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposePrismLaws verifies that the composed prism satisfies prism laws
|
||||
func TestComposePrismLaws(t *testing.T) {
|
||||
// Create a prism
|
||||
prism := P.FromEither[error, int]()
|
||||
|
||||
// Create an isomorphism from string to Either[error, int]
|
||||
iso := I.MakeIso(
|
||||
func(s string) E.Either[error, int] {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return E.Left[int](err)
|
||||
}
|
||||
return E.Right[error](i)
|
||||
},
|
||||
func(e E.Either[error, int]) string {
|
||||
return strconv.Itoa(E.GetOrElse(func(error) int { return 0 })(e))
|
||||
},
|
||||
)
|
||||
|
||||
// Compose them
|
||||
composed := Compose[string](prism)(iso)
|
||||
|
||||
t.Run("Law 1: GetOption(ReverseGet(b)) == Some(b)", func(t *testing.T) {
|
||||
value := 42
|
||||
|
||||
// ReverseGet then GetOption should return Some(value)
|
||||
source := composed.ReverseGet(value)
|
||||
result := composed.GetOption(source)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.GetOrElse(F.Constant(0))(result)
|
||||
assert.Equal(t, value, extracted)
|
||||
})
|
||||
|
||||
t.Run("Law 2: If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
|
||||
source := "100"
|
||||
|
||||
// First GetOption
|
||||
firstResult := composed.GetOption(source)
|
||||
assert.True(t, O.IsSome(firstResult))
|
||||
|
||||
// Extract the value
|
||||
value := O.GetOrElse(F.Constant(0))(firstResult)
|
||||
|
||||
// ReverseGet then GetOption again
|
||||
reconstructed := composed.ReverseGet(value)
|
||||
secondResult := composed.GetOption(reconstructed)
|
||||
|
||||
assert.True(t, O.IsSome(secondResult))
|
||||
finalValue := O.GetOrElse(F.Constant(0))(secondResult)
|
||||
assert.Equal(t, value, finalValue)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeWithJSON tests a practical example with JSON parsing
|
||||
func TestComposeWithJSON(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
// Prism that extracts Config from []byte (via JSON parsing)
|
||||
configPrism := P.MakePrism(
|
||||
func(b []byte) O.Option[Config] {
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(b, &cfg); err != nil {
|
||||
return O.None[Config]()
|
||||
}
|
||||
return O.Some(cfg)
|
||||
},
|
||||
func(cfg Config) []byte {
|
||||
b, _ := json.Marshal(cfg)
|
||||
return b
|
||||
},
|
||||
)
|
||||
|
||||
// Isomorphism between string and []byte
|
||||
stringBytesIso := I.MakeIso(
|
||||
func(s string) []byte { return []byte(s) },
|
||||
func(b []byte) string { return string(b) },
|
||||
)
|
||||
|
||||
// Compose: Prism[[]byte, Config] with Iso[string, []byte] -> Prism[string, Config]
|
||||
stringConfigPrism := Compose[string](configPrism)(stringBytesIso)
|
||||
|
||||
t.Run("GetOption parses valid JSON string", func(t *testing.T) {
|
||||
jsonStr := `{"host":"localhost","port":8080}`
|
||||
result := stringConfigPrism.GetOption(jsonStr)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
cfg := O.GetOrElse(F.Constant(Config{}))(result)
|
||||
assert.Equal(t, "localhost", cfg.Host)
|
||||
assert.Equal(t, 8080, cfg.Port)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None for invalid JSON", func(t *testing.T) {
|
||||
invalidJSON := `{invalid json}`
|
||||
result := stringConfigPrism.GetOption(invalidJSON)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet creates JSON string from Config", func(t *testing.T) {
|
||||
cfg := Config{Host: "example.com", Port: 443}
|
||||
result := stringConfigPrism.ReverseGet(cfg)
|
||||
|
||||
// Parse it back to verify
|
||||
var parsed Config
|
||||
err := json.Unmarshal([]byte(result), &parsed)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cfg, parsed)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeWithEmptyValues tests edge cases with empty/zero values
|
||||
func TestComposeWithEmptyValues(t *testing.T) {
|
||||
// Prism that extracts Right values
|
||||
prism := P.FromEither[error, []byte]()
|
||||
|
||||
// Isomorphism between string and Either[error, []byte]
|
||||
iso := I.MakeIso(
|
||||
func(s string) E.Either[error, []byte] {
|
||||
return E.Right[error]([]byte(s))
|
||||
},
|
||||
func(e E.Either[error, []byte]) string {
|
||||
return string(E.GetOrElse(func(error) []byte { return []byte{} })(e))
|
||||
},
|
||||
)
|
||||
|
||||
composed := Compose[string](prism)(iso)
|
||||
|
||||
t.Run("Empty string is handled correctly", func(t *testing.T) {
|
||||
result := composed.GetOption("")
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
bytes := O.GetOrElse(F.Constant([]byte("default")))(result)
|
||||
assert.Equal(t, []byte{}, bytes)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet with empty bytes", func(t *testing.T) {
|
||||
bytes := []byte{}
|
||||
result := composed.ReverseGet(bytes)
|
||||
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
}
|
||||
99
v2/optics/iso/prism/types.go
Normal file
99
v2/optics/iso/prism/types.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 prism provides utilities for composing prisms with isomorphisms.
|
||||
//
|
||||
// This package enables the composition of prisms (optics for sum types) with
|
||||
// isomorphisms (bidirectional transformations), allowing you to transform the
|
||||
// source type of a prism using an isomorphism. This is the inverse operation
|
||||
// of optics/prism/iso, where we transform the focus type instead of the source type.
|
||||
//
|
||||
// # Key Concepts
|
||||
//
|
||||
// A Prism[S, A] is an optic that focuses on a specific variant within a sum type S,
|
||||
// extracting values of type A. An Iso[S, A] represents a bidirectional transformation
|
||||
// between types S and A without loss of information.
|
||||
//
|
||||
// When you compose a Prism[A, B] with an Iso[S, A], you get a Prism[S, B] that:
|
||||
// - Transforms S to A using the isomorphism's Get
|
||||
// - Extracts values of type B from A (using the prism)
|
||||
// - Can construct S from B by first using the prism's ReverseGet to get A, then the iso's ReverseGet
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/iso"
|
||||
// "github.com/IBM/fp-go/v2/optics/prism"
|
||||
// IP "github.com/IBM/fp-go/v2/optics/iso/prism"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// )
|
||||
//
|
||||
// // Create an isomorphism between []byte and string
|
||||
// bytesStringIso := iso.MakeIso(
|
||||
// func(b []byte) string { return string(b) },
|
||||
// func(s string) []byte { return []byte(s) },
|
||||
// )
|
||||
//
|
||||
// // Create a prism that extracts Right values from Either[error, string]
|
||||
// rightPrism := prism.FromEither[error, string]()
|
||||
//
|
||||
// // Compose them to get a prism that works with []byte as source
|
||||
// bytesPrism := IP.Compose(rightPrism)(bytesStringIso)
|
||||
//
|
||||
// // Use the composed prism
|
||||
// bytes := []byte(`{"status":"ok"}`)
|
||||
// // First converts bytes to string via iso, then extracts Right value
|
||||
// result := bytesPrism.GetOption(either.Right[error](string(bytes)))
|
||||
//
|
||||
// # Comparison with optics/prism/iso
|
||||
//
|
||||
// This package (optics/iso/prism) is the dual of optics/prism/iso:
|
||||
// - optics/prism/iso: Composes Iso[A, B] with Prism[S, A] → Prism[S, B] (transforms focus type)
|
||||
// - optics/iso/prism: Composes Prism[A, B] with Iso[S, A] → Prism[S, B] (transforms source type)
|
||||
//
|
||||
// # Type Aliases
|
||||
//
|
||||
// This package re-exports key types from the iso and prism packages for convenience:
|
||||
// - Iso[S, A]: An isomorphism between types S and A
|
||||
// - Prism[S, A]: A prism focusing on type A within sum type S
|
||||
package prism
|
||||
|
||||
import (
|
||||
I "github.com/IBM/fp-go/v2/optics/iso"
|
||||
P "github.com/IBM/fp-go/v2/optics/prism"
|
||||
)
|
||||
|
||||
type (
|
||||
// Iso represents an isomorphism between types S and A.
|
||||
// It is a bidirectional transformation that converts between two types
|
||||
// without any loss of information.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The target type
|
||||
//
|
||||
// See github.com/IBM/fp-go/v2/optics/iso for the full Iso API.
|
||||
Iso[S, A any] = I.Iso[S, A]
|
||||
|
||||
// Prism is an optic used to select part of a sum type (tagged union).
|
||||
// It provides operations to extract and construct values within sum types.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type (sum type)
|
||||
// - A: The focus type (variant within the sum type)
|
||||
//
|
||||
// See github.com/IBM/fp-go/v2/optics/prism for the full Prism API.
|
||||
Prism[S, A any] = P.Prism[S, A]
|
||||
)
|
||||
156
v2/optics/prism/iso/compose.go
Normal file
156
v2/optics/prism/iso/compose.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// 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 iso
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
P "github.com/IBM/fp-go/v2/optics/prism"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Compose creates an operator that composes an isomorphism with a prism.
|
||||
//
|
||||
// This function takes an isomorphism Iso[A, B] and returns an operator that can
|
||||
// transform any Prism[S, A] into a Prism[S, B]. The resulting prism maintains
|
||||
// the same source type S but changes the focus type from A to B using the
|
||||
// bidirectional transformation provided by the isomorphism.
|
||||
//
|
||||
// The composition works as follows:
|
||||
// - GetOption: First extracts A from S using the prism, then transforms A to B using the iso's Get
|
||||
// - ReverseGet: First transforms B to A using the iso's ReverseGet, then constructs S using the prism's ReverseGet
|
||||
//
|
||||
// This is particularly useful when you have a prism that focuses on one type but
|
||||
// you need to work with a different type that has a lossless bidirectional
|
||||
// transformation to the original type.
|
||||
//
|
||||
// Haskell Equivalent:
|
||||
// This corresponds to the (.) operator for composing optics in Haskell's lens library,
|
||||
// specifically when composing a Prism with an Iso:
|
||||
//
|
||||
// prism . iso :: Prism s a -> Iso a b -> Prism s b
|
||||
//
|
||||
// In Haskell's lens library, this is part of the general optic composition mechanism.
|
||||
// See: https://hackage.haskell.org/package/lens/docs/Control-Lens-Prism.html
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type (sum type) that the prism operates on
|
||||
// - A: The original focus type of the prism
|
||||
// - B: The new focus type after applying the isomorphism
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: An isomorphism between types A and B that defines the bidirectional transformation
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator[S, A, B] that transforms Prism[S, A] into Prism[S, B]
|
||||
//
|
||||
// Laws:
|
||||
// The composed prism must satisfy the prism laws:
|
||||
// 1. GetOption(ReverseGet(b)) == Some(b) for all b: B
|
||||
// 2. If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)
|
||||
//
|
||||
// These laws are preserved because:
|
||||
// - The isomorphism satisfies: ab.ReverseGet(ab.Get(a)) == a and ab.Get(ab.ReverseGet(b)) == b
|
||||
// - The original prism satisfies the prism laws
|
||||
//
|
||||
// Example - Composing string/bytes isomorphism with Either prism:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/either"
|
||||
// "github.com/IBM/fp-go/v2/optics/iso"
|
||||
// "github.com/IBM/fp-go/v2/optics/prism"
|
||||
// PI "github.com/IBM/fp-go/v2/optics/prism/iso"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// )
|
||||
//
|
||||
// // Create an isomorphism between string and []byte
|
||||
// stringBytesIso := iso.MakeIso(
|
||||
// func(s string) []byte { return []byte(s) },
|
||||
// func(b []byte) string { return string(b) },
|
||||
// )
|
||||
//
|
||||
// // Create a prism that extracts Right values from Either[error, string]
|
||||
// rightPrism := prism.FromEither[error, string]()
|
||||
//
|
||||
// // Compose them to get a prism that works with []byte instead of string
|
||||
// bytesPrism := PI.Compose(stringBytesIso)(rightPrism)
|
||||
//
|
||||
// // Extract bytes from a Right value
|
||||
// success := either.Right[error]("hello")
|
||||
// result := bytesPrism.GetOption(success)
|
||||
// // result is Some([]byte("hello"))
|
||||
//
|
||||
// // Extract from a Left value returns None
|
||||
// failure := either.Left[string](errors.New("error"))
|
||||
// result = bytesPrism.GetOption(failure)
|
||||
// // result is None
|
||||
//
|
||||
// // Construct an Either from bytes
|
||||
// constructed := bytesPrism.ReverseGet([]byte("world"))
|
||||
// // constructed is Right("world")
|
||||
//
|
||||
// Example - Composing with custom types:
|
||||
//
|
||||
// type Celsius float64
|
||||
// type Fahrenheit float64
|
||||
//
|
||||
// // Isomorphism between Celsius and Fahrenheit
|
||||
// tempIso := iso.MakeIso(
|
||||
// func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
// func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
// )
|
||||
//
|
||||
// // Prism that extracts temperature from a weather report
|
||||
// type WeatherReport struct {
|
||||
// Temperature Celsius
|
||||
// Condition string
|
||||
// }
|
||||
// tempPrism := prism.MakePrism(
|
||||
// func(w WeatherReport) option.Option[Celsius] {
|
||||
// return option.Some(w.Temperature)
|
||||
// },
|
||||
// func(c Celsius) WeatherReport {
|
||||
// return WeatherReport{Temperature: c}
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Compose to work with Fahrenheit instead
|
||||
// fahrenheitPrism := PI.Compose(tempIso)(tempPrism)
|
||||
//
|
||||
// report := WeatherReport{Temperature: 20, Condition: "sunny"}
|
||||
// temp := fahrenheitPrism.GetOption(report)
|
||||
// // temp is Some(68.0) in Fahrenheit
|
||||
//
|
||||
// See also:
|
||||
// - github.com/IBM/fp-go/v2/optics/iso for isomorphism operations
|
||||
// - github.com/IBM/fp-go/v2/optics/prism for prism operations
|
||||
// - Operator for the type signature of the returned function
|
||||
func Compose[S, A, B any](ab Iso[A, B]) Operator[S, A, B] {
|
||||
return func(pa Prism[S, A]) Prism[S, B] {
|
||||
return P.MakePrismWithName(
|
||||
F.Flow2(
|
||||
pa.GetOption,
|
||||
O.Map(ab.Get),
|
||||
),
|
||||
F.Flow2(
|
||||
ab.ReverseGet,
|
||||
pa.ReverseGet,
|
||||
),
|
||||
fmt.Sprintf("PrismCompose[%s -> %s]", pa, ab),
|
||||
)
|
||||
}
|
||||
}
|
||||
369
v2/optics/prism/iso/compose_test.go
Normal file
369
v2/optics/prism/iso/compose_test.go
Normal file
@@ -0,0 +1,369 @@
|
||||
// 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 iso
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
I "github.com/IBM/fp-go/v2/optics/iso"
|
||||
P "github.com/IBM/fp-go/v2/optics/prism"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestComposeWithEitherPrism tests composing an isomorphism with an Either prism
|
||||
func TestComposeWithEitherPrism(t *testing.T) {
|
||||
// Create an isomorphism between string and []byte
|
||||
stringBytesIso := I.MakeIso(
|
||||
func(s string) []byte { return []byte(s) },
|
||||
func(b []byte) string { return string(b) },
|
||||
)
|
||||
|
||||
// Create a prism that extracts Right values from Either[error, string]
|
||||
rightPrism := P.FromEither[error, string]()
|
||||
|
||||
// Compose them
|
||||
bytesPrism := Compose[E.Either[error, string]](stringBytesIso)(rightPrism)
|
||||
|
||||
t.Run("GetOption extracts and transforms Right value", func(t *testing.T) {
|
||||
success := E.Right[error]("hello")
|
||||
result := bytesPrism.GetOption(success)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
bytes := O.GetOrElse(F.Constant([]byte{}))(result)
|
||||
assert.Equal(t, []byte("hello"), bytes)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None for Left value", func(t *testing.T) {
|
||||
failure := E.Left[string](errors.New("error"))
|
||||
result := bytesPrism.GetOption(failure)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet constructs Either from transformed value", func(t *testing.T) {
|
||||
bytes := []byte("world")
|
||||
result := bytesPrism.ReverseGet(bytes)
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
str := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Equal(t, "world", str)
|
||||
})
|
||||
|
||||
t.Run("Round-trip through GetOption and ReverseGet", func(t *testing.T) {
|
||||
// Start with bytes
|
||||
original := []byte("test")
|
||||
|
||||
// ReverseGet to create Either
|
||||
either := bytesPrism.ReverseGet(original)
|
||||
|
||||
// GetOption to extract bytes back
|
||||
result := bytesPrism.GetOption(either)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.GetOrElse(F.Constant([]byte{}))(result)
|
||||
assert.Equal(t, original, extracted)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeWithOptionPrism tests composing an isomorphism with an Option prism
|
||||
func TestComposeWithOptionPrism(t *testing.T) {
|
||||
// Create an isomorphism between int and string
|
||||
intStringIso := I.MakeIso(
|
||||
func(i int) string { return strconv.Itoa(i) },
|
||||
func(s string) int {
|
||||
i, _ := strconv.Atoi(s)
|
||||
return i
|
||||
},
|
||||
)
|
||||
|
||||
// Create a prism that extracts Some values from Option[int]
|
||||
somePrism := P.FromOption[int]()
|
||||
|
||||
// Compose them
|
||||
stringPrism := Compose[O.Option[int]](intStringIso)(somePrism)
|
||||
|
||||
t.Run("GetOption extracts and transforms Some value", func(t *testing.T) {
|
||||
some := O.Some(42)
|
||||
result := stringPrism.GetOption(some)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
str := O.GetOrElse(F.Constant(""))(result)
|
||||
assert.Equal(t, "42", str)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None for None value", func(t *testing.T) {
|
||||
none := O.None[int]()
|
||||
result := stringPrism.GetOption(none)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet constructs Option from transformed value", func(t *testing.T) {
|
||||
str := "100"
|
||||
result := stringPrism.ReverseGet(str)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
num := O.GetOrElse(F.Constant(0))(result)
|
||||
assert.Equal(t, 100, num)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeWithCustomPrism tests composing with a custom prism
|
||||
// Custom types for TestComposeWithCustomPrism
|
||||
type Celsius float64
|
||||
type Fahrenheit float64
|
||||
|
||||
type Temperature interface {
|
||||
isTemperature()
|
||||
}
|
||||
|
||||
type CelsiusTemp struct {
|
||||
Value Celsius
|
||||
}
|
||||
|
||||
func (c CelsiusTemp) isTemperature() {}
|
||||
|
||||
type KelvinTemp struct {
|
||||
Value float64
|
||||
}
|
||||
|
||||
func (k KelvinTemp) isTemperature() {}
|
||||
|
||||
func TestComposeWithCustomPrism(t *testing.T) {
|
||||
// Isomorphism between Celsius and Fahrenheit
|
||||
tempIso := I.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
|
||||
// Prism that extracts Celsius from Temperature
|
||||
celsiusPrism := P.MakePrism(
|
||||
func(t Temperature) O.Option[Celsius] {
|
||||
if ct, ok := t.(CelsiusTemp); ok {
|
||||
return O.Some(ct.Value)
|
||||
}
|
||||
return O.None[Celsius]()
|
||||
},
|
||||
func(c Celsius) Temperature {
|
||||
return CelsiusTemp{Value: c}
|
||||
},
|
||||
)
|
||||
|
||||
// Compose to work with Fahrenheit
|
||||
fahrenheitPrism := Compose[Temperature](tempIso)(celsiusPrism)
|
||||
|
||||
t.Run("GetOption extracts and converts Celsius to Fahrenheit", func(t *testing.T) {
|
||||
temp := CelsiusTemp{Value: 0}
|
||||
result := fahrenheitPrism.GetOption(temp)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
fahrenheit := O.GetOrElse(F.Constant(Fahrenheit(0)))(result)
|
||||
assert.InDelta(t, 32.0, float64(fahrenheit), 0.01)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None for non-Celsius temperature", func(t *testing.T) {
|
||||
temp := KelvinTemp{Value: 273.15}
|
||||
result := fahrenheitPrism.GetOption(temp)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet constructs Temperature from Fahrenheit", func(t *testing.T) {
|
||||
fahrenheit := Fahrenheit(68)
|
||||
result := fahrenheitPrism.ReverseGet(fahrenheit)
|
||||
|
||||
celsiusTemp, ok := result.(CelsiusTemp)
|
||||
assert.True(t, ok)
|
||||
assert.InDelta(t, 20.0, float64(celsiusTemp.Value), 0.01)
|
||||
})
|
||||
|
||||
t.Run("Round-trip preserves value", func(t *testing.T) {
|
||||
original := Fahrenheit(100)
|
||||
|
||||
// ReverseGet to create Temperature
|
||||
temp := fahrenheitPrism.ReverseGet(original)
|
||||
|
||||
// GetOption to extract Fahrenheit back
|
||||
result := fahrenheitPrism.GetOption(temp)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.GetOrElse(F.Constant(Fahrenheit(0)))(result)
|
||||
assert.InDelta(t, float64(original), float64(extracted), 0.01)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeIdentityIso tests composing with an identity isomorphism
|
||||
func TestComposeIdentityIso(t *testing.T) {
|
||||
// Identity isomorphism (no transformation)
|
||||
idIso := I.Id[string]()
|
||||
|
||||
// Prism that extracts Right values
|
||||
rightPrism := P.FromEither[error, string]()
|
||||
|
||||
// Compose with identity should not change behavior
|
||||
composedPrism := Compose[E.Either[error, string]](idIso)(rightPrism)
|
||||
|
||||
t.Run("Composed prism behaves like original prism", func(t *testing.T) {
|
||||
success := E.Right[error]("test")
|
||||
|
||||
// Original prism
|
||||
originalResult := rightPrism.GetOption(success)
|
||||
|
||||
// Composed prism
|
||||
composedResult := composedPrism.GetOption(success)
|
||||
|
||||
assert.Equal(t, originalResult, composedResult)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet produces same result", func(t *testing.T) {
|
||||
value := "test"
|
||||
|
||||
// Original prism
|
||||
originalEither := rightPrism.ReverseGet(value)
|
||||
|
||||
// Composed prism
|
||||
composedEither := composedPrism.ReverseGet(value)
|
||||
|
||||
assert.Equal(t, originalEither, composedEither)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeChaining tests chaining multiple compositions
|
||||
func TestComposeChaining(t *testing.T) {
|
||||
// First isomorphism: int to string
|
||||
intStringIso := I.MakeIso(
|
||||
func(i int) string { return strconv.Itoa(i) },
|
||||
func(s string) int {
|
||||
i, _ := strconv.Atoi(s)
|
||||
return i
|
||||
},
|
||||
)
|
||||
|
||||
// Second isomorphism: string to []byte
|
||||
stringBytesIso := I.MakeIso(
|
||||
func(s string) []byte { return []byte(s) },
|
||||
func(b []byte) string { return string(b) },
|
||||
)
|
||||
|
||||
// Prism that extracts Right values
|
||||
rightPrism := P.FromEither[error, int]()
|
||||
|
||||
// Chain compositions: Either[error, int] -> int -> string -> []byte
|
||||
step1 := Compose[E.Either[error, int]](intStringIso)(rightPrism) // Prism[Either[error, int], string]
|
||||
step2 := Compose[E.Either[error, int]](stringBytesIso)(step1) // Prism[Either[error, int], []byte]
|
||||
|
||||
t.Run("Chained composition extracts and transforms correctly", func(t *testing.T) {
|
||||
either := E.Right[error](42)
|
||||
result := step2.GetOption(either)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
bytes := O.GetOrElse(F.Constant([]byte{}))(result)
|
||||
assert.Equal(t, []byte("42"), bytes)
|
||||
})
|
||||
|
||||
t.Run("Chained composition ReverseGet works correctly", func(t *testing.T) {
|
||||
bytes := []byte("100")
|
||||
result := step2.ReverseGet(bytes)
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
num := E.GetOrElse(func(error) int { return 0 })(result)
|
||||
assert.Equal(t, 100, num)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposePrismLaws verifies that the composed prism satisfies prism laws
|
||||
func TestComposePrismLaws(t *testing.T) {
|
||||
// Create an isomorphism
|
||||
iso := I.MakeIso(
|
||||
func(i int) string { return strconv.Itoa(i) },
|
||||
func(s string) int {
|
||||
i, _ := strconv.Atoi(s)
|
||||
return i
|
||||
},
|
||||
)
|
||||
|
||||
// Create a prism
|
||||
prism := P.FromEither[error, int]()
|
||||
|
||||
// Compose them
|
||||
composed := Compose[E.Either[error, int]](iso)(prism)
|
||||
|
||||
t.Run("Law 1: GetOption(ReverseGet(b)) == Some(b)", func(t *testing.T) {
|
||||
value := "42"
|
||||
|
||||
// ReverseGet then GetOption should return Some(value)
|
||||
either := composed.ReverseGet(value)
|
||||
result := composed.GetOption(either)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.GetOrElse(F.Constant(""))(result)
|
||||
assert.Equal(t, value, extracted)
|
||||
})
|
||||
|
||||
t.Run("Law 2: If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
|
||||
either := E.Right[error](100)
|
||||
|
||||
// First GetOption
|
||||
firstResult := composed.GetOption(either)
|
||||
assert.True(t, O.IsSome(firstResult))
|
||||
|
||||
// Extract the value
|
||||
value := O.GetOrElse(F.Constant(""))(firstResult)
|
||||
|
||||
// ReverseGet then GetOption again
|
||||
reconstructed := composed.ReverseGet(value)
|
||||
secondResult := composed.GetOption(reconstructed)
|
||||
|
||||
assert.True(t, O.IsSome(secondResult))
|
||||
finalValue := O.GetOrElse(F.Constant(""))(secondResult)
|
||||
assert.Equal(t, value, finalValue)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeWithEmptyValues tests edge cases with empty/zero values
|
||||
func TestComposeWithEmptyValues(t *testing.T) {
|
||||
// Isomorphism that handles empty strings
|
||||
iso := I.MakeIso(
|
||||
func(s string) []byte { return []byte(s) },
|
||||
func(b []byte) string { return string(b) },
|
||||
)
|
||||
|
||||
prism := P.FromEither[error, string]()
|
||||
composed := Compose[E.Either[error, string]](iso)(prism)
|
||||
|
||||
t.Run("Empty string is handled correctly", func(t *testing.T) {
|
||||
either := E.Right[error]("")
|
||||
result := composed.GetOption(either)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
bytes := O.GetOrElse(F.Constant([]byte("default")))(result)
|
||||
assert.Equal(t, []byte{}, bytes)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet with empty bytes", func(t *testing.T) {
|
||||
bytes := []byte{}
|
||||
result := composed.ReverseGet(bytes)
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
str := E.GetOrElse(func(error) string { return "default" })(result)
|
||||
assert.Equal(t, "", str)
|
||||
})
|
||||
}
|
||||
112
v2/optics/prism/iso/types.go
Normal file
112
v2/optics/prism/iso/types.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// 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 iso provides utilities for composing isomorphisms with prisms.
|
||||
//
|
||||
// This package enables the composition of isomorphisms (bidirectional transformations)
|
||||
// with prisms (optics for sum types), allowing you to transform the focus type of a prism
|
||||
// using an isomorphism. This is particularly useful when you need to work with prisms
|
||||
// that focus on a type that can be bidirectionally converted to another type.
|
||||
//
|
||||
// # Key Concepts
|
||||
//
|
||||
// An Iso[S, A] represents a bidirectional transformation between types S and A without
|
||||
// loss of information. A Prism[S, A] is an optic that focuses on a specific variant
|
||||
// within a sum type S, extracting values of type A.
|
||||
//
|
||||
// When you compose an Iso[A, B] with a Prism[S, A], you get a Prism[S, B] that:
|
||||
// - Extracts values of type A from S (using the prism)
|
||||
// - Transforms them to type B (using the isomorphism's Get)
|
||||
// - Can construct S from B by reversing the transformation (using the isomorphism's ReverseGet)
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/iso"
|
||||
// "github.com/IBM/fp-go/v2/optics/prism"
|
||||
// PI "github.com/IBM/fp-go/v2/optics/prism/iso"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// )
|
||||
//
|
||||
// // Create an isomorphism between string and []byte
|
||||
// stringBytesIso := iso.MakeIso(
|
||||
// func(s string) []byte { return []byte(s) },
|
||||
// func(b []byte) string { return string(b) },
|
||||
// )
|
||||
//
|
||||
// // Create a prism that extracts Right values from Either[error, string]
|
||||
// rightPrism := prism.FromEither[error, string]()
|
||||
//
|
||||
// // Compose them to get a prism that extracts Right values as []byte
|
||||
// bytesPrism := PI.Compose(stringBytesIso)(rightPrism)
|
||||
//
|
||||
// // Use the composed prism
|
||||
// either := either.Right[error]("hello")
|
||||
// result := bytesPrism.GetOption(either) // Some([]byte("hello"))
|
||||
//
|
||||
// # Type Aliases
|
||||
//
|
||||
// This package re-exports key types from the iso and prism packages for convenience:
|
||||
// - Iso[S, A]: An isomorphism between types S and A
|
||||
// - Prism[S, A]: A prism focusing on type A within sum type S
|
||||
// - Operator[S, A, B]: A function that transforms Prism[S, A] to Prism[S, B]
|
||||
package iso
|
||||
|
||||
import (
|
||||
I "github.com/IBM/fp-go/v2/optics/iso"
|
||||
P "github.com/IBM/fp-go/v2/optics/prism"
|
||||
)
|
||||
|
||||
type (
|
||||
// Iso represents an isomorphism between types S and A.
|
||||
// It is a bidirectional transformation that converts between two types
|
||||
// without any loss of information.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The target type
|
||||
//
|
||||
// See github.com/IBM/fp-go/v2/optics/iso for the full Iso API.
|
||||
Iso[S, A any] = I.Iso[S, A]
|
||||
|
||||
// Prism is an optic used to select part of a sum type (tagged union).
|
||||
// It provides operations to extract and construct values within sum types.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type (sum type)
|
||||
// - A: The focus type (variant within the sum type)
|
||||
//
|
||||
// See github.com/IBM/fp-go/v2/optics/prism for the full Prism API.
|
||||
Prism[S, A any] = P.Prism[S, A]
|
||||
|
||||
// Operator represents a function that transforms one prism into another.
|
||||
// It takes a Prism[S, A] and returns a Prism[S, B], allowing for prism transformations.
|
||||
//
|
||||
// This is commonly used with the Compose function to create operators that
|
||||
// transform the focus type of a prism using an isomorphism.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type (remains constant)
|
||||
// - A: The original focus type
|
||||
// - B: The new focus type
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create an operator that transforms string prisms to []byte prisms
|
||||
// stringToBytesOp := Compose(stringBytesIso)
|
||||
// // Apply it to a prism
|
||||
// bytesPrism := stringToBytesOp(stringPrism)
|
||||
Operator[S, A, B any] = P.Operator[S, A, B]
|
||||
)
|
||||
@@ -23,8 +23,10 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
J "github.com/IBM/fp-go/v2/json"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
)
|
||||
@@ -322,6 +324,50 @@ func FromEither[E, T any]() Prism[Either[E, T], T] {
|
||||
return MakePrismWithName(either.ToOption[E, T], either.Of[E, T], "PrismFromEither")
|
||||
}
|
||||
|
||||
// FromResult creates a prism for extracting values from Result types.
|
||||
// It provides a safe way to work with Result values (which are Either[error, T]),
|
||||
// focusing on the success case and handling errors gracefully through the Option type.
|
||||
//
|
||||
// This is a convenience function that is equivalent to FromEither[error, T]().
|
||||
//
|
||||
// The prism's GetOption attempts to extract the success value from a Result.
|
||||
// If the Result is successful, it returns Some(value); if it's an error, it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, wrapping a value into a successful Result.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The value type contained in the Result
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[Result[T], T] that safely extracts success values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a prism for extracting successful results
|
||||
// resultPrism := FromResult[int]()
|
||||
//
|
||||
// // Extract from successful result
|
||||
// success := result.Of[int](42)
|
||||
// value := resultPrism.GetOption(success) // Some(42)
|
||||
//
|
||||
// // Extract from error result
|
||||
// failure := result.Error[int](errors.New("failed"))
|
||||
// value = resultPrism.GetOption(failure) // None[int]()
|
||||
//
|
||||
// // Wrap value into successful Result
|
||||
// wrapped := resultPrism.ReverseGet(100) // Result containing 100
|
||||
//
|
||||
// // Use with Set to update successful results
|
||||
// setter := Set[Result[int], int](200)
|
||||
// result := setter(resultPrism)(success) // Result containing 200
|
||||
// result = setter(resultPrism)(failure) // Error result (unchanged)
|
||||
//
|
||||
// Common use cases:
|
||||
// - Extracting successful values from Result types
|
||||
// - Filtering out errors in data pipelines
|
||||
// - Working with fallible operations that return Result
|
||||
// - Composing with other prisms for complex error handling
|
||||
//
|
||||
//go:inline
|
||||
func FromResult[T any]() Prism[Result[T], T] {
|
||||
return FromEither[error, T]()
|
||||
@@ -1261,3 +1307,71 @@ func MakeURLPrisms() URLPrisms {
|
||||
RawFragment: _prismRawFragment,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseJSON creates a prism for parsing and marshaling JSON data.
|
||||
// It provides a safe way to convert between JSON bytes and Go types,
|
||||
// handling parsing and marshaling errors gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to unmarshal JSON bytes into type A.
|
||||
// If unmarshaling succeeds, it returns Some(A); if it fails (e.g., invalid JSON
|
||||
// or type mismatch), it returns None.
|
||||
//
|
||||
// The prism's ReverseGet marshals a value of type A into JSON bytes.
|
||||
// If marshaling fails (which is rare), it returns an empty byte slice.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The Go type to unmarshal JSON into
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[[]byte, A] that safely handles JSON parsing/marshaling
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Define a struct type
|
||||
// type Person struct {
|
||||
// Name string `json:"name"`
|
||||
// Age int `json:"age"`
|
||||
// }
|
||||
//
|
||||
// // Create a JSON parsing prism
|
||||
// jsonPrism := ParseJSON[Person]()
|
||||
//
|
||||
// // Parse valid JSON
|
||||
// jsonData := []byte(`{"name":"Alice","age":30}`)
|
||||
// person := jsonPrism.GetOption(jsonData)
|
||||
// // Some(Person{Name: "Alice", Age: 30})
|
||||
//
|
||||
// // Parse invalid JSON
|
||||
// invalidJSON := []byte(`{invalid json}`)
|
||||
// result := jsonPrism.GetOption(invalidJSON) // None[Person]()
|
||||
//
|
||||
// // Marshal to JSON
|
||||
// p := Person{Name: "Bob", Age: 25}
|
||||
// jsonBytes := jsonPrism.ReverseGet(p)
|
||||
// // []byte(`{"name":"Bob","age":25}`)
|
||||
//
|
||||
// // Use with Set to update JSON data
|
||||
// newPerson := Person{Name: "Charlie", Age: 35}
|
||||
// setter := Set[[]byte, Person](newPerson)
|
||||
// updated := setter(jsonPrism)(jsonData)
|
||||
// // []byte(`{"name":"Charlie","age":35}`)
|
||||
//
|
||||
// Common use cases:
|
||||
// - Parsing JSON configuration files
|
||||
// - Working with JSON API responses
|
||||
// - Validating and transforming JSON data in pipelines
|
||||
// - Type-safe JSON deserialization
|
||||
// - Converting between JSON and Go structs
|
||||
func ParseJSON[A any]() Prism[[]byte, A] {
|
||||
return MakePrismWithName(
|
||||
F.Flow2(
|
||||
J.Unmarshal[A],
|
||||
either.ToOption[error, A],
|
||||
),
|
||||
F.Flow2(
|
||||
J.Marshal[A],
|
||||
either.GetOrElse(F.Constant1[error](array.Empty[byte]())),
|
||||
),
|
||||
"JSON",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
package prism
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -1396,3 +1399,403 @@ func TestNonEmptyStringValidation(t *testing.T) {
|
||||
assert.Equal(t, []string{"hello", "world", "test"}, nonEmpty)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromResult tests the FromResult prism with Result types
|
||||
func TestFromResult(t *testing.T) {
|
||||
t.Run("extract from successful result", func(t *testing.T) {
|
||||
prism := FromResult[int]()
|
||||
|
||||
success := result.Of[int](42)
|
||||
extracted := prism.GetOption(success)
|
||||
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
|
||||
})
|
||||
|
||||
t.Run("extract from error result", func(t *testing.T) {
|
||||
prism := FromResult[int]()
|
||||
|
||||
failure := E.Left[int](errors.New("test error"))
|
||||
extracted := prism.GetOption(failure)
|
||||
|
||||
assert.True(t, O.IsNone(extracted))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet wraps value in successful result", func(t *testing.T) {
|
||||
prism := FromResult[int]()
|
||||
|
||||
wrapped := prism.ReverseGet(100)
|
||||
|
||||
// Verify it's a successful result
|
||||
extracted := prism.GetOption(wrapped)
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
assert.Equal(t, 100, O.GetOrElse(F.Constant(-1))(extracted))
|
||||
})
|
||||
|
||||
t.Run("works with string type", func(t *testing.T) {
|
||||
prism := FromResult[string]()
|
||||
|
||||
success := result.Of[string]("hello")
|
||||
extracted := prism.GetOption(success)
|
||||
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
assert.Equal(t, "hello", O.GetOrElse(F.Constant(""))(extracted))
|
||||
})
|
||||
|
||||
t.Run("works with struct type", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
prism := FromResult[Person]()
|
||||
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
success := result.Of[Person](person)
|
||||
extracted := prism.GetOption(success)
|
||||
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
result := O.GetOrElse(F.Constant(Person{}))(extracted)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromResultWithSet tests using Set with FromResult prism
|
||||
func TestFromResultWithSet(t *testing.T) {
|
||||
t.Run("set on successful result", func(t *testing.T) {
|
||||
prism := FromResult[int]()
|
||||
setter := Set[result.Result[int], int](200)
|
||||
|
||||
success := result.Of[int](42)
|
||||
updated := setter(prism)(success)
|
||||
|
||||
// Verify the value was updated
|
||||
extracted := prism.GetOption(updated)
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
assert.Equal(t, 200, O.GetOrElse(F.Constant(-1))(extracted))
|
||||
})
|
||||
|
||||
t.Run("set on error result leaves it unchanged", func(t *testing.T) {
|
||||
prism := FromResult[int]()
|
||||
setter := Set[result.Result[int], int](200)
|
||||
|
||||
failure := E.Left[int](errors.New("test error"))
|
||||
updated := setter(prism)(failure)
|
||||
|
||||
// Verify it's still an error
|
||||
extracted := prism.GetOption(updated)
|
||||
assert.True(t, O.IsNone(extracted))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromResultPrismLaws tests that FromResult satisfies prism laws
|
||||
func TestFromResultPrismLaws(t *testing.T) {
|
||||
prism := FromResult[int]()
|
||||
|
||||
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
|
||||
value := 42
|
||||
wrapped := prism.ReverseGet(value)
|
||||
extracted := prism.GetOption(wrapped)
|
||||
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
assert.Equal(t, value, O.GetOrElse(F.Constant(-1))(extracted))
|
||||
})
|
||||
|
||||
t.Run("law 2: ReverseGet is consistent", func(t *testing.T) {
|
||||
value := 42
|
||||
result1 := prism.ReverseGet(value)
|
||||
result2 := prism.ReverseGet(value)
|
||||
|
||||
// Both should extract the same value
|
||||
extracted1 := prism.GetOption(result1)
|
||||
extracted2 := prism.GetOption(result2)
|
||||
|
||||
val1 := O.GetOrElse(F.Constant(-1))(extracted1)
|
||||
val2 := O.GetOrElse(F.Constant(-1))(extracted2)
|
||||
assert.Equal(t, val1, val2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromResultComposition tests composing FromResult with other prisms
|
||||
func TestFromResultComposition(t *testing.T) {
|
||||
t.Run("compose with predicate prism", func(t *testing.T) {
|
||||
// Create a prism that only matches positive numbers
|
||||
positivePrism := FromPredicate(func(n int) bool { return n > 0 })
|
||||
|
||||
// Compose: Result[int] -> int -> positive int
|
||||
composed := Compose[result.Result[int]](positivePrism)(FromResult[int]())
|
||||
|
||||
// Test with positive number
|
||||
success := result.Of[int](42)
|
||||
extracted := composed.GetOption(success)
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
|
||||
|
||||
// Test with negative number
|
||||
negativeSuccess := result.Of[int](-5)
|
||||
extracted = composed.GetOption(negativeSuccess)
|
||||
assert.True(t, O.IsNone(extracted))
|
||||
|
||||
// Test with error
|
||||
failure := E.Left[int](errors.New("test error"))
|
||||
extracted = composed.GetOption(failure)
|
||||
assert.True(t, O.IsNone(extracted))
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseJSON tests the ParseJSON prism with various JSON data
|
||||
func TestParseJSON(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
t.Run("parse valid JSON", func(t *testing.T) {
|
||||
prism := ParseJSON[Person]()
|
||||
|
||||
jsonData := []byte(`{"name":"Alice","age":30}`)
|
||||
parsed := prism.GetOption(jsonData)
|
||||
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
person := O.GetOrElse(F.Constant(Person{}))(parsed)
|
||||
assert.Equal(t, "Alice", person.Name)
|
||||
assert.Equal(t, 30, person.Age)
|
||||
})
|
||||
|
||||
t.Run("parse invalid JSON", func(t *testing.T) {
|
||||
prism := ParseJSON[Person]()
|
||||
|
||||
invalidJSON := []byte(`{invalid json}`)
|
||||
parsed := prism.GetOption(invalidJSON)
|
||||
|
||||
assert.True(t, O.IsNone(parsed))
|
||||
})
|
||||
|
||||
t.Run("parse JSON with missing fields", func(t *testing.T) {
|
||||
prism := ParseJSON[Person]()
|
||||
|
||||
// Missing age field - should use zero value
|
||||
jsonData := []byte(`{"name":"Bob"}`)
|
||||
parsed := prism.GetOption(jsonData)
|
||||
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
person := O.GetOrElse(F.Constant(Person{}))(parsed)
|
||||
assert.Equal(t, "Bob", person.Name)
|
||||
assert.Equal(t, 0, person.Age)
|
||||
})
|
||||
|
||||
t.Run("parse JSON with extra fields", func(t *testing.T) {
|
||||
prism := ParseJSON[Person]()
|
||||
|
||||
// Extra field should be ignored
|
||||
jsonData := []byte(`{"name":"Charlie","age":25,"extra":"ignored"}`)
|
||||
parsed := prism.GetOption(jsonData)
|
||||
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
person := O.GetOrElse(F.Constant(Person{}))(parsed)
|
||||
assert.Equal(t, "Charlie", person.Name)
|
||||
assert.Equal(t, 25, person.Age)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet marshals to JSON", func(t *testing.T) {
|
||||
prism := ParseJSON[Person]()
|
||||
|
||||
person := Person{Name: "David", Age: 35}
|
||||
jsonBytes := prism.ReverseGet(person)
|
||||
|
||||
// Parse it back to verify
|
||||
parsed := prism.GetOption(jsonBytes)
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
result := O.GetOrElse(F.Constant(Person{}))(parsed)
|
||||
assert.Equal(t, "David", result.Name)
|
||||
assert.Equal(t, 35, result.Age)
|
||||
})
|
||||
|
||||
t.Run("works with primitive types", func(t *testing.T) {
|
||||
prism := ParseJSON[int]()
|
||||
|
||||
jsonData := []byte(`42`)
|
||||
parsed := prism.GetOption(jsonData)
|
||||
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(parsed))
|
||||
})
|
||||
|
||||
t.Run("works with arrays", func(t *testing.T) {
|
||||
prism := ParseJSON[[]string]()
|
||||
|
||||
jsonData := []byte(`["hello","world","test"]`)
|
||||
parsed := prism.GetOption(jsonData)
|
||||
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
arr := O.GetOrElse(F.Constant([]string{}))(parsed)
|
||||
assert.Equal(t, []string{"hello", "world", "test"}, arr)
|
||||
})
|
||||
|
||||
t.Run("works with maps", func(t *testing.T) {
|
||||
prism := ParseJSON[map[string]int]()
|
||||
|
||||
jsonData := []byte(`{"a":1,"b":2,"c":3}`)
|
||||
parsed := prism.GetOption(jsonData)
|
||||
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
m := O.GetOrElse(F.Constant(map[string]int{}))(parsed)
|
||||
assert.Equal(t, 1, m["a"])
|
||||
assert.Equal(t, 2, m["b"])
|
||||
assert.Equal(t, 3, m["c"])
|
||||
})
|
||||
|
||||
t.Run("works with nested structures", func(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string `json:"street"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
type PersonWithAddress struct {
|
||||
Name string `json:"name"`
|
||||
Address Address `json:"address"`
|
||||
}
|
||||
|
||||
prism := ParseJSON[PersonWithAddress]()
|
||||
|
||||
jsonData := []byte(`{"name":"Eve","address":{"street":"123 Main St","city":"NYC"}}`)
|
||||
parsed := prism.GetOption(jsonData)
|
||||
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
person := O.GetOrElse(F.Constant(PersonWithAddress{}))(parsed)
|
||||
assert.Equal(t, "Eve", person.Name)
|
||||
assert.Equal(t, "123 Main St", person.Address.Street)
|
||||
assert.Equal(t, "NYC", person.Address.City)
|
||||
})
|
||||
|
||||
t.Run("parse empty JSON object", func(t *testing.T) {
|
||||
prism := ParseJSON[Person]()
|
||||
|
||||
jsonData := []byte(`{}`)
|
||||
parsed := prism.GetOption(jsonData)
|
||||
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
person := O.GetOrElse(F.Constant(Person{}))(parsed)
|
||||
assert.Equal(t, "", person.Name)
|
||||
assert.Equal(t, 0, person.Age)
|
||||
})
|
||||
|
||||
t.Run("parse null JSON", func(t *testing.T) {
|
||||
prism := ParseJSON[*Person]()
|
||||
|
||||
jsonData := []byte(`null`)
|
||||
parsed := prism.GetOption(jsonData)
|
||||
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
person := O.GetOrElse(F.Constant(&Person{}))(parsed)
|
||||
assert.Nil(t, person)
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseJSONWithSet tests using Set with ParseJSON prism
|
||||
func TestParseJSONWithSet(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
t.Run("set updates JSON data", func(t *testing.T) {
|
||||
prism := ParseJSON[Person]()
|
||||
|
||||
originalJSON := []byte(`{"name":"Alice","age":30}`)
|
||||
newPerson := Person{Name: "Bob", Age: 25}
|
||||
|
||||
setter := Set[[]byte, Person](newPerson)
|
||||
updatedJSON := setter(prism)(originalJSON)
|
||||
|
||||
// Parse the updated JSON
|
||||
parsed := prism.GetOption(updatedJSON)
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
person := O.GetOrElse(F.Constant(Person{}))(parsed)
|
||||
assert.Equal(t, "Bob", person.Name)
|
||||
assert.Equal(t, 25, person.Age)
|
||||
})
|
||||
|
||||
t.Run("set on invalid JSON returns original unchanged", func(t *testing.T) {
|
||||
prism := ParseJSON[Person]()
|
||||
|
||||
invalidJSON := []byte(`{invalid}`)
|
||||
newPerson := Person{Name: "Charlie", Age: 35}
|
||||
|
||||
setter := Set[[]byte, Person](newPerson)
|
||||
result := setter(prism)(invalidJSON)
|
||||
|
||||
// Should return original unchanged since it couldn't be parsed
|
||||
assert.Equal(t, invalidJSON, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseJSONPrismLaws tests that ParseJSON satisfies prism laws
|
||||
func TestParseJSONPrismLaws(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
prism := ParseJSON[Person]()
|
||||
|
||||
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
jsonBytes := prism.ReverseGet(person)
|
||||
parsed := prism.GetOption(jsonBytes)
|
||||
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
result := O.GetOrElse(F.Constant(Person{}))(parsed)
|
||||
assert.Equal(t, person.Name, result.Name)
|
||||
assert.Equal(t, person.Age, result.Age)
|
||||
})
|
||||
|
||||
t.Run("law 2: ReverseGet is consistent", func(t *testing.T) {
|
||||
person := Person{Name: "Bob", Age: 25}
|
||||
json1 := prism.ReverseGet(person)
|
||||
json2 := prism.ReverseGet(person)
|
||||
|
||||
// Both should parse to the same value
|
||||
parsed1 := prism.GetOption(json1)
|
||||
parsed2 := prism.GetOption(json2)
|
||||
|
||||
result1 := O.GetOrElse(F.Constant(Person{}))(parsed1)
|
||||
result2 := O.GetOrElse(F.Constant(Person{}))(parsed2)
|
||||
|
||||
assert.Equal(t, result1.Name, result2.Name)
|
||||
assert.Equal(t, result1.Age, result2.Age)
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseJSONComposition tests composing ParseJSON with other prisms
|
||||
func TestParseJSONComposition(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
t.Run("compose with predicate prism", func(t *testing.T) {
|
||||
// Create a prism that only matches adults (age >= 18)
|
||||
adultPrism := FromPredicate(func(p Person) bool { return p.Age >= 18 })
|
||||
|
||||
// Compose: []byte -> Person -> Adult
|
||||
composed := Compose[[]byte](adultPrism)(ParseJSON[Person]())
|
||||
|
||||
// Test with adult
|
||||
adultJSON := []byte(`{"name":"Alice","age":30}`)
|
||||
parsed := composed.GetOption(adultJSON)
|
||||
assert.True(t, O.IsSome(parsed))
|
||||
person := O.GetOrElse(F.Constant(Person{}))(parsed)
|
||||
assert.Equal(t, "Alice", person.Name)
|
||||
|
||||
// Test with minor
|
||||
minorJSON := []byte(`{"name":"Bob","age":15}`)
|
||||
parsed = composed.GetOption(minorJSON)
|
||||
assert.True(t, O.IsNone(parsed))
|
||||
|
||||
// Test with invalid JSON
|
||||
invalidJSON := []byte(`{invalid}`)
|
||||
parsed = composed.GetOption(invalidJSON)
|
||||
assert.True(t, O.IsNone(parsed))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -667,12 +667,12 @@ func FromPredicate[R, E, A any](pred func(A) bool, onFalse func(A) E) func(A) Re
|
||||
// This is useful for converting a ReaderIOEither into a ReaderIO by handling all cases.
|
||||
//
|
||||
//go:inline
|
||||
func Fold[R, E, A, B any](onLeft func(E) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOEither[R, E, A]) ReaderIO[R, B] {
|
||||
func Fold[R, E, A, B any](onLeft readerio.Kleisli[R, E, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOEither[R, E, A]) ReaderIO[R, B] {
|
||||
return eithert.MatchE(readerio.MonadChain[R, either.Either[E, A], B], onLeft, onRight)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadFold[R, E, A, B any](ma ReaderIOEither[R, E, A], onLeft func(E) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) ReaderIO[R, B] {
|
||||
func MonadFold[R, E, A, B any](ma ReaderIOEither[R, E, A], onLeft readerio.Kleisli[R, E, B], onRight func(A) ReaderIO[R, B]) ReaderIO[R, B] {
|
||||
return eithert.FoldE(readerio.MonadChain[R, either.Either[E, A], B], ma, onLeft, onRight)
|
||||
}
|
||||
|
||||
@@ -680,7 +680,7 @@ func MonadFold[R, E, A, B any](ma ReaderIOEither[R, E, A], onLeft func(E) Reader
|
||||
// The default is computed lazily via a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[R, E, A any](onLeft func(E) ReaderIO[R, A]) func(ReaderIOEither[R, E, A]) ReaderIO[R, A] {
|
||||
func GetOrElse[R, E, A any](onLeft readerio.Kleisli[R, E, A]) func(ReaderIOEither[R, E, A]) ReaderIO[R, A] {
|
||||
return eithert.GetOrElse(readerio.MonadChain[R, either.Either[E, A], A], readerio.Of[R, A], onLeft)
|
||||
}
|
||||
|
||||
|
||||
@@ -358,5 +358,3 @@ func TestChainFirstConsumer_ComplexType(t *testing.T) {
|
||||
assert.InDelta(t, 10.989, finalProduct.Price, 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -636,7 +636,7 @@ func FromPredicate[R, A any](pred func(A) bool, onFalse func(A) error) Kleisli[R
|
||||
// This is useful for converting a ReaderIOResult into a ReaderIO by handling all cases.
|
||||
//
|
||||
//go:inline
|
||||
func Fold[R, A, B any](onLeft func(error) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOResult[R, A]) ReaderIO[R, B] {
|
||||
func Fold[R, A, B any](onLeft readerio.Kleisli[R, error, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOResult[R, A]) ReaderIO[R, B] {
|
||||
return RIOE.Fold(onLeft, onRight)
|
||||
}
|
||||
|
||||
@@ -644,7 +644,7 @@ func Fold[R, A, B any](onLeft func(error) ReaderIO[R, B], onRight func(A) Reader
|
||||
// The default is computed lazily via a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[R, A any](onLeft func(error) ReaderIO[R, A]) func(ReaderIOResult[R, A]) ReaderIO[R, A] {
|
||||
func GetOrElse[R, A any](onLeft readerio.Kleisli[R, error, A]) func(ReaderIOResult[R, A]) ReaderIO[R, A] {
|
||||
return RIOE.GetOrElse(onLeft)
|
||||
}
|
||||
|
||||
@@ -659,7 +659,7 @@ func OrElse[R, A any](onLeft Kleisli[R, error, A]) Operator[R, A, A] {
|
||||
// The success value is preserved unchanged.
|
||||
//
|
||||
//go:inline
|
||||
func OrLeft[A, R, E any](onLeft func(error) ReaderIO[R, E]) func(ReaderIOResult[R, A]) RIOE.ReaderIOEither[R, E, A] {
|
||||
func OrLeft[A, R, E any](onLeft readerio.Kleisli[R, error, E]) func(ReaderIOResult[R, A]) RIOE.ReaderIOEither[R, E, A] {
|
||||
return RIOE.OrLeft[A](onLeft)
|
||||
}
|
||||
|
||||
|
||||
262
v2/result/filterable.go
Normal file
262
v2/result/filterable.go
Normal file
@@ -0,0 +1,262 @@
|
||||
// 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 result provides filterable operations for Result types.
|
||||
//
|
||||
// This package implements the Fantasy Land Filterable specification:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// Since Result[A] is an alias for Either[error, A], these functions are
|
||||
// thin wrappers around the corresponding either package functions, specialized
|
||||
// for the common case where the error type is Go's built-in error interface.
|
||||
package result
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Partition separates a [Result] value into a [Pair] based on a predicate function.
|
||||
// It returns a function that takes a Result and produces a Pair of Result values,
|
||||
// where the first element contains values that fail the predicate and the second
|
||||
// contains values that pass the predicate.
|
||||
//
|
||||
// This function implements the Filterable specification's partition operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is an error (Left), both elements of the resulting Pair will be the same error
|
||||
// - If the input is Ok (Right) and the predicate returns true, the result is (Err(empty), Ok(value))
|
||||
// - If the input is Ok (Right) and the predicate returns false, the result is (Ok(value), Err(empty))
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A predicate function that tests values of type A
|
||||
// - empty: The default error to use when creating error Results for partitioning
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A function that takes a Result[A] and returns a Pair where:
|
||||
// - First element: Result values that fail the predicate (or original error)
|
||||
// - Second element: Result values that pass the predicate (or original error)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// R "github.com/IBM/fp-go/v2/result"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// P "github.com/IBM/fp-go/v2/pair"
|
||||
// "errors"
|
||||
// )
|
||||
//
|
||||
// // Partition positive and non-positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// partition := R.Partition(isPositive, errors.New("not positive"))
|
||||
//
|
||||
// // Ok value that passes predicate
|
||||
// result1 := partition(R.Of(5))
|
||||
// // result1 = Pair(Err("not positive"), Ok(5))
|
||||
//
|
||||
// // Ok value that fails predicate
|
||||
// result2 := partition(R.Of(-3))
|
||||
// // result2 = Pair(Ok(-3), Err("not positive"))
|
||||
//
|
||||
// // Error passes through unchanged in both positions
|
||||
// result3 := partition(R.Error[int](errors.New("original error")))
|
||||
// // result3 = Pair(Err("original error"), Err("original error"))
|
||||
//
|
||||
//go:inline
|
||||
func Partition[A any](p Predicate[A], empty error) func(Result[A]) Pair[Result[A], Result[A]] {
|
||||
return either.Partition(p, empty)
|
||||
}
|
||||
|
||||
// Filter creates a filtering operation for [Result] values based on a predicate function.
|
||||
// It returns a function that takes a Result and produces a Result, where Ok values
|
||||
// that fail the predicate are converted to error Results with the provided error.
|
||||
//
|
||||
// This function implements the Filterable specification's filter operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is an error, it passes through unchanged
|
||||
// - If the input is Ok and the predicate returns true, the Ok value passes through unchanged
|
||||
// - If the input is Ok and the predicate returns false, it's converted to Err(empty)
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A predicate function that tests values of type A
|
||||
// - empty: The default error to use when filtering out Ok values that fail the predicate
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator function that takes a Result[A] and returns a Result[A] where:
|
||||
// - Error values pass through unchanged
|
||||
// - Ok values that pass the predicate remain as Ok
|
||||
// - Ok values that fail the predicate become Err(empty)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// R "github.com/IBM/fp-go/v2/result"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// "errors"
|
||||
// )
|
||||
//
|
||||
// // Filter to keep only positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// filterPositive := R.Filter(isPositive, errors.New("not positive"))
|
||||
//
|
||||
// // Ok value that passes predicate - remains Ok
|
||||
// result1 := filterPositive(R.Of(5))
|
||||
// // result1 = Ok(5)
|
||||
//
|
||||
// // Ok value that fails predicate - becomes Err
|
||||
// result2 := filterPositive(R.Of(-3))
|
||||
// // result2 = Err("not positive")
|
||||
//
|
||||
// // Error passes through unchanged
|
||||
// result3 := filterPositive(R.Error[int](errors.New("original error")))
|
||||
// // result3 = Err("original error")
|
||||
//
|
||||
// // Chaining filters
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// filterEven := R.Filter(isEven, errors.New("not even"))
|
||||
//
|
||||
// result4 := filterEven(filterPositive(R.Of(4)))
|
||||
// // result4 = Ok(4) - passes both filters
|
||||
//
|
||||
// result5 := filterEven(filterPositive(R.Of(3)))
|
||||
// // result5 = Err("not even") - passes first, fails second
|
||||
//
|
||||
//go:inline
|
||||
func Filter[A any](p Predicate[A], empty error) Operator[A, A] {
|
||||
return either.Filter(p, empty)
|
||||
}
|
||||
|
||||
// FilterMap combines filtering and mapping operations for [Result] values using an [Option]-returning function.
|
||||
// It returns a function that takes a Result[A] and produces a Result[B], where Ok values
|
||||
// are transformed by applying the function f. If f returns Some(B), the result is Ok(B). If f returns
|
||||
// None, the result is Err(empty).
|
||||
//
|
||||
// This function implements the Filterable specification's filterMap operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is an error, it passes through with its error value preserved
|
||||
// - If the input is Ok and f returns Some(B), the result is Ok(B)
|
||||
// - If the input is Ok and f returns None, the result is Err(empty)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An Option Kleisli function that transforms values of type A to Option[B]
|
||||
// - empty: The default error to use when f returns None
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator function that takes a Result[A] and returns a Result[B] where:
|
||||
// - Error values pass through with error preserved
|
||||
// - Ok values are transformed by f: Some(B) becomes Ok(B), None becomes Err(empty)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// R "github.com/IBM/fp-go/v2/result"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// "errors"
|
||||
// "strconv"
|
||||
// )
|
||||
//
|
||||
// // Parse string to int, filtering out invalid values
|
||||
// parseInt := func(s string) O.Option[int] {
|
||||
// if n, err := strconv.Atoi(s); err == nil {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
// filterMapInt := R.FilterMap(parseInt, errors.New("invalid number"))
|
||||
//
|
||||
// // Valid number string - transforms to Ok(int)
|
||||
// result1 := filterMapInt(R.Of("42"))
|
||||
// // result1 = Ok(42)
|
||||
//
|
||||
// // Invalid number string - becomes Err
|
||||
// result2 := filterMapInt(R.Of("abc"))
|
||||
// // result2 = Err("invalid number")
|
||||
//
|
||||
// // Error passes through with error preserved
|
||||
// result3 := filterMapInt(R.Error[string](errors.New("original error")))
|
||||
// // result3 = Err("original error")
|
||||
//
|
||||
//go:inline
|
||||
func FilterMap[A, B any](f option.Kleisli[A, B], empty error) Operator[A, B] {
|
||||
return either.FilterMap(f, empty)
|
||||
}
|
||||
|
||||
// PartitionMap separates and transforms a [Result] value into a [Pair] of Result values using a mapping function.
|
||||
// It returns a function that takes a Result[A] and produces a Pair of Result values, where the mapping
|
||||
// function f transforms the Ok value into Either[B, C]. The result is partitioned based on whether f
|
||||
// produces a Left or Right value.
|
||||
//
|
||||
// This function implements the Filterable specification's partitionMap operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is an error, both elements of the resulting Pair will be errors with the original error
|
||||
// - If the input is Ok and f returns Left(B), the result is (Ok(B), Err(empty))
|
||||
// - If the input is Ok and f returns Right(C), the result is (Err(empty), Ok(C))
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that transforms values of type A to Either[B, C]
|
||||
// - empty: The default error to use when creating error Results for partitioning
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A function that takes a Result[A] and returns a Pair[Result[B], Result[C]] where:
|
||||
// - If input is error: (Err(original_error), Err(original_error))
|
||||
// - If f returns Left(B): (Ok(B), Err(empty))
|
||||
// - If f returns Right(C): (Err(empty), Ok(C))
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// R "github.com/IBM/fp-go/v2/result"
|
||||
// E "github.com/IBM/fp-go/v2/either"
|
||||
// P "github.com/IBM/fp-go/v2/pair"
|
||||
// "errors"
|
||||
// "strconv"
|
||||
// )
|
||||
//
|
||||
// // Classify and transform numbers: negative -> error message, positive -> squared value
|
||||
// classifyNumber := func(n int) E.Either[string, int] {
|
||||
// if n < 0 {
|
||||
// return E.Left[int]("negative: " + strconv.Itoa(n))
|
||||
// }
|
||||
// return E.Right[string](n * n)
|
||||
// }
|
||||
// partitionMap := R.PartitionMap(classifyNumber, errors.New("not classified"))
|
||||
//
|
||||
// // Positive number - goes to right side as squared value
|
||||
// result1 := partitionMap(R.Of(5))
|
||||
// // result1 = Pair(Err("not classified"), Ok(25))
|
||||
//
|
||||
// // Negative number - goes to left side with error message
|
||||
// result2 := partitionMap(R.Of(-3))
|
||||
// // result2 = Pair(Ok("negative: -3"), Err("not classified"))
|
||||
//
|
||||
// // Original error - appears in both positions
|
||||
// result3 := partitionMap(R.Error[int](errors.New("original error")))
|
||||
// // result3 = Pair(Err("original error"), Err("original error"))
|
||||
//
|
||||
//go:inline
|
||||
func PartitionMap[A, B, C any](f either.Kleisli[B, A, C], empty error) func(Result[A]) Pair[Result[B], Result[C]] {
|
||||
return either.PartitionMap(f, empty)
|
||||
}
|
||||
689
v2/result/filterable_test.go
Normal file
689
v2/result/filterable_test.go
Normal file
@@ -0,0 +1,689 @@
|
||||
// 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 result
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPartition(t *testing.T) {
|
||||
t.Run("Ok value that passes predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
partition := Partition(isPositive, errors.New("not positive"))
|
||||
input := Of(5)
|
||||
|
||||
// Act
|
||||
result := partition(input)
|
||||
left, right := P.Unpack(result)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsLeft(left), "left should be error")
|
||||
assert.True(t, IsRight(right), "right should be Ok")
|
||||
|
||||
rightVal, _ := Unwrap(right)
|
||||
assert.Equal(t, 5, rightVal)
|
||||
})
|
||||
|
||||
t.Run("Ok value that fails predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
partition := Partition(isPositive, errors.New("not positive"))
|
||||
input := Of(-3)
|
||||
|
||||
// Act
|
||||
result := partition(input)
|
||||
left, right := P.Unpack(result)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsRight(left), "left should be Ok (failed predicate)")
|
||||
assert.True(t, IsLeft(right), "right should be error")
|
||||
|
||||
leftVal, _ := Unwrap(left)
|
||||
assert.Equal(t, -3, leftVal)
|
||||
})
|
||||
|
||||
t.Run("Ok value at boundary (zero)", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
partition := Partition(isPositive, errors.New("not positive"))
|
||||
input := Of(0)
|
||||
|
||||
// Act
|
||||
result := partition(input)
|
||||
left, right := P.Unpack(result)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsRight(left), "left should be Ok (zero fails predicate)")
|
||||
assert.True(t, IsLeft(right), "right should be error")
|
||||
|
||||
leftVal, _ := Unwrap(left)
|
||||
assert.Equal(t, 0, leftVal)
|
||||
})
|
||||
|
||||
t.Run("Error passes through unchanged", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
partition := Partition(isPositive, errors.New("not positive"))
|
||||
originalError := errors.New("original error")
|
||||
input := Left[int](originalError)
|
||||
|
||||
// Act
|
||||
result := partition(input)
|
||||
left, right := P.Unpack(result)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsLeft(left), "left should be error")
|
||||
assert.True(t, IsLeft(right), "right should be error")
|
||||
|
||||
_, leftErr := Unwrap(left)
|
||||
_, rightErr := Unwrap(right)
|
||||
|
||||
assert.Equal(t, originalError, leftErr)
|
||||
assert.Equal(t, originalError, rightErr)
|
||||
})
|
||||
|
||||
t.Run("String predicate - even length strings", func(t *testing.T) {
|
||||
// Arrange
|
||||
isEvenLength := func(s string) bool { return len(s)%2 == 0 }
|
||||
partition := Partition(isEvenLength, errors.New("odd length"))
|
||||
|
||||
// Act & Assert - passes predicate
|
||||
result1 := partition(Of("test"))
|
||||
left1, right1 := P.Unpack(result1)
|
||||
assert.True(t, IsLeft(left1))
|
||||
assert.True(t, IsRight(right1))
|
||||
rightVal1, _ := Unwrap(right1)
|
||||
assert.Equal(t, "test", rightVal1)
|
||||
|
||||
// Act & Assert - fails predicate
|
||||
result2 := partition(Of("hello"))
|
||||
left2, right2 := P.Unpack(result2)
|
||||
assert.True(t, IsRight(left2))
|
||||
assert.True(t, IsLeft(right2))
|
||||
leftVal2, _ := Unwrap(left2)
|
||||
assert.Equal(t, "hello", leftVal2)
|
||||
})
|
||||
|
||||
t.Run("Complex type predicate - struct field check", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
isAdult := func(p Person) bool { return p.Age >= 18 }
|
||||
partition := Partition(isAdult, errors.New("minor"))
|
||||
|
||||
// Act & Assert - adult passes
|
||||
adult := Person{Name: "Alice", Age: 25}
|
||||
result1 := partition(Of(adult))
|
||||
left1, right1 := P.Unpack(result1)
|
||||
assert.True(t, IsLeft(left1))
|
||||
assert.True(t, IsRight(right1))
|
||||
rightVal1, _ := Unwrap(right1)
|
||||
assert.Equal(t, adult, rightVal1)
|
||||
|
||||
// Act & Assert - minor fails
|
||||
minor := Person{Name: "Bob", Age: 15}
|
||||
result2 := partition(Of(minor))
|
||||
left2, right2 := P.Unpack(result2)
|
||||
assert.True(t, IsRight(left2))
|
||||
assert.True(t, IsLeft(right2))
|
||||
leftVal2, _ := Unwrap(left2)
|
||||
assert.Equal(t, minor, leftVal2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
t.Run("Ok value that passes predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filter := Filter(isPositive, errors.New("not positive"))
|
||||
input := Of(5)
|
||||
|
||||
// Act
|
||||
result := filter(input)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsRight(result), "result should be Ok")
|
||||
val, _ := Unwrap(result)
|
||||
assert.Equal(t, 5, val)
|
||||
})
|
||||
|
||||
t.Run("Ok value that fails predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filter := Filter(isPositive, errors.New("not positive"))
|
||||
input := Of(-3)
|
||||
|
||||
// Act
|
||||
result := filter(input)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsLeft(result), "result should be error")
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "not positive", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Ok value at boundary (zero)", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filter := Filter(isPositive, errors.New("not positive"))
|
||||
input := Of(0)
|
||||
|
||||
// Act
|
||||
result := filter(input)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsLeft(result), "zero should fail predicate")
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "not positive", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Error passes through unchanged", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filter := Filter(isPositive, errors.New("not positive"))
|
||||
originalError := errors.New("original error")
|
||||
input := Left[int](originalError)
|
||||
|
||||
// Act
|
||||
result := filter(input)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsLeft(result), "result should be error")
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, originalError, err)
|
||||
})
|
||||
|
||||
t.Run("Chaining multiple filters", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterPositive := Filter(isPositive, errors.New("not positive"))
|
||||
filterEven := Filter(isEven, errors.New("not even"))
|
||||
|
||||
// Act & Assert - passes both filters
|
||||
result1 := filterEven(filterPositive(Of(4)))
|
||||
assert.True(t, IsRight(result1))
|
||||
val1, _ := Unwrap(result1)
|
||||
assert.Equal(t, 4, val1)
|
||||
|
||||
// Act & Assert - passes first, fails second
|
||||
result2 := filterEven(filterPositive(Of(3)))
|
||||
assert.True(t, IsLeft(result2))
|
||||
_, err2 := Unwrap(result2)
|
||||
assert.Equal(t, "not even", err2.Error())
|
||||
|
||||
// Act & Assert - fails first filter
|
||||
result3 := filterEven(filterPositive(Of(-2)))
|
||||
assert.True(t, IsLeft(result3))
|
||||
_, err3 := Unwrap(result3)
|
||||
assert.Equal(t, "not positive", err3.Error())
|
||||
|
||||
// Act & Assert - error passes through both
|
||||
originalErr := errors.New("original")
|
||||
result4 := filterEven(filterPositive(Left[int](originalErr)))
|
||||
assert.True(t, IsLeft(result4))
|
||||
_, err4 := Unwrap(result4)
|
||||
assert.Equal(t, originalErr, err4)
|
||||
})
|
||||
|
||||
t.Run("Filter preserves error", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filter := Filter(isPositive, errors.New("default error"))
|
||||
|
||||
// Act - error with different message
|
||||
originalError := errors.New("server error")
|
||||
result := filter(Left[int](originalError))
|
||||
|
||||
// Assert - original error preserved
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, originalError, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMap(t *testing.T) {
|
||||
t.Run("Ok value with Some result", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseInt := func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMap := FilterMap(parseInt, errors.New("invalid number"))
|
||||
input := Of("42")
|
||||
|
||||
// Act
|
||||
result := filterMap(input)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsRight(result), "result should be Ok")
|
||||
val, _ := Unwrap(result)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
|
||||
t.Run("Ok value with None result", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseInt := func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMap := FilterMap(parseInt, errors.New("invalid number"))
|
||||
input := Of("abc")
|
||||
|
||||
// Act
|
||||
result := filterMap(input)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsLeft(result), "result should be error")
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, "invalid number", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Error passes through", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseInt := func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMap := FilterMap(parseInt, errors.New("invalid number"))
|
||||
originalError := errors.New("original error")
|
||||
input := Left[string](originalError)
|
||||
|
||||
// Act
|
||||
result := filterMap(input)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsLeft(result), "result should be error")
|
||||
_, err := Unwrap(result)
|
||||
assert.Equal(t, originalError, err)
|
||||
})
|
||||
|
||||
t.Run("Extract optional field from struct", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Person struct {
|
||||
Name string
|
||||
Email O.Option[string]
|
||||
}
|
||||
extractEmail := func(p Person) O.Option[string] { return p.Email }
|
||||
filterMap := FilterMap(extractEmail, errors.New("no email"))
|
||||
|
||||
// Act & Assert - has email
|
||||
result1 := filterMap(Of(Person{Name: "Alice", Email: O.Some("alice@example.com")}))
|
||||
assert.True(t, IsRight(result1))
|
||||
val1, _ := Unwrap(result1)
|
||||
assert.Equal(t, "alice@example.com", val1)
|
||||
|
||||
// Act & Assert - no email
|
||||
result2 := filterMap(Of(Person{Name: "Bob", Email: O.None[string]()}))
|
||||
assert.True(t, IsLeft(result2))
|
||||
_, err2 := Unwrap(result2)
|
||||
assert.Equal(t, "no email", err2.Error())
|
||||
})
|
||||
|
||||
t.Run("Transform and filter numbers", func(t *testing.T) {
|
||||
// Arrange
|
||||
sqrtIfPositive := func(n int) O.Option[float64] {
|
||||
if n >= 0 {
|
||||
return O.Some(math.Sqrt(float64(n)))
|
||||
}
|
||||
return O.None[float64]()
|
||||
}
|
||||
filterMap := FilterMap(sqrtIfPositive, errors.New("negative number"))
|
||||
|
||||
// Act & Assert - positive number
|
||||
result1 := filterMap(Of(16))
|
||||
assert.True(t, IsRight(result1))
|
||||
val1, _ := Unwrap(result1)
|
||||
assert.Equal(t, 4.0, val1)
|
||||
|
||||
// Act & Assert - negative number
|
||||
result2 := filterMap(Of(-4))
|
||||
assert.True(t, IsLeft(result2))
|
||||
_, err2 := Unwrap(result2)
|
||||
assert.Equal(t, "negative number", err2.Error())
|
||||
|
||||
// Act & Assert - zero
|
||||
result3 := filterMap(Of(0))
|
||||
assert.True(t, IsRight(result3))
|
||||
val3, _ := Unwrap(result3)
|
||||
assert.Equal(t, 0.0, val3)
|
||||
})
|
||||
|
||||
t.Run("Chain multiple FilterMap operations", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseInt := func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
doubleIfEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMap1 := FilterMap(parseInt, errors.New("invalid number"))
|
||||
filterMap2 := FilterMap(doubleIfEven, errors.New("not even"))
|
||||
|
||||
// Act & Assert - valid even number
|
||||
result1 := filterMap2(filterMap1(Of("4")))
|
||||
assert.True(t, IsRight(result1))
|
||||
val1, _ := Unwrap(result1)
|
||||
assert.Equal(t, 8, val1)
|
||||
|
||||
// Act & Assert - valid odd number
|
||||
result2 := filterMap2(filterMap1(Of("3")))
|
||||
assert.True(t, IsLeft(result2))
|
||||
_, err2 := Unwrap(result2)
|
||||
assert.Equal(t, "not even", err2.Error())
|
||||
|
||||
// Act & Assert - invalid number
|
||||
result3 := filterMap2(filterMap1(Of("abc")))
|
||||
assert.True(t, IsLeft(result3))
|
||||
_, err3 := Unwrap(result3)
|
||||
assert.Equal(t, "invalid number", err3.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPartitionMap(t *testing.T) {
|
||||
t.Run("Ok value that maps to Left", func(t *testing.T) {
|
||||
// Arrange
|
||||
classifyNumber := func(n int) E.Either[string, int] {
|
||||
if n < 0 {
|
||||
return E.Left[int]("negative: " + strconv.Itoa(n))
|
||||
}
|
||||
return E.Right[string](n * n)
|
||||
}
|
||||
partitionMap := PartitionMap(classifyNumber, errors.New("not classified"))
|
||||
input := Of(-3)
|
||||
|
||||
// Act
|
||||
result := partitionMap(input)
|
||||
left, right := P.Unpack(result)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsRight(left), "left should be Ok (contains error from f)")
|
||||
assert.True(t, IsLeft(right), "right should be error")
|
||||
|
||||
leftVal, _ := Unwrap(left)
|
||||
assert.Equal(t, "negative: -3", leftVal)
|
||||
})
|
||||
|
||||
t.Run("Ok value that maps to Right", func(t *testing.T) {
|
||||
// Arrange
|
||||
classifyNumber := func(n int) E.Either[string, int] {
|
||||
if n < 0 {
|
||||
return E.Left[int]("negative: " + strconv.Itoa(n))
|
||||
}
|
||||
return E.Right[string](n * n)
|
||||
}
|
||||
partitionMap := PartitionMap(classifyNumber, errors.New("not classified"))
|
||||
input := Of(5)
|
||||
|
||||
// Act
|
||||
result := partitionMap(input)
|
||||
left, right := P.Unpack(result)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsLeft(left), "left should be error")
|
||||
assert.True(t, IsRight(right), "right should be Ok (contains value from f)")
|
||||
|
||||
rightVal, _ := Unwrap(right)
|
||||
assert.Equal(t, 25, rightVal)
|
||||
})
|
||||
|
||||
t.Run("Error passes through to both sides", func(t *testing.T) {
|
||||
// Arrange
|
||||
classifyNumber := func(n int) E.Either[string, int] {
|
||||
if n < 0 {
|
||||
return E.Left[int]("negative")
|
||||
}
|
||||
return E.Right[string](n * n)
|
||||
}
|
||||
partitionMap := PartitionMap(classifyNumber, errors.New("not classified"))
|
||||
originalError := errors.New("original error")
|
||||
input := Left[int](originalError)
|
||||
|
||||
// Act
|
||||
result := partitionMap(input)
|
||||
left, right := P.Unpack(result)
|
||||
|
||||
// Assert
|
||||
assert.True(t, IsLeft(left), "left should be error")
|
||||
assert.True(t, IsLeft(right), "right should be error")
|
||||
|
||||
_, leftErr := Unwrap(left)
|
||||
_, rightErr := Unwrap(right)
|
||||
|
||||
assert.Equal(t, originalError, leftErr)
|
||||
assert.Equal(t, originalError, rightErr)
|
||||
})
|
||||
|
||||
t.Run("Validate and transform user input", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
validateUser := func(input map[string]string) E.Either[ValidationError, User] {
|
||||
name, hasName := input["name"]
|
||||
ageStr, hasAge := input["age"]
|
||||
if !hasName {
|
||||
return E.Left[User](ValidationError{"name", "missing"})
|
||||
}
|
||||
if !hasAge {
|
||||
return E.Left[User](ValidationError{"age", "missing"})
|
||||
}
|
||||
age, err := strconv.Atoi(ageStr)
|
||||
if err != nil {
|
||||
return E.Left[User](ValidationError{"age", "invalid"})
|
||||
}
|
||||
return E.Right[ValidationError](User{name, age})
|
||||
}
|
||||
partitionMap := PartitionMap(validateUser, errors.New("not processed"))
|
||||
|
||||
// Act & Assert - valid input
|
||||
validInput := map[string]string{"name": "Alice", "age": "30"}
|
||||
result1 := partitionMap(Of(validInput))
|
||||
left1, right1 := P.Unpack(result1)
|
||||
assert.True(t, IsLeft(left1))
|
||||
assert.True(t, IsRight(right1))
|
||||
rightVal1, _ := Unwrap(right1)
|
||||
assert.Equal(t, User{"Alice", 30}, rightVal1)
|
||||
|
||||
// Act & Assert - invalid input (missing age)
|
||||
invalidInput := map[string]string{"name": "Bob"}
|
||||
result2 := partitionMap(Of(invalidInput))
|
||||
left2, right2 := P.Unpack(result2)
|
||||
assert.True(t, IsRight(left2))
|
||||
assert.True(t, IsLeft(right2))
|
||||
leftVal2, _ := Unwrap(left2)
|
||||
assert.Equal(t, ValidationError{"age", "missing"}, leftVal2)
|
||||
})
|
||||
|
||||
t.Run("Classify strings by length", func(t *testing.T) {
|
||||
// Arrange
|
||||
classifyString := func(s string) E.Either[string, int] {
|
||||
if len(s) < 5 {
|
||||
return E.Left[int]("too short: " + s)
|
||||
}
|
||||
return E.Right[string](len(s))
|
||||
}
|
||||
partitionMap := PartitionMap(classifyString, errors.New("not classified"))
|
||||
|
||||
// Act & Assert - short string
|
||||
result1 := partitionMap(Of("hi"))
|
||||
left1, right1 := P.Unpack(result1)
|
||||
assert.True(t, IsRight(left1))
|
||||
assert.True(t, IsLeft(right1))
|
||||
leftVal1, _ := Unwrap(left1)
|
||||
assert.Equal(t, "too short: hi", leftVal1)
|
||||
|
||||
// Act & Assert - long string
|
||||
result2 := partitionMap(Of("hello world"))
|
||||
left2, right2 := P.Unpack(result2)
|
||||
assert.True(t, IsLeft(left2))
|
||||
assert.True(t, IsRight(right2))
|
||||
rightVal2, _ := Unwrap(right2)
|
||||
assert.Equal(t, 11, rightVal2)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkPartition(b *testing.B) {
|
||||
isPositive := N.MoreThan(0)
|
||||
partition := Partition(isPositive, errors.New("not positive"))
|
||||
input := Of(42)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = partition(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPartitionError(b *testing.B) {
|
||||
isPositive := N.MoreThan(0)
|
||||
partition := Partition(isPositive, errors.New("not positive"))
|
||||
input := Left[int](errors.New("error"))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = partition(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilter(b *testing.B) {
|
||||
isPositive := N.MoreThan(0)
|
||||
filter := Filter(isPositive, errors.New("not positive"))
|
||||
input := Of(42)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = filter(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilterError(b *testing.B) {
|
||||
isPositive := N.MoreThan(0)
|
||||
filter := Filter(isPositive, errors.New("not positive"))
|
||||
input := Left[int](errors.New("error"))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = filter(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilterChained(b *testing.B) {
|
||||
isPositive := N.MoreThan(0)
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterPositive := Filter(isPositive, errors.New("not positive"))
|
||||
filterEven := Filter(isEven, errors.New("not even"))
|
||||
input := Of(42)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = filterEven(filterPositive(input))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilterMap(b *testing.B) {
|
||||
parseInt := func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMap := FilterMap(parseInt, errors.New("invalid"))
|
||||
input := Of("42")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = filterMap(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilterMapError(b *testing.B) {
|
||||
parseInt := func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMap := FilterMap(parseInt, errors.New("invalid"))
|
||||
input := Left[string](errors.New("error"))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = filterMap(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPartitionMap(b *testing.B) {
|
||||
classify := func(n int) E.Either[string, int] {
|
||||
if n < 0 {
|
||||
return E.Left[int]("negative")
|
||||
}
|
||||
return E.Right[string](n * n)
|
||||
}
|
||||
partitionMap := PartitionMap(classify, errors.New("not classified"))
|
||||
input := Of(42)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = partitionMap(input)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPartitionMapError(b *testing.B) {
|
||||
classify := func(n int) E.Either[string, int] {
|
||||
if n < 0 {
|
||||
return E.Left[int]("negative")
|
||||
}
|
||||
return E.Right[string](n * n)
|
||||
}
|
||||
partitionMap := PartitionMap(classify, errors.New("not classified"))
|
||||
input := Left[int](errors.New("error"))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = partitionMap(input)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
@@ -61,4 +62,6 @@ type (
|
||||
// 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]
|
||||
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user