mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-14 13:42:48 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5a3217251 | ||
|
|
c5cbdaad68 | ||
|
|
5d0f27ad10 |
108
v2/AGENTS.md
108
v2/AGENTS.md
@@ -2,6 +2,20 @@
|
||||
|
||||
This document provides guidelines for AI agents working on the fp-go/v2 project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Documentation Standards](#documentation-standards)
|
||||
- [Go Doc Comments](#go-doc-comments)
|
||||
- [File Headers](#file-headers)
|
||||
- [Testing Standards](#testing-standards)
|
||||
- [Test Structure](#test-structure)
|
||||
- [Test Coverage](#test-coverage)
|
||||
- [Example Test Pattern](#example-test-pattern)
|
||||
- [Code Style](#code-style)
|
||||
- [Functional Patterns](#functional-patterns)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Checklist for New Code](#checklist-for-new-code)
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Go Doc Comments
|
||||
@@ -102,6 +116,50 @@ Always include the Apache 2.0 license header:
|
||||
- Use `result.Of` for success values
|
||||
- Use `result.Left` for error values
|
||||
|
||||
4. **Folding Either/Result Values in Tests**
|
||||
- Use `F.Pipe1(result, Fold(onLeft, onRight))` — avoid the `_ = Fold(...)(result)` discard pattern
|
||||
- Use `slices.Collect[T]` instead of a manual `for n := range seq { collected = append(...) }` loop
|
||||
- Use `t.Fatal` in the unexpected branch to combine the `IsLeft`/`IsRight` check with value extraction:
|
||||
```go
|
||||
// Good: single fold combines assertion and extraction
|
||||
collected := F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
|
||||
// Avoid: separate IsRight check + manual loop
|
||||
assert.True(t, IsRight(result))
|
||||
var collected []int
|
||||
_ = MonadFold(result,
|
||||
func(e error) []int { return nil },
|
||||
func(seq iter.Seq[int]) []int {
|
||||
for n := range seq { collected = append(collected, n) }
|
||||
return collected
|
||||
},
|
||||
)
|
||||
```
|
||||
- Use `F.Identity[error]` as the Left branch when extracting an error value:
|
||||
```go
|
||||
err := F.Pipe1(result, Fold(
|
||||
F.Identity[error],
|
||||
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
|
||||
))
|
||||
```
|
||||
- Extract repeated fold patterns as local helper closures within the test function:
|
||||
```go
|
||||
collectInts := func(r Result[iter.Seq[int]]) []int {
|
||||
return F.Pipe1(r, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
5. **Other Test Style Details**
|
||||
- Use `for i := range 10` instead of `for i := 0; i < 10; i++`
|
||||
- Chain curried calls directly: `TraverseSeq(parse)(input)` — no need for an intermediate `traverseFn` variable
|
||||
- Use direct slice literals (`[]string{"a", "b"}`) rather than `A.From("a", "b")` in tests
|
||||
|
||||
### Test Coverage
|
||||
|
||||
Include tests for:
|
||||
@@ -168,56 +226,6 @@ func TestFromReaderResult_Success(t *testing.T) {
|
||||
- Check error context is preserved
|
||||
- Test error accumulation when applicable
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Converting Error-Based Functions
|
||||
|
||||
```go
|
||||
// Good: Use Eitherize1
|
||||
parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
|
||||
// Avoid: Manual error handling
|
||||
parseIntRR := func(input string) result.Result[int] {
|
||||
val, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return result.Left[int](err)
|
||||
}
|
||||
return result.Of(val)
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Validation Results
|
||||
|
||||
```go
|
||||
// Good: Direct comparison
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Avoid: Verbose extraction (unless you need to verify specific fields)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
```
|
||||
|
||||
### Documentation Examples
|
||||
|
||||
```go
|
||||
// Good: Concise and idiomatic
|
||||
// parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
// validator := FromReaderResult[string, int](parseIntRR)
|
||||
|
||||
// Avoid: Verbose manual patterns
|
||||
// parseIntRR := func(input string) result.Result[int] {
|
||||
// val, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return result.Left[int](err)
|
||||
// }
|
||||
// return result.Of(val)
|
||||
// }
|
||||
```
|
||||
|
||||
## Checklist for New Code
|
||||
|
||||
- [ ] Apache 2.0 license header included
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
package either
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"slices"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RA "github.com/IBM/fp-go/v2/internal/array"
|
||||
)
|
||||
@@ -178,3 +181,92 @@ func CompactArrayG[A1 ~[]Either[E, A], A2 ~[]A, E, A any](fa A1) A2 {
|
||||
func CompactArray[E, A any](fa []Either[E, A]) []A {
|
||||
return CompactArrayG[[]Either[E, A], []A](fa)
|
||||
}
|
||||
|
||||
// TraverseSeq transforms an iterator by applying a function that returns an Either to each element.
|
||||
// If any element produces a Left, the entire result is that Left (short-circuits).
|
||||
// Otherwise, returns Right containing an iterator of all Right values.
|
||||
//
|
||||
// The function eagerly evaluates all elements in the input iterator to detect any Left values,
|
||||
// then returns an iterator over the collected Right values. This is necessary because Either
|
||||
// represents computations that can fail, and we need to know if any element failed before
|
||||
// producing the result iterator.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - E: The error type for Left values
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that transforms each element into an Either
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A function that takes an iterator of A and returns Either containing an iterator of B
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// parse := func(s string) either.Either[error, int] {
|
||||
// v, err := strconv.Atoi(s)
|
||||
// return either.FromError(v, err)
|
||||
// }
|
||||
// input := slices.Values([]string{"1", "2", "3"})
|
||||
// result := either.TraverseSeq(parse)(input)
|
||||
// // result is Right(iterator over [1, 2, 3])
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - TraverseArray: For slice-based traversal
|
||||
// - SequenceSeq: For sequencing iterators of Either values
|
||||
func TraverseSeq[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, iter.Seq[A], iter.Seq[B]] {
|
||||
return func(ga iter.Seq[A]) Either[E, iter.Seq[B]] {
|
||||
var bs []B
|
||||
for a := range ga {
|
||||
b := f(a)
|
||||
if b.isLeft {
|
||||
return Left[iter.Seq[B]](b.l)
|
||||
}
|
||||
bs = append(bs, b.r)
|
||||
}
|
||||
return Of[E](slices.Values(bs))
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceSeq converts an iterator of Either into an Either of iterator.
|
||||
// If any element is Left, returns that Left (short-circuits).
|
||||
// Otherwise, returns Right containing an iterator of all the Right values.
|
||||
//
|
||||
// This function eagerly evaluates all Either values in the input iterator to detect
|
||||
// any Left values, then returns an iterator over the collected Right values.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - E: The error type for Left values
|
||||
// - A: The value type for Right values
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - ma: An iterator of Either values
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Either containing an iterator of Right values, or the first Left encountered
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// eithers := slices.Values([]either.Either[error, int]{
|
||||
// either.Right[error](1),
|
||||
// either.Right[error](2),
|
||||
// either.Right[error](3),
|
||||
// })
|
||||
// result := either.SequenceSeq(eithers)
|
||||
// // result is Right(iterator over [1, 2, 3])
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - SequenceArray: For slice-based sequencing
|
||||
// - TraverseSeq: For transforming and sequencing in one step
|
||||
func SequenceSeq[E, A any](ma iter.Seq[Either[E, A]]) Either[E, iter.Seq[A]] {
|
||||
return TraverseSeq(F.Identity[Either[E, A]])(ma)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
package either
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
TST "github.com/IBM/fp-go/v2/internal/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCompactArray(t *testing.T) {
|
||||
ar := A.From(
|
||||
ar := []Either[string, string]{
|
||||
Of[string]("ok"),
|
||||
Left[string]("err"),
|
||||
Of[string]("ok"),
|
||||
)
|
||||
|
||||
res := CompactArray(ar)
|
||||
assert.Equal(t, 2, len(res))
|
||||
}
|
||||
assert.Equal(t, 2, len(CompactArray(ar)))
|
||||
}
|
||||
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
|
||||
s := TST.SequenceArrayTest(
|
||||
FromStrictEquals[error, bool](),
|
||||
Pointed[error, string](),
|
||||
@@ -29,14 +30,12 @@ func TestSequenceArray(t *testing.T) {
|
||||
Functor[error, []string, bool](),
|
||||
SequenceArray[error, string],
|
||||
)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
t.Run(fmt.Sprintf("TestSequenceArray %d", i), s(i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceArrayError(t *testing.T) {
|
||||
|
||||
s := TST.SequenceArrayErrorTest(
|
||||
FromStrictEquals[error, bool](),
|
||||
Left[string, error],
|
||||
@@ -46,6 +45,243 @@ func TestSequenceArrayError(t *testing.T) {
|
||||
Functor[error, []string, bool](),
|
||||
SequenceArray[error, string],
|
||||
)
|
||||
// run across four bits
|
||||
s(4)(t)
|
||||
}
|
||||
|
||||
func TestTraverseSeq_Success(t *testing.T) {
|
||||
parse := func(s string) Either[error, int] {
|
||||
v, err := strconv.Atoi(s)
|
||||
return TryCatchError(v, err)
|
||||
}
|
||||
|
||||
collectInts := func(result Either[error, iter.Seq[int]]) []int {
|
||||
return F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("transforms all elements successfully", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"1", "2", "3"}))
|
||||
assert.Equal(t, []int{1, 2, 3}, collectInts(result))
|
||||
})
|
||||
|
||||
t.Run("works with empty iterator", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{}))
|
||||
assert.Empty(t, collectInts(result))
|
||||
})
|
||||
|
||||
t.Run("works with single element", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"42"}))
|
||||
assert.Equal(t, []int{42}, collectInts(result))
|
||||
})
|
||||
|
||||
t.Run("preserves order of elements", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"10", "20", "30", "40", "50"}))
|
||||
assert.Equal(t, []int{10, 20, 30, 40, 50}, collectInts(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseSeq_Failure(t *testing.T) {
|
||||
parse := func(s string) Either[error, int] {
|
||||
v, err := strconv.Atoi(s)
|
||||
return TryCatchError(v, err)
|
||||
}
|
||||
|
||||
extractErr := func(result Either[error, iter.Seq[int]]) error {
|
||||
return F.Pipe1(result, Fold(
|
||||
F.Identity[error],
|
||||
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("short-circuits on first Left", func(t *testing.T) {
|
||||
err := extractErr(TraverseSeq(parse)(slices.Values([]string{"1", "invalid", "3"})))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid syntax")
|
||||
})
|
||||
|
||||
t.Run("returns first error when multiple failures exist", func(t *testing.T) {
|
||||
err := extractErr(TraverseSeq(parse)(slices.Values([]string{"1", "bad1", "bad2"})))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "bad1")
|
||||
})
|
||||
|
||||
t.Run("handles custom error types", func(t *testing.T) {
|
||||
customErr := errors.New("custom validation error")
|
||||
validate := func(n int) Either[error, int] {
|
||||
if n == 2 {
|
||||
return Left[int](customErr)
|
||||
}
|
||||
return Right[error](n * 10)
|
||||
}
|
||||
err := extractErr(TraverseSeq(validate)(slices.Values([]int{1, 2, 3})))
|
||||
assert.Equal(t, customErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseSeq_EdgeCases(t *testing.T) {
|
||||
t.Run("handles complex transformations", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
transform := func(id int) Either[error, User] {
|
||||
return Right[error](User{ID: id, Name: fmt.Sprintf("User%d", id)})
|
||||
}
|
||||
|
||||
result := TraverseSeq(transform)(slices.Values([]int{1, 2, 3}))
|
||||
collected := F.Pipe1(result, Fold(
|
||||
func(e error) []User { t.Fatal(e); return nil },
|
||||
slices.Collect[User],
|
||||
))
|
||||
|
||||
assert.Equal(t, []User{
|
||||
{ID: 1, Name: "User1"},
|
||||
{ID: 2, Name: "User2"},
|
||||
{ID: 3, Name: "User3"},
|
||||
}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with identity transformation", func(t *testing.T) {
|
||||
input := slices.Values([]Either[error, int]{
|
||||
Right[error](1),
|
||||
Right[error](2),
|
||||
Right[error](3),
|
||||
})
|
||||
|
||||
result := TraverseSeq(F.Identity[Either[error, int]])(input)
|
||||
collected := F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceSeq_Success(t *testing.T) {
|
||||
collectInts := func(result Either[error, iter.Seq[int]]) []int {
|
||||
return F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("sequences multiple Right values", func(t *testing.T) {
|
||||
input := slices.Values([]Either[error, int]{Right[error](1), Right[error](2), Right[error](3)})
|
||||
assert.Equal(t, []int{1, 2, 3}, collectInts(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("works with empty iterator", func(t *testing.T) {
|
||||
input := slices.Values([]Either[error, string]{})
|
||||
result := F.Pipe1(SequenceSeq(input), Fold(
|
||||
func(e error) []string { t.Fatal(e); return nil },
|
||||
slices.Collect[string],
|
||||
))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("works with single Right value", func(t *testing.T) {
|
||||
input := slices.Values([]Either[error, string]{Right[error]("hello")})
|
||||
result := F.Pipe1(SequenceSeq(input), Fold(
|
||||
func(e error) []string { t.Fatal(e); return nil },
|
||||
slices.Collect[string],
|
||||
))
|
||||
assert.Equal(t, []string{"hello"}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves order of results", func(t *testing.T) {
|
||||
input := slices.Values([]Either[error, int]{
|
||||
Right[error](5), Right[error](4), Right[error](3), Right[error](2), Right[error](1),
|
||||
})
|
||||
assert.Equal(t, []int{5, 4, 3, 2, 1}, collectInts(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("works with complex types", func(t *testing.T) {
|
||||
type Item struct {
|
||||
Value int
|
||||
Label string
|
||||
}
|
||||
|
||||
input := slices.Values([]Either[error, Item]{
|
||||
Right[error](Item{Value: 1, Label: "first"}),
|
||||
Right[error](Item{Value: 2, Label: "second"}),
|
||||
Right[error](Item{Value: 3, Label: "third"}),
|
||||
})
|
||||
|
||||
collected := F.Pipe1(SequenceSeq(input), Fold(
|
||||
func(e error) []Item { t.Fatal(e); return nil },
|
||||
slices.Collect[Item],
|
||||
))
|
||||
|
||||
assert.Equal(t, []Item{
|
||||
{Value: 1, Label: "first"},
|
||||
{Value: 2, Label: "second"},
|
||||
{Value: 3, Label: "third"},
|
||||
}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceSeq_Failure(t *testing.T) {
|
||||
extractErr := func(result Either[error, iter.Seq[int]]) error {
|
||||
return F.Pipe1(result, Fold(
|
||||
F.Identity[error],
|
||||
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("short-circuits on first Left", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
input := slices.Values([]Either[error, int]{Right[error](1), Left[int](testErr), Right[error](3)})
|
||||
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("returns first error when multiple Left values exist", func(t *testing.T) {
|
||||
err1 := errors.New("error 1")
|
||||
err2 := errors.New("error 2")
|
||||
input := slices.Values([]Either[error, int]{Right[error](1), Left[int](err1), Left[int](err2)})
|
||||
assert.Equal(t, err1, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("handles Left at the beginning", func(t *testing.T) {
|
||||
testErr := errors.New("first error")
|
||||
input := slices.Values([]Either[error, int]{Left[int](testErr), Right[error](2), Right[error](3)})
|
||||
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("handles Left at the end", func(t *testing.T) {
|
||||
testErr := errors.New("last error")
|
||||
input := slices.Values([]Either[error, int]{Right[error](1), Right[error](2), Left[int](testErr)})
|
||||
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceSeq_Integration(t *testing.T) {
|
||||
t.Run("integrates with TraverseSeq", func(t *testing.T) {
|
||||
parse := func(s string) Either[error, int] {
|
||||
v, err := strconv.Atoi(s)
|
||||
return TryCatchError(v, err)
|
||||
}
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"1", "2", "3"}))
|
||||
assert.True(t, IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("SequenceSeq is equivalent to TraverseSeq with Identity", func(t *testing.T) {
|
||||
mkInput := func() []Either[error, int] {
|
||||
return []Either[error, int]{Right[error](10), Right[error](20), Right[error](30)}
|
||||
}
|
||||
|
||||
collected1 := F.Pipe1(SequenceSeq(slices.Values(mkInput())), Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
collected2 := F.Pipe1(TraverseSeq(F.Identity[Either[error, int]])(slices.Values(mkInput())), Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
|
||||
assert.Equal(t, collected1, collected2)
|
||||
})
|
||||
}
|
||||
|
||||
169
v2/optics/codec/iso.go
Normal file
169
v2/optics/codec/iso.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/decode"
|
||||
)
|
||||
|
||||
// FromIso creates a Type codec from an Iso (isomorphism).
|
||||
//
|
||||
// An isomorphism represents a bidirectional transformation between types I and A
|
||||
// without any loss of information. This function converts an Iso[I, A] into a
|
||||
// Type[A, I, I] codec that can validate, decode, and encode values using the
|
||||
// isomorphism's transformations.
|
||||
//
|
||||
// The resulting codec:
|
||||
// - Decode: Uses iso.Get to transform I → A, always succeeds (no validation)
|
||||
// - Encode: Uses iso.ReverseGet to transform A → I
|
||||
// - Validation: Always succeeds since isomorphisms are lossless transformations
|
||||
// - Type checking: Uses standard type checking for type A
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Creating codecs for newtype patterns (wrapping/unwrapping types)
|
||||
// - Building codecs for types with lossless conversions
|
||||
// - Composing with other codecs using Pipe or other operators
|
||||
// - Implementing bidirectional transformations in codec pipelines
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type (what we decode to and encode from)
|
||||
// - I: The input/output type (what we decode from and encode to)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - iso: An Iso[I, A] that defines the bidirectional transformation:
|
||||
// - Get: I → A (converts input to target type)
|
||||
// - ReverseGet: A → I (converts target back to input type)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Type[A, I, I] codec where:
|
||||
// - Decode: I → Validation[A] - transforms using iso.Get, always succeeds
|
||||
// - Encode: A → I - transforms using iso.ReverseGet
|
||||
// - Is: Checks if a value is of type A
|
||||
// - Name: Returns "FromIso[iso_string_representation]"
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// Decoding:
|
||||
// - Applies iso.Get to transform the input value
|
||||
// - Wraps the result in decode.Of (always successful validation)
|
||||
// - No validation errors can occur since isomorphisms are lossless
|
||||
//
|
||||
// Encoding:
|
||||
// - Applies iso.ReverseGet to transform back to the input type
|
||||
// - Always succeeds as isomorphisms guarantee reversibility
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// Creating a codec for a newtype pattern:
|
||||
//
|
||||
// type UserId int
|
||||
//
|
||||
// // Define an isomorphism between int and UserId
|
||||
// userIdIso := iso.MakeIso(
|
||||
// func(id UserId) int { return int(id) },
|
||||
// func(i int) UserId { return UserId(i) },
|
||||
// )
|
||||
//
|
||||
// // Create a codec from the isomorphism
|
||||
// userIdCodec := codec.FromIso[int, UserId](userIdIso)
|
||||
//
|
||||
// // Decode: UserId → int
|
||||
// result := userIdCodec.Decode(UserId(42)) // Success: Right(42)
|
||||
//
|
||||
// // Encode: int → UserId
|
||||
// encoded := userIdCodec.Encode(42) // Returns: UserId(42)
|
||||
//
|
||||
// Using with temperature conversions:
|
||||
//
|
||||
// type Celsius float64
|
||||
// type Fahrenheit float64
|
||||
//
|
||||
// celsiusToFahrenheit := iso.MakeIso(
|
||||
// func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
// func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
// )
|
||||
//
|
||||
// tempCodec := codec.FromIso[Fahrenheit, Celsius](celsiusToFahrenheit)
|
||||
//
|
||||
// // Decode: Celsius → Fahrenheit
|
||||
// result := tempCodec.Decode(Celsius(20)) // Success: Right(68°F)
|
||||
//
|
||||
// // Encode: Fahrenheit → Celsius
|
||||
// encoded := tempCodec.Encode(Fahrenheit(68)) // Returns: 20°C
|
||||
//
|
||||
// Composing with other codecs:
|
||||
//
|
||||
// type Email string
|
||||
// type ValidatedEmail struct{ value Email }
|
||||
//
|
||||
// emailIso := iso.MakeIso(
|
||||
// func(ve ValidatedEmail) Email { return ve.value },
|
||||
// func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
|
||||
// )
|
||||
//
|
||||
// // Compose with string codec for validation
|
||||
// emailCodec := F.Pipe2(
|
||||
// codec.String(), // Type[string, string, any]
|
||||
// codec.Pipe(codec.FromIso[Email, string]( // Add string → Email iso
|
||||
// iso.MakeIso(
|
||||
// func(s string) Email { return Email(s) },
|
||||
// func(e Email) string { return string(e) },
|
||||
// ),
|
||||
// )),
|
||||
// codec.Pipe(codec.FromIso[ValidatedEmail, Email](emailIso)), // Add Email → ValidatedEmail iso
|
||||
// )
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Newtype patterns: Wrapping primitive types for type safety
|
||||
// - Unit conversions: Temperature, distance, time, etc.
|
||||
// - Format transformations: Between equivalent representations
|
||||
// - Type aliasing: Creating semantic types from base types
|
||||
// - Codec composition: Building complex codecs from simple isomorphisms
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Isomorphisms must satisfy the round-trip laws:
|
||||
// - iso.ReverseGet(iso.Get(i)) == i
|
||||
// - iso.Get(iso.ReverseGet(a)) == a
|
||||
// - Validation always succeeds since isomorphisms are lossless
|
||||
// - The codec name includes the isomorphism's string representation
|
||||
// - Type checking is performed using the standard Is[A]() function
|
||||
// - This codec is ideal for lossless transformations without validation logic
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - iso.Iso: The isomorphism type used by this function
|
||||
// - iso.MakeIso: Constructor for creating isomorphisms
|
||||
// - Pipe: For composing this codec with other codecs
|
||||
// - MakeType: For creating codecs with custom validation logic
|
||||
func FromIso[A, I any](iso Iso[I, A]) Type[A, I, I] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("FromIso[%s]", iso),
|
||||
Is[A](),
|
||||
F.Flow2(
|
||||
iso.Get,
|
||||
decode.Of[Context],
|
||||
),
|
||||
iso.ReverseGet,
|
||||
)
|
||||
}
|
||||
504
v2/optics/codec/iso_test.go
Normal file
504
v2/optics/codec/iso_test.go
Normal file
@@ -0,0 +1,504 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package codec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/iso"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for newtype pattern
|
||||
type UserId int
|
||||
type Email string
|
||||
type Celsius float64
|
||||
type Fahrenheit float64
|
||||
|
||||
func TestFromIso_Success(t *testing.T) {
|
||||
t.Run("decodes using iso.Get", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(42))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("encodes using iso.ReverseGet", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(42)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, UserId(42), encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip preserves value", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
original := UserId(123)
|
||||
|
||||
// Act
|
||||
decoded := codec.Decode(original)
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(decoded))
|
||||
roundTrip := either.Fold[validation.Errors, int, UserId](
|
||||
func(validation.Errors) UserId { return UserId(0) },
|
||||
codec.Encode,
|
||||
)(decoded)
|
||||
assert.Equal(t, original, roundTrip)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_StringTypes(t *testing.T) {
|
||||
t.Run("handles string newtype", func(t *testing.T) {
|
||||
// Arrange
|
||||
emailIso := iso.MakeIso(
|
||||
func(e Email) string { return string(e) },
|
||||
func(s string) Email { return Email(s) },
|
||||
)
|
||||
codec := FromIso[string, Email](emailIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Email("user@example.com"))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success("user@example.com"), result)
|
||||
})
|
||||
|
||||
t.Run("encodes string newtype", func(t *testing.T) {
|
||||
// Arrange
|
||||
emailIso := iso.MakeIso(
|
||||
func(e Email) string { return string(e) },
|
||||
func(s string) Email { return Email(s) },
|
||||
)
|
||||
codec := FromIso[string, Email](emailIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode("admin@example.com")
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, Email("admin@example.com"), encoded)
|
||||
})
|
||||
|
||||
t.Run("handles empty string", func(t *testing.T) {
|
||||
// Arrange
|
||||
emailIso := iso.MakeIso(
|
||||
func(e Email) string { return string(e) },
|
||||
func(s string) Email { return Email(s) },
|
||||
)
|
||||
codec := FromIso[string, Email](emailIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Email(""))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_NumericConversions(t *testing.T) {
|
||||
t.Run("converts Celsius to Fahrenheit", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Celsius(0))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(Fahrenheit(32)), result)
|
||||
})
|
||||
|
||||
t.Run("converts Fahrenheit to Celsius", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(Fahrenheit(68))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, Celsius(20), encoded)
|
||||
})
|
||||
|
||||
t.Run("handles negative temperatures", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Celsius(-40))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(Fahrenheit(-40)), result)
|
||||
})
|
||||
|
||||
t.Run("temperature round-trip", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
original := Celsius(25)
|
||||
|
||||
// Act
|
||||
decoded := codec.Decode(original)
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(decoded))
|
||||
roundTrip := either.Fold[validation.Errors, Fahrenheit, Celsius](
|
||||
func(validation.Errors) Celsius { return Celsius(0) },
|
||||
codec.Encode,
|
||||
)(decoded)
|
||||
// Allow small floating point error
|
||||
diff := float64(original - roundTrip)
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
assert.True(t, diff < 0.0001)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero values", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(0))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(0), result)
|
||||
})
|
||||
|
||||
t.Run("handles negative values", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(-1))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(-1), result)
|
||||
})
|
||||
|
||||
t.Run("handles large values", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(999999999))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(999999999), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_TypeChecking(t *testing.T) {
|
||||
t.Run("Is checks target type", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
isResult := codec.Is(42)
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(isResult))
|
||||
})
|
||||
|
||||
t.Run("Is rejects wrong type", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
isResult := codec.Is("not an int")
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsLeft(isResult))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Name(t *testing.T) {
|
||||
t.Run("includes iso in name", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
name := codec.Name()
|
||||
|
||||
// Assert
|
||||
assert.True(t, len(name) > 0)
|
||||
assert.True(t, name[:7] == "FromIso")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Composition(t *testing.T) {
|
||||
t.Run("composes with Pipe", func(t *testing.T) {
|
||||
// Arrange
|
||||
type PositiveInt int
|
||||
|
||||
// First iso: UserId -> int
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
|
||||
// Second iso: int -> PositiveInt (no validation, just type conversion)
|
||||
positiveIso := iso.MakeIso(
|
||||
func(i int) PositiveInt { return PositiveInt(i) },
|
||||
func(p PositiveInt) int { return int(p) },
|
||||
)
|
||||
|
||||
// Compose codecs
|
||||
codec := F.Pipe1(
|
||||
FromIso[int, UserId](userIdIso),
|
||||
Pipe[UserId, UserId](FromIso[PositiveInt, int](positiveIso)),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(42))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Of(PositiveInt(42)), result)
|
||||
})
|
||||
|
||||
t.Run("composed codec encodes correctly", func(t *testing.T) {
|
||||
// Arrange
|
||||
type PositiveInt int
|
||||
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
|
||||
positiveIso := iso.MakeIso(
|
||||
func(i int) PositiveInt { return PositiveInt(i) },
|
||||
func(p PositiveInt) int { return int(p) },
|
||||
)
|
||||
|
||||
codec := F.Pipe1(
|
||||
FromIso[int, UserId](userIdIso),
|
||||
Pipe[UserId, UserId](FromIso[PositiveInt, int](positiveIso)),
|
||||
)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(PositiveInt(42))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, UserId(42), encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Integration(t *testing.T) {
|
||||
t.Run("works with Array codec", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
userIdCodec := FromIso[int, UserId](userIdIso)
|
||||
arrayCodec := TranscodeArray(userIdCodec)
|
||||
|
||||
// Act
|
||||
result := arrayCodec.Decode([]UserId{UserId(1), UserId(2), UserId(3)})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success([]int{1, 2, 3}), result)
|
||||
})
|
||||
|
||||
t.Run("encodes array correctly", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
userIdCodec := FromIso[int, UserId](userIdIso)
|
||||
arrayCodec := TranscodeArray(userIdCodec)
|
||||
|
||||
// Act
|
||||
encoded := arrayCodec.Encode([]int{1, 2, 3})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []UserId{UserId(1), UserId(2), UserId(3)}, encoded)
|
||||
})
|
||||
|
||||
t.Run("handles empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
userIdCodec := FromIso[int, UserId](userIdIso)
|
||||
arrayCodec := TranscodeArray(userIdCodec)
|
||||
|
||||
// Act
|
||||
result := arrayCodec.Decode([]UserId{})
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(result))
|
||||
decoded := either.Fold[validation.Errors, []int, []int](
|
||||
func(validation.Errors) []int { return nil },
|
||||
func(arr []int) []int { return arr },
|
||||
)(result)
|
||||
assert.Equal(t, 0, len(decoded))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_ComplexTypes(t *testing.T) {
|
||||
t.Run("handles struct wrapping", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Wrapper struct{ Value int }
|
||||
|
||||
wrapperIso := iso.MakeIso(
|
||||
func(w Wrapper) int { return w.Value },
|
||||
func(i int) Wrapper { return Wrapper{Value: i} },
|
||||
)
|
||||
codec := FromIso[int, Wrapper](wrapperIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Wrapper{Value: 42})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("encodes struct wrapping", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Wrapper struct{ Value int }
|
||||
|
||||
wrapperIso := iso.MakeIso(
|
||||
func(w Wrapper) int { return w.Value },
|
||||
func(i int) Wrapper { return Wrapper{Value: i} },
|
||||
)
|
||||
codec := FromIso[int, Wrapper](wrapperIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(42)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, Wrapper{Value: 42}, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_AsDecoder(t *testing.T) {
|
||||
t.Run("returns decoder interface", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
decoder := codec.AsDecoder()
|
||||
|
||||
// Assert
|
||||
result := decoder.Decode(UserId(42))
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_AsEncoder(t *testing.T) {
|
||||
t.Run("returns encoder interface", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
encoder := codec.AsEncoder()
|
||||
|
||||
// Assert
|
||||
encoded := encoder.Encode(42)
|
||||
assert.Equal(t, UserId(42), encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Validate(t *testing.T) {
|
||||
t.Run("validate method works correctly", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
validateFn := codec.Validate(UserId(42))
|
||||
result := validateFn([]validation.ContextEntry{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/decoder"
|
||||
"github.com/IBM/fp-go/v2/optics/encoder"
|
||||
"github.com/IBM/fp-go/v2/optics/iso"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
@@ -494,4 +495,7 @@ type (
|
||||
// - function.VOID: The single value of type Void
|
||||
// - Empty: Codec function that uses Void for unit types
|
||||
Void = function.Void
|
||||
|
||||
// Iso represents an isomorphism - a bidirectional transformation between two types.
|
||||
Iso[S, A any] = iso.Iso[S, A]
|
||||
)
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
package result
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
@@ -155,3 +157,84 @@ func CompactArrayG[A1 ~[]Result[A], A2 ~[]A, A any](fa A1) A2 {
|
||||
func CompactArray[A any](fa []Result[A]) []A {
|
||||
return either.CompactArray(fa)
|
||||
}
|
||||
|
||||
// TraverseSeq transforms an iterator by applying a function that returns a Result to each element.
|
||||
// If any element produces a Left, the entire result is that Left (short-circuits).
|
||||
// Otherwise, returns Right containing an iterator of all Right values.
|
||||
//
|
||||
// The function eagerly evaluates all elements in the input iterator to detect any Left values,
|
||||
// then returns an iterator over the collected Right values. This is necessary because Result
|
||||
// represents computations that can fail, and we need to know if any element failed before
|
||||
// producing the result iterator.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that transforms each element into a Result
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A function that takes an iterator of A and returns Result containing an iterator of B
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// parse := func(s string) result.Result[int] {
|
||||
// v, err := strconv.Atoi(s)
|
||||
// return result.TryCatchError(v, err)
|
||||
// }
|
||||
// input := slices.Values([]string{"1", "2", "3"})
|
||||
// result := result.TraverseSeq(parse)(input)
|
||||
// // result is Right(iterator over [1, 2, 3])
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - TraverseArray: For slice-based traversal
|
||||
// - SequenceSeq: For sequencing iterators of Result values
|
||||
//
|
||||
//go:inline
|
||||
func TraverseSeq[A, B any](f Kleisli[A, B]) Kleisli[iter.Seq[A], iter.Seq[B]] {
|
||||
return either.TraverseSeq(f)
|
||||
}
|
||||
|
||||
// SequenceSeq converts an iterator of Result into a Result of iterator.
|
||||
// If any element is Left, returns that Left (short-circuits).
|
||||
// Otherwise, returns Right containing an iterator of all the Right values.
|
||||
//
|
||||
// This function eagerly evaluates all Result values in the input iterator to detect
|
||||
// any Left values, then returns an iterator over the collected Right values.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The value type for Right values
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - ma: An iterator of Result values
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Result containing an iterator of Right values, or the first Left encountered
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// results := slices.Values([]result.Result[int]{
|
||||
// result.Of(1),
|
||||
// result.Of(2),
|
||||
// result.Of(3),
|
||||
// })
|
||||
// result := result.SequenceSeq(results)
|
||||
// // result is Right(iterator over [1, 2, 3])
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - SequenceArray: For slice-based sequencing
|
||||
// - TraverseSeq: For transforming and sequencing in one step
|
||||
//
|
||||
//go:inline
|
||||
func SequenceSeq[A any](ma iter.Seq[Result[A]]) Result[iter.Seq[A]] {
|
||||
return either.SequenceSeq(ma)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ package result
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
TST "github.com/IBM/fp-go/v2/internal/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -15,13 +19,10 @@ func TestCompactArray(t *testing.T) {
|
||||
Left[string](errors.New("err")),
|
||||
Of("ok"),
|
||||
}
|
||||
|
||||
res := CompactArray(ar)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, 2, len(CompactArray(ar)))
|
||||
}
|
||||
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
|
||||
s := TST.SequenceArrayTest(
|
||||
FromStrictEquals[bool](),
|
||||
Pointed[string](),
|
||||
@@ -29,14 +30,12 @@ func TestSequenceArray(t *testing.T) {
|
||||
Functor[[]string, bool](),
|
||||
SequenceArray[string],
|
||||
)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
t.Run(fmt.Sprintf("TestSequenceArray %d", i), s(i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceArrayError(t *testing.T) {
|
||||
|
||||
s := TST.SequenceArrayErrorTest(
|
||||
FromStrictEquals[bool](),
|
||||
Left[string],
|
||||
@@ -46,6 +45,237 @@ func TestSequenceArrayError(t *testing.T) {
|
||||
Functor[[]string, bool](),
|
||||
SequenceArray[string],
|
||||
)
|
||||
// run across four bits
|
||||
s(4)(t)
|
||||
}
|
||||
|
||||
func TestTraverseSeq_Success(t *testing.T) {
|
||||
parse := func(s string) Result[int] {
|
||||
v, err := strconv.Atoi(s)
|
||||
return TryCatchError(v, err)
|
||||
}
|
||||
|
||||
collectInts := func(result Result[iter.Seq[int]]) []int {
|
||||
return F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("transforms all elements successfully", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"1", "2", "3"}))
|
||||
assert.Equal(t, []int{1, 2, 3}, collectInts(result))
|
||||
})
|
||||
|
||||
t.Run("works with empty iterator", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{}))
|
||||
assert.Empty(t, collectInts(result))
|
||||
})
|
||||
|
||||
t.Run("works with single element", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"42"}))
|
||||
assert.Equal(t, []int{42}, collectInts(result))
|
||||
})
|
||||
|
||||
t.Run("preserves order of elements", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"10", "20", "30", "40", "50"}))
|
||||
assert.Equal(t, []int{10, 20, 30, 40, 50}, collectInts(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseSeq_Failure(t *testing.T) {
|
||||
parse := func(s string) Result[int] {
|
||||
v, err := strconv.Atoi(s)
|
||||
return TryCatchError(v, err)
|
||||
}
|
||||
|
||||
extractErr := func(result Result[iter.Seq[int]]) error {
|
||||
return F.Pipe1(result, Fold(
|
||||
F.Identity[error],
|
||||
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("short-circuits on first Left", func(t *testing.T) {
|
||||
err := extractErr(TraverseSeq(parse)(slices.Values([]string{"1", "invalid", "3"})))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid syntax")
|
||||
})
|
||||
|
||||
t.Run("returns first error when multiple failures exist", func(t *testing.T) {
|
||||
err := extractErr(TraverseSeq(parse)(slices.Values([]string{"1", "bad1", "bad2"})))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "bad1")
|
||||
})
|
||||
|
||||
t.Run("handles custom error types", func(t *testing.T) {
|
||||
customErr := errors.New("custom validation error")
|
||||
validate := func(n int) Result[int] {
|
||||
if n == 2 {
|
||||
return Left[int](customErr)
|
||||
}
|
||||
return Of(n * 10)
|
||||
}
|
||||
err := extractErr(TraverseSeq(validate)(slices.Values([]int{1, 2, 3})))
|
||||
assert.Equal(t, customErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseSeq_EdgeCases(t *testing.T) {
|
||||
t.Run("handles complex transformations", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
transform := func(id int) Result[User] {
|
||||
return Of(User{ID: id, Name: fmt.Sprintf("User%d", id)})
|
||||
}
|
||||
|
||||
result := TraverseSeq(transform)(slices.Values([]int{1, 2, 3}))
|
||||
collected := F.Pipe1(result, Fold(
|
||||
func(e error) []User { t.Fatal(e); return nil },
|
||||
slices.Collect[User],
|
||||
))
|
||||
|
||||
assert.Equal(t, []User{
|
||||
{ID: 1, Name: "User1"},
|
||||
{ID: 2, Name: "User2"},
|
||||
{ID: 3, Name: "User3"},
|
||||
}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with identity transformation", func(t *testing.T) {
|
||||
input := slices.Values([]Result[int]{Of(1), Of(2), Of(3)})
|
||||
|
||||
result := TraverseSeq(F.Identity[Result[int]])(input)
|
||||
collected := F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceSeq_Success(t *testing.T) {
|
||||
collectInts := func(result Result[iter.Seq[int]]) []int {
|
||||
return F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("sequences multiple Right values", func(t *testing.T) {
|
||||
input := slices.Values([]Result[int]{Of(1), Of(2), Of(3)})
|
||||
assert.Equal(t, []int{1, 2, 3}, collectInts(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("works with empty iterator", func(t *testing.T) {
|
||||
input := slices.Values([]Result[string]{})
|
||||
result := F.Pipe1(SequenceSeq(input), Fold(
|
||||
func(e error) []string { t.Fatal(e); return nil },
|
||||
slices.Collect[string],
|
||||
))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("works with single Right value", func(t *testing.T) {
|
||||
input := slices.Values([]Result[string]{Of("hello")})
|
||||
result := F.Pipe1(SequenceSeq(input), Fold(
|
||||
func(e error) []string { t.Fatal(e); return nil },
|
||||
slices.Collect[string],
|
||||
))
|
||||
assert.Equal(t, []string{"hello"}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves order of results", func(t *testing.T) {
|
||||
input := slices.Values([]Result[int]{Of(5), Of(4), Of(3), Of(2), Of(1)})
|
||||
assert.Equal(t, []int{5, 4, 3, 2, 1}, collectInts(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("works with complex types", func(t *testing.T) {
|
||||
type Item struct {
|
||||
Value int
|
||||
Label string
|
||||
}
|
||||
|
||||
input := slices.Values([]Result[Item]{
|
||||
Of(Item{Value: 1, Label: "first"}),
|
||||
Of(Item{Value: 2, Label: "second"}),
|
||||
Of(Item{Value: 3, Label: "third"}),
|
||||
})
|
||||
|
||||
collected := F.Pipe1(SequenceSeq(input), Fold(
|
||||
func(e error) []Item { t.Fatal(e); return nil },
|
||||
slices.Collect[Item],
|
||||
))
|
||||
|
||||
assert.Equal(t, []Item{
|
||||
{Value: 1, Label: "first"},
|
||||
{Value: 2, Label: "second"},
|
||||
{Value: 3, Label: "third"},
|
||||
}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceSeq_Failure(t *testing.T) {
|
||||
extractErr := func(result Result[iter.Seq[int]]) error {
|
||||
return F.Pipe1(result, Fold(
|
||||
F.Identity[error],
|
||||
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("short-circuits on first Left", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
input := slices.Values([]Result[int]{Of(1), Left[int](testErr), Of(3)})
|
||||
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("returns first error when multiple Left values exist", func(t *testing.T) {
|
||||
err1 := errors.New("error 1")
|
||||
err2 := errors.New("error 2")
|
||||
input := slices.Values([]Result[int]{Of(1), Left[int](err1), Left[int](err2)})
|
||||
assert.Equal(t, err1, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("handles Left at the beginning", func(t *testing.T) {
|
||||
testErr := errors.New("first error")
|
||||
input := slices.Values([]Result[int]{Left[int](testErr), Of(2), Of(3)})
|
||||
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("handles Left at the end", func(t *testing.T) {
|
||||
testErr := errors.New("last error")
|
||||
input := slices.Values([]Result[int]{Of(1), Of(2), Left[int](testErr)})
|
||||
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceSeq_Integration(t *testing.T) {
|
||||
t.Run("integrates with TraverseSeq", func(t *testing.T) {
|
||||
parse := func(s string) Result[int] {
|
||||
v, err := strconv.Atoi(s)
|
||||
return TryCatchError(v, err)
|
||||
}
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"1", "2", "3"}))
|
||||
assert.True(t, IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("SequenceSeq is equivalent to TraverseSeq with Identity", func(t *testing.T) {
|
||||
mkInput := func() []Result[int] {
|
||||
return []Result[int]{Of(10), Of(20), Of(30)}
|
||||
}
|
||||
|
||||
collected1 := F.Pipe1(SequenceSeq(slices.Values(mkInput())), Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
collected2 := F.Pipe1(TraverseSeq(F.Identity[Result[int]])(slices.Values(mkInput())), Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
|
||||
assert.Equal(t, collected1, collected2)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user