mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-29 10:36:04 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df07599a9e | ||
|
|
30ad0e4dd8 | ||
|
|
2374d7f1e4 | ||
|
|
eafc008798 | ||
|
|
46bf065e34 | ||
|
|
b4e303423b | ||
|
|
7afc098f58 | ||
|
|
617e43de19 | ||
|
|
0f7a6c0589 | ||
|
|
e7f78e1a33 | ||
|
|
6505ab1791 | ||
|
|
cfa48985ec |
14
.github/workflows/build.yml
vendored
14
.github/workflows/build.yml
vendored
@@ -28,11 +28,11 @@ jobs:
|
||||
fail-fast: false # Continue with other versions if one fails
|
||||
steps:
|
||||
# full checkout for semantic-release
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true # Enable Go module caching
|
||||
@@ -66,11 +66,11 @@ jobs:
|
||||
matrix:
|
||||
go-version: ['1.24.x', '1.25.x']
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true # Enable Go module caching
|
||||
@@ -126,17 +126,17 @@ jobs:
|
||||
steps:
|
||||
# full checkout for semantic-release
|
||||
- name: Full checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.LATEST_GO_VERSION }}
|
||||
cache: true # Enable Go module caching
|
||||
|
||||
16
go.sum
16
go.sum
@@ -1,7 +1,3 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -10,20 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
|
||||
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
|
||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"matchDepTypes": [
|
||||
"golang"
|
||||
],
|
||||
"enabled": false
|
||||
"enabled": false,
|
||||
"description": "Disable updates to the go directive in go.mod files - the directive identifies the minimum compatible Go version and should stay as small as possible for maximum compatibility"
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": [
|
||||
|
||||
@@ -465,7 +465,7 @@ func process() IOResult[string] {
|
||||
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
|
||||
- **Array** - Functional array operations
|
||||
- **Record** - Functional record/map operations
|
||||
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
|
||||
- **[Optics](./optics/README.md)** - Lens, Prism, Optional, and Traversal for immutable updates
|
||||
|
||||
#### Idiomatic Packages (Tuple-based, High Performance)
|
||||
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples
|
||||
|
||||
@@ -190,6 +190,11 @@ func MonadReduce[A, B any](fa []A, f func(B, A) B, initial B) B {
|
||||
return G.MonadReduce(fa, f, initial)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadReduceWithIndex[A, B any](fa []A, f func(int, B, A) B, initial B) B {
|
||||
return G.MonadReduceWithIndex(fa, f, initial)
|
||||
}
|
||||
|
||||
// Reduce folds an array from left to right, applying a function to accumulate a result.
|
||||
//
|
||||
// Example:
|
||||
|
||||
@@ -764,14 +764,14 @@ func TestFoldMap(t *testing.T) {
|
||||
t.Run("FoldMap with sum semigroup", func(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
arr := From(1, 2, 3, 4)
|
||||
result := FoldMap[int, int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
|
||||
result := FoldMap[int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
|
||||
assert.Equal(t, 20, result) // (1*2) + (2*2) + (3*2) + (4*2) = 20
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation", func(t *testing.T) {
|
||||
concatSemigroup := STR.Semigroup
|
||||
arr := From(1, 2, 3)
|
||||
result := FoldMap[int, string](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
|
||||
result := FoldMap[int](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
|
||||
assert.Equal(t, "123", result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateTraverseTuple(f *os.File, i int) {
|
||||
@@ -422,10 +423,10 @@ func ApplyCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateApplyHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func createCombinations(n int, all, prev []int) [][]int {
|
||||
@@ -284,10 +285,10 @@ func BindCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateBindHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func Commands() []*C.Command {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"strings"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// Deprecated:
|
||||
@@ -261,10 +262,10 @@ func ContextReaderIOEitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateContextReaderIOEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateMakeProvider(f *os.File, i int) {
|
||||
@@ -221,10 +222,10 @@ func DICommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateDIHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func eitherHKT(typeE string) func(typeA string) string {
|
||||
@@ -190,10 +191,10 @@ func EitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func identityHKT(typeA string) string {
|
||||
@@ -93,10 +94,10 @@ func IdentityCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIdentityHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func nonGenericIO(param string) string {
|
||||
@@ -102,10 +103,10 @@ func IOCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIOHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// [GA ~func() ET.Either[E, A], GB ~func() ET.Either[E, B], GTAB ~func() ET.Either[E, T.Tuple2[A, B]], E, A, B any](a GA, b GB) GTAB {
|
||||
@@ -273,10 +274,10 @@ func IOEitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIOEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func nonGenericIOOption(param string) string {
|
||||
@@ -107,10 +108,10 @@ func IOOptionCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIOOptionHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
@@ -28,7 +29,7 @@ import (
|
||||
"text/template"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -535,9 +536,9 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
|
||||
}
|
||||
|
||||
for _, name := range field.Names {
|
||||
// Only export lenses for exported fields
|
||||
if name.IsExported() {
|
||||
fieldTypeName := getTypeName(field.Type)
|
||||
// Generate lenses for both exported and unexported fields
|
||||
fieldTypeName := getTypeName(field.Type)
|
||||
if true { // Keep the block structure for minimal changes
|
||||
isOptional := false
|
||||
baseType := fieldTypeName
|
||||
|
||||
@@ -697,9 +698,9 @@ func parseFile(filename string) ([]structInfo, string, error) {
|
||||
continue
|
||||
}
|
||||
for _, name := range field.Names {
|
||||
// Only export lenses for exported fields
|
||||
if name.IsExported() {
|
||||
typeName := getTypeName(field.Type)
|
||||
// Generate lenses for both exported and unexported fields
|
||||
typeName := getTypeName(field.Type)
|
||||
if true { // Keep the block structure for minimal changes
|
||||
isOptional := false
|
||||
baseType := typeName
|
||||
isComparable := false
|
||||
@@ -934,12 +935,12 @@ func LensCommand() *C.Command {
|
||||
flagVerbose,
|
||||
flagIncludeTestFiles,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateLensHelpers(
|
||||
ctx.String(keyLensDir),
|
||||
ctx.String(keyFilename),
|
||||
ctx.Bool(keyVerbose),
|
||||
ctx.Bool(keyIncludeTestFile),
|
||||
cmd.String(keyLensDir),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Bool(keyVerbose),
|
||||
cmd.Bool(keyIncludeTestFile),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1086,3 +1086,255 @@ type ComparableBox[T comparable] struct {
|
||||
// Verify that MakeLensRef is NOT used (since both fields are comparable)
|
||||
assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
|
||||
}
|
||||
|
||||
func TestParseFileWithUnexportedFields(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type Config struct {
|
||||
PublicName string
|
||||
privateName string
|
||||
PublicValue int
|
||||
privateValue *int
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 1)
|
||||
|
||||
// Check Config struct
|
||||
config := structs[0]
|
||||
assert.Equal(t, "Config", config.Name)
|
||||
assert.Len(t, config.Fields, 4, "Should include both exported and unexported fields")
|
||||
|
||||
// Check exported field
|
||||
assert.Equal(t, "PublicName", config.Fields[0].Name)
|
||||
assert.Equal(t, "string", config.Fields[0].TypeName)
|
||||
assert.False(t, config.Fields[0].IsOptional)
|
||||
|
||||
// Check unexported field
|
||||
assert.Equal(t, "privateName", config.Fields[1].Name)
|
||||
assert.Equal(t, "string", config.Fields[1].TypeName)
|
||||
assert.False(t, config.Fields[1].IsOptional)
|
||||
|
||||
// Check exported int field
|
||||
assert.Equal(t, "PublicValue", config.Fields[2].Name)
|
||||
assert.Equal(t, "int", config.Fields[2].TypeName)
|
||||
assert.False(t, config.Fields[2].IsOptional)
|
||||
|
||||
// Check unexported pointer field
|
||||
assert.Equal(t, "privateValue", config.Fields[3].Name)
|
||||
assert.Equal(t, "*int", config.Fields[3].TypeName)
|
||||
assert.True(t, config.Fields[3].IsOptional)
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersWithUnexportedFields(t *testing.T) {
|
||||
// Create a temporary directory with test files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type MixedStruct struct {
|
||||
PublicField string
|
||||
privateField int
|
||||
OptionalPrivate *string
|
||||
}
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen_lens.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
genPath := filepath.Join(tmpDir, outputFile)
|
||||
_, err = os.Stat(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read and verify the generated content
|
||||
content, err := os.ReadFile(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// Check for expected content
|
||||
assert.Contains(t, contentStr, "package testpkg")
|
||||
assert.Contains(t, contentStr, "MixedStructLenses")
|
||||
assert.Contains(t, contentStr, "MakeMixedStructLenses")
|
||||
|
||||
// Check that lenses are generated for all fields (exported and unexported)
|
||||
assert.Contains(t, contentStr, "PublicField __lens.Lens[MixedStruct, string]")
|
||||
assert.Contains(t, contentStr, "privateField __lens.Lens[MixedStruct, int]")
|
||||
assert.Contains(t, contentStr, "OptionalPrivate __lens.Lens[MixedStruct, *string]")
|
||||
|
||||
// Check lens constructors
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct) string { return s.PublicField }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct) int { return s.privateField }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct) *string { return s.OptionalPrivate }")
|
||||
|
||||
// Check setters
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct, v string) MixedStruct { s.PublicField = v; return s }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct, v int) MixedStruct { s.privateField = v; return s }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct, v *string) MixedStruct { s.OptionalPrivate = v; return s }")
|
||||
}
|
||||
|
||||
func TestParseFileWithOnlyUnexportedFields(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type PrivateConfig struct {
|
||||
name string
|
||||
value int
|
||||
enabled bool
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 1)
|
||||
|
||||
// Check PrivateConfig struct
|
||||
config := structs[0]
|
||||
assert.Equal(t, "PrivateConfig", config.Name)
|
||||
assert.Len(t, config.Fields, 3, "Should include all unexported fields")
|
||||
|
||||
// Check all fields are unexported
|
||||
assert.Equal(t, "name", config.Fields[0].Name)
|
||||
assert.Equal(t, "value", config.Fields[1].Name)
|
||||
assert.Equal(t, "enabled", config.Fields[2].Name)
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersWithUnexportedEmbeddedFields(t *testing.T) {
|
||||
// Create a temporary directory with test files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
type BaseConfig struct {
|
||||
publicBase string
|
||||
privateBase int
|
||||
}
|
||||
|
||||
// fp-go:Lens
|
||||
type ExtendedConfig struct {
|
||||
BaseConfig
|
||||
PublicField string
|
||||
privateField bool
|
||||
}
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen_lens.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
genPath := filepath.Join(tmpDir, outputFile)
|
||||
_, err = os.Stat(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read and verify the generated content
|
||||
content, err := os.ReadFile(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// Check for expected content
|
||||
assert.Contains(t, contentStr, "package testpkg")
|
||||
assert.Contains(t, contentStr, "ExtendedConfigLenses")
|
||||
|
||||
// Check that lenses are generated for embedded unexported fields
|
||||
assert.Contains(t, contentStr, "publicBase __lens.Lens[ExtendedConfig, string]")
|
||||
assert.Contains(t, contentStr, "privateBase __lens.Lens[ExtendedConfig, int]")
|
||||
|
||||
// Check that lenses are generated for direct fields (both exported and unexported)
|
||||
assert.Contains(t, contentStr, "PublicField __lens.Lens[ExtendedConfig, string]")
|
||||
assert.Contains(t, contentStr, "privateField __lens.Lens[ExtendedConfig, bool]")
|
||||
}
|
||||
|
||||
func TestParseFileWithMixedFieldVisibility(t *testing.T) {
|
||||
// Create a temporary test file with various field visibility patterns
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type ComplexStruct struct {
|
||||
// Exported fields
|
||||
Name string
|
||||
Age int
|
||||
Email *string
|
||||
|
||||
// Unexported fields
|
||||
password string
|
||||
secretKey []byte
|
||||
internalID *int
|
||||
|
||||
// Mixed with tags
|
||||
PublicWithTag string ` + "`json:\"public,omitempty\"`" + `
|
||||
privateWithTag int ` + "`json:\"private,omitempty\"`" + `
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 1)
|
||||
|
||||
// Check ComplexStruct
|
||||
complex := structs[0]
|
||||
assert.Equal(t, "ComplexStruct", complex.Name)
|
||||
assert.Len(t, complex.Fields, 8, "Should include all fields regardless of visibility")
|
||||
|
||||
// Verify field names and types
|
||||
fieldNames := []string{"Name", "Age", "Email", "password", "secretKey", "internalID", "PublicWithTag", "privateWithTag"}
|
||||
for i, expectedName := range fieldNames {
|
||||
assert.Equal(t, expectedName, complex.Fields[i].Name, "Field %d should be %s", i, expectedName)
|
||||
}
|
||||
|
||||
// Check optional fields
|
||||
assert.False(t, complex.Fields[0].IsOptional, "Name should not be optional")
|
||||
assert.True(t, complex.Fields[2].IsOptional, "Email (pointer) should be optional")
|
||||
assert.True(t, complex.Fields[5].IsOptional, "internalID (pointer) should be optional")
|
||||
assert.True(t, complex.Fields[6].IsOptional, "PublicWithTag (with omitempty) should be optional")
|
||||
assert.True(t, complex.Fields[7].IsOptional, "privateWithTag (with omitempty) should be optional")
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func optionHKT(typeA string) string {
|
||||
@@ -200,10 +201,10 @@ func OptionCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateOptionHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateUnsliced(f *os.File, i int) {
|
||||
@@ -423,10 +424,10 @@ func PipeCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generatePipeHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateReaderFrom(f, fg *os.File, i int) {
|
||||
@@ -154,10 +155,10 @@ func ReaderCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateReaderHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateReaderIOEitherFrom(f, fg *os.File, i int) {
|
||||
@@ -284,10 +285,10 @@ func ReaderIOEitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateReaderIOEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func writeTupleType(f *os.File, symbol string, i int) {
|
||||
@@ -615,10 +616,10 @@ func TupleCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateTupleHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -177,3 +177,255 @@ func Local[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose is an alias for Local that emphasizes the composition aspect of consumer transformation.
|
||||
// It composes a preprocessing function with a consumer, creating a new consumer that applies
|
||||
// the function before consuming the value.
|
||||
//
|
||||
// This function is semantically identical to Local but uses terminology that may be more familiar
|
||||
// to developers coming from functional programming backgrounds where "compose" is a common operation.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// The name "Compose" highlights that we're composing two operations:
|
||||
// 1. The transformation function f: R2 -> R1
|
||||
// 2. The consumer c: R1 -> ()
|
||||
//
|
||||
// Result: A composed consumer: R2 -> ()
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The input type of the original Consumer (what it expects)
|
||||
// - R2: The input type of the new Consumer (what you have)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that converts R2 to R1 (preprocessing function)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Consumer[R1] into Consumer[R2]
|
||||
//
|
||||
// Example - Basic composition:
|
||||
//
|
||||
// // Consumer that logs integers
|
||||
// logInt := func(x int) {
|
||||
// fmt.Printf("Value: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Compose with a string-to-int parser
|
||||
// parseToInt := func(s string) int {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// return n
|
||||
// }
|
||||
//
|
||||
// logString := consumer.Compose(parseToInt)(logInt)
|
||||
// logString("42") // Logs: "Value: 42"
|
||||
//
|
||||
// Example - Composing multiple transformations:
|
||||
//
|
||||
// type Data struct {
|
||||
// Value string
|
||||
// }
|
||||
//
|
||||
// type Wrapper struct {
|
||||
// Data Data
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs strings
|
||||
// logString := func(s string) {
|
||||
// fmt.Println(s)
|
||||
// }
|
||||
//
|
||||
// // Compose transformations step by step
|
||||
// extractData := func(w Wrapper) Data { return w.Data }
|
||||
// extractValue := func(d Data) string { return d.Value }
|
||||
//
|
||||
// logData := consumer.Compose(extractValue)(logString)
|
||||
// logWrapper := consumer.Compose(extractData)(logData)
|
||||
//
|
||||
// logWrapper(Wrapper{Data: Data{Value: "Hello"}}) // Logs: "Hello"
|
||||
//
|
||||
// Example - Function composition style:
|
||||
//
|
||||
// // Compose is particularly useful when thinking in terms of function composition
|
||||
// type Request struct {
|
||||
// Body []byte
|
||||
// }
|
||||
//
|
||||
// // Consumer that processes strings
|
||||
// processString := func(s string) {
|
||||
// fmt.Printf("Processing: %s\n", s)
|
||||
// }
|
||||
//
|
||||
// // Compose byte-to-string conversion with processing
|
||||
// bytesToString := func(b []byte) string {
|
||||
// return string(b)
|
||||
// }
|
||||
// extractBody := func(r Request) []byte {
|
||||
// return r.Body
|
||||
// }
|
||||
//
|
||||
// // Chain compositions
|
||||
// processBytes := consumer.Compose(bytesToString)(processString)
|
||||
// processRequest := consumer.Compose(extractBody)(processBytes)
|
||||
//
|
||||
// processRequest(Request{Body: []byte("test")}) // Logs: "Processing: test"
|
||||
//
|
||||
// Relationship to Local:
|
||||
// - Compose and Local are identical in implementation
|
||||
// - Compose emphasizes the functional composition aspect
|
||||
// - Local emphasizes the environment/context transformation aspect
|
||||
// - Use Compose when thinking about function composition
|
||||
// - Use Local when thinking about adapting to different contexts
|
||||
//
|
||||
// Use Cases:
|
||||
// - Building processing pipelines with clear composition semantics
|
||||
// - Adapting consumers in a functional programming style
|
||||
// - Creating reusable consumer transformations
|
||||
// - Chaining multiple preprocessing steps
|
||||
func Compose[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return Local(f)
|
||||
}
|
||||
|
||||
// Contramap is the categorical name for the contravariant functor operation on Consumers.
|
||||
// It transforms a Consumer by preprocessing its input, making it the dual of the covariant
|
||||
// functor's map operation.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#contravariant
|
||||
//
|
||||
// In category theory, a contravariant functor reverses the direction of morphisms.
|
||||
// While a covariant functor maps f: A -> B to map(f): F[A] -> F[B],
|
||||
// a contravariant functor maps f: A -> B to contramap(f): F[B] -> F[A].
|
||||
//
|
||||
// For Consumers:
|
||||
// - Consumer[A] is contravariant in A
|
||||
// - Given f: R2 -> R1, contramap(f) transforms Consumer[R1] to Consumer[R2]
|
||||
// - The direction is reversed: we go from Consumer[R1] to Consumer[R2]
|
||||
//
|
||||
// This is semantically identical to Local and Compose, but uses the standard
|
||||
// categorical terminology that emphasizes the contravariant nature of the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The input type of the original Consumer (what it expects)
|
||||
// - R2: The input type of the new Consumer (what you have)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that converts R2 to R1 (preprocessing function)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Consumer[R1] into Consumer[R2]
|
||||
//
|
||||
// Example - Basic contravariant mapping:
|
||||
//
|
||||
// // Consumer that logs integers
|
||||
// logInt := func(x int) {
|
||||
// fmt.Printf("Value: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Contramap with a string-to-int parser
|
||||
// parseToInt := func(s string) int {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// return n
|
||||
// }
|
||||
//
|
||||
// logString := consumer.Contramap(parseToInt)(logInt)
|
||||
// logString("42") // Logs: "Value: 42"
|
||||
//
|
||||
// Example - Demonstrating contravariance:
|
||||
//
|
||||
// // In covariant functors (like Option, Array), map goes "forward":
|
||||
// // map: (A -> B) -> F[A] -> F[B]
|
||||
// //
|
||||
// // In contravariant functors (like Consumer), contramap goes "backward":
|
||||
// // contramap: (B -> A) -> F[A] -> F[B]
|
||||
//
|
||||
// type Animal struct{ Name string }
|
||||
// type Dog struct{ Animal Animal; Breed string }
|
||||
//
|
||||
// // Consumer for animals
|
||||
// consumeAnimal := func(a Animal) {
|
||||
// fmt.Printf("Animal: %s\n", a.Name)
|
||||
// }
|
||||
//
|
||||
// // Function from Dog to Animal (B -> A)
|
||||
// dogToAnimal := func(d Dog) Animal {
|
||||
// return d.Animal
|
||||
// }
|
||||
//
|
||||
// // Contramap creates Consumer[Dog] from Consumer[Animal]
|
||||
// // Direction is reversed: Consumer[Animal] -> Consumer[Dog]
|
||||
// consumeDog := consumer.Contramap(dogToAnimal)(consumeAnimal)
|
||||
//
|
||||
// consumeDog(Dog{
|
||||
// Animal: Animal{Name: "Buddy"},
|
||||
// Breed: "Golden Retriever",
|
||||
// }) // Logs: "Animal: Buddy"
|
||||
//
|
||||
// Example - Contravariant functor laws:
|
||||
//
|
||||
// // Law 1: Identity
|
||||
// // contramap(identity) = identity
|
||||
// identity := func(x int) int { return x }
|
||||
// consumer1 := consumer.Contramap(identity)(consumeInt)
|
||||
// // consumer1 behaves identically to consumeInt
|
||||
//
|
||||
// // Law 2: Composition
|
||||
// // contramap(f . g) = contramap(g) . contramap(f)
|
||||
// // Note: composition order is reversed compared to covariant map
|
||||
// f := func(s string) int { n, _ := strconv.Atoi(s); return n }
|
||||
// g := func(b bool) string { if b { return "1" } else { return "0" } }
|
||||
//
|
||||
// // These two are equivalent:
|
||||
// consumer2 := consumer.Contramap(func(b bool) int { return f(g(b)) })(consumeInt)
|
||||
// consumer3 := consumer.Contramap(g)(consumer.Contramap(f)(consumeInt))
|
||||
//
|
||||
// Example - Practical use with type hierarchies:
|
||||
//
|
||||
// type Logger interface {
|
||||
// Log(string)
|
||||
// }
|
||||
//
|
||||
// type Message struct {
|
||||
// Text string
|
||||
// Timestamp time.Time
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs strings
|
||||
// logString := func(s string) {
|
||||
// fmt.Println(s)
|
||||
// }
|
||||
//
|
||||
// // Contramap to handle Message types
|
||||
// extractText := func(m Message) string {
|
||||
// return fmt.Sprintf("[%s] %s", m.Timestamp.Format(time.RFC3339), m.Text)
|
||||
// }
|
||||
//
|
||||
// logMessage := consumer.Contramap(extractText)(logString)
|
||||
// logMessage(Message{
|
||||
// Text: "Hello",
|
||||
// Timestamp: time.Now(),
|
||||
// }) // Logs: "[2024-01-20T10:00:00Z] Hello"
|
||||
//
|
||||
// Relationship to Local and Compose:
|
||||
// - Contramap, Local, and Compose are identical in implementation
|
||||
// - Contramap emphasizes the categorical/theoretical aspect
|
||||
// - Local emphasizes the context transformation aspect
|
||||
// - Compose emphasizes the function composition aspect
|
||||
// - Use Contramap when working with category theory concepts
|
||||
// - Use Local when adapting to different contexts
|
||||
// - Use Compose when building functional pipelines
|
||||
//
|
||||
// Category Theory Background:
|
||||
// - Consumer[A] forms a contravariant functor
|
||||
// - The contravariant functor laws must hold:
|
||||
// 1. contramap(id) = id
|
||||
// 2. contramap(f ∘ g) = contramap(g) ∘ contramap(f)
|
||||
// - This is dual to the covariant functor (map) operation
|
||||
// - Consumers are contravariant because they consume rather than produce values
|
||||
//
|
||||
// Use Cases:
|
||||
// - Working with contravariant functors in a categorical style
|
||||
// - Adapting consumers to work with more specific types
|
||||
// - Building type-safe consumer transformations
|
||||
// - Implementing profunctor patterns (Consumer is a profunctor)
|
||||
func Contramap[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return Local(f)
|
||||
}
|
||||
|
||||
@@ -381,3 +381,513 @@ func TestLocal(t *testing.T) {
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContramap(t *testing.T) {
|
||||
t.Run("basic contravariant mapping", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
parseToInt := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
consumeString := Contramap(parseToInt)(consumeInt)
|
||||
consumeString("42")
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("contravariant identity law", func(t *testing.T) {
|
||||
// contramap(identity) = identity
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
identity := function.Identity[int]
|
||||
consumeIdentity := Contramap(identity)(consumeInt)
|
||||
|
||||
consumeIdentity(42)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
// Should behave identically to original consumer
|
||||
consumeInt(100)
|
||||
capturedDirect := captured
|
||||
consumeIdentity(100)
|
||||
capturedMapped := captured
|
||||
|
||||
assert.Equal(t, capturedDirect, capturedMapped)
|
||||
})
|
||||
|
||||
t.Run("contravariant composition law", func(t *testing.T) {
|
||||
// contramap(f . g) = contramap(g) . contramap(f)
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
f := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
g := func(b bool) string {
|
||||
if b {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
// Compose f and g manually
|
||||
fg := func(b bool) int {
|
||||
return f(g(b))
|
||||
}
|
||||
|
||||
// Method 1: contramap(f . g)
|
||||
consumer1 := Contramap(fg)(consumeInt)
|
||||
consumer1(true)
|
||||
result1 := captured
|
||||
|
||||
// Method 2: contramap(g) . contramap(f)
|
||||
consumer2 := Contramap(g)(Contramap(f)(consumeInt))
|
||||
consumer2(true)
|
||||
result2 := captured
|
||||
|
||||
assert.Equal(t, result1, result2)
|
||||
assert.Equal(t, 1, result1)
|
||||
})
|
||||
|
||||
t.Run("type hierarchy adaptation", func(t *testing.T) {
|
||||
type Animal struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type Dog struct {
|
||||
Animal Animal
|
||||
Breed string
|
||||
}
|
||||
|
||||
var capturedName string
|
||||
consumeAnimal := func(a Animal) {
|
||||
capturedName = a.Name
|
||||
}
|
||||
|
||||
dogToAnimal := func(d Dog) Animal {
|
||||
return d.Animal
|
||||
}
|
||||
|
||||
consumeDog := Contramap(dogToAnimal)(consumeAnimal)
|
||||
consumeDog(Dog{
|
||||
Animal: Animal{Name: "Buddy"},
|
||||
Breed: "Golden Retriever",
|
||||
})
|
||||
|
||||
assert.Equal(t, "Buddy", capturedName)
|
||||
})
|
||||
|
||||
t.Run("field extraction with contramap", func(t *testing.T) {
|
||||
type Message struct {
|
||||
Text string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
var capturedText string
|
||||
consumeString := func(s string) {
|
||||
capturedText = s
|
||||
}
|
||||
|
||||
extractText := func(m Message) string {
|
||||
return m.Text
|
||||
}
|
||||
|
||||
consumeMessage := Contramap(extractText)(consumeString)
|
||||
consumeMessage(Message{
|
||||
Text: "Hello",
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
assert.Equal(t, "Hello", capturedText)
|
||||
})
|
||||
|
||||
t.Run("multiple contramap applications", func(t *testing.T) {
|
||||
type Level3 struct{ Value int }
|
||||
type Level2 struct{ L3 Level3 }
|
||||
type Level1 struct{ L2 Level2 }
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
extract3 := func(l3 Level3) int { return l3.Value }
|
||||
extract2 := func(l2 Level2) Level3 { return l2.L3 }
|
||||
extract1 := func(l1 Level1) Level2 { return l1.L2 }
|
||||
|
||||
// Chain contramap operations
|
||||
consumeLevel3 := Contramap(extract3)(consumeInt)
|
||||
consumeLevel2 := Contramap(extract2)(consumeLevel3)
|
||||
consumeLevel1 := Contramap(extract1)(consumeLevel2)
|
||||
|
||||
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("contramap with calculation", func(t *testing.T) {
|
||||
type Rectangle struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
var capturedArea int
|
||||
consumeArea := func(area int) {
|
||||
capturedArea = area
|
||||
}
|
||||
|
||||
calculateArea := func(r Rectangle) int {
|
||||
return r.Width * r.Height
|
||||
}
|
||||
|
||||
consumeRectangle := Contramap(calculateArea)(consumeArea)
|
||||
consumeRectangle(Rectangle{Width: 5, Height: 10})
|
||||
|
||||
assert.Equal(t, 50, capturedArea)
|
||||
})
|
||||
|
||||
t.Run("contramap preserves side effects", func(t *testing.T) {
|
||||
callCount := 0
|
||||
consumer := func(x int) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
contramappedConsumer := Contramap(transform)(consumer)
|
||||
|
||||
contramappedConsumer("1")
|
||||
contramappedConsumer("2")
|
||||
contramappedConsumer("3")
|
||||
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("contramap with pointer types", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
dereference := func(p *int) int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
consumePointer := Contramap(dereference)(consumeInt)
|
||||
|
||||
value := 42
|
||||
consumePointer(&value)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumePointer(nil)
|
||||
assert.Equal(t, 0, captured)
|
||||
})
|
||||
|
||||
t.Run("contramap equivalence with Local", func(t *testing.T) {
|
||||
var capturedLocal, capturedContramap int
|
||||
|
||||
consumeIntLocal := func(x int) {
|
||||
capturedLocal = x
|
||||
}
|
||||
|
||||
consumeIntContramap := func(x int) {
|
||||
capturedContramap = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// Both should produce identical results
|
||||
consumerLocal := Local(transform)(consumeIntLocal)
|
||||
consumerContramap := Contramap(transform)(consumeIntContramap)
|
||||
|
||||
consumerLocal("42")
|
||||
consumerContramap("42")
|
||||
|
||||
assert.Equal(t, capturedLocal, capturedContramap)
|
||||
assert.Equal(t, 42, capturedLocal)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose(t *testing.T) {
|
||||
t.Run("basic composition", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
parseToInt := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
consumeString := Compose(parseToInt)(consumeInt)
|
||||
consumeString("42")
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("composing multiple transformations", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
type Wrapper struct {
|
||||
Data Data
|
||||
}
|
||||
|
||||
var captured string
|
||||
consumeString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
extractData := func(w Wrapper) Data { return w.Data }
|
||||
extractValue := func(d Data) string { return d.Value }
|
||||
|
||||
// Compose step by step
|
||||
consumeData := Compose(extractValue)(consumeString)
|
||||
consumeWrapper := Compose(extractData)(consumeData)
|
||||
|
||||
consumeWrapper(Wrapper{Data: Data{Value: "Hello"}})
|
||||
|
||||
assert.Equal(t, "Hello", captured)
|
||||
})
|
||||
|
||||
t.Run("function composition style", func(t *testing.T) {
|
||||
type Request struct {
|
||||
Body []byte
|
||||
}
|
||||
|
||||
var captured string
|
||||
processString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
bytesToString := func(b []byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
extractBody := func(r Request) []byte {
|
||||
return r.Body
|
||||
}
|
||||
|
||||
// Chain compositions
|
||||
processBytes := Compose(bytesToString)(processString)
|
||||
processRequest := Compose(extractBody)(processBytes)
|
||||
|
||||
processRequest(Request{Body: []byte("test")})
|
||||
|
||||
assert.Equal(t, "test", captured)
|
||||
})
|
||||
|
||||
t.Run("compose with identity", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
identity := function.Identity[int]
|
||||
composedConsumer := Compose(identity)(consumeInt)
|
||||
|
||||
composedConsumer(42)
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("compose with field extraction", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
var capturedName string
|
||||
consumeName := func(name string) {
|
||||
capturedName = name
|
||||
}
|
||||
|
||||
extractName := func(u User) string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
consumeUser := Compose(extractName)(consumeName)
|
||||
consumeUser(User{Name: "Alice", Email: "alice@example.com", Age: 30})
|
||||
|
||||
assert.Equal(t, "Alice", capturedName)
|
||||
})
|
||||
|
||||
t.Run("compose with calculation", func(t *testing.T) {
|
||||
type Circle struct {
|
||||
Radius float64
|
||||
}
|
||||
|
||||
var capturedArea float64
|
||||
consumeArea := func(area float64) {
|
||||
capturedArea = area
|
||||
}
|
||||
|
||||
calculateArea := func(c Circle) float64 {
|
||||
return 3.14159 * c.Radius * c.Radius
|
||||
}
|
||||
|
||||
consumeCircle := Compose(calculateArea)(consumeArea)
|
||||
consumeCircle(Circle{Radius: 5.0})
|
||||
|
||||
assert.InDelta(t, 78.53975, capturedArea, 0.00001)
|
||||
})
|
||||
|
||||
t.Run("compose with slice operations", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeLength := func(n int) {
|
||||
captured = n
|
||||
}
|
||||
|
||||
getLength := func(s []string) int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
consumeSlice := Compose(getLength)(consumeLength)
|
||||
consumeSlice([]string{"a", "b", "c", "d"})
|
||||
|
||||
assert.Equal(t, 4, captured)
|
||||
})
|
||||
|
||||
t.Run("compose with map operations", func(t *testing.T) {
|
||||
var captured bool
|
||||
consumeHasKey := func(has bool) {
|
||||
captured = has
|
||||
}
|
||||
|
||||
hasKey := func(m map[string]int) bool {
|
||||
_, exists := m["key"]
|
||||
return exists
|
||||
}
|
||||
|
||||
consumeMap := Compose(hasKey)(consumeHasKey)
|
||||
|
||||
consumeMap(map[string]int{"key": 42})
|
||||
assert.True(t, captured)
|
||||
|
||||
consumeMap(map[string]int{"other": 42})
|
||||
assert.False(t, captured)
|
||||
})
|
||||
|
||||
t.Run("compose preserves consumer behavior", func(t *testing.T) {
|
||||
callCount := 0
|
||||
consumer := func(x int) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
composedConsumer := Compose(transform)(consumer)
|
||||
|
||||
composedConsumer("1")
|
||||
composedConsumer("2")
|
||||
composedConsumer("3")
|
||||
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("compose with error handling", func(t *testing.T) {
|
||||
type Result struct {
|
||||
Value int
|
||||
Error error
|
||||
}
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
extractValue := func(r Result) int {
|
||||
if r.Error != nil {
|
||||
return -1
|
||||
}
|
||||
return r.Value
|
||||
}
|
||||
|
||||
consumeResult := Compose(extractValue)(consumeInt)
|
||||
|
||||
consumeResult(Result{Value: 42, Error: nil})
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumeResult(Result{Value: 100, Error: assert.AnError})
|
||||
assert.Equal(t, -1, captured)
|
||||
})
|
||||
|
||||
t.Run("compose equivalence with Local", func(t *testing.T) {
|
||||
var capturedLocal, capturedCompose int
|
||||
|
||||
consumeIntLocal := func(x int) {
|
||||
capturedLocal = x
|
||||
}
|
||||
|
||||
consumeIntCompose := func(x int) {
|
||||
capturedCompose = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// Both should produce identical results
|
||||
consumerLocal := Local(transform)(consumeIntLocal)
|
||||
consumerCompose := Compose(transform)(consumeIntCompose)
|
||||
|
||||
consumerLocal("42")
|
||||
consumerCompose("42")
|
||||
|
||||
assert.Equal(t, capturedLocal, capturedCompose)
|
||||
assert.Equal(t, 42, capturedLocal)
|
||||
})
|
||||
|
||||
t.Run("compose equivalence with Contramap", func(t *testing.T) {
|
||||
var capturedCompose, capturedContramap int
|
||||
|
||||
consumeIntCompose := func(x int) {
|
||||
capturedCompose = x
|
||||
}
|
||||
|
||||
consumeIntContramap := func(x int) {
|
||||
capturedContramap = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// All three should produce identical results
|
||||
consumerCompose := Compose(transform)(consumeIntCompose)
|
||||
consumerContramap := Contramap(transform)(consumeIntContramap)
|
||||
|
||||
consumerCompose("42")
|
||||
consumerContramap("42")
|
||||
|
||||
assert.Equal(t, capturedCompose, capturedContramap)
|
||||
assert.Equal(t, 42, capturedCompose)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import (
|
||||
// return result.Of("done")
|
||||
// }
|
||||
//
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel() // Cancel immediately
|
||||
//
|
||||
// wrapped := WithContext(ctx, computation)
|
||||
|
||||
@@ -61,7 +61,7 @@ import (
|
||||
//
|
||||
// // Safely read file with automatic cleanup
|
||||
// safeRead := Bracket(acquireFile, readFile, closeFile)
|
||||
// result := safeRead(context.Background())()
|
||||
// result := safeRead(t.Context())()
|
||||
//
|
||||
//go:inline
|
||||
func Bracket[
|
||||
|
||||
@@ -50,7 +50,7 @@ import (
|
||||
// // Sequence it to apply Config first
|
||||
// sequenced := SequenceReader[Config, int](getMultiplier)
|
||||
// cfg := Config{Timeout: 30}
|
||||
// result := sequenced(cfg)(context.Background())() // Returns 60
|
||||
// result := sequenced(cfg)(t.Context())() // Returns 60
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
|
||||
@@ -107,7 +107,7 @@ func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]]
|
||||
//
|
||||
// // Provide Config to get final result
|
||||
// cfg := Config{Multiplier: 5}
|
||||
// finalResult := result(cfg)(context.Background())() // Returns 50
|
||||
// finalResult := result(cfg)(t.Context())() // Returns 50
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
|
||||
@@ -81,7 +81,7 @@ func SLogWithCallback[A any](
|
||||
// Chain(SLog[string]("Extracted name")),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // Logs: "Extracted name" value="Alice"
|
||||
//
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestPromapBasic(t *testing.T) {
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addKey, toString)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, "42", result)
|
||||
})
|
||||
@@ -69,7 +69,7 @@ func TestContramapBasic(t *testing.T) {
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
@@ -90,7 +90,7 @@ func TestLocalBasic(t *testing.T) {
|
||||
}
|
||||
|
||||
adapted := Local[bool](addTimeout)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.True(t, result)
|
||||
})
|
||||
|
||||
@@ -594,7 +594,7 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
|
||||
// )
|
||||
//
|
||||
// // Create context with side effects (e.g., loading config)
|
||||
// createContext := G.Of(context.WithValue(context.Background(), "key", "value"))
|
||||
// createContext := G.Of(context.WithValue(t.Context(), "key", "value"))
|
||||
//
|
||||
// // A computation that uses the context
|
||||
// getValue := readerio.FromReader(func(ctx context.Context) string {
|
||||
@@ -664,7 +664,7 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
// getUser,
|
||||
// addUser,
|
||||
// )
|
||||
// user := result(context.Background())() // Returns "Alice"
|
||||
// user := result(t.Context())() // Returns "Alice"
|
||||
//
|
||||
// Timeout Example:
|
||||
//
|
||||
@@ -731,7 +731,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// fetchData,
|
||||
// readerio.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// data := result(context.Background())() // Returns Data{} after 5s timeout
|
||||
// data := result(t.Context())() // Returns Data{} after 5s timeout
|
||||
//
|
||||
// Successful Example:
|
||||
//
|
||||
@@ -740,7 +740,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// quickFetch,
|
||||
// readerio.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// data := result(context.Background())() // Returns Data{Value: "quick"}
|
||||
// data := result(t.Context())() // Returns Data{Value: "quick"}
|
||||
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
@@ -791,12 +791,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
// fetchData,
|
||||
// readerio.WithDeadline[Data](deadline),
|
||||
// )
|
||||
// data := result(context.Background())() // Returns Data{} if past deadline
|
||||
// data := result(t.Context())() // Returns Data{} if past deadline
|
||||
//
|
||||
// Combining with Parent Context:
|
||||
//
|
||||
// // If parent context already has a deadline, the earlier one takes precedence
|
||||
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
|
||||
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
|
||||
// defer cancel()
|
||||
//
|
||||
// laterDeadline := time.Now().Add(2 * time.Hour)
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestMonadMap(t *testing.T) {
|
||||
rio := Of(5)
|
||||
doubled := MonadMap(rio, N.Mul(2))
|
||||
|
||||
result := doubled(context.Background())()
|
||||
result := doubled(t.Context())()
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
@@ -41,14 +41,14 @@ func TestMap(t *testing.T) {
|
||||
Map(utils.Double),
|
||||
)
|
||||
|
||||
assert.Equal(t, 2, g(context.Background())())
|
||||
assert.Equal(t, 2, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
rio := Of(42)
|
||||
replaced := MonadMapTo(rio, "constant")
|
||||
|
||||
result := replaced(context.Background())()
|
||||
result := replaced(t.Context())()
|
||||
assert.Equal(t, "constant", result)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestMapTo(t *testing.T) {
|
||||
MapTo[int]("constant"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "constant", result(context.Background())())
|
||||
assert.Equal(t, "constant", result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
@@ -67,7 +67,7 @@ func TestMonadChain(t *testing.T) {
|
||||
return Of(n * 3)
|
||||
})
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
@@ -78,7 +78,7 @@ func TestChain(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
@@ -89,7 +89,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
return Of("side effect")
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func TestChainFirst(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func TestMonadTap(t *testing.T) {
|
||||
return Of(func() {})
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -132,14 +132,14 @@ func TestTap(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
rio := Of(100)
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
|
||||
assert.Equal(t, 100, result)
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func TestMonadAp(t *testing.T) {
|
||||
faIO := Of(5)
|
||||
result := MonadAp(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
assert.Equal(t, 10, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
@@ -158,7 +158,7 @@ func TestAp(t *testing.T) {
|
||||
Ap[int](Of(1)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 2, g(context.Background())())
|
||||
assert.Equal(t, 2, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
@@ -166,7 +166,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
faIO := Of(5)
|
||||
result := MonadApSeq(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestApSeq(t *testing.T) {
|
||||
@@ -175,7 +175,7 @@ func TestApSeq(t *testing.T) {
|
||||
ApSeq[int](Of(5)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, g(context.Background())())
|
||||
assert.Equal(t, 15, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadApPar(t *testing.T) {
|
||||
@@ -183,7 +183,7 @@ func TestMonadApPar(t *testing.T) {
|
||||
faIO := Of(5)
|
||||
result := MonadApPar(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
@@ -192,12 +192,12 @@ func TestApPar(t *testing.T) {
|
||||
ApPar[int](Of(5)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, g(context.Background())())
|
||||
assert.Equal(t, 15, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
rio := Ask()
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
ctx := context.WithValue(t.Context(), "key", "value")
|
||||
result := rio(ctx)()
|
||||
|
||||
assert.Equal(t, ctx, result)
|
||||
@@ -207,7 +207,7 @@ func TestFromIO(t *testing.T) {
|
||||
ioAction := G.Of(42)
|
||||
rio := FromIO(ioAction)
|
||||
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func TestFromReader(t *testing.T) {
|
||||
}
|
||||
|
||||
rio := FromReader(rdr)
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func TestFromLazy(t *testing.T) {
|
||||
lazy := func() int { return 42 }
|
||||
rio := FromLazy(lazy)
|
||||
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ func TestMonadChainIOK(t *testing.T) {
|
||||
return G.Of(n * 4)
|
||||
})
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())())
|
||||
assert.Equal(t, 20, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainIOK(t *testing.T) {
|
||||
@@ -247,7 +247,7 @@ func TestChainIOK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())())
|
||||
assert.Equal(t, 20, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
@@ -258,7 +258,7 @@ func TestMonadChainFirstIOK(t *testing.T) {
|
||||
return G.Of("side effect")
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -273,7 +273,7 @@ func TestChainFirstIOK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -286,7 +286,7 @@ func TestMonadTapIOK(t *testing.T) {
|
||||
return G.Of(func() {})
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -301,7 +301,7 @@ func TestTapIOK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -313,8 +313,8 @@ func TestDefer(t *testing.T) {
|
||||
return Of(counter)
|
||||
})
|
||||
|
||||
result1 := rio(context.Background())()
|
||||
result2 := rio(context.Background())()
|
||||
result1 := rio(t.Context())()
|
||||
result2 := rio(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, result1)
|
||||
assert.Equal(t, 2, result2)
|
||||
@@ -328,8 +328,8 @@ func TestMemoize(t *testing.T) {
|
||||
return counter
|
||||
}))
|
||||
|
||||
result1 := memoized(context.Background())()
|
||||
result2 := memoized(context.Background())()
|
||||
result1 := memoized(t.Context())()
|
||||
result2 := memoized(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, result1)
|
||||
assert.Equal(t, 1, result2) // Same value, memoized
|
||||
@@ -339,7 +339,7 @@ func TestFlatten(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
flattened := Flatten(nested)
|
||||
|
||||
result := flattened(context.Background())()
|
||||
result := flattened(t.Context())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ func TestMonadFlap(t *testing.T) {
|
||||
fabIO := Of(N.Mul(3))
|
||||
result := MonadFlap(fabIO, 7)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
assert.Equal(t, 21, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
@@ -356,7 +356,7 @@ func TestFlap(t *testing.T) {
|
||||
Flap[int](7),
|
||||
)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
assert.Equal(t, 21, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainReaderK(t *testing.T) {
|
||||
@@ -365,7 +365,7 @@ func TestMonadChainReaderK(t *testing.T) {
|
||||
return func(ctx context.Context) int { return n * 2 }
|
||||
})
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
assert.Equal(t, 10, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainReaderK(t *testing.T) {
|
||||
@@ -376,7 +376,7 @@ func TestChainReaderK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
assert.Equal(t, 10, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstReaderK(t *testing.T) {
|
||||
@@ -389,7 +389,7 @@ func TestMonadChainFirstReaderK(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -406,7 +406,7 @@ func TestChainFirstReaderK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -421,7 +421,7 @@ func TestMonadTapReaderK(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -438,14 +438,14 @@ func TestTapReaderK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
rio := Of(42)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
ioAction := Read[int](ctx)(rio)
|
||||
result := ioAction()
|
||||
|
||||
@@ -463,7 +463,7 @@ func TestComplexPipeline(t *testing.T) {
|
||||
Map(N.Add(10)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())()) // (5 * 2) + 10 = 20
|
||||
assert.Equal(t, 20, result(t.Context())()) // (5 * 2) + 10 = 20
|
||||
}
|
||||
|
||||
func TestFromIOWithChain(t *testing.T) {
|
||||
@@ -476,7 +476,7 @@ func TestFromIOWithChain(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestTapWithLogging(t *testing.T) {
|
||||
@@ -496,14 +496,14 @@ func TestTapWithLogging(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 84, value)
|
||||
assert.Equal(t, []int{42, 84}, logged)
|
||||
}
|
||||
|
||||
func TestReadIO(t *testing.T) {
|
||||
// Test basic ReadIO functionality
|
||||
contextIO := G.Of(context.WithValue(context.Background(), "testKey", "testValue"))
|
||||
contextIO := G.Of(context.WithValue(t.Context(), "testKey", "testValue"))
|
||||
rio := FromReader(func(ctx context.Context) string {
|
||||
if val := ctx.Value("testKey"); val != nil {
|
||||
return val.(string)
|
||||
@@ -519,7 +519,7 @@ func TestReadIO(t *testing.T) {
|
||||
|
||||
func TestReadIOWithBackground(t *testing.T) {
|
||||
// Test ReadIO with plain background context
|
||||
contextIO := G.Of(context.Background())
|
||||
contextIO := G.Of(t.Context())
|
||||
rio := Of(42)
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(rio)
|
||||
@@ -530,7 +530,7 @@ func TestReadIOWithBackground(t *testing.T) {
|
||||
|
||||
func TestReadIOWithChain(t *testing.T) {
|
||||
// Test ReadIO with chained operations
|
||||
contextIO := G.Of(context.WithValue(context.Background(), "multiplier", 3))
|
||||
contextIO := G.Of(context.WithValue(t.Context(), "multiplier", 3))
|
||||
|
||||
result := F.Pipe1(
|
||||
FromReader(func(ctx context.Context) int {
|
||||
@@ -552,7 +552,7 @@ func TestReadIOWithChain(t *testing.T) {
|
||||
|
||||
func TestReadIOWithMap(t *testing.T) {
|
||||
// Test ReadIO with Map operations
|
||||
contextIO := G.Of(context.Background())
|
||||
contextIO := G.Of(t.Context())
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(5),
|
||||
@@ -571,7 +571,7 @@ func TestReadIOWithSideEffects(t *testing.T) {
|
||||
counter := 0
|
||||
contextIO := func() context.Context {
|
||||
counter++
|
||||
return context.WithValue(context.Background(), "counter", counter)
|
||||
return context.WithValue(t.Context(), "counter", counter)
|
||||
}
|
||||
|
||||
rio := FromReader(func(ctx context.Context) int {
|
||||
@@ -593,7 +593,7 @@ func TestReadIOMultipleExecutions(t *testing.T) {
|
||||
counter := 0
|
||||
contextIO := func() context.Context {
|
||||
counter++
|
||||
return context.Background()
|
||||
return t.Context()
|
||||
}
|
||||
|
||||
rio := Of(42)
|
||||
@@ -609,7 +609,7 @@ func TestReadIOMultipleExecutions(t *testing.T) {
|
||||
|
||||
func TestReadIOComparisonWithRead(t *testing.T) {
|
||||
// Compare ReadIO with Read to show the difference
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
ctx := context.WithValue(t.Context(), "key", "value")
|
||||
|
||||
rio := FromReader(func(ctx context.Context) string {
|
||||
if val := ctx.Value("key"); val != nil {
|
||||
@@ -642,7 +642,7 @@ func TestReadIOWithComplexContext(t *testing.T) {
|
||||
|
||||
contextIO := G.Of(
|
||||
context.WithValue(
|
||||
context.WithValue(context.Background(), userKey, "Alice"),
|
||||
context.WithValue(t.Context(), userKey, "Alice"),
|
||||
tokenKey,
|
||||
"secret123",
|
||||
),
|
||||
@@ -668,7 +668,7 @@ func TestReadIOWithComplexContext(t *testing.T) {
|
||||
|
||||
func TestReadIOWithAsk(t *testing.T) {
|
||||
// Test ReadIO combined with Ask
|
||||
contextIO := G.Of(context.WithValue(context.Background(), "data", 100))
|
||||
contextIO := G.Of(context.WithValue(t.Context(), "data", 100))
|
||||
|
||||
result := F.Pipe1(
|
||||
Ask(),
|
||||
|
||||
@@ -53,7 +53,7 @@ import (
|
||||
// }
|
||||
//
|
||||
// countdown := TailRec(countdownStep)
|
||||
// result := countdown(10)(context.Background())() // Returns "Done!"
|
||||
// result := countdown(10)(t.Context())() // Returns "Done!"
|
||||
//
|
||||
// Example - Sum with context:
|
||||
//
|
||||
@@ -77,7 +77,7 @@ import (
|
||||
// }
|
||||
//
|
||||
// sum := TailRec(sumStep)
|
||||
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(context.Background())()
|
||||
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(t.Context())()
|
||||
// // Returns 15, safe even for very large slices
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -80,7 +80,7 @@ import (
|
||||
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
|
||||
//
|
||||
// // Execute
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := retryingFetch(ctx)() // Returns "success" after 3 attempts
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -74,7 +74,7 @@ func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
// safeFetch := WithContextK(fetchUser)
|
||||
//
|
||||
// // If context is cancelled, returns immediately without executing fetchUser
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel() // Cancel immediately
|
||||
// result := safeFetch(123)(ctx)() // Returns context.Canceled error
|
||||
//
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
// }
|
||||
//
|
||||
// // Execute the computation
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := fetchUser("123")(ctx)()
|
||||
// // result is Either[error, User]
|
||||
//
|
||||
@@ -161,7 +161,7 @@
|
||||
// All operations respect context cancellation. When a context is cancelled, operations
|
||||
// will return an error containing the cancellation cause:
|
||||
//
|
||||
// ctx, cancel := context.WithCancelCause(context.Background())
|
||||
// ctx, cancel := context.WithCancelCause(t.Context())
|
||||
// cancel(errors.New("operation cancelled"))
|
||||
// result := computation(ctx)() // Returns Left with cancellation error
|
||||
//
|
||||
|
||||
@@ -37,7 +37,7 @@ import (
|
||||
// return either.Eq(eq.FromEquals(func(x, y int) bool { return x == y }))(a, b)
|
||||
// })
|
||||
// eqRIE := Eq(eqInt)
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -43,7 +43,7 @@ import (
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := readerioresult.FilterOrElse(isPositive, onNegative)
|
||||
// result := filter(readerioresult.Right(42))(context.Background())()
|
||||
// result := filter(readerioresult.Right(42))(t.Context())()
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
|
||||
@@ -71,7 +71,7 @@ import (
|
||||
//
|
||||
// // Now we can partially apply the Config
|
||||
// cfg := Config{Timeout: 30}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := sequenced(ctx)(cfg)() // Returns Right(60)
|
||||
//
|
||||
// This is especially useful in point-free style when building computation pipelines:
|
||||
@@ -133,7 +133,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
|
||||
//
|
||||
// // Partially apply the Database
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := sequenced(ctx)(db)() // Executes IO and returns Right("Query result...")
|
||||
//
|
||||
// In point-free style, this enables clean composition:
|
||||
@@ -195,7 +195,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R
|
||||
//
|
||||
// // Partially apply the Config
|
||||
// cfg := Config{MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := sequenced(ctx)(cfg)() // Returns Right(3)
|
||||
//
|
||||
// // With invalid config
|
||||
@@ -276,7 +276,7 @@ func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kl
|
||||
//
|
||||
// // Now we can provide the Config to get the final result
|
||||
// cfg := Config{Multiplier: 5}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// finalResult := result(cfg)(ctx)() // Returns Right(50)
|
||||
//
|
||||
// In point-free style, this enables clean composition:
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
// The Reader environment (string) is now the first parameter
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original
|
||||
result1 := original(ctx)()
|
||||
@@ -75,7 +75,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
expected := "Query on localhost:5432"
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
@@ -132,7 +132,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Sequence
|
||||
sequenced := SequenceReader(original)
|
||||
@@ -158,7 +158,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test with zero values
|
||||
@@ -184,7 +184,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
@@ -217,14 +217,14 @@ func TestSequenceReader(t *testing.T) {
|
||||
withConfig := sequenced(cfg)
|
||||
|
||||
// Now we have a ReaderIOResult[int] that can be used in different contexts
|
||||
ctx1 := context.Background()
|
||||
ctx1 := t.Context()
|
||||
result1 := withConfig(ctx1)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
value1, _ := either.Unwrap(result1)
|
||||
assert.Equal(t, 50, value1)
|
||||
|
||||
// Can reuse with different context
|
||||
ctx2 := context.Background()
|
||||
ctx2 := t.Context()
|
||||
result2 := withConfig(ctx2)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
@@ -246,7 +246,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
// Test original
|
||||
@@ -273,7 +273,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
@@ -303,7 +303,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
@@ -327,7 +327,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReaderResult(original)
|
||||
|
||||
// Test original
|
||||
@@ -356,7 +356,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
@@ -384,7 +384,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with inner error
|
||||
result1 := original(ctx)()
|
||||
@@ -421,7 +421,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test outer error
|
||||
sequenced1 := SequenceReaderResult(makeOriginal(-20))
|
||||
@@ -460,7 +460,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReaderResult(original)
|
||||
@@ -484,7 +484,7 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
empty := Empty{}
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
@@ -514,7 +514,7 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
data := &Data{Value: 100}
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
@@ -544,7 +544,7 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Call multiple times with same inputs
|
||||
@@ -583,7 +583,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 5}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -614,7 +614,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 5}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsLeft(finalResult))
|
||||
@@ -643,7 +643,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Database and execute
|
||||
db := Database{Prefix: "ID"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(db)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -673,7 +673,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Settings and execute
|
||||
settings := Settings{Prefix: "[", Suffix: "]"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(settings)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -705,14 +705,14 @@ func TestTraverseReader(t *testing.T) {
|
||||
withConfig := result(cfg)
|
||||
|
||||
// Can now use with different contexts
|
||||
ctx1 := context.Background()
|
||||
ctx1 := t.Context()
|
||||
finalResult1 := withConfig(ctx1)()
|
||||
assert.True(t, either.IsRight(finalResult1))
|
||||
value1, _ := either.Unwrap(finalResult1)
|
||||
assert.Equal(t, 30, value1)
|
||||
|
||||
// Reuse with different context
|
||||
ctx2 := context.Background()
|
||||
ctx2 := t.Context()
|
||||
finalResult2 := withConfig(ctx2)()
|
||||
assert.True(t, either.IsRight(finalResult2))
|
||||
value2, _ := either.Unwrap(finalResult2)
|
||||
@@ -746,7 +746,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
result := traversed(original)
|
||||
|
||||
// Use canceled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
cfg := Config{Value: 5}
|
||||
@@ -778,7 +778,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config with zero offset
|
||||
cfg := Config{Offset: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -807,7 +807,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 4}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -843,7 +843,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Test with value within range
|
||||
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult1 := result(rules1)(ctx)()
|
||||
assert.True(t, either.IsRight(finalResult1))
|
||||
value1, _ := either.Unwrap(finalResult1)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
// )
|
||||
//
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
// result := requester(t.Context())()
|
||||
package builder
|
||||
|
||||
import (
|
||||
@@ -103,7 +103,7 @@ import (
|
||||
// B.WithJSONBody(map[string]string{"name": "John"}),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
// result := requester(t.Context())()
|
||||
//
|
||||
// Example without body:
|
||||
//
|
||||
@@ -113,7 +113,7 @@ import (
|
||||
// B.WithMethod("GET"),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
// result := requester(t.Context())()
|
||||
func Requester(builder *R.Builder) RIOEH.Requester {
|
||||
|
||||
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOResult[*http.Request] {
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestBuilderWithQuery(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.True(t, E.IsRight(req(context.Background())()))
|
||||
assert.True(t, E.IsRight(req(t.Context())()))
|
||||
}
|
||||
|
||||
// TestBuilderWithoutBody tests creating a request without a body
|
||||
@@ -67,7 +67,7 @@ func TestBuilderWithoutBody(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestBuilderWithBody(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestBuilderWithHeaders(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestBuilderWithInvalidURL(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func TestBuilderWithEmptyMethod(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
// Empty method should still work (defaults to GET in http.NewRequest)
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
@@ -161,7 +161,7 @@ func TestBuilderWithMultipleHeaders(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -185,7 +185,7 @@ func TestBuilderWithBodyAndHeaders(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -207,7 +207,7 @@ func TestBuilderContextCancellation(t *testing.T) {
|
||||
requester := Requester(builder)
|
||||
|
||||
// Create a cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
result := requester(ctx)()
|
||||
@@ -233,7 +233,7 @@ func TestBuilderWithDifferentMethods(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result for method %s", method)
|
||||
|
||||
@@ -256,7 +256,7 @@ func TestBuilderWithJSON(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -277,7 +277,7 @@ func TestBuilderWithBearer(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// result := ReadJSON[MyType](client)(request)
|
||||
// response := result(context.Background())()
|
||||
// response := result(t.Context())()
|
||||
package http
|
||||
|
||||
import (
|
||||
@@ -157,7 +157,7 @@ func MakeClient(httpClient *http.Client) Client {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// fullResp := ReadFullResponse(client)(request)
|
||||
// result := fullResp(context.Background())()
|
||||
// result := fullResp(t.Context())()
|
||||
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
|
||||
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
|
||||
return F.Flow3(
|
||||
@@ -194,7 +194,7 @@ func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// readBytes := ReadAll(client)
|
||||
// result := readBytes(request)(context.Background())()
|
||||
// result := readBytes(request)(t.Context())()
|
||||
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
@@ -218,7 +218,7 @@ func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/text")
|
||||
// readText := ReadText(client)
|
||||
// result := readText(request)(context.Background())()
|
||||
// result := readText(request)(t.Context())()
|
||||
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
@@ -277,7 +277,7 @@ func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/user/1")
|
||||
// readUser := ReadJSON[User](client)
|
||||
// result := readUser(request)(context.Background())()
|
||||
// result := readUser(request)(t.Context())()
|
||||
func ReadJSON[A any](client Client) RIOE.Kleisli[Requester, A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
|
||||
@@ -429,7 +429,7 @@ func LogEntryExitWithCallback[A any](
|
||||
// loggedFetch := LogEntryExit[User]("fetchUser")(fetchUser(123))
|
||||
//
|
||||
// // Execute
|
||||
// result := loggedFetch(context.Background())()
|
||||
// result := loggedFetch(t.Context())()
|
||||
// // Logs:
|
||||
// // [entering 1] fetchUser
|
||||
// // [exiting 1] fetchUser [0.1s]
|
||||
@@ -441,7 +441,7 @@ func LogEntryExitWithCallback[A any](
|
||||
// }
|
||||
//
|
||||
// logged := LogEntryExit[string]("failingOp")(failingOp())
|
||||
// result := logged(context.Background())()
|
||||
// result := logged(t.Context())()
|
||||
// // Logs:
|
||||
// // [entering 2] failingOp
|
||||
// // [throwing 2] failingOp [0.0s]: connection timeout
|
||||
@@ -461,7 +461,7 @@ func LogEntryExitWithCallback[A any](
|
||||
// LogEntryExit[[]Order]("fetchOrders"),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // Logs:
|
||||
// // [entering 3] fetchUser
|
||||
// // [exiting 3] fetchUser [0.1s]
|
||||
@@ -474,8 +474,8 @@ func LogEntryExitWithCallback[A any](
|
||||
// op1 := LogEntryExit[Data]("operation1")(fetchData(1))
|
||||
// op2 := LogEntryExit[Data]("operation2")(fetchData(2))
|
||||
//
|
||||
// go op1(context.Background())()
|
||||
// go op2(context.Background())()
|
||||
// go op1(t.Context())()
|
||||
// go op2(t.Context())()
|
||||
// // Logs (order may vary):
|
||||
// // [entering 5] operation1
|
||||
// // [entering 6] operation2
|
||||
@@ -615,7 +615,7 @@ func SLogWithCallback[A any](
|
||||
// Map(func(u User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // If error, logs: "Fetched user" error="user not found"
|
||||
//
|
||||
@@ -679,7 +679,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
// Map(func(u User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // Returns: result.Of("Alice")
|
||||
//
|
||||
@@ -694,7 +694,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
// TapSLog[Payment]("Payment processed"),
|
||||
// )
|
||||
//
|
||||
// result := processOrder(context.Background())()
|
||||
// result := processOrder(t.Context())()
|
||||
// // Logs each successful step with the intermediate values
|
||||
// // If any step fails, subsequent TapSLog calls don't log
|
||||
//
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestLoggingContext(t *testing.T) {
|
||||
LogEntryExit[string]("TestLoggingContext2"),
|
||||
)
|
||||
|
||||
assert.Equal(t, result.Of("Sample"), data(context.Background())())
|
||||
assert.Equal(t, result.Of("Sample"), data(t.Context())())
|
||||
}
|
||||
|
||||
// TestLogEntryExitSuccess tests successful operation logging
|
||||
@@ -43,7 +43,7 @@ func TestLogEntryExitSuccess(t *testing.T) {
|
||||
LogEntryExit[string]("TestOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("success value"), res)
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestLogEntryExitError(t *testing.T) {
|
||||
LogEntryExit[string]("FailingOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestLogEntryExitNested(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
res := outerOp(context.Background())()
|
||||
res := outerOp(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
@@ -137,7 +137,7 @@ func TestLogEntryExitWithCallback(t *testing.T) {
|
||||
LogEntryExitWithCallback[int](slog.LevelDebug, customCallback, "DebugOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
|
||||
@@ -163,7 +163,7 @@ func TestLogEntryExitDisabled(t *testing.T) {
|
||||
LogEntryExit[string]("DisabledOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
@@ -197,7 +197,7 @@ func TestLogEntryExitF(t *testing.T) {
|
||||
LogEntryExitF(onEntry, onExit),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
|
||||
@@ -234,7 +234,7 @@ func TestLogEntryExitFWithError(t *testing.T) {
|
||||
LogEntryExitF(onEntry, onExit),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
|
||||
@@ -257,7 +257,7 @@ func TestLoggingIDUniqueness(t *testing.T) {
|
||||
Of(i),
|
||||
LogEntryExit[int]("Operation"),
|
||||
)
|
||||
op(context.Background())()
|
||||
op(t.Context())()
|
||||
}
|
||||
|
||||
logOutput := buf.String()
|
||||
@@ -287,7 +287,7 @@ func TestLogEntryExitWithContextLogger(t *testing.T) {
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
ctx := logging.WithLogger(contextLogger)(t.Context())
|
||||
|
||||
operation := F.Pipe1(
|
||||
Of("context value"),
|
||||
@@ -326,7 +326,7 @@ func TestLogEntryExitTiming(t *testing.T) {
|
||||
LogEntryExit[string]("SlowOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
@@ -379,7 +379,7 @@ func TestLogEntryExitChainedOperations(t *testing.T) {
|
||||
)),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("2"), res)
|
||||
|
||||
@@ -408,7 +408,7 @@ func TestTapSLog(t *testing.T) {
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(84), res)
|
||||
|
||||
@@ -443,7 +443,7 @@ func TestTapSLogInPipeline(t *testing.T) {
|
||||
TapSLog[int]("Step 3: Final length"),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(11), res)
|
||||
|
||||
@@ -472,7 +472,7 @@ func TestTapSLogWithError(t *testing.T) {
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
|
||||
@@ -504,7 +504,7 @@ func TestTapSLogWithStruct(t *testing.T) {
|
||||
Map(func(u User) string { return u.Name }),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("Alice"), res)
|
||||
|
||||
@@ -530,7 +530,7 @@ func TestTapSLogDisabled(t *testing.T) {
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(84), res)
|
||||
|
||||
@@ -546,7 +546,7 @@ func TestTapSLogWithContextLogger(t *testing.T) {
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
ctx := logging.WithLogger(contextLogger)(t.Context())
|
||||
|
||||
operation := F.Pipe2(
|
||||
Of("test value"),
|
||||
@@ -572,7 +572,7 @@ func TestSLogLogsSuccessValue(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create a Result and log it
|
||||
res1 := result.Of(42)
|
||||
@@ -594,7 +594,7 @@ func TestSLogLogsErrorValue(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
// Create an error Result and log it
|
||||
@@ -620,7 +620,7 @@ func TestSLogWithCallbackCustomLevel(t *testing.T) {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create a Result and log it with custom callback
|
||||
res1 := result.Of(42)
|
||||
@@ -645,7 +645,7 @@ func TestSLogWithCallbackLogsError(t *testing.T) {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("warning error")
|
||||
|
||||
// Create an error Result and log it with custom callback
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestPromapBasic(t *testing.T) {
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addKey, toString)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of("42"), result)
|
||||
})
|
||||
@@ -67,7 +67,7 @@ func TestContramapBasic(t *testing.T) {
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of(100), result)
|
||||
})
|
||||
@@ -91,7 +91,7 @@ func TestLocalBasic(t *testing.T) {
|
||||
}
|
||||
|
||||
adapted := Local[string](addUser)(getValue)
|
||||
result := adapted(context.Background())()
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of("Alice"), result)
|
||||
})
|
||||
|
||||
@@ -1041,7 +1041,7 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
// getUser,
|
||||
// addUser,
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns ("Alice", nil)
|
||||
// value, err := result(t.Context())() // Returns ("Alice", nil)
|
||||
//
|
||||
// Timeout Example:
|
||||
//
|
||||
@@ -1112,7 +1112,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// fetchData,
|
||||
// readerioresult.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) after 5s
|
||||
// value, err := result(t.Context())() // Returns (Data{}, context.DeadlineExceeded) after 5s
|
||||
//
|
||||
// Successful Example:
|
||||
//
|
||||
@@ -1121,7 +1121,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// quickFetch,
|
||||
// readerioresult.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns (Data{Value: "quick"}, nil)
|
||||
// value, err := result(t.Context())() // Returns (Data{Value: "quick"}, nil)
|
||||
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
@@ -1173,12 +1173,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
// fetchData,
|
||||
// readerioresult.WithDeadline[Data](deadline),
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
|
||||
// value, err := result(t.Context())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
|
||||
//
|
||||
// Combining with Parent Context:
|
||||
//
|
||||
// // If parent context already has a deadline, the earlier one takes precedence
|
||||
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
|
||||
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
|
||||
// defer cancel()
|
||||
//
|
||||
// laterDeadline := time.Now().Add(2 * time.Hour)
|
||||
|
||||
@@ -36,56 +36,56 @@ func TestFromEither(t *testing.T) {
|
||||
t.Run("Right value", func(t *testing.T) {
|
||||
either := E.Right[error]("success")
|
||||
result := FromEither(either)
|
||||
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("success"), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Left value", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
either := E.Left[string](err)
|
||||
result := FromEither(either)
|
||||
assert.Equal(t, E.Left[string](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[string](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromResult(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
result := FromResult(E.Right[error](42))
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := FromResult(E.Left[int](err))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLeft(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := Left[string](err)
|
||||
assert.Equal(t, E.Left[string](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[string](err), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestRight(t *testing.T) {
|
||||
result := Right("success")
|
||||
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("success"), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
result := Of(42)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("Map over Right", func(t *testing.T) {
|
||||
result := MonadMap(Of(5), N.Mul(2))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Map over Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := MonadMap(Left[int](err), N.Mul(2))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,34 +93,34 @@ func TestMap(t *testing.T) {
|
||||
t.Run("Map with success", func(t *testing.T) {
|
||||
mapper := Map(N.Mul(2))
|
||||
result := mapper(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Map with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
mapper := Map(N.Mul(2))
|
||||
result := mapper(Left[int](err))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
t.Run("MapTo with success", func(t *testing.T) {
|
||||
result := MonadMapTo(Of("original"), 42)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("MapTo with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := MonadMapTo(Left[string](err), 42)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
mapper := MapTo[string](42)
|
||||
result := mapper(Of("original"))
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
@@ -128,7 +128,7 @@ func TestMonadChain(t *testing.T) {
|
||||
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
|
||||
return Of(x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Chain with error in first", func(t *testing.T) {
|
||||
@@ -136,7 +136,7 @@ func TestMonadChain(t *testing.T) {
|
||||
result := MonadChain(Left[int](err), func(x int) ReaderIOResult[int] {
|
||||
return Of(x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Chain with error in second", func(t *testing.T) {
|
||||
@@ -144,7 +144,7 @@ func TestMonadChain(t *testing.T) {
|
||||
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
|
||||
return Left[int](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestChain(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
@@ -161,7 +161,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
|
||||
return Of("ignored")
|
||||
})
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainFirst propagates error from second", func(t *testing.T) {
|
||||
@@ -169,7 +169,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
|
||||
return Left[string](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ func TestChainFirst(t *testing.T) {
|
||||
return Of("ignored")
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
@@ -186,7 +186,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
fab := Of(N.Mul(2))
|
||||
fa := Of(5)
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ApSeq with error in function", func(t *testing.T) {
|
||||
@@ -194,7 +194,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
fab := Left[func(int) int](err)
|
||||
fa := Of(5)
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ApSeq with error in value", func(t *testing.T) {
|
||||
@@ -202,7 +202,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
fab := Of(N.Mul(2))
|
||||
fa := Left[int](err)
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func TestApSeq(t *testing.T) {
|
||||
fa := Of(5)
|
||||
fab := Of(N.Mul(2))
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
@@ -218,11 +218,11 @@ func TestApPar(t *testing.T) {
|
||||
fa := Of(5)
|
||||
fab := Of(N.Mul(2))
|
||||
result := MonadApPar(fab, fa)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ApPar with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
fa := Of(5)
|
||||
fab := Of(N.Mul(2))
|
||||
@@ -239,7 +239,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
|
||||
)
|
||||
result := pred(5)
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Predicate false", func(t *testing.T) {
|
||||
@@ -248,7 +248,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
|
||||
)
|
||||
result := pred(-5)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsLeft(res))
|
||||
})
|
||||
}
|
||||
@@ -259,7 +259,7 @@ func TestOrElse(t *testing.T) {
|
||||
return Of(42)
|
||||
})
|
||||
result := fallback(Of(10))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("OrElse with error", func(t *testing.T) {
|
||||
@@ -268,13 +268,13 @@ func TestOrElse(t *testing.T) {
|
||||
return Of(42)
|
||||
})
|
||||
result := fallback(Left[int](err))
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
result := Ask()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
res := result(ctx)()
|
||||
assert.True(t, E.IsRight(res))
|
||||
ctxResult := E.ToOption(res)
|
||||
@@ -286,7 +286,7 @@ func TestMonadChainEitherK(t *testing.T) {
|
||||
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
|
||||
return E.Right[error](x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainEitherK with error", func(t *testing.T) {
|
||||
@@ -294,7 +294,7 @@ func TestMonadChainEitherK(t *testing.T) {
|
||||
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
|
||||
return E.Left[int](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ func TestChainEitherK(t *testing.T) {
|
||||
return E.Right[error](x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
@@ -311,7 +311,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
|
||||
return E.Right[error]("ignored")
|
||||
})
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainFirstEitherK propagates error", func(t *testing.T) {
|
||||
@@ -319,7 +319,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
|
||||
return E.Left[string](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ func TestChainFirstEitherK(t *testing.T) {
|
||||
return E.Right[error]("ignored")
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
@@ -339,7 +339,7 @@ func TestChainOptionK(t *testing.T) {
|
||||
return O.Some(x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainOptionK with None", func(t *testing.T) {
|
||||
@@ -349,7 +349,7 @@ func TestChainOptionK(t *testing.T) {
|
||||
return O.None[int]()
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsLeft(res))
|
||||
})
|
||||
}
|
||||
@@ -358,44 +358,44 @@ func TestFromIOEither(t *testing.T) {
|
||||
t.Run("FromIOEither with success", func(t *testing.T) {
|
||||
ioe := IOE.Of[error](42)
|
||||
result := FromIOEither(ioe)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("FromIOEither with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
ioe := IOE.Left[int](err)
|
||||
result := FromIOEither(ioe)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIOResult(t *testing.T) {
|
||||
ioe := IOE.Of[error](42)
|
||||
result := FromIOResult(ioe)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
io := IOG.Of(42)
|
||||
result := FromIO(io)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
reader := R.Of[context.Context](42)
|
||||
result := FromReader(reader)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFromLazy(t *testing.T) {
|
||||
lazy := func() int { return 42 }
|
||||
result := FromLazy(lazy)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestNever(t *testing.T) {
|
||||
t.Run("Never with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
result := Never[int]()
|
||||
|
||||
// Cancel immediately
|
||||
@@ -406,7 +406,7 @@ func TestNever(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Never with timeout", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
result := Never[int]()
|
||||
@@ -419,7 +419,7 @@ func TestMonadChainIOK(t *testing.T) {
|
||||
result := MonadChainIOK(Of(5), func(x int) IOG.IO[int] {
|
||||
return IOG.Of(x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainIOK(t *testing.T) {
|
||||
@@ -427,14 +427,14 @@ func TestChainIOK(t *testing.T) {
|
||||
return IOG.Of(x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
result := MonadChainFirstIOK(Of(5), func(x int) IOG.IO[string] {
|
||||
return IOG.Of("ignored")
|
||||
})
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainFirstIOK(t *testing.T) {
|
||||
@@ -442,7 +442,7 @@ func TestChainFirstIOK(t *testing.T) {
|
||||
return IOG.Of("ignored")
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainIOEitherK(t *testing.T) {
|
||||
@@ -451,7 +451,7 @@ func TestChainIOEitherK(t *testing.T) {
|
||||
return IOE.Of[error](x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainIOEitherK with error", func(t *testing.T) {
|
||||
@@ -460,7 +460,7 @@ func TestChainIOEitherK(t *testing.T) {
|
||||
return IOE.Left[int](err)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ func TestDelay(t *testing.T) {
|
||||
start := time.Now()
|
||||
delayed := Delay[int](100 * time.Millisecond)
|
||||
result := delayed(Of(42))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
assert.True(t, E.IsRight(res))
|
||||
@@ -477,7 +477,7 @@ func TestDelay(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Delay with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
delayed := Delay[int](100 * time.Millisecond)
|
||||
result := delayed(Of(42))
|
||||
@@ -500,11 +500,11 @@ func TestDefer(t *testing.T) {
|
||||
})
|
||||
|
||||
// First execution
|
||||
res1 := deferred(context.Background())()
|
||||
res1 := deferred(t.Context())()
|
||||
assert.True(t, E.IsRight(res1))
|
||||
|
||||
// Second execution should generate a new computation
|
||||
res2 := deferred(context.Background())()
|
||||
res2 := deferred(t.Context())()
|
||||
assert.True(t, E.IsRight(res2))
|
||||
|
||||
// Counter should be incremented for each execution
|
||||
@@ -518,7 +518,7 @@ func TestTryCatch(t *testing.T) {
|
||||
return 42, nil
|
||||
}
|
||||
})
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("TryCatch with error", func(t *testing.T) {
|
||||
@@ -528,7 +528,7 @@ func TestTryCatch(t *testing.T) {
|
||||
return 0, err
|
||||
}
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
first := Of(42)
|
||||
second := func() ReaderIOResult[int] { return Of(100) }
|
||||
result := MonadAlt(first, second)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Alt with first error", func(t *testing.T) {
|
||||
@@ -545,7 +545,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
first := Left[int](err)
|
||||
second := func() ReaderIOResult[int] { return Of(100) }
|
||||
result := MonadAlt(first, second)
|
||||
assert.Equal(t, E.Right[error](100), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](100), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ func TestAlt(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
alternative := Alt(func() ReaderIOResult[int] { return Of(100) })
|
||||
result := alternative(Left[int](err))
|
||||
assert.Equal(t, E.Right[error](100), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](100), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMemoize(t *testing.T) {
|
||||
@@ -564,13 +564,13 @@ func TestMemoize(t *testing.T) {
|
||||
}))
|
||||
|
||||
// First execution
|
||||
res1 := computation(context.Background())()
|
||||
res1 := computation(t.Context())()
|
||||
assert.True(t, E.IsRight(res1))
|
||||
val1 := E.ToOption(res1)
|
||||
assert.Equal(t, O.Of(1), val1)
|
||||
|
||||
// Second execution should return cached value
|
||||
res2 := computation(context.Background())()
|
||||
res2 := computation(t.Context())()
|
||||
assert.True(t, E.IsRight(res2))
|
||||
val2 := E.ToOption(res2)
|
||||
assert.Equal(t, O.Of(1), val2)
|
||||
@@ -582,19 +582,19 @@ func TestMemoize(t *testing.T) {
|
||||
func TestFlatten(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
result := Flatten(nested)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fab := Of(N.Mul(2))
|
||||
result := MonadFlap(fab, 5)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
flapper := Flap[int](5)
|
||||
result := flapper(Of(N.Mul(2)))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
@@ -608,7 +608,7 @@ func TestFold(t *testing.T) {
|
||||
},
|
||||
)
|
||||
result := folder(Of(42))
|
||||
assert.Equal(t, E.Right[error]("success: 42"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("success: 42"), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Fold with error", func(t *testing.T) {
|
||||
@@ -622,7 +622,7 @@ func TestFold(t *testing.T) {
|
||||
},
|
||||
)
|
||||
result := folder(Left[int](err))
|
||||
assert.Equal(t, E.Right[error]("error: test error"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("error: test error"), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -634,7 +634,7 @@ func TestGetOrElse(t *testing.T) {
|
||||
}
|
||||
})
|
||||
result := getter(Of(42))
|
||||
assert.Equal(t, 42, result(context.Background())())
|
||||
assert.Equal(t, 42, result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("GetOrElse with error", func(t *testing.T) {
|
||||
@@ -645,19 +645,19 @@ func TestGetOrElse(t *testing.T) {
|
||||
}
|
||||
})
|
||||
result := getter(Left[int](err))
|
||||
assert.Equal(t, 0, result(context.Background())())
|
||||
assert.Equal(t, 0, result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithContext(t *testing.T) {
|
||||
t.Run("WithContext with valid context", func(t *testing.T) {
|
||||
computation := WithContext(Of(42))
|
||||
result := computation(context.Background())()
|
||||
result := computation(t.Context())()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("WithContext with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
computation := WithContext(Of(42))
|
||||
@@ -672,7 +672,7 @@ func TestEitherize0(t *testing.T) {
|
||||
}
|
||||
eitherized := Eitherize0(f)
|
||||
result := eitherized()
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestUneitherize0(t *testing.T) {
|
||||
@@ -680,7 +680,7 @@ func TestUneitherize0(t *testing.T) {
|
||||
return Of(42)
|
||||
}
|
||||
uneitherized := Uneitherize0(f)
|
||||
result, err := uneitherized(context.Background())
|
||||
result, err := uneitherized(t.Context())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
@@ -691,7 +691,7 @@ func TestEitherize1(t *testing.T) {
|
||||
}
|
||||
eitherized := Eitherize1(f)
|
||||
result := eitherized(5)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestUneitherize1(t *testing.T) {
|
||||
@@ -699,14 +699,14 @@ func TestUneitherize1(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
}
|
||||
uneitherized := Uneitherize1(f)
|
||||
result, err := uneitherized(context.Background(), 5)
|
||||
result, err := uneitherized(t.Context(), 5)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
func TestSequenceT2(t *testing.T) {
|
||||
result := SequenceT2(Of(1), Of(2))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
tuple := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(tuple))
|
||||
@@ -717,13 +717,13 @@ func TestSequenceT2(t *testing.T) {
|
||||
|
||||
func TestSequenceSeqT2(t *testing.T) {
|
||||
result := SequenceSeqT2(Of(1), Of(2))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
}
|
||||
|
||||
func TestSequenceParT2(t *testing.T) {
|
||||
result := SequenceParT2(Of(1), Of(2))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
}
|
||||
|
||||
@@ -734,7 +734,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
})
|
||||
result := traverser(arr)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
arrOpt := E.ToOption(res)
|
||||
assert.Equal(t, O.Of([]int{2, 4, 6}), arrOpt)
|
||||
@@ -750,7 +750,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
})
|
||||
result := traverser(arr)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsLeft(res))
|
||||
})
|
||||
}
|
||||
@@ -758,7 +758,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
arr := []ReaderIOResult[int]{Of(1), Of(2), Of(3)}
|
||||
result := SequenceArray(arr)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
arrOpt := E.ToOption(res)
|
||||
assert.Equal(t, O.Of([]int{1, 2, 3}), arrOpt)
|
||||
@@ -769,7 +769,7 @@ func TestTraverseRecord(t *testing.T) {
|
||||
result := TraverseRecord[string](func(x int) ReaderIOResult[int] {
|
||||
return Of(x * 2)
|
||||
})(rec)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
recOpt := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(recOpt))
|
||||
@@ -784,7 +784,7 @@ func TestSequenceRecord(t *testing.T) {
|
||||
"b": Of(2),
|
||||
}
|
||||
result := SequenceRecord(rec)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
recOpt := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(recOpt))
|
||||
@@ -798,7 +798,7 @@ func TestAltSemigroup(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
|
||||
result := sg.Concat(Left[int](err), Of(42))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.Equal(t, E.Right[error](42), res)
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
))
|
||||
|
||||
result := intAddMonoid.Concat(Of(5), Of(10))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.Equal(t, E.Right[error](15), res)
|
||||
}
|
||||
|
||||
@@ -835,7 +835,7 @@ func TestBracket(t *testing.T) {
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, released)
|
||||
@@ -863,7 +863,7 @@ func TestBracket(t *testing.T) {
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, released)
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
func TestInnerContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, parentCancel := context.WithCancel(outer)
|
||||
defer parentCancel()
|
||||
@@ -49,7 +49,7 @@ func TestInnerContextCancelSemantics(t *testing.T) {
|
||||
|
||||
func TestOuterContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, outerCancel := context.WithCancel(outer)
|
||||
defer outerCancel()
|
||||
@@ -69,7 +69,7 @@ func TestOuterContextCancelSemantics(t *testing.T) {
|
||||
|
||||
func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, outerCancel := context.WithCancel(outer)
|
||||
defer outerCancel()
|
||||
@@ -95,7 +95,7 @@ func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
|
||||
|
||||
func TestCancelCauseSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, outerCancel := context.WithCancelCause(outer)
|
||||
defer outerCancel(nil)
|
||||
@@ -119,7 +119,7 @@ func TestCancelCauseSemantics(t *testing.T) {
|
||||
func TestTimer(t *testing.T) {
|
||||
delta := 3 * time.Second
|
||||
timer := Timer(delta)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
t0 := time.Now()
|
||||
res := timer(ctx)()
|
||||
@@ -146,7 +146,7 @@ func TestCanceledApply(t *testing.T) {
|
||||
Ap[string](errValue),
|
||||
)
|
||||
|
||||
res := applied(context.Background())()
|
||||
res := applied(t.Context())()
|
||||
assert.Equal(t, E.Left[string](err), res)
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestRegularApply(t *testing.T) {
|
||||
Ap[string](value),
|
||||
)
|
||||
|
||||
res := applied(context.Background())()
|
||||
res := applied(t.Context())()
|
||||
assert.Equal(t, E.Of[error]("CARSTEN"), res)
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ func TestWithResourceNoErrors(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, countAcquire)
|
||||
assert.Equal(t, 1, countBody)
|
||||
@@ -217,7 +217,7 @@ func TestWithResourceErrorInBody(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, countAcquire)
|
||||
assert.Equal(t, 0, countBody)
|
||||
@@ -247,7 +247,7 @@ func TestWithResourceErrorInAcquire(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 0, countAcquire)
|
||||
assert.Equal(t, 0, countBody)
|
||||
@@ -277,7 +277,7 @@ func TestWithResourceErrorInRelease(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, countAcquire)
|
||||
assert.Equal(t, 1, countBody)
|
||||
@@ -286,7 +286,7 @@ func TestWithResourceErrorInRelease(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainFirstLeft(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Left value - function returns Left, always preserves original error
|
||||
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
|
||||
@@ -353,7 +353,7 @@ func TestMonadChainFirstLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChainFirstLeft(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Left value - function returns Left, always preserves original error
|
||||
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {
|
||||
|
||||
@@ -108,7 +108,7 @@ import (
|
||||
// countdown := readerioresult.TailRec(countdownStep)
|
||||
//
|
||||
// // With cancellation
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
|
||||
// defer cancel()
|
||||
// result := countdown(10)(ctx)() // Will be cancelled after ~500ms
|
||||
//
|
||||
@@ -141,7 +141,7 @@ import (
|
||||
// }
|
||||
//
|
||||
// processFiles := readerioresult.TailRec(processStep)
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
//
|
||||
// // Can be cancelled at any point during processing
|
||||
// go func() {
|
||||
@@ -159,7 +159,7 @@ import (
|
||||
//
|
||||
// // Safe for very large inputs with cancellation support
|
||||
// largeCountdown := readerioresult.TailRec(countdownStep)
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
|
||||
//
|
||||
// # Performance Considerations
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestTailRec_BasicRecursion(t *testing.T) {
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(5)(context.Background())()
|
||||
result := countdown(5)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func TestTailRec_FactorialRecursion(t *testing.T) {
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result := factorial(FactorialState{n: 5, acc: 1})(context.Background())()
|
||||
result := factorial(FactorialState{n: 5, acc: 1})(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](120), result) // 5! = 120
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func TestTailRec_ErrorHandling(t *testing.T) {
|
||||
}
|
||||
|
||||
errorRecursion := TailRec(errorStep)
|
||||
result := errorRecursion(5)(context.Background())()
|
||||
result := errorRecursion(5)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
@@ -125,7 +125,7 @@ func TestTailRec_ContextCancellation(t *testing.T) {
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Create a context that will be cancelled after 100ms
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
@@ -159,7 +159,7 @@ func TestTailRec_ImmediateCancellation(t *testing.T) {
|
||||
countdown := TailRec(countdownStep)
|
||||
|
||||
// Create an already cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := countdown(5)(ctx)()
|
||||
@@ -186,7 +186,7 @@ func TestTailRec_StackSafety(t *testing.T) {
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(largeN)(context.Background())()
|
||||
result := countdown(largeN)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](0), result)
|
||||
}
|
||||
@@ -217,7 +217,7 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
|
||||
countdown := TailRec(countdownStep)
|
||||
|
||||
// Cancel after 50ms to allow some iterations but not all
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
result := countdown(largeN)(ctx)()
|
||||
@@ -274,7 +274,7 @@ func TestTailRec_ComplexState(t *testing.T) {
|
||||
errors: []error{},
|
||||
}
|
||||
|
||||
result := processItems(initialState)(context.Background())()
|
||||
result := processItems(initialState)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error]([]string{"item1", "item2", "item3"}), result)
|
||||
})
|
||||
@@ -286,7 +286,7 @@ func TestTailRec_ComplexState(t *testing.T) {
|
||||
errors: []error{},
|
||||
}
|
||||
|
||||
result := processItems(initialState)(context.Background())()
|
||||
result := processItems(initialState)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
@@ -336,7 +336,7 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
|
||||
}
|
||||
|
||||
// Cancel after 100ms (should allow ~5 files to be processed)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
@@ -366,7 +366,7 @@ func TestTailRec_ZeroIterations(t *testing.T) {
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result := immediate(100)(context.Background())()
|
||||
result := immediate(100)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error]("immediate"), result)
|
||||
}
|
||||
@@ -392,7 +392,7 @@ func TestTailRec_ContextWithDeadline(t *testing.T) {
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Set deadline 80ms from now
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(80*time.Millisecond))
|
||||
ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(80*time.Millisecond))
|
||||
defer cancel()
|
||||
|
||||
result := slowRecursion(10)(ctx)()
|
||||
@@ -427,7 +427,7 @@ func TestTailRec_ContextWithValue(t *testing.T) {
|
||||
}
|
||||
|
||||
valueRecursion := TailRec(valueStep)
|
||||
ctx := context.WithValue(context.Background(), testKey, "test-value")
|
||||
ctx := context.WithValue(t.Context(), testKey, "test-value")
|
||||
result := valueRecursion(3)(ctx)()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
|
||||
@@ -107,7 +107,7 @@ import (
|
||||
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
|
||||
//
|
||||
// // Execute with a cancellable context
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
// defer cancel()
|
||||
// ioResult := retryingFetch(ctx)
|
||||
// finalResult := ioResult()
|
||||
|
||||
@@ -306,7 +306,7 @@ func TestBindReaderIOK(t *testing.T) {
|
||||
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{Value: 10}),
|
||||
BindReaderIOK[AppConfig](
|
||||
BindReaderIOK(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
@@ -662,7 +662,7 @@ func TestApOperations(t *testing.T) {
|
||||
t.Run("ApReaderS", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApReaderS[AppConfig](
|
||||
ApReaderS(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
@@ -681,7 +681,7 @@ func TestApOperations(t *testing.T) {
|
||||
t.Run("ApReaderIOS", func(t *testing.T) {
|
||||
res := F.Pipe2(
|
||||
Do[AppConfig](State{}),
|
||||
ApReaderIOS[AppConfig](
|
||||
ApReaderIOS(
|
||||
func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value1 = v
|
||||
|
||||
@@ -87,9 +87,8 @@ import (
|
||||
//go:inline
|
||||
func Bracket[
|
||||
R, A, B, ANY any](
|
||||
|
||||
acquire ReaderReaderIOResult[R, A],
|
||||
use func(A) ReaderReaderIOResult[R, B],
|
||||
use Kleisli[R, A, B],
|
||||
release func(A, Result[B]) ReaderReaderIOResult[R, ANY],
|
||||
) ReaderReaderIOResult[R, B] {
|
||||
return RRIOE.Bracket(acquire, use, release)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"time"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -54,7 +55,7 @@ func TestContextCancellationInChain(t *testing.T) {
|
||||
executed := false
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
Chain[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
@@ -231,7 +232,7 @@ func TestContextPropagationThroughMonadTransforms(t *testing.T) {
|
||||
var capturedCtx context.Context
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
Chain[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
@@ -255,7 +256,7 @@ func TestContextPropagationThroughMonadTransforms(t *testing.T) {
|
||||
return func(ctx context.Context) IOResult[func(int) int] {
|
||||
return func() Result[func(int) int] {
|
||||
capturedCtx = ctx
|
||||
return result.Of(func(n int) int { return n * 2 })
|
||||
return result.Of(N.Mul(2))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,7 +405,7 @@ func TestContextCancellationBetweenSteps(t *testing.T) {
|
||||
}
|
||||
}
|
||||
},
|
||||
Chain[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return func(c AppConfig) ReaderIOResult[context.Context, int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
|
||||
76
v2/context/readerreaderioresult/di_test.go
Normal file
76
v2/context/readerreaderioresult/di_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
RES "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type (
|
||||
ConsoleDependency interface {
|
||||
Log(msg string) IO[Void]
|
||||
}
|
||||
|
||||
Res[A any] = RES.ReaderIOResult[A]
|
||||
|
||||
ConsoleEnv[A any] = ReaderReaderIOResult[ConsoleDependency, A]
|
||||
|
||||
consoleOnArray struct {
|
||||
logs []string
|
||||
mu sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
logConsole = reader.Curry1(ConsoleDependency.Log)
|
||||
)
|
||||
|
||||
func (c *consoleOnArray) Log(msg string) IO[Void] {
|
||||
return func() Void {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logs = append(c.logs, msg)
|
||||
return function.VOID
|
||||
}
|
||||
}
|
||||
|
||||
func makeConsoleOnArray() *consoleOnArray {
|
||||
return &consoleOnArray{}
|
||||
}
|
||||
|
||||
func TestConsoleEnv(t *testing.T) {
|
||||
console := makeConsoleOnArray()
|
||||
|
||||
prg := F.Pipe1(
|
||||
Of[ConsoleDependency]("Hello World!"),
|
||||
TapReaderIOK(logConsole),
|
||||
)
|
||||
|
||||
res := prg(console)(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("Hello World!"), res)
|
||||
assert.Equal(t, A.Of("Hello World!"), console.logs)
|
||||
}
|
||||
|
||||
func TestConsoleEnvWithLocal(t *testing.T) {
|
||||
console := makeConsoleOnArray()
|
||||
|
||||
prg := F.Pipe1(
|
||||
Of[ConsoleDependency](42),
|
||||
TapReaderIOK(reader.WithLocal(logConsole, strconv.Itoa)),
|
||||
)
|
||||
|
||||
res := prg(console)(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
assert.Equal(t, A.Of("42"), console.logs)
|
||||
}
|
||||
@@ -238,6 +238,236 @@
|
||||
// - Retry logic with policy configuration and execution context
|
||||
// - Resource management with bracket pattern across multiple contexts
|
||||
//
|
||||
// # Dependency Injection with the Outer Context
|
||||
//
|
||||
// The outer Reader context (type parameter R) provides a powerful mechanism for dependency injection
|
||||
// in functional programming. This pattern is explained in detail in Scott Wlaschin's talk:
|
||||
// "Dependency Injection, The Functional Way" - https://www.youtube.com/watch?v=xPlsVVaMoB0
|
||||
//
|
||||
// ## Core Concept
|
||||
//
|
||||
// Instead of using traditional OOP dependency injection frameworks, the Reader monad allows you to:
|
||||
// 1. Define functions that declare their dependencies as type parameters
|
||||
// 2. Compose these functions without providing the dependencies
|
||||
// 3. Supply all dependencies at the "end of the world" (program entry point)
|
||||
//
|
||||
// This approach provides:
|
||||
// - Compile-time safety: Missing dependencies cause compilation errors
|
||||
// - Explicit dependencies: Function signatures show exactly what they need
|
||||
// - Easy testing: Mock dependencies by providing different values
|
||||
// - Pure functions: Dependencies are passed as parameters, not global state
|
||||
//
|
||||
// ## Examples from the Video Adapted to fp-go
|
||||
//
|
||||
// ### Example 1: Basic Reader Pattern (Video: "Reader Monad Basics")
|
||||
//
|
||||
// In the video, Scott shows how to pass configuration through a chain of functions.
|
||||
// In fp-go with ReaderReaderIOResult:
|
||||
//
|
||||
// // Define your dependencies
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// APIKey string
|
||||
// MaxRetries int
|
||||
// }
|
||||
//
|
||||
// // Functions declare their dependencies via the R type parameter
|
||||
// func getConnectionString() ReaderReaderIOResult[AppConfig, string] {
|
||||
// return Asks[AppConfig](func(cfg AppConfig) string {
|
||||
// return cfg.DatabaseURL
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// func connectToDatabase() ReaderReaderIOResult[AppConfig, *sql.DB] {
|
||||
// return MonadChain(
|
||||
// getConnectionString(),
|
||||
// func(connStr string) ReaderReaderIOResult[AppConfig, *sql.DB] {
|
||||
// return FromIO[AppConfig](func() result.Result[*sql.DB] {
|
||||
// db, err := sql.Open("postgres", connStr)
|
||||
// return result.FromEither(either.FromError(db, err))
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// ### Example 2: Composing Dependencies (Video: "Composing Reader Functions")
|
||||
//
|
||||
// The video demonstrates how Reader functions compose naturally.
|
||||
// In fp-go, you can compose operations that all share the same dependency:
|
||||
//
|
||||
// func fetchUser(id int) ReaderReaderIOResult[AppConfig, User] {
|
||||
// return MonadChain(
|
||||
// connectToDatabase(),
|
||||
// func(db *sql.DB) ReaderReaderIOResult[AppConfig, User] {
|
||||
// return FromIO[AppConfig](func() result.Result[User] {
|
||||
// // Query database using db and return user
|
||||
// // The AppConfig is still available if needed
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// func enrichUser(user User) ReaderReaderIOResult[AppConfig, EnrichedUser] {
|
||||
// return Asks[AppConfig, EnrichedUser](func(cfg AppConfig) EnrichedUser {
|
||||
// // Use cfg.APIKey to call external service
|
||||
// return EnrichedUser{User: user, Extra: "data"}
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Compose without providing dependencies
|
||||
// pipeline := function.Pipe2(
|
||||
// fetchUser(123),
|
||||
// Chain[AppConfig](enrichUser),
|
||||
// )
|
||||
//
|
||||
// // Provide dependencies at the end
|
||||
// config := AppConfig{DatabaseURL: "...", APIKey: "...", MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
// result := pipeline(config)(ctx)()
|
||||
//
|
||||
// ### Example 3: Local Context Modification (Video: "Local Environment")
|
||||
//
|
||||
// The video shows how to temporarily modify the environment for a sub-computation.
|
||||
// In fp-go, use the Local function:
|
||||
//
|
||||
// // Run a computation with modified configuration
|
||||
// func withRetries(retries int, action ReaderReaderIOResult[AppConfig, string]) ReaderReaderIOResult[AppConfig, string] {
|
||||
// return Local[string](func(cfg AppConfig) AppConfig {
|
||||
// // Create a modified config with different retry count
|
||||
// return AppConfig{
|
||||
// DatabaseURL: cfg.DatabaseURL,
|
||||
// APIKey: cfg.APIKey,
|
||||
// MaxRetries: retries,
|
||||
// }
|
||||
// })(action)
|
||||
// }
|
||||
//
|
||||
// // Use it
|
||||
// result := withRetries(5, fetchUser(123))
|
||||
//
|
||||
// ### Example 4: Testing with Mock Dependencies (Video: "Testing with Reader")
|
||||
//
|
||||
// The video emphasizes how Reader makes testing easy by allowing mock dependencies.
|
||||
// In fp-go:
|
||||
//
|
||||
// func TestFetchUser(t *testing.T) {
|
||||
// // Create a test configuration
|
||||
// testConfig := AppConfig{
|
||||
// DatabaseURL: "mock://test",
|
||||
// APIKey: "test-key",
|
||||
// MaxRetries: 1,
|
||||
// }
|
||||
//
|
||||
// // Run the computation with test config
|
||||
// ctx := context.Background()
|
||||
// result := fetchUser(123)(testConfig)(ctx)()
|
||||
//
|
||||
// // Assert on the result
|
||||
// assert.True(t, either.IsRight(result))
|
||||
// }
|
||||
//
|
||||
// ### Example 5: Multi-Layer Dependencies (Video: "Nested Readers")
|
||||
//
|
||||
// The video discusses nested readers for multi-layer architectures.
|
||||
// ReaderReaderIOResult provides exactly this with R (outer) and context.Context (inner):
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // Outer context: Application-level configuration (AppConfig)
|
||||
// // Inner context: Request-level context (context.Context)
|
||||
// func handleRequest(userID int) ReaderReaderIOResult[AppConfig, Response] {
|
||||
// return func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, Response] {
|
||||
// // cfg is available here (outer context)
|
||||
// return func(ctx context.Context) ioresult.IOResult[Response] {
|
||||
// // ctx is available here (inner context)
|
||||
// // Both cfg and ctx can be used
|
||||
// return func() result.Result[Response] {
|
||||
// // Perform operation using both contexts
|
||||
// select {
|
||||
// case <-ctx.Done():
|
||||
// return result.Error[Response](ctx.Err())
|
||||
// default:
|
||||
// // Use cfg.DatabaseURL to connect
|
||||
// return result.Of(Response{})
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ### Example 6: Avoiding Global State (Video: "Problems with Global State")
|
||||
//
|
||||
// The video criticizes global state and shows how Reader solves this.
|
||||
// In fp-go, instead of:
|
||||
//
|
||||
// // BAD: Global state
|
||||
// var globalConfig AppConfig
|
||||
//
|
||||
// func fetchUser(id int) result.Result[User] {
|
||||
// // Uses globalConfig implicitly
|
||||
// db := connectTo(globalConfig.DatabaseURL)
|
||||
// // ...
|
||||
// }
|
||||
//
|
||||
// Use Reader to make dependencies explicit:
|
||||
//
|
||||
// // GOOD: Explicit dependencies
|
||||
// func fetchUser(id int) ReaderReaderIOResult[AppConfig, User] {
|
||||
// return MonadChain(
|
||||
// Ask[AppConfig](), // Explicitly request the config
|
||||
// func(cfg AppConfig) ReaderReaderIOResult[AppConfig, User] {
|
||||
// // Use cfg explicitly
|
||||
// return FromIO[AppConfig](func() result.Result[User] {
|
||||
// db := connectTo(cfg.DatabaseURL)
|
||||
// // ...
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// ## Benefits of This Approach
|
||||
//
|
||||
// 1. **Type Safety**: The compiler ensures all dependencies are provided
|
||||
// 2. **Testability**: Easy to provide mock dependencies for testing
|
||||
// 3. **Composability**: Functions compose naturally without dependency wiring
|
||||
// 4. **Explicitness**: Function signatures document their dependencies
|
||||
// 5. **Immutability**: Dependencies are immutable values, not mutable global state
|
||||
// 6. **Flexibility**: Use Local to modify dependencies for sub-computations
|
||||
// 7. **Separation of Concerns**: Business logic is separate from dependency resolution
|
||||
//
|
||||
// ## Comparison with Traditional DI
|
||||
//
|
||||
// Traditional OOP DI (e.g., Spring, Guice):
|
||||
// - Runtime dependency resolution
|
||||
// - Magic/reflection-based wiring
|
||||
// - Implicit dependencies (hidden in constructors)
|
||||
// - Mutable containers
|
||||
//
|
||||
// Reader-based DI (fp-go):
|
||||
// - Compile-time dependency resolution
|
||||
// - Explicit function composition
|
||||
// - Explicit dependencies (in type signatures)
|
||||
// - Immutable values
|
||||
//
|
||||
// ## When to Use Each Layer
|
||||
//
|
||||
// - **Outer Reader (R)**: Application-level dependencies that rarely change
|
||||
// - Database connection pools
|
||||
// - API keys and secrets
|
||||
// - Feature flags
|
||||
// - Application configuration
|
||||
//
|
||||
// - **Inner Reader (context.Context)**: Request-level dependencies that change per operation
|
||||
// - Request IDs and tracing
|
||||
// - Cancellation signals
|
||||
// - Deadlines and timeouts
|
||||
// - User authentication tokens
|
||||
//
|
||||
// This two-layer approach mirrors the video's discussion of nested readers and provides
|
||||
// a clean separation between application-level and request-level concerns.
|
||||
//
|
||||
// # Relationship to Other Packages
|
||||
//
|
||||
// - readerreaderioeither: The generic version with configurable error and context types
|
||||
|
||||
291
v2/context/readerreaderioresult/flip.go
Normal file
291
v2/context/readerreaderioresult/flip.go
Normal file
@@ -0,0 +1,291 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerioeither"
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
)
|
||||
|
||||
// Sequence swaps the order of nested environment parameters in a ReaderReaderIOResult computation.
|
||||
//
|
||||
// This function takes a ReaderReaderIOResult that produces another ReaderReaderIOResult and returns a
|
||||
// Kleisli arrow that reverses the order of the outer environment parameters (R1 and R2). The result is
|
||||
// a curried function that takes R1 first, then R2, and produces a computation with context.Context and error handling.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The first outer environment type (becomes the outermost after sequence)
|
||||
// - R2: The second outer environment type (becomes inner after sequence)
|
||||
// - A: The success value type
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: A ReaderReaderIOResult[R2, ReaderReaderIOResult[R1, A]]
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli[R2, R1, A], which is func(R1) ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
// The function preserves error handling and IO effects at all levels while reordering the
|
||||
// outer environment dependencies. The inner context.Context layer remains unchanged.
|
||||
//
|
||||
// This is particularly useful when you need to change the order in which contexts are provided
|
||||
// to a nested computation, such as when composing operations that have different dependency orders.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
// type UserPrefs struct {
|
||||
// Theme string
|
||||
// }
|
||||
//
|
||||
// // Original: takes AppConfig, returns computation that may produce
|
||||
// // another computation depending on UserPrefs
|
||||
// original := func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context,
|
||||
// ReaderReaderIOResult[UserPrefs, string]] {
|
||||
// return readerioresult.Of[context.Context](
|
||||
// Of[UserPrefs]("result"),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // Sequence swaps UserPrefs and AppConfig order
|
||||
// sequenced := Sequence[UserPrefs, AppConfig, string](original)
|
||||
//
|
||||
// // Now provide UserPrefs first, then AppConfig
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(UserPrefs{Theme: "dark"})(AppConfig{DatabaseURL: "db"})(ctx)()
|
||||
func Sequence[R1, R2, A any](ma ReaderReaderIOResult[R2, ReaderReaderIOResult[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return readert.Sequence(
|
||||
readerioeither.Chain,
|
||||
ma,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceReader swaps the order of environment parameters when the inner computation is a pure Reader.
|
||||
//
|
||||
// This function is similar to Sequence but specialized for the case where the innermost computation
|
||||
// is a pure Reader (without IO or error handling) rather than another ReaderReaderIOResult. It takes
|
||||
// a ReaderReaderIOResult that produces a Reader and returns a Kleisli arrow that reverses the order
|
||||
// of the outer environment parameters.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The first environment type (becomes outermost after sequence)
|
||||
// - R2: The second environment type (becomes inner after sequence)
|
||||
// - A: The success value type
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: A ReaderReaderIOResult[R2, Reader[R1, A]]
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli[R2, R1, A], which is func(R1) ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
// The function lifts the pure Reader computation into the ReaderIOResult context (with context.Context
|
||||
// and error handling) while reordering the environment dependencies.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// Multiplier int
|
||||
// }
|
||||
// type Database struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
//
|
||||
// // Original: takes AppConfig, may produce a Reader[Database, int]
|
||||
// original := func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, reader.Reader[Database, int]] {
|
||||
// return readerioresult.Of[context.Context](func(db Database) int {
|
||||
// return len(db.ConnectionString) * cfg.Multiplier
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Sequence to provide Database first, then AppConfig
|
||||
// sequenced := SequenceReader[Database, AppConfig, int](original)
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(Database{ConnectionString: "localhost"})(AppConfig{Multiplier: 2})(ctx)()
|
||||
func SequenceReader[R1, R2, A any](ma ReaderReaderIOResult[R2, Reader[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return readert.SequenceReader(
|
||||
readerioeither.Map,
|
||||
ma,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceReaderIO swaps the order of environment parameters when the inner computation is a ReaderIO.
|
||||
//
|
||||
// This function is specialized for the case where the innermost computation is a ReaderIO
|
||||
// (with IO effects but no error handling) rather than another ReaderReaderIOResult. It takes
|
||||
// a ReaderReaderIOResult that produces a ReaderIO and returns a Kleisli arrow that reverses
|
||||
// the order of the outer environment parameters.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The first environment type (becomes outermost after sequence)
|
||||
// - R2: The second environment type (becomes inner after sequence)
|
||||
// - A: The success value type
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: A ReaderReaderIOResult[R2, ReaderIO[R1, A]]
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli[R2, R1, A], which is func(R1) ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
// The function lifts the ReaderIO computation (which has IO effects but no error handling)
|
||||
// into the ReaderIOResult context (with context.Context and error handling) while reordering
|
||||
// the environment dependencies.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// FilePath string
|
||||
// }
|
||||
// type Logger struct {
|
||||
// Level string
|
||||
// }
|
||||
//
|
||||
// // Original: takes AppConfig, may produce a ReaderIO[Logger, string]
|
||||
// original := func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, readerio.ReaderIO[Logger, string]] {
|
||||
// return readerioresult.Of[context.Context](func(logger Logger) io.IO[string] {
|
||||
// return func() string {
|
||||
// return fmt.Sprintf("[%s] Reading from %s", logger.Level, cfg.FilePath)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Sequence to provide Logger first, then AppConfig
|
||||
// sequenced := SequenceReaderIO[Logger, AppConfig, string](original)
|
||||
// ctx := context.Background()
|
||||
// result := sequenced(Logger{Level: "INFO"})(AppConfig{FilePath: "/data"})(ctx)()
|
||||
func SequenceReaderIO[R1, R2, A any](ma ReaderReaderIOResult[R2, ReaderIO[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return RRIOE.SequenceReaderIO(ma)
|
||||
}
|
||||
|
||||
// Traverse transforms a ReaderReaderIOResult computation by applying a function that produces
|
||||
// another ReaderReaderIOResult, effectively swapping the order of outer environment parameters.
|
||||
//
|
||||
// This function is useful when you have a computation that depends on environment R2 and
|
||||
// produces a value of type A, and you want to transform it using a function that takes A
|
||||
// and produces a computation depending on environment R1. The result is a curried function
|
||||
// that takes R1 first, then R2, and produces a computation with context.Context and error handling.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R2: The outer environment type from the original computation
|
||||
// - R1: The inner environment type introduced by the transformation
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow that transforms A into a ReaderReaderIOResult[R1, B]
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R2, A] and returns a Kleisli[R2, R1, B],
|
||||
// which is func(R1) ReaderReaderIOResult[R2, B]
|
||||
//
|
||||
// The function preserves error handling and IO effects while reordering the environment dependencies.
|
||||
// This is the generalized version of Sequence that also applies a transformation function.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// SystemID string
|
||||
// }
|
||||
// type UserConfig struct {
|
||||
// UserID int
|
||||
// }
|
||||
//
|
||||
// // Original computation depending on AppConfig
|
||||
// original := Of[AppConfig](42)
|
||||
//
|
||||
// // Transformation that introduces UserConfig dependency
|
||||
// transform := func(n int) ReaderReaderIOResult[UserConfig, string] {
|
||||
// return func(userCfg UserConfig) readerioresult.ReaderIOResult[context.Context, string] {
|
||||
// return readerioresult.Of[context.Context](fmt.Sprintf("User %d: %d", userCfg.UserID, n))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Apply traverse to swap order and transform
|
||||
// traversed := Traverse[AppConfig, UserConfig, int, string](transform)(original)
|
||||
//
|
||||
// // Provide UserConfig first, then AppConfig
|
||||
// ctx := context.Background()
|
||||
// result := traversed(UserConfig{UserID: 1})(AppConfig{SystemID: "sys1"})(ctx)()
|
||||
func Traverse[R2, R1, A, B any](
|
||||
f Kleisli[R1, A, B],
|
||||
) func(ReaderReaderIOResult[R2, A]) Kleisli[R2, R1, B] {
|
||||
return readert.Traverse[ReaderReaderIOResult[R2, A]](
|
||||
readerioeither.Map,
|
||||
readerioeither.Chain,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseReader transforms a ReaderReaderIOResult computation by applying a Reader-based function,
|
||||
// effectively introducing a new environment dependency.
|
||||
//
|
||||
// This function takes a Reader-based transformation (Kleisli arrow) and returns a function that
|
||||
// can transform a ReaderReaderIOResult. The result allows you to provide the Reader's environment (R1)
|
||||
// first, which then produces a ReaderReaderIOResult that depends on environment R2.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R2: The outer environment type from the original ReaderReaderIOResult
|
||||
// - R1: The inner environment type introduced by the Reader transformation
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Reader-based Kleisli arrow that transforms A to B using environment R1
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R2, A] and returns a Kleisli[R2, R1, B],
|
||||
// which is func(R1) ReaderReaderIOResult[R2, B]
|
||||
//
|
||||
// The function preserves error handling and IO effects while adding the Reader environment dependency
|
||||
// and reordering the environment parameters. This is useful when you want to introduce a pure
|
||||
// (non-IO, non-error) environment dependency to an existing computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// Timeout int
|
||||
// }
|
||||
// type UserPreferences struct {
|
||||
// Theme string
|
||||
// }
|
||||
//
|
||||
// // Original computation depending on AppConfig
|
||||
// original := Of[AppConfig](100)
|
||||
//
|
||||
// // Pure Reader transformation that introduces UserPreferences dependency
|
||||
// formatWithTheme := func(value int) reader.Reader[UserPreferences, string] {
|
||||
// return func(prefs UserPreferences) string {
|
||||
// return fmt.Sprintf("[%s theme] Value: %d", prefs.Theme, value)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Apply traverse to introduce UserPreferences and swap order
|
||||
// traversed := TraverseReader[AppConfig, UserPreferences, int, string](formatWithTheme)(original)
|
||||
//
|
||||
// // Provide UserPreferences first, then AppConfig
|
||||
// ctx := context.Background()
|
||||
// result := traversed(UserPreferences{Theme: "dark"})(AppConfig{Timeout: 30})(ctx)()
|
||||
func TraverseReader[R2, R1, A, B any](
|
||||
f reader.Kleisli[R1, A, B],
|
||||
) func(ReaderReaderIOResult[R2, A]) Kleisli[R2, R1, B] {
|
||||
return readert.TraverseReader[ReaderReaderIOResult[R2, A]](
|
||||
readerioeither.Map,
|
||||
readerioeither.Map,
|
||||
f,
|
||||
)
|
||||
}
|
||||
778
v2/context/readerreaderioresult/flip_test.go
Normal file
778
v2/context/readerreaderioresult/flip_test.go
Normal file
@@ -0,0 +1,778 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Config1 struct {
|
||||
value1 int
|
||||
}
|
||||
|
||||
type Config2 struct {
|
||||
value2 string
|
||||
}
|
||||
|
||||
func TestSequence(t *testing.T) {
|
||||
t.Run("swaps parameter order for simple types", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original: takes Config2, returns ReaderIOResult that may produce ReaderReaderIOResult[Config1, int]
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx1 context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[int] {
|
||||
return func(ctx2 context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Of(cfg1.value1 + len(cfg2.value2))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// Test original: Config2 -> Context -> Config1 -> Context
|
||||
result1 := original(cfg2)(ctx)()
|
||||
assert.True(t, result.IsRight(result1))
|
||||
innerFunc1, _ := result.Unwrap(result1)
|
||||
innerResult1 := innerFunc1(cfg1)(ctx)()
|
||||
assert.Equal(t, result.Of(15), innerResult1)
|
||||
|
||||
// Test sequenced: Config1 -> Config2 -> Context
|
||||
innerFunc2 := sequenced(cfg1)
|
||||
innerResult2 := innerFunc2(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(15), innerResult2)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
// Original that returns an error
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Left[ReaderReaderIOResult[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// Test sequenced preserves error
|
||||
innerFunc := sequenced(cfg1)
|
||||
outcome := innerFunc(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with nested computations", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original with nested logic
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, string]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, string]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, string]] {
|
||||
if len(cfg2.value2) == 0 {
|
||||
return result.Left[ReaderReaderIOResult[Config1, string]](errors.New("empty string"))
|
||||
}
|
||||
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
if cfg1.value1 < 0 {
|
||||
return result.Left[string](errors.New("negative value"))
|
||||
}
|
||||
return result.Of(fmt.Sprintf("%s:%d", cfg2.value2, cfg1.value1))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence(original)
|
||||
|
||||
// Test with valid inputs
|
||||
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of("test:42"), result1)
|
||||
|
||||
// Test with empty string
|
||||
result2 := sequenced(Config1{value1: 42})(Config2{value2: ""})(ctx)()
|
||||
assert.True(t, result.IsLeft(result2))
|
||||
|
||||
// Test with negative value
|
||||
result3 := sequenced(Config1{value1: -1})(Config2{value2: "test"})(ctx)()
|
||||
assert.True(t, result.IsLeft(result3))
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Of(cfg1.value1 + len(cfg2.value2))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
|
||||
t.Run("maintains referential transparency", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Of(cfg1.value1 * len(cfg2.value2))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := Sequence(original)
|
||||
|
||||
cfg1 := Config1{value1: 3}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
// Call multiple times with same inputs
|
||||
for range 5 {
|
||||
outcome := sequenced(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(12), outcome)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceReader(t *testing.T) {
|
||||
t.Run("swaps parameter order for Reader types", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original: takes Config2, returns ReaderIOResult that may produce Reader[Config1, int]
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
|
||||
return func() Result[Reader[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) int {
|
||||
return cfg1.value1 + len(cfg2.value2)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// Test original
|
||||
result1 := original(cfg2)(ctx)()
|
||||
assert.True(t, result.IsRight(result1))
|
||||
innerFunc1, _ := result.Unwrap(result1)
|
||||
value1 := innerFunc1(cfg1)
|
||||
assert.Equal(t, 15, value1)
|
||||
|
||||
// Test sequenced
|
||||
innerFunc2 := sequenced(cfg1)
|
||||
result2 := innerFunc2(cfg2)(ctx)()
|
||||
assert.True(t, result.IsRight(result2))
|
||||
value2, _ := result.Unwrap(result2)
|
||||
assert.Equal(t, 15, value2)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
|
||||
return func() Result[Reader[Config1, int]] {
|
||||
return result.Left[Reader[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 10})(Config2{value2: "hello"})(ctx)()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with pure Reader computations", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, string]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, string]] {
|
||||
return func() Result[Reader[Config1, string]] {
|
||||
if len(cfg2.value2) == 0 {
|
||||
return result.Left[Reader[Config1, string]](errors.New("empty string"))
|
||||
}
|
||||
return result.Of(func(cfg1 Config1) string {
|
||||
return fmt.Sprintf("%s:%d", cfg2.value2, cfg1.value1)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test with valid inputs
|
||||
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of("test:42"), result1)
|
||||
|
||||
// Test with empty string
|
||||
result2 := sequenced(Config1{value1: 42})(Config2{value2: ""})(ctx)()
|
||||
assert.True(t, result.IsLeft(result2))
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
|
||||
return func() Result[Reader[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) int {
|
||||
return cfg1.value1 + len(cfg2.value2)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
|
||||
t.Run("maintains referential transparency", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
|
||||
return func() Result[Reader[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) int {
|
||||
return cfg1.value1 * len(cfg2.value2)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
cfg1 := Config1{value1: 3}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
// Call multiple times with same inputs
|
||||
for range 5 {
|
||||
outcome := sequenced(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(12), outcome)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceReaderIO(t *testing.T) {
|
||||
t.Run("swaps parameter order for ReaderIO types", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original: takes Config2, returns ReaderIOResult that may produce ReaderIO[Config1, int]
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
|
||||
return func() Result[ReaderIO[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) io.IO[int] {
|
||||
return io.Of(cfg1.value1 + len(cfg2.value2))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence swaps Config1 and Config2 order
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// Test original
|
||||
result1 := original(cfg2)(ctx)()
|
||||
assert.True(t, result.IsRight(result1))
|
||||
innerFunc1, _ := result.Unwrap(result1)
|
||||
value1 := innerFunc1(cfg1)()
|
||||
assert.Equal(t, 15, value1)
|
||||
|
||||
// Test sequenced
|
||||
innerFunc2 := sequenced(cfg1)
|
||||
result2 := innerFunc2(cfg2)(ctx)()
|
||||
assert.True(t, result.IsRight(result2))
|
||||
value2, _ := result.Unwrap(result2)
|
||||
assert.Equal(t, 15, value2)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
|
||||
return func() Result[ReaderIO[Config1, int]] {
|
||||
return result.Left[ReaderIO[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 10})(Config2{value2: "hello"})(ctx)()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with IO effects", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
sideEffect := 0
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, string]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, string]] {
|
||||
return func() Result[ReaderIO[Config1, string]] {
|
||||
if len(cfg2.value2) == 0 {
|
||||
return result.Left[ReaderIO[Config1, string]](errors.New("empty string"))
|
||||
}
|
||||
return result.Of(func(cfg1 Config1) io.IO[string] {
|
||||
return func() string {
|
||||
sideEffect = cfg1.value1
|
||||
return fmt.Sprintf("%s:%d", cfg2.value2, cfg1.value1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
// Test with valid inputs
|
||||
sideEffect = 0
|
||||
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of("test:42"), result1)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
|
||||
// Test with empty string
|
||||
sideEffect = 0
|
||||
result2 := sequenced(Config1{value1: 42})(Config2{value2: ""})(ctx)()
|
||||
assert.True(t, result.IsLeft(result2))
|
||||
assert.Equal(t, 0, sideEffect) // Side effect should not occur
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
|
||||
return func() Result[ReaderIO[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) io.IO[int] {
|
||||
return io.Of(cfg1.value1 + len(cfg2.value2))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
|
||||
t.Run("executes IO effects correctly", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
counter := 0
|
||||
|
||||
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
|
||||
return func() Result[ReaderIO[Config1, int]] {
|
||||
return result.Of(func(cfg1 Config1) io.IO[int] {
|
||||
return func() int {
|
||||
counter++
|
||||
return cfg1.value1 + len(cfg2.value2)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// Each execution should increment counter
|
||||
counter = 0
|
||||
result1 := sequenced(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(15), result1)
|
||||
assert.Equal(t, 1, counter)
|
||||
|
||||
result2 := sequenced(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(15), result2)
|
||||
assert.Equal(t, 2, counter)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverse(t *testing.T) {
|
||||
t.Run("transforms and swaps parameter order", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original computation depending on Config2
|
||||
original := Of[Config2](42)
|
||||
|
||||
// Transformation that introduces Config1 dependency
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
return func(cfg1 Config1) RIORES.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("value=%d, cfg1=%d", n, cfg1.value1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply traverse to swap order and transform
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
cfg1 := Config1{value1: 100}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
outcome := traversed(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of("value=42, cfg1=100"), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling in original", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
original := Left[Config2, int](testErr)
|
||||
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
return Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Left[string](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling in transformation", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](42)
|
||||
testErr := errors.New("transform error")
|
||||
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
if n < 0 {
|
||||
return Left[Config1, string](testErr)
|
||||
}
|
||||
return Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
// Test with negative value
|
||||
originalNeg := Of[Config2](-1)
|
||||
traversedNeg := Traverse[Config2](transform)(originalNeg)
|
||||
resultNeg := traversedNeg(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Left[string](testErr), resultNeg)
|
||||
|
||||
// Test with positive value
|
||||
traversedPos := Traverse[Config2](transform)(original)
|
||||
resultPos := traversedPos(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of("42"), resultPos)
|
||||
})
|
||||
|
||||
t.Run("works with complex transformations", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](10)
|
||||
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, int] {
|
||||
return func(cfg1 Config1) RIORES.ReaderIOResult[int] {
|
||||
return func(ctx context.Context) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Of(n * cfg1.value1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 5})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of(50), outcome)
|
||||
})
|
||||
|
||||
t.Run("can be composed with other operations", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](10)
|
||||
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, int] {
|
||||
return Of[Config1](n * 2)
|
||||
}
|
||||
|
||||
outcome := F.Pipe2(
|
||||
original,
|
||||
Traverse[Config2](transform),
|
||||
func(k Kleisli[Config2, Config1, int]) ReaderReaderIOResult[Config2, int] {
|
||||
return k(Config1{value1: 5})
|
||||
},
|
||||
)
|
||||
|
||||
res := outcome(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of(20), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseReader(t *testing.T) {
|
||||
t.Run("transforms with pure Reader and swaps parameter order", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Original computation depending on Config2
|
||||
original := Of[Config2](100)
|
||||
|
||||
// Pure Reader transformation that introduces Config1 dependency
|
||||
formatWithConfig := func(value int) reader.Reader[Config1, string] {
|
||||
return func(cfg1 Config1) string {
|
||||
return fmt.Sprintf("value=%d, multiplier=%d, result=%d", value, cfg1.value1, value*cfg1.value1)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply traverse to introduce Config1 and swap order
|
||||
traversed := TraverseReader[Config2](formatWithConfig)(original)
|
||||
|
||||
cfg1 := Config1{value1: 5}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
outcome := traversed(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of("value=100, multiplier=5, result=500"), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error handling", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
original := Left[Config2, int](testErr)
|
||||
|
||||
transform := func(n int) reader.Reader[Config1, string] {
|
||||
return reader.Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 5})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Left[string](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with pure computations", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](42)
|
||||
|
||||
// Pure transformation using Reader
|
||||
double := func(n int) reader.Reader[Config1, int] {
|
||||
return func(cfg1 Config1) int {
|
||||
return n * cfg1.value1
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2](double)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 3})(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of(126), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with zero values", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](0)
|
||||
|
||||
transform := func(n int) reader.Reader[Config1, int] {
|
||||
return func(cfg1 Config1) int {
|
||||
return n + cfg1.value1
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2](transform)(original)
|
||||
|
||||
outcome := traversed(Config1{value1: 0})(Config2{value2: ""})(ctx)()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
|
||||
t.Run("maintains referential transparency", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](10)
|
||||
|
||||
transform := func(n int) reader.Reader[Config1, int] {
|
||||
return func(cfg1 Config1) int {
|
||||
return n * cfg1.value1
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseReader[Config2](transform)(original)
|
||||
|
||||
cfg1 := Config1{value1: 5}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
// Call multiple times with same inputs
|
||||
for range 5 {
|
||||
outcome := traversed(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(50), outcome)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("can be used in composition", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
original := Of[Config2](10)
|
||||
|
||||
multiply := func(n int) reader.Reader[Config1, int] {
|
||||
return func(cfg1 Config1) int {
|
||||
return n * cfg1.value1
|
||||
}
|
||||
}
|
||||
|
||||
outcome := F.Pipe2(
|
||||
original,
|
||||
TraverseReader[Config2](multiply),
|
||||
func(k Kleisli[Config2, Config1, int]) ReaderReaderIOResult[Config2, int] {
|
||||
return k(Config1{value1: 3})
|
||||
},
|
||||
)
|
||||
|
||||
res := outcome(Config2{value2: "test"})(ctx)()
|
||||
assert.Equal(t, result.Of(30), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFlipIntegration(t *testing.T) {
|
||||
t.Run("Sequence and Traverse work together", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Create a nested computation
|
||||
nested := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Of(Of[Config1](len(cfg2.value2)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sequence it
|
||||
sequenced := Sequence(nested)
|
||||
|
||||
// Then traverse with a transformation
|
||||
transform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
return Of[Config1](fmt.Sprintf("length=%d", n))
|
||||
}
|
||||
|
||||
// Apply both operations
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "hello"}
|
||||
|
||||
// First sequence
|
||||
intermediate := sequenced(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of(5), intermediate)
|
||||
|
||||
// Then apply traverse on a new computation
|
||||
original := Of[Config2](5)
|
||||
traversed := Traverse[Config2](transform)(original)
|
||||
outcome := traversed(cfg1)(cfg2)(ctx)()
|
||||
assert.Equal(t, result.Of("length=5"), outcome)
|
||||
})
|
||||
|
||||
t.Run("all flip functions preserve error semantics", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
cfg1 := Config1{value1: 10}
|
||||
cfg2 := Config2{value2: "test"}
|
||||
|
||||
// Test Sequence with error
|
||||
seqErr := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
|
||||
return func() Result[ReaderReaderIOResult[Config1, int]] {
|
||||
return result.Left[ReaderReaderIOResult[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
seqResult := Sequence(seqErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(seqResult))
|
||||
|
||||
// Test SequenceReader with error
|
||||
seqReaderErr := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
|
||||
return func() Result[Reader[Config1, int]] {
|
||||
return result.Left[Reader[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
seqReaderResult := SequenceReader(seqReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(seqReaderResult))
|
||||
|
||||
// Test SequenceReaderIO with error
|
||||
seqReaderIOErr := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
|
||||
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
|
||||
return func() Result[ReaderIO[Config1, int]] {
|
||||
return result.Left[ReaderIO[Config1, int]](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
seqReaderIOResult := SequenceReaderIO(seqReaderIOErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(seqReaderIOResult))
|
||||
|
||||
// Test Traverse with error
|
||||
travErr := Left[Config2, int](testErr)
|
||||
travTransform := func(n int) ReaderReaderIOResult[Config1, string] {
|
||||
return Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
travResult := Traverse[Config2](travTransform)(travErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(travResult))
|
||||
|
||||
// Test TraverseReader with error
|
||||
travReaderErr := Left[Config2, int](testErr)
|
||||
travReaderTransform := func(n int) reader.Reader[Config1, string] {
|
||||
return reader.Of[Config1](fmt.Sprintf("%d", n))
|
||||
}
|
||||
travReaderResult := TraverseReader[Config2](travReaderTransform)(travReaderErr)(cfg1)(cfg2)(ctx)()
|
||||
assert.True(t, result.IsLeft(travReaderResult))
|
||||
})
|
||||
}
|
||||
@@ -20,9 +20,32 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Monoid represents a monoid structure for ReaderReaderIOResult[R, A].
|
||||
// A monoid provides an identity element (empty) and an associative binary operation (concat).
|
||||
Monoid[R, A any] = monoid.Monoid[ReaderReaderIOResult[R, A]]
|
||||
)
|
||||
|
||||
// ApplicativeMonoid creates a monoid for ReaderReaderIOResult using applicative composition.
|
||||
// It combines values using the provided monoid m and the applicative Ap operation.
|
||||
// This allows combining multiple ReaderReaderIOResult values in parallel while merging their results.
|
||||
//
|
||||
// The resulting monoid satisfies:
|
||||
// - Identity: concat(empty, x) = concat(x, empty) = x
|
||||
// - Associativity: concat(concat(x, y), z) = concat(x, concat(y, z))
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/monoid"
|
||||
// import "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// // Create a monoid for combining integers with addition
|
||||
// intMonoid := ApplicativeMonoid[Config](number.MonoidSum)
|
||||
//
|
||||
// // Combine multiple computations
|
||||
// result := intMonoid.Concat(
|
||||
// Of[Config](10),
|
||||
// intMonoid.Concat(Of[Config](20), Of[Config](30)),
|
||||
// ) // Results in 60
|
||||
func ApplicativeMonoid[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
@@ -32,6 +55,13 @@ func ApplicativeMonoid[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoidSeq creates a monoid for ReaderReaderIOResult using sequential applicative composition.
|
||||
// Similar to ApplicativeMonoid but evaluates effects sequentially rather than in parallel.
|
||||
//
|
||||
// Use this when:
|
||||
// - Effects must be executed in a specific order
|
||||
// - Side effects depend on sequential execution
|
||||
// - You want to avoid concurrent execution
|
||||
func ApplicativeMonoidSeq[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
@@ -41,6 +71,13 @@ func ApplicativeMonoidSeq[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// ApplicativeMonoidPar creates a monoid for ReaderReaderIOResult using parallel applicative composition.
|
||||
// Similar to ApplicativeMonoid but explicitly evaluates effects in parallel.
|
||||
//
|
||||
// Use this when:
|
||||
// - Effects are independent and can run concurrently
|
||||
// - You want to maximize performance through parallelism
|
||||
// - Order of execution doesn't matter
|
||||
func ApplicativeMonoidPar[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
return monoid.ApplicativeMonoid(
|
||||
Of[R, A],
|
||||
@@ -50,6 +87,26 @@ func ApplicativeMonoidPar[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a monoid that combines ReaderReaderIOResult values using both
|
||||
// applicative composition and alternative (Alt) semantics.
|
||||
//
|
||||
// This monoid:
|
||||
// - Uses Ap for combining successful values
|
||||
// - Uses Alt for handling failures (tries alternatives on failure)
|
||||
// - Provides a way to combine multiple computations with fallback behavior
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "github.com/IBM/fp-go/v2/monoid"
|
||||
// import "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// intMonoid := AlternativeMonoid[Config](number.MonoidSum)
|
||||
//
|
||||
// // If first computation fails, tries the second
|
||||
// result := intMonoid.Concat(
|
||||
// Left[Config, int](errors.New("failed")),
|
||||
// Of[Config](42),
|
||||
// ) // Results in Right(42)
|
||||
func AlternativeMonoid[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
return monoid.AlternativeMonoid(
|
||||
Of[R, A],
|
||||
@@ -60,6 +117,29 @@ func AlternativeMonoid[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// AltMonoid creates a monoid based solely on the Alt operation.
|
||||
// It provides a way to chain computations with fallback behavior.
|
||||
//
|
||||
// The monoid:
|
||||
// - Uses the provided zero as the identity element
|
||||
// - Uses Alt for concatenation (tries first, falls back to second on failure)
|
||||
// - Implements a "first success" strategy
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// zero := func() ReaderReaderIOResult[Config, int] {
|
||||
// return Left[Config, int](errors.New("no value"))
|
||||
// }
|
||||
// altMonoid := AltMonoid[Config, int](zero)
|
||||
//
|
||||
// // Tries computations in order until one succeeds
|
||||
// result := altMonoid.Concat(
|
||||
// Left[Config, int](errors.New("first failed")),
|
||||
// altMonoid.Concat(
|
||||
// Left[Config, int](errors.New("second failed")),
|
||||
// Of[Config](42),
|
||||
// ),
|
||||
// ) // Results in Right(42)
|
||||
func AltMonoid[R, A any](zero Lazy[ReaderReaderIOResult[R, A]]) Monoid[R, A] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
|
||||
@@ -39,51 +39,80 @@ import (
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
)
|
||||
|
||||
// FromReaderOption converts a ReaderOption to a ReaderReaderIOResult.
|
||||
// If the option is None, it uses the provided onNone function to generate an error.
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderOption[R, A any](onNone Lazy[error]) Kleisli[R, ReaderOption[R, A], A] {
|
||||
return RRIOE.FromReaderOption[R, context.Context, A](onNone)
|
||||
}
|
||||
|
||||
// FromReaderIOResult lifts a ReaderIOResult into a ReaderReaderIOResult.
|
||||
// This adds an additional reader layer to the computation.
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderIOResult[R, A any](ma ReaderIOResult[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromReaderIOEither[context.Context, error](ma)
|
||||
return RRIOE.FromReaderIOEither[context.Context](ma)
|
||||
}
|
||||
|
||||
// FromReaderIO lifts a ReaderIO into a ReaderReaderIOResult.
|
||||
// The IO computation is wrapped in a Right (success) value.
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderIO[R, A any](ma ReaderIO[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromReaderIO[context.Context, error](ma)
|
||||
}
|
||||
|
||||
// RightReaderIO lifts a ReaderIO into a ReaderReaderIOResult as a Right (success) value.
|
||||
// Alias for FromReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func RightReaderIO[R, A any](ma ReaderIO[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.RightReaderIO[context.Context, error](ma)
|
||||
}
|
||||
|
||||
// LeftReaderIO lifts a ReaderIO that produces an error into a ReaderReaderIOResult as a Left (failure) value.
|
||||
//
|
||||
//go:inline
|
||||
func LeftReaderIO[A, R any](me ReaderIO[R, error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.LeftReaderIO[context.Context, A](me)
|
||||
}
|
||||
|
||||
// MonadMap applies a function to the value inside a ReaderReaderIOResult (Functor operation).
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[R, A, B any](fa ReaderReaderIOResult[R, A], f func(A) B) ReaderReaderIOResult[R, B] {
|
||||
return reader.MonadMap(fa, RIOE.Map(f))
|
||||
}
|
||||
|
||||
// Map applies a function to the value inside a ReaderReaderIOResult (Functor operation).
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
|
||||
return reader.Map[R](RIOE.Map(f))
|
||||
}
|
||||
|
||||
// MonadMapTo replaces the value inside a ReaderReaderIOResult with a constant value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[R, A, B any](fa ReaderReaderIOResult[R, A], b B) ReaderReaderIOResult[R, B] {
|
||||
return reader.MonadMap(fa, RIOE.MapTo[A](b))
|
||||
}
|
||||
|
||||
// MapTo replaces the value inside a ReaderReaderIOResult with a constant value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[R, A, B any](b B) Operator[R, A, B] {
|
||||
return reader.Map[R](RIOE.MapTo[A](b))
|
||||
}
|
||||
|
||||
// MonadChain sequences two computations, where the second depends on the result of the first (Monad operation).
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return readert.MonadChain(
|
||||
@@ -93,6 +122,10 @@ func MonadChain[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B])
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirst sequences two computations but returns the result of the first.
|
||||
// Useful for performing side effects while preserving the original value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirst[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return chain.MonadChainFirst(
|
||||
@@ -102,11 +135,18 @@ func MonadChainFirst[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A,
|
||||
f)
|
||||
}
|
||||
|
||||
// MonadTap is an alias for MonadChainFirst.
|
||||
// Executes a side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTap[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirst(fa, f)
|
||||
}
|
||||
|
||||
// MonadChainEitherK chains a computation that returns an Either.
|
||||
// The Either is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromeither.MonadChainEitherK(
|
||||
@@ -117,6 +157,10 @@ func MonadChainEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either.Klei
|
||||
)
|
||||
}
|
||||
|
||||
// ChainEitherK chains a computation that returns an Either.
|
||||
// The Either is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B] {
|
||||
return fromeither.ChainEitherK(
|
||||
@@ -126,6 +170,10 @@ func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B]
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstEitherK chains a computation that returns an Either but preserves the original value.
|
||||
// Useful for validation or side effects that may fail.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return fromeither.MonadChainFirstEitherK(
|
||||
@@ -137,11 +185,17 @@ func MonadChainFirstEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapEitherK is an alias for MonadChainFirstEitherK.
|
||||
// Executes an Either-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirstEitherK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstEitherK chains a computation that returns an Either but preserves the original value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
|
||||
return fromeither.ChainFirstEitherK(
|
||||
@@ -152,11 +206,18 @@ func ChainFirstEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A
|
||||
)
|
||||
}
|
||||
|
||||
// TapEitherK is an alias for ChainFirstEitherK.
|
||||
// Executes an Either-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstEitherK[R](f)
|
||||
}
|
||||
|
||||
// MonadChainReaderK chains a computation that returns a Reader.
|
||||
// The Reader is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromreader.MonadChainReaderK(
|
||||
@@ -167,6 +228,10 @@ func MonadChainReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader.Klei
|
||||
)
|
||||
}
|
||||
|
||||
// ChainReaderK chains a computation that returns a Reader.
|
||||
// The Reader is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return fromreader.ChainReaderK(
|
||||
@@ -176,6 +241,9 @@ func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstReaderK chains a computation that returns a Reader but preserves the original value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return fromreader.MonadChainFirstReaderK(
|
||||
@@ -186,11 +254,17 @@ func MonadChainFirstReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapReaderK is an alias for MonadChainFirstReaderK.
|
||||
// Executes a Reader-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirstReaderK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstReaderK chains a computation that returns a Reader but preserves the original value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return fromreader.ChainFirstReaderK(
|
||||
@@ -200,11 +274,18 @@ func ChainFirstReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A]
|
||||
)
|
||||
}
|
||||
|
||||
// TapReaderK is an alias for ChainFirstReaderK.
|
||||
// Executes a Reader-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstReaderK(f)
|
||||
}
|
||||
|
||||
// MonadChainReaderIOK chains a computation that returns a ReaderIO.
|
||||
// The ReaderIO is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromreader.MonadChainReaderK(
|
||||
@@ -215,6 +296,10 @@ func MonadChainReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f readerio.
|
||||
)
|
||||
}
|
||||
|
||||
// ChainReaderIOK chains a computation that returns a ReaderIO.
|
||||
// The ReaderIO is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return fromreader.ChainReaderK(
|
||||
@@ -224,6 +309,9 @@ func ChainReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, B]
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstReaderIOK chains a computation that returns a ReaderIO but preserves the original value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return fromreader.MonadChainFirstReaderK(
|
||||
@@ -234,11 +322,17 @@ func MonadChainFirstReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f read
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapReaderIOK is an alias for MonadChainFirstReaderIOK.
|
||||
// Executes a ReaderIO-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirstReaderIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstReaderIOK chains a computation that returns a ReaderIO but preserves the original value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return fromreader.ChainFirstReaderK(
|
||||
@@ -248,11 +342,18 @@ func ChainFirstReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A
|
||||
)
|
||||
}
|
||||
|
||||
// TapReaderIOK is an alias for ChainFirstReaderIOK.
|
||||
// Executes a ReaderIO-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstReaderIOK(f)
|
||||
}
|
||||
|
||||
// MonadChainReaderEitherK chains a computation that returns a ReaderEither.
|
||||
// The ReaderEither is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromreader.MonadChainReaderK(
|
||||
@@ -263,6 +364,10 @@ func MonadChainReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f RE.Kl
|
||||
)
|
||||
}
|
||||
|
||||
// ChainReaderEitherK chains a computation that returns a ReaderEither.
|
||||
// The ReaderEither is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, B] {
|
||||
return fromreader.ChainReaderK(
|
||||
@@ -272,6 +377,9 @@ func ChainReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstReaderEitherK chains a computation that returns a ReaderEither but preserves the original value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return fromreader.MonadChainFirstReaderK(
|
||||
@@ -282,11 +390,17 @@ func MonadChainFirstReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapReaderEitherK is an alias for MonadChainFirstReaderEitherK.
|
||||
// Executes a ReaderEither-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirstReaderEitherK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstReaderEitherK chains a computation that returns a ReaderEither but preserves the original value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
|
||||
return fromreader.ChainFirstReaderK(
|
||||
@@ -296,25 +410,42 @@ func ChainFirstReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator
|
||||
)
|
||||
}
|
||||
|
||||
// TapReaderEitherK is an alias for ChainFirstReaderEitherK.
|
||||
// Executes a ReaderEither-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstReaderEitherK(f)
|
||||
}
|
||||
|
||||
// ChainReaderOptionK chains a computation that returns a ReaderOption.
|
||||
// If the option is None, it uses the provided onNone function to generate an error.
|
||||
// Returns a function that takes a ReaderOption Kleisli and returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return RRIOE.ChainReaderOptionK[R, context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
// ChainFirstReaderOptionK chains a computation that returns a ReaderOption but preserves the original value.
|
||||
// If the option is None, it uses the provided onNone function to generate an error.
|
||||
// Returns a function that takes a ReaderOption Kleisli and returns an operator.
|
||||
func ChainFirstReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return RRIOE.ChainFirstReaderOptionK[R, context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
// TapReaderOptionK is an alias for ChainFirstReaderOptionK.
|
||||
// Executes a ReaderOption-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return ChainFirstReaderOptionK[R, A, B](onNone)
|
||||
}
|
||||
|
||||
// MonadChainIOEitherK chains a computation that returns an IOEither.
|
||||
// The IOEither is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainIOEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f IOE.Kleisli[error, A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromioeither.MonadChainIOEitherK(
|
||||
@@ -325,6 +456,10 @@ func MonadChainIOEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f IOE.Kleis
|
||||
)
|
||||
}
|
||||
|
||||
// ChainIOEitherK chains a computation that returns an IOEither.
|
||||
// The IOEither is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOEitherK[R, A, B any](f IOE.Kleisli[error, A, B]) Operator[R, A, B] {
|
||||
return fromioeither.ChainIOEitherK(
|
||||
@@ -334,6 +469,10 @@ func ChainIOEitherK[R, A, B any](f IOE.Kleisli[error, A, B]) Operator[R, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainIOK chains a computation that returns an IO.
|
||||
// The IO is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderReaderIOResult[R, B] {
|
||||
return fromio.MonadChainIOK(
|
||||
@@ -344,6 +483,10 @@ func MonadChainIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B
|
||||
)
|
||||
}
|
||||
|
||||
// ChainIOK chains a computation that returns an IO.
|
||||
// The IO is automatically lifted into ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return fromio.ChainIOK(
|
||||
@@ -353,6 +496,9 @@ func ChainIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstIOK chains a computation that returns an IO but preserves the original value.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderReaderIOResult[R, A] {
|
||||
return fromio.MonadChainFirstIOK(
|
||||
@@ -364,11 +510,17 @@ func MonadChainFirstIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli
|
||||
)
|
||||
}
|
||||
|
||||
// MonadTapIOK is an alias for MonadChainFirstIOK.
|
||||
// Executes an IO-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadTapIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstIOK chains a computation that returns an IO but preserves the original value.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
|
||||
return fromio.ChainFirstIOK(
|
||||
@@ -379,11 +531,18 @@ func ChainFirstIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
|
||||
)
|
||||
}
|
||||
|
||||
// TapIOK is an alias for ChainFirstIOK.
|
||||
// Executes an IO-returning side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func TapIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
|
||||
return ChainFirstIOK[R](f)
|
||||
}
|
||||
|
||||
// ChainOptionK chains a computation that returns an Option.
|
||||
// If the option is None, it uses the provided onNone function to generate an error.
|
||||
// Returns a function that takes an Option Kleisli and returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[R, A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return fromeither.ChainOptionK(
|
||||
@@ -393,6 +552,9 @@ func ChainOptionK[R, A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Op
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAp applies a function wrapped in a ReaderReaderIOResult to a value wrapped in a ReaderReaderIOResult (Applicative operation).
|
||||
// This is the monadic version that takes both computations as parameters.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAp[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, B] {
|
||||
return readert.MonadAp[
|
||||
@@ -405,6 +567,8 @@ func MonadAp[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderReade
|
||||
)
|
||||
}
|
||||
|
||||
// MonadApSeq is like MonadAp but evaluates effects sequentially.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApSeq[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, B] {
|
||||
return readert.MonadAp[
|
||||
@@ -417,6 +581,8 @@ func MonadApSeq[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderRe
|
||||
)
|
||||
}
|
||||
|
||||
// MonadApPar is like MonadAp but evaluates effects in parallel.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApPar[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, B] {
|
||||
return readert.MonadAp[
|
||||
@@ -429,6 +595,9 @@ func MonadApPar[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderRe
|
||||
)
|
||||
}
|
||||
|
||||
// Ap applies a function wrapped in a ReaderReaderIOResult to a value wrapped in a ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, R, A any](fa ReaderReaderIOResult[R, A]) Operator[R, func(A) B, B] {
|
||||
return readert.Ap[
|
||||
@@ -440,6 +609,9 @@ func Ap[B, R, A any](fa ReaderReaderIOResult[R, A]) Operator[R, func(A) B, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// Chain sequences two computations, where the second depends on the result of the first (Monad operation).
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return readert.Chain[ReaderReaderIOResult[R, A]](
|
||||
@@ -448,6 +620,9 @@ func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// ChainFirst sequences two computations but returns the result of the first.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return chain.ChainFirst(
|
||||
@@ -456,166 +631,263 @@ func ChainFirst[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
f)
|
||||
}
|
||||
|
||||
// Tap is an alias for ChainFirst.
|
||||
// Executes a side effect while preserving the original value.
|
||||
//
|
||||
//go:inline
|
||||
func Tap[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
|
||||
return ChainFirst(f)
|
||||
}
|
||||
|
||||
// Right creates a ReaderReaderIOResult that succeeds with the given value.
|
||||
// This is the success constructor for the Result type.
|
||||
//
|
||||
//go:inline
|
||||
func Right[R, A any](a A) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.Right[R, context.Context, error](a)
|
||||
}
|
||||
|
||||
// Left creates a ReaderReaderIOResult that fails with the given error.
|
||||
// This is the failure constructor for the Result type.
|
||||
//
|
||||
//go:inline
|
||||
func Left[R, A any](e error) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.Left[R, context.Context, A](e)
|
||||
}
|
||||
|
||||
// Of creates a ReaderReaderIOResult that succeeds with the given value (Pointed operation).
|
||||
// Alias for Right.
|
||||
//
|
||||
//go:inline
|
||||
func Of[R, A any](a A) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.Of[R, context.Context, error](a)
|
||||
}
|
||||
|
||||
// Flatten removes one level of nesting from a nested ReaderReaderIOResult.
|
||||
// Converts ReaderReaderIOResult[R, ReaderReaderIOResult[R, A]] to ReaderReaderIOResult[R, A].
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[R, A any](mma ReaderReaderIOResult[R, ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
|
||||
return MonadChain(mma, function.Identity[ReaderReaderIOResult[R, A]])
|
||||
}
|
||||
|
||||
// FromEither lifts an Either into a ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func FromEither[R, A any](t Either[error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromEither[R, context.Context](t)
|
||||
}
|
||||
|
||||
// FromResult lifts a Result into a ReaderReaderIOResult.
|
||||
// Alias for FromEither since Result is Either[error, A].
|
||||
//
|
||||
//go:inline
|
||||
func FromResult[R, A any](t Result[A]) ReaderReaderIOResult[R, A] {
|
||||
return FromEither[R](t)
|
||||
}
|
||||
|
||||
// RightReader lifts a Reader into a ReaderReaderIOResult as a Right (success) value.
|
||||
//
|
||||
//go:inline
|
||||
func RightReader[R, A any](ma Reader[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.RightReader[context.Context, error](ma)
|
||||
}
|
||||
|
||||
// LeftReader lifts a Reader that produces an error into a ReaderReaderIOResult as a Left (failure) value.
|
||||
//
|
||||
//go:inline
|
||||
func LeftReader[A, R any](ma Reader[R, error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.LeftReader[context.Context, A](ma)
|
||||
}
|
||||
|
||||
// FromReader lifts a Reader into a ReaderReaderIOResult.
|
||||
// The Reader's result is wrapped in a Right (success) value.
|
||||
//
|
||||
//go:inline
|
||||
func FromReader[R, A any](ma Reader[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromReader[context.Context, error](ma)
|
||||
}
|
||||
|
||||
// RightIO lifts an IO into a ReaderReaderIOResult as a Right (success) value.
|
||||
//
|
||||
//go:inline
|
||||
func RightIO[R, A any](ma IO[A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.RightIO[R, context.Context, error](ma)
|
||||
}
|
||||
|
||||
// LeftIO lifts an IO that produces an error into a ReaderReaderIOResult as a Left (failure) value.
|
||||
//
|
||||
//go:inline
|
||||
func LeftIO[R, A any](ma IO[error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.LeftIO[R, context.Context, A](ma)
|
||||
}
|
||||
|
||||
// FromIO lifts an IO into a ReaderReaderIOResult.
|
||||
// The IO's result is wrapped in a Right (success) value.
|
||||
//
|
||||
//go:inline
|
||||
func FromIO[R, A any](ma IO[A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromIO[R, context.Context, error](ma)
|
||||
}
|
||||
|
||||
// FromIOEither lifts an IOEither into a ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func FromIOEither[R, A any](ma IOEither[error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromIOEither[R, context.Context, error](ma)
|
||||
return RRIOE.FromIOEither[R, context.Context](ma)
|
||||
}
|
||||
|
||||
// FromIOResult lifts an IOResult into a ReaderReaderIOResult.
|
||||
// Alias for FromIOEither since IOResult is IOEither[error, A].
|
||||
//
|
||||
//go:inline
|
||||
func FromIOResult[R, A any](ma IOResult[A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromIOEither[R, context.Context, error](ma)
|
||||
return RRIOE.FromIOEither[R, context.Context](ma)
|
||||
}
|
||||
|
||||
// FromReaderEither lifts a ReaderEither into a ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func FromReaderEither[R, A any](ma RE.ReaderEither[R, error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromReaderEither[R, context.Context, error](ma)
|
||||
return RRIOE.FromReaderEither[R, context.Context](ma)
|
||||
}
|
||||
|
||||
// Ask retrieves the outer environment R.
|
||||
// Returns a ReaderReaderIOResult that succeeds with the environment value.
|
||||
//
|
||||
//go:inline
|
||||
func Ask[R any]() ReaderReaderIOResult[R, R] {
|
||||
return RRIOE.Ask[R, context.Context, error]()
|
||||
}
|
||||
|
||||
// Asks retrieves a value derived from the outer environment R using the provided function.
|
||||
//
|
||||
//go:inline
|
||||
func Asks[R, A any](r Reader[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.Asks[context.Context, error](r)
|
||||
}
|
||||
|
||||
// FromOption converts an Option to a ReaderReaderIOResult.
|
||||
// If the option is None, it uses the provided onNone function to generate an error.
|
||||
// Returns a function that takes an Option and returns a ReaderReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func FromOption[R, A any](onNone Lazy[error]) func(Option[A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.FromOption[R, context.Context, A](onNone)
|
||||
}
|
||||
|
||||
// FromPredicate creates a ReaderReaderIOResult from a predicate.
|
||||
// If the predicate returns true, the value is wrapped in Right.
|
||||
// If false, onFalse is called to generate an error wrapped in Left.
|
||||
//
|
||||
//go:inline
|
||||
func FromPredicate[R, A any](pred func(A) bool, onFalse func(A) error) Kleisli[R, A, A] {
|
||||
return RRIOE.FromPredicate[R, context.Context, error](pred, onFalse)
|
||||
return RRIOE.FromPredicate[R, context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
// MonadAlt provides alternative/fallback behavior.
|
||||
// If the first computation fails, it tries the second (lazy-evaluated).
|
||||
// This is the monadic version that takes both computations as parameters.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAlt[R, A any](first ReaderReaderIOResult[R, A], second Lazy[ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadAlt(first, second)
|
||||
}
|
||||
|
||||
// Alt provides alternative/fallback behavior.
|
||||
// If the first computation fails, it tries the second (lazy-evaluated).
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func Alt[R, A any](second Lazy[ReaderReaderIOResult[R, A]]) Operator[R, A, A] {
|
||||
return RRIOE.Alt(second)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to a function wrapped in a ReaderReaderIOResult.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[R, B, A any](fab ReaderReaderIOResult[R, func(A) B], a A) ReaderReaderIOResult[R, B] {
|
||||
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
|
||||
}
|
||||
|
||||
// Flap applies a value to a function wrapped in a ReaderReaderIOResult.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
return functor.Flap(Map[R, func(A) B, B], a)
|
||||
}
|
||||
|
||||
// MonadMapLeft transforms the error value if the computation fails.
|
||||
// Has no effect if the computation succeeds.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadMapLeft[R, context.Context](fa, f)
|
||||
return RRIOE.MonadMapLeft(fa, f)
|
||||
}
|
||||
|
||||
// MapLeft transforms the error value if the computation fails.
|
||||
// Has no effect if the computation succeeds.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
|
||||
return RRIOE.MapLeft[R, context.Context, A](f)
|
||||
}
|
||||
|
||||
// Local modifies the outer environment before passing it to a computation.
|
||||
// Useful for providing different configurations to sub-computations.
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.Local[context.Context, error, A](f)
|
||||
}
|
||||
|
||||
// Read provides a specific outer environment value to a computation.
|
||||
// Converts ReaderReaderIOResult[R, A] to ReaderIOResult[context.Context, A].
|
||||
//
|
||||
//go:inline
|
||||
func Read[A, R any](r R) func(ReaderReaderIOResult[R, A]) ReaderIOResult[context.Context, A] {
|
||||
return RRIOE.Read[context.Context, error, A](r)
|
||||
}
|
||||
|
||||
// ReadIOEither provides an outer environment value from an IOEither to a computation.
|
||||
//
|
||||
//go:inline
|
||||
func ReadIOEither[A, R any](rio IOEither[error, R]) func(ReaderReaderIOResult[R, A]) ReaderIOResult[context.Context, A] {
|
||||
return RRIOE.ReadIOEither[A, R, context.Context](rio)
|
||||
}
|
||||
|
||||
// ReadIO provides an outer environment value from an IO to a computation.
|
||||
//
|
||||
//go:inline
|
||||
func ReadIO[A, R any](rio IO[R]) func(ReaderReaderIOResult[R, A]) ReaderIOResult[context.Context, A] {
|
||||
return RRIOE.ReadIO[context.Context, error, A, R](rio)
|
||||
return RRIOE.ReadIO[context.Context, error, A](rio)
|
||||
}
|
||||
|
||||
// MonadChainLeft handles errors by chaining a recovery computation.
|
||||
// If the computation fails, the error is passed to f for recovery.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[R, A any](fa ReaderReaderIOResult[R, A], f Kleisli[R, error, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadChainLeft[R, context.Context, error, error, A](fa, f)
|
||||
return RRIOE.MonadChainLeft(fa, f)
|
||||
}
|
||||
|
||||
// ChainLeft handles errors by chaining a recovery computation.
|
||||
// If the computation fails, the error is passed to f for recovery.
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.ChainLeft[R, context.Context, error, error, A](f)
|
||||
return RRIOE.ChainLeft(f)
|
||||
}
|
||||
|
||||
// Delay adds a time delay before executing the computation.
|
||||
// Useful for rate limiting, retry backoff, or scheduled execution.
|
||||
//
|
||||
//go:inline
|
||||
func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
|
||||
return reader.Map[R](RIOE.Delay[A](delay))
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RE "github.com/IBM/fp-go/v2/readereither"
|
||||
@@ -56,7 +57,7 @@ func TestLeft(t *testing.T) {
|
||||
func TestMonadMap(t *testing.T) {
|
||||
computation := MonadMap(
|
||||
Of[AppConfig](21),
|
||||
func(n int) int { return n * 2 },
|
||||
N.Mul(2),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
@@ -65,7 +66,7 @@ func TestMonadMap(t *testing.T) {
|
||||
func TestMap(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
Map[AppConfig](func(n int) int { return n * 2 }),
|
||||
Map[AppConfig](N.Mul(2)),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
@@ -100,7 +101,7 @@ func TestMonadChain(t *testing.T) {
|
||||
func TestChain(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](21),
|
||||
Chain[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](n * 2)
|
||||
}),
|
||||
)
|
||||
@@ -126,7 +127,7 @@ func TestChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
ChainFirst[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
ChainFirst(func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
sideEffect = n
|
||||
return Of[AppConfig]("ignored")
|
||||
}),
|
||||
@@ -140,7 +141,7 @@ func TestTap(t *testing.T) {
|
||||
sideEffect := 0
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](42),
|
||||
Tap[AppConfig](func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
Tap(func(n int) ReaderReaderIOResult[AppConfig, string] {
|
||||
sideEffect = n
|
||||
return Of[AppConfig]("ignored")
|
||||
}),
|
||||
@@ -166,7 +167,7 @@ func TestFromEither(t *testing.T) {
|
||||
|
||||
t.Run("left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromEither[AppConfig, int](either.Left[int](err))
|
||||
computation := FromEither[AppConfig](either.Left[int](err))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
@@ -188,7 +189,7 @@ func TestFromResult(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
computation := FromReader[AppConfig](func(cfg AppConfig) int {
|
||||
computation := FromReader(func(cfg AppConfig) int {
|
||||
return len(cfg.DatabaseURL)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -196,7 +197,7 @@ func TestFromReader(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRightReader(t *testing.T) {
|
||||
computation := RightReader[AppConfig](func(cfg AppConfig) int {
|
||||
computation := RightReader(func(cfg AppConfig) int {
|
||||
return len(cfg.LogLevel)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -240,7 +241,7 @@ func TestFromIOEither(t *testing.T) {
|
||||
|
||||
t.Run("left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromIOEither[AppConfig, int](ioeither.Left[int](err))
|
||||
computation := FromIOEither[AppConfig](ioeither.Left[int](err))
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
@@ -266,7 +267,7 @@ func TestFromIOResult(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromReaderIO(t *testing.T) {
|
||||
computation := FromReaderIO[AppConfig](func(cfg AppConfig) io.IO[int] {
|
||||
computation := FromReaderIO(func(cfg AppConfig) io.IO[int] {
|
||||
return func() int { return len(cfg.DatabaseURL) }
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -274,7 +275,7 @@ func TestFromReaderIO(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRightReaderIO(t *testing.T) {
|
||||
computation := RightReaderIO[AppConfig](func(cfg AppConfig) io.IO[int] {
|
||||
computation := RightReaderIO(func(cfg AppConfig) io.IO[int] {
|
||||
return func() int { return len(cfg.LogLevel) }
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -292,7 +293,7 @@ func TestLeftReaderIO(t *testing.T) {
|
||||
|
||||
func TestFromReaderEither(t *testing.T) {
|
||||
t.Run("right", func(t *testing.T) {
|
||||
computation := FromReaderEither[AppConfig](func(cfg AppConfig) either.Either[error, int] {
|
||||
computation := FromReaderEither(func(cfg AppConfig) either.Either[error, int] {
|
||||
return either.Right[error](len(cfg.DatabaseURL))
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -301,7 +302,7 @@ func TestFromReaderEither(t *testing.T) {
|
||||
|
||||
t.Run("left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
computation := FromReaderEither[AppConfig, int](func(cfg AppConfig) either.Either[error, int] {
|
||||
computation := FromReaderEither(func(cfg AppConfig) either.Either[error, int] {
|
||||
return either.Left[int](err)
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -316,7 +317,7 @@ func TestAsk(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAsks(t *testing.T) {
|
||||
computation := Asks[AppConfig](func(cfg AppConfig) string {
|
||||
computation := Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
})
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -395,7 +396,7 @@ func TestAlt(t *testing.T) {
|
||||
|
||||
computation := F.Pipe1(
|
||||
Left[AppConfig, int](err),
|
||||
Alt[AppConfig](func() ReaderReaderIOResult[AppConfig, int] {
|
||||
Alt(func() ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](99)
|
||||
}),
|
||||
)
|
||||
@@ -404,7 +405,7 @@ func TestAlt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fab := Of[AppConfig](func(n int) int { return n * 2 })
|
||||
fab := Of[AppConfig](N.Mul(2))
|
||||
computation := MonadFlap(fab, 21)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
@@ -412,7 +413,7 @@ func TestMonadFlap(t *testing.T) {
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](func(n int) int { return n * 2 }),
|
||||
Of[AppConfig](N.Mul(2)),
|
||||
Flap[AppConfig, int](21),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -457,10 +458,10 @@ func TestLocal(t *testing.T) {
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks[AppConfig](func(cfg AppConfig) string {
|
||||
Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
}),
|
||||
Local[string, AppConfig, OtherConfig](func(other OtherConfig) AppConfig {
|
||||
Local[string](func(other OtherConfig) AppConfig {
|
||||
return AppConfig{DatabaseURL: other.URL, LogLevel: "debug"}
|
||||
}),
|
||||
)
|
||||
@@ -470,7 +471,7 @@ func TestLocal(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
computation := Asks[AppConfig](func(cfg AppConfig) string {
|
||||
computation := Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
})
|
||||
|
||||
@@ -480,7 +481,7 @@ func TestRead(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReadIOEither(t *testing.T) {
|
||||
computation := Asks[AppConfig](func(cfg AppConfig) string {
|
||||
computation := Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
})
|
||||
|
||||
@@ -491,7 +492,7 @@ func TestReadIOEither(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReadIO(t *testing.T) {
|
||||
computation := Asks[AppConfig](func(cfg AppConfig) string {
|
||||
computation := Asks(func(cfg AppConfig) string {
|
||||
return cfg.DatabaseURL
|
||||
})
|
||||
|
||||
@@ -517,7 +518,7 @@ func TestChainLeft(t *testing.T) {
|
||||
err := errors.New("original error")
|
||||
computation := F.Pipe1(
|
||||
Left[AppConfig, int](err),
|
||||
ChainLeft[AppConfig](func(e error) ReaderReaderIOResult[AppConfig, int] {
|
||||
ChainLeft(func(e error) ReaderReaderIOResult[AppConfig, int] {
|
||||
return Of[AppConfig](99)
|
||||
}),
|
||||
)
|
||||
@@ -552,7 +553,7 @@ func TestChainEitherK(t *testing.T) {
|
||||
func TestChainReaderK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderK[AppConfig](func(n int) reader.Reader[AppConfig, int] {
|
||||
ChainReaderK(func(n int) reader.Reader[AppConfig, int] {
|
||||
return func(cfg AppConfig) int {
|
||||
return n + len(cfg.LogLevel)
|
||||
}
|
||||
@@ -565,7 +566,7 @@ func TestChainReaderK(t *testing.T) {
|
||||
func TestChainReaderIOK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderIOK[AppConfig](func(n int) readerio.ReaderIO[AppConfig, int] {
|
||||
ChainReaderIOK(func(n int) readerio.ReaderIO[AppConfig, int] {
|
||||
return func(cfg AppConfig) io.IO[int] {
|
||||
return func() int {
|
||||
return n + len(cfg.DatabaseURL)
|
||||
@@ -580,7 +581,7 @@ func TestChainReaderIOK(t *testing.T) {
|
||||
func TestChainReaderEitherK(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](10),
|
||||
ChainReaderEitherK[AppConfig](func(n int) RE.ReaderEither[AppConfig, error, int] {
|
||||
ChainReaderEitherK(func(n int) RE.ReaderEither[AppConfig, error, int] {
|
||||
return func(cfg AppConfig) either.Either[error, int] {
|
||||
return either.Right[error](n + len(cfg.LogLevel))
|
||||
}
|
||||
@@ -669,7 +670,7 @@ func TestChainOptionK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromReaderIOResult(t *testing.T) {
|
||||
computation := FromReaderIOResult[AppConfig](func(cfg AppConfig) ioresult.IOResult[int] {
|
||||
computation := FromReaderIOResult(func(cfg AppConfig) ioresult.IOResult[int] {
|
||||
return func() result.Result[int] {
|
||||
return result.Of(len(cfg.DatabaseURL))
|
||||
}
|
||||
@@ -699,7 +700,7 @@ func TestFromReaderOption(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fab := Of[AppConfig](func(n int) int { return n * 2 })
|
||||
fab := Of[AppConfig](N.Mul(2))
|
||||
fa := Of[AppConfig](21)
|
||||
computation := MonadAp(fab, fa)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
@@ -709,8 +710,8 @@ func TestMonadAp(t *testing.T) {
|
||||
func TestAp(t *testing.T) {
|
||||
fa := Of[AppConfig](21)
|
||||
computation := F.Pipe1(
|
||||
Of[AppConfig](func(n int) int { return n * 2 }),
|
||||
Ap[int, AppConfig](fa),
|
||||
Of[AppConfig](N.Mul(2)),
|
||||
Ap[int](fa),
|
||||
)
|
||||
outcome := computation(defaultConfig)(t.Context())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
|
||||
@@ -15,14 +15,73 @@
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
)
|
||||
|
||||
// Retrying executes an action with automatic retry logic based on a retry policy.
|
||||
// It retries the action when it fails or when the check predicate returns false.
|
||||
//
|
||||
// This function is useful for handling transient failures in operations like:
|
||||
// - Network requests that may temporarily fail
|
||||
// - Database operations that may encounter locks
|
||||
// - External service calls that may be temporarily unavailable
|
||||
//
|
||||
// Parameters:
|
||||
// - policy: Defines the retry behavior (number of retries, delays, backoff strategy)
|
||||
// - action: The computation to retry, receives retry status information
|
||||
// - check: Predicate to determine if the result should trigger a retry (returns true to continue, false to retry)
|
||||
//
|
||||
// The action receives a retry.RetryStatus that contains:
|
||||
// - IterNumber: Current iteration number (0-based)
|
||||
// - CumulativeDelay: Total delay accumulated so far
|
||||
// - PreviousDelay: Delay from the previous iteration
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderReaderIOResult that executes the action with retry logic
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "errors"
|
||||
// "time"
|
||||
// "github.com/IBM/fp-go/v2/retry"
|
||||
// )
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxRetries int
|
||||
// BaseDelay time.Duration
|
||||
// }
|
||||
//
|
||||
// // Create a retry policy with exponential backoff
|
||||
// policy := retry.ExponentialBackoff(100*time.Millisecond, 5*time.Second)
|
||||
// policy = retry.LimitRetries(3, policy)
|
||||
//
|
||||
// // Action that may fail transiently
|
||||
// action := func(status retry.RetryStatus) ReaderReaderIOResult[Config, string] {
|
||||
// return func(cfg Config) ReaderIOResult[context.Context, string] {
|
||||
// return func(ctx context.Context) IOResult[string] {
|
||||
// return func() Either[error, string] {
|
||||
// // Simulate transient failure
|
||||
// if status.IterNumber < 2 {
|
||||
// return either.Left[string](errors.New("transient error"))
|
||||
// }
|
||||
// return either.Right[error]("success")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Check if we should retry (retry on any error)
|
||||
// check := func(result Result[string]) bool {
|
||||
// return either.IsRight(result) // Continue only if successful
|
||||
// }
|
||||
//
|
||||
// // Execute with retry logic
|
||||
// result := Retrying(policy, action, check)
|
||||
//
|
||||
//go:inline
|
||||
func Retrying[R, A any](
|
||||
policy retry.RetryPolicy,
|
||||
@@ -30,7 +89,10 @@ func Retrying[R, A any](
|
||||
check Predicate[Result[A]],
|
||||
) ReaderReaderIOResult[R, A] {
|
||||
// get an implementation for the types
|
||||
return func(r R) ReaderIOResult[context.Context, A] {
|
||||
return RIOE.Retrying(policy, F.Pipe1(action, reader.Map[retry.RetryStatus](reader.Read[ReaderIOResult[context.Context, A]](r))), check)
|
||||
}
|
||||
return F.Flow4(
|
||||
reader.Read[RIOE.ReaderIOResult[A]],
|
||||
reader.Map[retry.RetryStatus],
|
||||
reader.Read[RIOE.Kleisli[retry.RetryStatus, A]](action),
|
||||
F.Bind13of3(RIOE.Retrying[A])(policy, check),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -45,9 +47,7 @@ func TestRetryingSuccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
check := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
check := result.IsLeft[int]
|
||||
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
@@ -76,9 +76,7 @@ func TestRetryingFailureExhaustsRetries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
check := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
check := result.IsLeft[int]
|
||||
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
@@ -105,9 +103,7 @@ func TestRetryingNoRetryNeeded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
check := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
check := result.IsLeft[int]
|
||||
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
@@ -139,9 +135,7 @@ func TestRetryingWithDelay(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
check := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
check := result.IsLeft[int]
|
||||
|
||||
// Policy with delay
|
||||
policy := retry.CapDelay(
|
||||
@@ -181,9 +175,7 @@ func TestRetryingAccessesConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
check := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
check := result.IsLeft[int]
|
||||
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
@@ -214,9 +206,7 @@ func TestRetryingWithExponentialBackoff(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
check := func(r Result[int]) bool {
|
||||
return result.IsLeft(r)
|
||||
}
|
||||
check := result.IsLeft[int]
|
||||
|
||||
// Exponential backoff policy
|
||||
policy := retry.CapDelay(
|
||||
@@ -250,8 +240,8 @@ func TestRetryingCheckFunction(t *testing.T) {
|
||||
// Retry while result is less than 3
|
||||
check := func(r Result[int]) bool {
|
||||
return result.Fold(
|
||||
func(error) bool { return true },
|
||||
func(v int) bool { return v < 3 },
|
||||
reader.Of[error](true),
|
||||
N.LessThan(3),
|
||||
)(r)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// Copyright (c) 2024 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
@@ -5,6 +20,7 @@ import (
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
@@ -22,27 +38,117 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
ReaderIOResult[R, A any] = readerioresult.ReaderIOResult[R, A]
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
Result[A any] = result.Result[A]
|
||||
IOEither[E, A any] = ioeither.IOEither[E, A]
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
IO[A any] = io.IO[A]
|
||||
// Option represents an optional value that may or may not be present.
|
||||
// It's an alias for option.Option[A].
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Lazy represents a lazily evaluated computation that produces a value of type A.
|
||||
// It's an alias for lazy.Lazy[A].
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Reader represents a computation that depends on an environment of type R
|
||||
// and produces a value of type A.
|
||||
// It's an alias for reader.Reader[R, A].
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// ReaderOption represents a computation that depends on an environment of type R
|
||||
// and produces an optional value of type A.
|
||||
// It's an alias for readeroption.ReaderOption[R, A].
|
||||
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
|
||||
|
||||
// ReaderIO represents a computation that depends on an environment of type R
|
||||
// and performs side effects to produce a value of type A.
|
||||
// It's an alias for readerio.ReaderIO[R, A].
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
|
||||
// ReaderIOResult represents a computation that depends on an environment of type R,
|
||||
// performs side effects, and may fail with an error.
|
||||
// It's an alias for readerioresult.ReaderIOResult[R, A].
|
||||
ReaderIOResult[R, A any] = readerioresult.ReaderIOResult[R, A]
|
||||
|
||||
// Either represents a value that can be one of two types: Left (error) or Right (success).
|
||||
// It's an alias for either.Either[E, A].
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Result is a specialized Either with error as the left type.
|
||||
// It's an alias for result.Result[A] which is Either[error, A].
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// IOEither represents a side-effecting computation that may fail with an error of type E
|
||||
// or succeed with a value of type A.
|
||||
// It's an alias for ioeither.IOEither[E, A].
|
||||
IOEither[E, A any] = ioeither.IOEither[E, A]
|
||||
|
||||
// IOResult represents a side-effecting computation that may fail with an error
|
||||
// or succeed with a value of type A.
|
||||
// It's an alias for ioresult.IOResult[A] which is IOEither[error, A].
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
|
||||
// IO represents a side-effecting computation that produces a value of type A.
|
||||
// It's an alias for io.IO[A].
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// ReaderReaderIOEither is the base monad transformer that combines:
|
||||
// - Reader[R, ...] for outer dependency injection
|
||||
// - Reader[C, ...] for inner dependency injection (typically context.Context)
|
||||
// - IO for side effects
|
||||
// - Either[E, A] for error handling
|
||||
// It's an alias for readerreaderioeither.ReaderReaderIOEither[R, C, E, A].
|
||||
ReaderReaderIOEither[R, C, E, A any] = readerreaderioeither.ReaderReaderIOEither[R, C, E, A]
|
||||
|
||||
// ReaderReaderIOResult is the main type of this package, specializing ReaderReaderIOEither
|
||||
// with context.Context as the inner reader type and error as the error type.
|
||||
//
|
||||
// Type structure:
|
||||
// ReaderReaderIOResult[R, A] = R -> context.Context -> IO[Either[error, A]]
|
||||
//
|
||||
// This represents a computation that:
|
||||
// 1. Depends on an outer environment of type R (e.g., application config)
|
||||
// 2. Depends on a context.Context for cancellation and request-scoped values
|
||||
// 3. Performs side effects (IO)
|
||||
// 4. May fail with an error or succeed with a value of type A
|
||||
//
|
||||
// This is the primary type used throughout the package for composing
|
||||
// context-aware, effectful computations with error handling.
|
||||
ReaderReaderIOResult[R, A any] = ReaderReaderIOEither[R, context.Context, error, A]
|
||||
|
||||
Kleisli[R, A, B any] = Reader[A, ReaderReaderIOResult[R, B]]
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderReaderIOResult[R, A], B]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Trampoline[L, B any] = tailrec.Trampoline[L, B]
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
// Kleisli represents a function from A to a monadic value ReaderReaderIOResult[R, B].
|
||||
// It's used for composing monadic functions using Kleisli composition.
|
||||
//
|
||||
// Type structure:
|
||||
// Kleisli[R, A, B] = A -> ReaderReaderIOResult[R, B]
|
||||
//
|
||||
// Kleisli arrows can be composed using Chain operations to build complex
|
||||
// data transformation pipelines.
|
||||
Kleisli[R, A, B any] = Reader[A, ReaderReaderIOResult[R, B]]
|
||||
|
||||
// Operator is a specialized Kleisli arrow that operates on monadic values.
|
||||
// It takes a ReaderReaderIOResult[R, A] and produces a ReaderReaderIOResult[R, B].
|
||||
//
|
||||
// Type structure:
|
||||
// Operator[R, A, B] = ReaderReaderIOResult[R, A] -> ReaderReaderIOResult[R, B]
|
||||
//
|
||||
// Operators are useful for transforming monadic computations, such as
|
||||
// adding retry logic, logging, or error recovery.
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderReaderIOResult[R, A], B]
|
||||
|
||||
// Lens represents an optic for focusing on a part of a data structure.
|
||||
// It provides a way to get and set a field T within a structure S.
|
||||
// It's an alias for lens.Lens[S, T].
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Trampoline is used for stack-safe recursion through tail call optimization.
|
||||
// It's an alias for tailrec.Trampoline[L, B].
|
||||
Trampoline[L, B any] = tailrec.Trampoline[L, B]
|
||||
|
||||
// Predicate represents a function that tests whether a value of type A
|
||||
// satisfies some condition.
|
||||
// It's an alias for predicate.Predicate[A].
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Endmorphism represents a function from type A to type A.
|
||||
// It's an alias for endomorphism.Endomorphism[A].
|
||||
Endmorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -169,7 +169,7 @@ func TestContramapMemoize(t *testing.T) {
|
||||
}
|
||||
|
||||
// Cache by ID only
|
||||
cacheByID := ContramapMemoize[string, User, int](func(u User) int {
|
||||
cacheByID := ContramapMemoize[string](func(u User) int {
|
||||
return u.ID
|
||||
})
|
||||
|
||||
@@ -206,7 +206,7 @@ func TestContramapMemoize(t *testing.T) {
|
||||
return p.Price * 1.1 // Add 10% markup
|
||||
}
|
||||
|
||||
cacheBySKU := ContramapMemoize[float64, Product, string](func(p Product) string {
|
||||
cacheBySKU := ContramapMemoize[float64](func(p Product) string {
|
||||
return p.SKU
|
||||
})
|
||||
|
||||
@@ -238,7 +238,7 @@ func TestContramapMemoize(t *testing.T) {
|
||||
}
|
||||
|
||||
// Cache by method and path, ignore body
|
||||
cacheByMethodPath := ContramapMemoize[string, Request, string](func(r Request) string {
|
||||
cacheByMethodPath := ContramapMemoize[string](func(r Request) string {
|
||||
return r.Method + ":" + r.Path
|
||||
})
|
||||
|
||||
@@ -300,7 +300,7 @@ func TestCacheCallback(t *testing.T) {
|
||||
return fmt.Sprintf("Result: %d", n)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback[string, int, int](
|
||||
memoizer := CacheCallback(
|
||||
Identity[int],
|
||||
boundedCache(),
|
||||
)
|
||||
@@ -372,7 +372,7 @@ func TestCacheCallback(t *testing.T) {
|
||||
return fmt.Sprintf("Processed: %s", item.Value)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback[string, Item, int](
|
||||
memoizer := CacheCallback(
|
||||
func(item Item) int { return item.ID },
|
||||
simpleCache(),
|
||||
)
|
||||
@@ -445,7 +445,7 @@ func TestSingleElementCache(t *testing.T) {
|
||||
return fmt.Sprintf("Result: %d", n*n)
|
||||
}
|
||||
|
||||
memoizer := CacheCallback[string, int, int](
|
||||
memoizer := CacheCallback(
|
||||
Identity[int],
|
||||
cache,
|
||||
)
|
||||
@@ -591,7 +591,7 @@ func TestMemoizeIntegration(t *testing.T) {
|
||||
}
|
||||
|
||||
// First level: cache by UserID
|
||||
cacheByUser := ContramapMemoize[string, Request, int](func(r Request) int {
|
||||
cacheByUser := ContramapMemoize[string](func(r Request) int {
|
||||
return r.UserID
|
||||
})
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ func Second[T1, T2 any](_ T1, t2 T2) T2 {
|
||||
}
|
||||
|
||||
// Zero returns the zero value of the given type.
|
||||
func Zero[A comparable]() A {
|
||||
func Zero[A any]() A {
|
||||
var zero A
|
||||
return zero
|
||||
}
|
||||
|
||||
@@ -4,14 +4,11 @@ go 1.24
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v2 v2.27.7
|
||||
github.com/urfave/cli/v3 v3.6.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
10
v2/go.sum
10
v2/go.sum
@@ -1,17 +1,11 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
|
||||
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -204,7 +204,7 @@ func BenchmarkMonadChain_Left(b *testing.B) {
|
||||
|
||||
func BenchmarkChain_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := Chain[benchConfig](func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
chainer := Chain(func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -214,7 +214,7 @@ func BenchmarkChain_Right(b *testing.B) {
|
||||
|
||||
func BenchmarkChain_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
chainer := Chain[benchConfig](func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
chainer := Chain(func(a int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](a * 2) })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -224,7 +224,7 @@ func BenchmarkChain_Left(b *testing.B) {
|
||||
|
||||
func BenchmarkChainFirst_Right(b *testing.B) {
|
||||
rioe := Right[benchConfig](42)
|
||||
chainer := ChainFirst[benchConfig](func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
chainer := ChainFirst(func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -234,7 +234,7 @@ func BenchmarkChainFirst_Right(b *testing.B) {
|
||||
|
||||
func BenchmarkChainFirst_Left(b *testing.B) {
|
||||
rioe := Left[benchConfig, int](benchErr)
|
||||
chainer := ChainFirst[benchConfig](func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
chainer := ChainFirst(func(a int) ReaderIOResult[benchConfig, string] { return Right[benchConfig]("logged") })
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
@@ -443,7 +443,7 @@ func BenchmarkPipeline_Chain_Right(b *testing.B) {
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -455,7 +455,7 @@ func BenchmarkPipeline_Chain_Left(b *testing.B) {
|
||||
for b.Loop() {
|
||||
benchRIOE = F.Pipe1(
|
||||
rioe,
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x * 2) }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -468,7 +468,7 @@ func BenchmarkPipeline_Complex_Right(b *testing.B) {
|
||||
benchRIOE = F.Pipe3(
|
||||
rioe,
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
}
|
||||
@@ -482,7 +482,7 @@ func BenchmarkPipeline_Complex_Left(b *testing.B) {
|
||||
benchRIOE = F.Pipe3(
|
||||
rioe,
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
}
|
||||
@@ -492,7 +492,7 @@ func BenchmarkExecutePipeline_Complex_Right(b *testing.B) {
|
||||
rioe := F.Pipe3(
|
||||
Right[benchConfig](10),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
Chain[benchConfig](func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Chain(func(x int) ReaderIOResult[benchConfig, int] { return Right[benchConfig](x + 1) }),
|
||||
Map[benchConfig](N.Mul(2)),
|
||||
)
|
||||
b.ResetTimer()
|
||||
|
||||
@@ -17,11 +17,12 @@ package bracket
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
)
|
||||
|
||||
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
|
||||
// whether the body action returns and error or not.
|
||||
func Bracket[
|
||||
func MonadBracket[
|
||||
GA, // IOEither[E, A]
|
||||
GB, // IOEither[E, A]
|
||||
GANY, // IOEither[E, ANY]
|
||||
@@ -50,3 +51,41 @@ func Bracket[
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
|
||||
// whether the body action returns and error or not.
|
||||
func Bracket[
|
||||
GA, // IOEither[E, A]
|
||||
GB, // IOEither[E, A]
|
||||
GANY, // IOEither[E, ANY]
|
||||
|
||||
EB, // Either[E, B]
|
||||
|
||||
A, B, ANY any](
|
||||
|
||||
ofeb func(EB) GB,
|
||||
|
||||
chainab chain.ChainType[A, GA, GB],
|
||||
chainebb chain.ChainType[EB, GB, GB],
|
||||
chainany chain.ChainType[ANY, GANY, GB],
|
||||
|
||||
acquire GA,
|
||||
use func(A) GB,
|
||||
release func(A, EB) GANY,
|
||||
) GB {
|
||||
return F.Pipe1(
|
||||
acquire,
|
||||
chainab(
|
||||
func(a A) GB {
|
||||
return F.Pipe1(
|
||||
use(a),
|
||||
chainebb(func(eb EB) GB {
|
||||
return F.Pipe1(
|
||||
release(a, eb),
|
||||
chainany(F.Constant1[ANY](ofeb(eb))),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
70
v2/internal/readert/monoid.go
Normal file
70
v2/internal/readert/monoid.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package readert
|
||||
|
||||
import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// ApplySemigroup lifts a Semigroup[A] into a Semigroup[Reader[R, A]].
|
||||
// This allows you to combine two Readers that produce semigroup values by combining
|
||||
// their results using the semigroup's concat operation.
|
||||
//
|
||||
// The _map and _ap parameters are the Map and Ap operations for the Reader type,
|
||||
// typically obtained from the reader package.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
// // Using the additive semigroup for integers
|
||||
// intSemigroup := semigroup.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
// readerSemigroup := reader.ApplySemigroup(
|
||||
// reader.MonadMap[Config, int, func(int) int],
|
||||
// reader.MonadAp[int, Config, int],
|
||||
// intSemigroup,
|
||||
// )
|
||||
//
|
||||
// r1 := reader.Of[Config](5)
|
||||
// r2 := reader.Of[Config](3)
|
||||
// combined := readerSemigroup.Concat(r1, r2)
|
||||
// result := combined(Config{Multiplier: 1}) // 8
|
||||
func ApplySemigroup[R, A any](
|
||||
_map func(func(R) A, func(A) func(A) A) func(R, func(A) A),
|
||||
_ap func(func(R, func(A) A), func(R) A) func(R) A,
|
||||
|
||||
s S.Semigroup[A],
|
||||
) S.Semigroup[func(R) A] {
|
||||
return S.ApplySemigroup(_map, _ap, s)
|
||||
}
|
||||
|
||||
// ApplicativeMonoid lifts a Monoid[A] into a Monoid[Reader[R, A]].
|
||||
// This allows you to combine Readers that produce monoid values, with an empty/identity Reader.
|
||||
//
|
||||
// The _of parameter is the Of operation (pure/return) for the Reader type.
|
||||
// The _map and _ap parameters are the Map and Ap operations for the Reader type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Prefix string }
|
||||
// // Using the string concatenation monoid
|
||||
// stringMonoid := monoid.MakeMonoid("", func(a, b string) string { return a + b })
|
||||
// readerMonoid := reader.ApplicativeMonoid(
|
||||
// reader.Of[Config, string],
|
||||
// reader.MonadMap[Config, string, func(string) string],
|
||||
// reader.MonadAp[string, Config, string],
|
||||
// stringMonoid,
|
||||
// )
|
||||
//
|
||||
// r1 := reader.Asks(func(c Config) string { return c.Prefix })
|
||||
// r2 := reader.Of[Config]("hello")
|
||||
// combined := readerMonoid.Concat(r1, r2)
|
||||
// result := combined(Config{Prefix: ">> "}) // ">> hello"
|
||||
// empty := readerMonoid.Empty()(Config{Prefix: "any"}) // ""
|
||||
func ApplicativeMonoid[R, A any](
|
||||
_of func(A) func(R) A,
|
||||
_map func(func(R) A, func(A) func(A) A) func(R, func(A) A),
|
||||
_ap func(func(R, func(A) A), func(R) A) func(R) A,
|
||||
|
||||
m M.Monoid[A],
|
||||
) M.Monoid[func(R) A] {
|
||||
return M.ApplicativeMonoid(_of, _map, _ap, m)
|
||||
}
|
||||
@@ -17,7 +17,10 @@ package readert
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
R "github.com/IBM/fp-go/v2/reader/generic"
|
||||
)
|
||||
|
||||
@@ -33,7 +36,7 @@ func MonadMap[GEA ~func(E) HKTA, GEB ~func(E) HKTB, E, A, B, HKTA, HKTB any](
|
||||
}
|
||||
|
||||
func Map[GEA ~func(E) HKTA, GEB ~func(E) HKTB, E, A, B, HKTA, HKTB any](
|
||||
fmap func(func(A) B) func(HKTA) HKTB,
|
||||
fmap functor.MapType[A, B, HKTA, HKTB],
|
||||
f func(A) B,
|
||||
) func(GEA) GEB {
|
||||
return F.Pipe2(
|
||||
@@ -64,7 +67,7 @@ func Chain[GEA ~func(E) HKTA, GEB ~func(E) HKTB, A, E, HKTA, HKTB any](
|
||||
}
|
||||
}
|
||||
|
||||
func MonadOf[GEA ~func(E) HKTA, E, A, HKTA any](fof func(A) HKTA, a A) GEA {
|
||||
func MonadOf[GEA ~func(E) HKTA, E, A, HKTA any](fof pointed.OfType[A, HKTA], a A) GEA {
|
||||
return R.MakeReader(func(_ E) HKTA {
|
||||
return fof(a)
|
||||
})
|
||||
@@ -77,7 +80,9 @@ func MonadAp[GEA ~func(E) HKTA, GEB ~func(E) HKTB, GEFAB ~func(E) HKTFAB, E, A,
|
||||
})
|
||||
}
|
||||
|
||||
func Ap[GEA ~func(E) HKTA, GEB ~func(E) HKTB, GEFAB ~func(E) HKTFAB, E, A, HKTA, HKTB, HKTFAB any](fap func(HKTA) func(HKTFAB) HKTB, fa GEA) func(GEFAB) GEB {
|
||||
func Ap[GEA ~func(E) HKTA, GEB ~func(E) HKTB, GEFAB ~func(E) HKTFAB, E, A, HKTA, HKTB, HKTFAB any](
|
||||
fap apply.ApType[HKTA, HKTB, HKTFAB],
|
||||
fa GEA) func(GEFAB) GEB {
|
||||
return func(fab GEFAB) GEB {
|
||||
return func(r E) HKTB {
|
||||
return fap(fa(r))(fab(r))
|
||||
@@ -86,11 +91,11 @@ func Ap[GEA ~func(E) HKTA, GEB ~func(E) HKTB, GEFAB ~func(E) HKTFAB, E, A, HKTA,
|
||||
}
|
||||
|
||||
func MonadFromReader[GA ~func(E) A, GEA ~func(E) HKTA, E, A, HKTA any](
|
||||
fof func(A) HKTA, ma GA) GEA {
|
||||
fof pointed.OfType[A, HKTA], ma GA) GEA {
|
||||
return R.MakeReader(F.Flow2(ma, fof))
|
||||
}
|
||||
|
||||
func FromReader[GA ~func(E) A, GEA ~func(E) HKTA, E, A, HKTA any](
|
||||
fof func(A) HKTA) func(ma GA) GEA {
|
||||
fof pointed.OfType[A, HKTA]) func(ma GA) GEA {
|
||||
return F.Bind1st(MonadFromReader[GA, GEA, E, A, HKTA], fof)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func Bracket[A, B, ANY any](
|
||||
use Kleisli[A, B],
|
||||
release func(A, B) IO[ANY],
|
||||
) IO[B] {
|
||||
return INTB.Bracket[IO[A], IO[B], IO[ANY], B, A, B](
|
||||
return INTB.MonadBracket[IO[A], IO[B], IO[ANY], B, A, B](
|
||||
Of[B],
|
||||
MonadChain[A, B],
|
||||
MonadChain[B, B],
|
||||
|
||||
@@ -35,7 +35,7 @@ import (
|
||||
//
|
||||
// safeOperation := io.WithLock(lock)(dangerousOperation)
|
||||
// result := safeOperation()
|
||||
func WithLock[A any](lock IO[context.CancelFunc]) func(fa IO[A]) IO[A] {
|
||||
func WithLock[A any](lock IO[context.CancelFunc]) Operator[A, A] {
|
||||
return func(fa IO[A]) IO[A] {
|
||||
return func() A {
|
||||
defer lock()()
|
||||
|
||||
@@ -27,7 +27,7 @@ func Bracket[E, A, B, ANY any](
|
||||
use Kleisli[E, A, B],
|
||||
release func(A, Either[E, B]) IOEither[E, ANY],
|
||||
) IOEither[E, B] {
|
||||
return BR.Bracket[IOEither[E, A], IOEither[E, B], IOEither[E, ANY], Either[E, B], A, B](
|
||||
return BR.MonadBracket[IOEither[E, A], IOEither[E, B], IOEither[E, ANY], Either[E, B], A, B](
|
||||
io.Of[Either[E, B]],
|
||||
MonadChain[E, A, B],
|
||||
io.MonadChain[Either[E, B], Either[E, B]],
|
||||
|
||||
@@ -27,7 +27,7 @@ func Bracket[A, B, ANY any](
|
||||
use Kleisli[A, B],
|
||||
release func(A, Option[B]) IOOption[ANY],
|
||||
) IOOption[B] {
|
||||
return G.Bracket[IOOption[A], IOOption[B], IOOption[ANY], Option[B], A, B](
|
||||
return G.MonadBracket[IOOption[A], IOOption[B], IOOption[ANY], Option[B], A, B](
|
||||
io.Of[Option[B]],
|
||||
MonadChain[A, B],
|
||||
io.MonadChain[Option[B], Option[B]],
|
||||
|
||||
11
v2/main.go
11
v2/main.go
@@ -17,23 +17,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/IBM/fp-go/v2/cli"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
app := &C.App{
|
||||
app := &C.Command{
|
||||
Name: "fp-go",
|
||||
Usage: "Code generation for fp-go",
|
||||
Commands: cli.Commands(),
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
if err := app.Run(ctx, os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ Lenses can be automatically generated using the `fp-go` CLI tool and a simple an
|
||||
1. **Annotate your struct** with the `fp-go:Lens` comment:
|
||||
|
||||
```go
|
||||
//go:generate go run github.com/IBM/fp-go/v2/main.go lens --dir . --filename gen_lens.go
|
||||
//go:generate go run github.com/IBM/fp-go/v2 lens --dir . --filename gen_lens.go
|
||||
|
||||
// fp-go:Lens
|
||||
type Person struct {
|
||||
@@ -230,8 +230,16 @@ type Person struct {
|
||||
Email string
|
||||
Phone *string // Optional field
|
||||
}
|
||||
|
||||
// fp-go:Lens
|
||||
type Config struct {
|
||||
PublicField string
|
||||
privateField int // Unexported fields are supported!
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The generator supports both exported (uppercase) and unexported (lowercase) fields. Generated lenses for unexported fields will have lowercase names and can only be used within the same package as the struct.
|
||||
|
||||
2. **Run `go generate`**:
|
||||
|
||||
```bash
|
||||
@@ -268,6 +276,7 @@ The generator supports:
|
||||
- ✅ Embedded structs (fields are promoted)
|
||||
- ✅ Optional fields (pointers and `omitempty` tags)
|
||||
- ✅ Custom package imports
|
||||
- ✅ **Unexported fields** (lowercase names) - lenses will have lowercase names matching the field names
|
||||
|
||||
See [samples/lens](../samples/lens) for complete examples.
|
||||
|
||||
@@ -293,13 +302,23 @@ More specific optics can be converted to more general ones.
|
||||
|
||||
## 📦 Package Structure
|
||||
|
||||
### Core Optics
|
||||
- **[optics/lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens)**: Lenses for product types (structs)
|
||||
- **[optics/prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism)**: Prisms for sum types ([`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either), [`Result`](https://pkg.go.dev/github.com/IBM/fp-go/v2/result), etc.)
|
||||
- **[optics/iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso)**: Isomorphisms for equivalent types
|
||||
- **[optics/optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)**: Optional optics for maybe values
|
||||
- **[optics/traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)**: Traversals for multiple values
|
||||
|
||||
Each package includes specialized sub-packages for common patterns:
|
||||
### Utilities
|
||||
- **[optics/builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/builder)**: Builder pattern for constructing complex optics
|
||||
- **[optics/codec](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/codec)**: Type-safe encoding/decoding with validation
|
||||
- Provides `Type[A, O, I]` for bidirectional transformations with validation
|
||||
- Includes codecs for primitives (String, Int, Bool), collections (Array), and sum types (Either)
|
||||
- Supports refinement types and codec composition via `Pipe`
|
||||
- Integrates validation errors with context tracking
|
||||
|
||||
### Specialized Sub-packages
|
||||
Each core optics package includes specialized sub-packages for common patterns:
|
||||
- **array**: Optics for arrays/slices
|
||||
- **either**: Optics for [`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either) types
|
||||
- **option**: Optics for [`Option`](https://pkg.go.dev/github.com/IBM/fp-go/v2/option) types
|
||||
|
||||
31
v2/optics/builder/builder.go
Normal file
31
v2/optics/builder/builder.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
func MakeBuilder[S, A any](get func(S) Option[A], set func(A) Endomorphism[S], name string) Builder[S, A] {
|
||||
return Builder[S, A]{
|
||||
GetOption: get,
|
||||
Set: set,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func ComposeLensPrism[S, A, B any](r Prism[A, B]) func(Lens[S, A]) Builder[S, B] {
|
||||
return func(l Lens[S, A]) Builder[S, B] {
|
||||
return MakeBuilder(
|
||||
F.Flow2(
|
||||
l.Get,
|
||||
r.GetOption,
|
||||
),
|
||||
F.Flow2(
|
||||
r.ReverseGet,
|
||||
l.Set,
|
||||
),
|
||||
fmt.Sprintf("Compose[%s -> %s]", l, r),
|
||||
)
|
||||
}
|
||||
}
|
||||
27
v2/optics/builder/types.go
Normal file
27
v2/optics/builder/types.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Builder[S, A any] struct {
|
||||
GetOption func(S) Option[A]
|
||||
|
||||
Set func(A) Endomorphism[S]
|
||||
|
||||
name string
|
||||
}
|
||||
|
||||
Kleisli[S, A, B any] = func(A) Builder[S, B]
|
||||
Operator[S, A, B any] = Kleisli[S, Builder[S, A], B]
|
||||
)
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -27,6 +28,8 @@ type typeImpl[A, O, I any] struct {
|
||||
encode Encode[A, O]
|
||||
}
|
||||
|
||||
var emptyContext = A.Empty[validation.ContextEntry]()
|
||||
|
||||
// MakeType creates a new Type with the given name, type checker, validator, and encoder.
|
||||
//
|
||||
// Parameters:
|
||||
@@ -52,7 +55,7 @@ func MakeType[A, O, I any](
|
||||
|
||||
// Validate validates the input value in the context of a validation path.
|
||||
// Returns a Reader that takes a Context and produces a Validation result.
|
||||
func (t *typeImpl[A, O, I]) Validate(i I) Reader[Context, Validation[A]] {
|
||||
func (t *typeImpl[A, O, I]) Validate(i I) Decode[Context, A] {
|
||||
return t.validate(i)
|
||||
}
|
||||
|
||||
@@ -138,16 +141,16 @@ func isTypedNil[A any](x any) Result[*A] {
|
||||
return result.Left[*A](errors.New("expecting nil"))
|
||||
}
|
||||
|
||||
func validateFromIs[A any](
|
||||
is ReaderResult[any, A],
|
||||
func validateFromIs[A, I any](
|
||||
is ReaderResult[I, A],
|
||||
msg string,
|
||||
) Reader[any, Reader[Context, Validation[A]]] {
|
||||
return func(u any) Reader[Context, Validation[A]] {
|
||||
) Validate[I, A] {
|
||||
return func(i I) Decode[Context, A] {
|
||||
return F.Pipe2(
|
||||
u,
|
||||
i,
|
||||
is,
|
||||
result.Fold(
|
||||
validation.FailureWithError[A](u, msg),
|
||||
validation.FailureWithError[A](F.ToAny(i), msg),
|
||||
F.Flow2(
|
||||
validation.Success[A],
|
||||
reader.Of[Context],
|
||||
@@ -157,6 +160,17 @@ func validateFromIs[A any](
|
||||
}
|
||||
}
|
||||
|
||||
func isFromValidate[T, I any](val Validate[I, T]) ReaderResult[any, T] {
|
||||
invalidType := result.Left[T](errors.New("invalid input type"))
|
||||
return func(u any) Result[T] {
|
||||
i, ok := u.(I)
|
||||
if !ok {
|
||||
return invalidType
|
||||
}
|
||||
return validation.ToResult(val(i)(emptyContext))
|
||||
}
|
||||
}
|
||||
|
||||
// MakeNilType creates a Type that validates nil values.
|
||||
// It accepts any input and validates that it is nil, returning a typed nil pointer.
|
||||
//
|
||||
@@ -178,8 +192,7 @@ func Nil[A any]() Type[*A, *A, any] {
|
||||
}
|
||||
|
||||
func MakeSimpleType[A any]() Type[A, A, any] {
|
||||
var zero A
|
||||
name := fmt.Sprintf("%T", zero)
|
||||
name := fmt.Sprintf("%T", *new(A))
|
||||
is := Is[A]()
|
||||
|
||||
return MakeType(
|
||||
@@ -190,14 +203,53 @@ func MakeSimpleType[A any]() Type[A, A, any] {
|
||||
)
|
||||
}
|
||||
|
||||
// String creates a Type for string values.
|
||||
// It validates that input is a string type and provides identity encoding/decoding.
|
||||
// This is a simple type that accepts any input and validates it's a string.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[string, string, any] that can validate, decode, and encode string values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// stringType := codec.String()
|
||||
// result := stringType.Decode("hello") // Success: Right("hello")
|
||||
// result := stringType.Decode(123) // Failure: Left(validation errors)
|
||||
// encoded := stringType.Encode("world") // Returns: "world"
|
||||
func String() Type[string, string, any] {
|
||||
return MakeSimpleType[string]()
|
||||
}
|
||||
|
||||
// Int creates a Type for int values.
|
||||
// It validates that input is an int type and provides identity encoding/decoding.
|
||||
// This is a simple type that accepts any input and validates it's an int.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[int, int, any] that can validate, decode, and encode int values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intType := codec.Int()
|
||||
// result := intType.Decode(42) // Success: Right(42)
|
||||
// result := intType.Decode("42") // Failure: Left(validation errors)
|
||||
// encoded := intType.Encode(100) // Returns: 100
|
||||
func Int() Type[int, int, any] {
|
||||
return MakeSimpleType[int]()
|
||||
}
|
||||
|
||||
// Bool creates a Type for bool values.
|
||||
// It validates that input is a bool type and provides identity encoding/decoding.
|
||||
// This is a simple type that accepts any input and validates it's a bool.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[bool, bool, any] that can validate, decode, and encode bool values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// boolType := codec.Bool()
|
||||
// result := boolType.Decode(true) // Success: Right(true)
|
||||
// result := boolType.Decode(1) // Failure: Left(validation errors)
|
||||
// encoded := boolType.Encode(false) // Returns: false
|
||||
func Bool() Type[bool, bool, any] {
|
||||
return MakeSimpleType[bool]()
|
||||
}
|
||||
@@ -216,7 +268,7 @@ func pairToValidation[T any](p validationPair[T]) Validation[T] {
|
||||
return either.Of[validation.Errors](value)
|
||||
}
|
||||
|
||||
func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Validation[[]T]] {
|
||||
func validateArrayFromArray[T, O, I any](item Type[T, O, I]) Validate[[]I, []T] {
|
||||
|
||||
appendErrors := F.Flow2(
|
||||
A.Concat,
|
||||
@@ -232,8 +284,48 @@ func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Vali
|
||||
|
||||
zero := pair.Zero[validation.Errors, []T]()
|
||||
|
||||
return func(u any) Reader[Context, Validation[[]T]] {
|
||||
val := reflect.ValueOf(u)
|
||||
return func(is []I) Decode[Context, []T] {
|
||||
|
||||
return func(c Context) Validation[[]T] {
|
||||
|
||||
return F.Pipe1(
|
||||
A.MonadReduceWithIndex(is, func(i int, p validationPair[[]T], v I) validationPair[[]T] {
|
||||
return either.MonadFold(
|
||||
item.Validate(v)(appendContext(strconv.Itoa(i), itemName, v)(c)),
|
||||
appendErrors,
|
||||
appendValues,
|
||||
)(p)
|
||||
}, zero),
|
||||
pairToValidation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateArray[T, O any](item Type[T, O, any]) Validate[any, []T] {
|
||||
|
||||
appendErrors := F.Flow2(
|
||||
A.Concat,
|
||||
pair.MapHead[[]T, validation.Errors],
|
||||
)
|
||||
|
||||
appendValues := F.Flow2(
|
||||
A.Push,
|
||||
pair.MapTail[validation.Errors, []T],
|
||||
)
|
||||
|
||||
itemName := item.Name()
|
||||
|
||||
zero := pair.Zero[validation.Errors, []T]()
|
||||
|
||||
return func(i any) Decode[Context, []T] {
|
||||
|
||||
res, ok := i.([]T)
|
||||
if ok {
|
||||
return reader.Of[Context](validation.Success(res))
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(i)
|
||||
if !val.IsValid() {
|
||||
return validation.FailureWithMessage[[]T](val, "invalid value")
|
||||
}
|
||||
@@ -246,8 +338,9 @@ func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Vali
|
||||
|
||||
return F.Pipe1(
|
||||
R.MonadReduceWithIndex(val, func(i int, p validationPair[[]T], v reflect.Value) validationPair[[]T] {
|
||||
vIface := v.Interface()
|
||||
return either.MonadFold(
|
||||
item.Validate(v)(appendContext(strconv.Itoa(i), itemName, v)(c)),
|
||||
item.Validate(vIface)(appendContext(strconv.Itoa(i), itemName, vIface)(c)),
|
||||
appendErrors,
|
||||
appendValues,
|
||||
)(p)
|
||||
@@ -260,3 +353,397 @@ func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Vali
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Array creates a Type for array/slice values with elements of type T.
|
||||
// It validates that input is an array, slice, or string, and validates each element
|
||||
// using the provided item Type. During encoding, it maps the encode function over all elements.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the decoded array
|
||||
// - O: The type of elements in the encoded array
|
||||
//
|
||||
// Parameters:
|
||||
// - item: A Type[T, O, any] that defines how to validate/encode individual elements
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[[]T, []O, any] that can validate, decode, and encode array values
|
||||
//
|
||||
// The function handles:
|
||||
// - Native Go slices of type []T (passed through directly)
|
||||
// - reflect.Array, reflect.Slice, reflect.String (validated element by element)
|
||||
// - Collects all validation errors from individual elements
|
||||
// - Provides detailed context for each element's position in error messages
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intArray := codec.Array(codec.Int())
|
||||
// result := intArray.Decode([]int{1, 2, 3}) // Success: Right([1, 2, 3])
|
||||
// result := intArray.Decode([]any{1, "2", 3}) // Failure: validation error at index 1
|
||||
// encoded := intArray.Encode([]int{1, 2, 3}) // Returns: []int{1, 2, 3}
|
||||
//
|
||||
// stringArray := codec.Array(codec.String())
|
||||
// result := stringArray.Decode([]string{"a", "b"}) // Success: Right(["a", "b"])
|
||||
// result := stringArray.Decode("hello") // Success: Right(["h", "e", "l", "l", "o"])
|
||||
func Array[T, O any](item Type[T, O, any]) Type[[]T, []O, any] {
|
||||
|
||||
validate := validateArray(item)
|
||||
is := isFromValidate(validate)
|
||||
name := fmt.Sprintf("Array[%s]", item.Name())
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
is,
|
||||
validate,
|
||||
A.Map(item.Encode),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// TranscodeArray creates a Type for array/slice values with strongly-typed input.
|
||||
// Unlike Array which accepts any input type, TranscodeArray requires the input to be
|
||||
// a slice of type []I, providing type safety at the input level.
|
||||
//
|
||||
// This function validates each element of the input slice using the provided item Type,
|
||||
// transforming []I -> []T during decoding and []T -> []O during encoding.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the decoded array
|
||||
// - O: The type of elements in the encoded array
|
||||
// - I: The type of elements in the input array (must be a slice)
|
||||
//
|
||||
// Parameters:
|
||||
// - item: A Type[T, O, I] that defines how to validate/encode individual elements
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[[]T, []O, []I] that can validate, decode, and encode array values
|
||||
//
|
||||
// The function:
|
||||
// - Requires input to be exactly []I (not any)
|
||||
// - Validates each element using the item Type's validation logic
|
||||
// - Collects all validation errors from individual elements
|
||||
// - Provides detailed context for each element's position in error messages
|
||||
// - Maps the encode function over all elements during encoding
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec that transforms string slices to int slices
|
||||
// stringToInt := codec.MakeType[int, int, string](
|
||||
// "StringToInt",
|
||||
// func(s any) result.Result[int] { ... },
|
||||
// func(s string) codec.Validate[int] { ... },
|
||||
// func(i int) int { return i },
|
||||
// )
|
||||
// arrayCodec := codec.TranscodeArray(stringToInt)
|
||||
//
|
||||
// // Decode: []string -> []int
|
||||
// result := arrayCodec.Decode([]string{"1", "2", "3"}) // Success: Right([1, 2, 3])
|
||||
// result := arrayCodec.Decode([]string{"1", "x", "3"}) // Failure: validation error at index 1
|
||||
//
|
||||
// // Encode: []int -> []int
|
||||
// encoded := arrayCodec.Encode([]int{1, 2, 3}) // Returns: []int{1, 2, 3}
|
||||
//
|
||||
// Use TranscodeArray when:
|
||||
// - You need type-safe input validation ([]I instead of any)
|
||||
// - You're transforming between different slice element types
|
||||
// - You want compile-time guarantees about input types
|
||||
//
|
||||
// Use Array when:
|
||||
// - You need to accept various input types (any, reflect.Value, etc.)
|
||||
// - You're working with dynamic or unknown input types
|
||||
func TranscodeArray[T, O, I any](item Type[T, O, I]) Type[[]T, []O, []I] {
|
||||
validate := validateArrayFromArray(item)
|
||||
is := isFromValidate(validate)
|
||||
name := fmt.Sprintf("Array[%s]", item.Name())
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
is,
|
||||
validate,
|
||||
A.Map(item.Encode),
|
||||
)
|
||||
}
|
||||
|
||||
func validateEitherFromEither[L, R, OL, OR, IL, IR any](
|
||||
leftItem Type[L, OL, IL],
|
||||
rightItem Type[R, OR, IR],
|
||||
) Validate[either.Either[IL, IR], either.Either[L, R]] {
|
||||
|
||||
// leftName := left.Name()
|
||||
// rightName := right.Name()
|
||||
|
||||
return func(is either.Either[IL, IR]) Decode[Context, either.Either[L, R]] {
|
||||
|
||||
return either.MonadFold(
|
||||
is,
|
||||
F.Flow2(
|
||||
leftItem.Validate,
|
||||
readereither.Map[Context, validation.Errors](either.Left[R, L]),
|
||||
),
|
||||
F.Flow2(
|
||||
rightItem.Validate,
|
||||
readereither.Map[Context, validation.Errors](either.Right[L, R]),
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// TranscodeEither creates a Type for Either values with strongly-typed left and right branches.
|
||||
// It validates and transforms Either[IL, IR] to Either[L, R] during decoding, and
|
||||
// Either[L, R] to Either[OL, OR] during encoding.
|
||||
//
|
||||
// This function is useful for handling sum types (discriminated unions) where a value can be
|
||||
// one of two possible types. Each branch (Left and Right) is validated and transformed
|
||||
// independently using its respective Type codec.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - L: The type of the decoded Left value
|
||||
// - R: The type of the decoded Right value
|
||||
// - OL: The type of the encoded Left value
|
||||
// - OR: The type of the encoded Right value
|
||||
// - IL: The type of the input Left value
|
||||
// - IR: The type of the input Right value
|
||||
//
|
||||
// Parameters:
|
||||
// - leftItem: A Type[L, OL, IL] that defines how to validate/encode Left values
|
||||
// - rightItem: A Type[R, OR, IR] that defines how to validate/encode Right values
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[Either[L, R], Either[OL, OR], Either[IL, IR]] that can validate, decode, and encode Either values
|
||||
//
|
||||
// The function:
|
||||
// - Validates Left values using leftItem's validation logic
|
||||
// - Validates Right values using rightItem's validation logic
|
||||
// - Preserves the Either structure (Left stays Left, Right stays Right)
|
||||
// - Provides context-aware error messages indicating which branch failed
|
||||
// - Transforms values through the respective codecs during encoding
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for Either[string, int]
|
||||
// stringCodec := codec.String()
|
||||
// intCodec := codec.Int()
|
||||
// eitherCodec := codec.TranscodeEither(stringCodec, intCodec)
|
||||
//
|
||||
// // Decode Left value
|
||||
// leftResult := eitherCodec.Decode(either.Left[int]("error"))
|
||||
// // Success: Right(Either.Left("error"))
|
||||
//
|
||||
// // Decode Right value
|
||||
// rightResult := eitherCodec.Decode(either.Right[string](42))
|
||||
// // Success: Right(Either.Right(42))
|
||||
//
|
||||
// // Encode Left value
|
||||
// encodedLeft := eitherCodec.Encode(either.Left[int]("error"))
|
||||
// // Returns: Either.Left("error")
|
||||
//
|
||||
// // Encode Right value
|
||||
// encodedRight := eitherCodec.Encode(either.Right[string](42))
|
||||
// // Returns: Either.Right(42)
|
||||
//
|
||||
// Use TranscodeEither when:
|
||||
// - You need to handle sum types or discriminated unions
|
||||
// - You want to validate and transform both branches of an Either independently
|
||||
// - You're working with error handling patterns (Left for errors, Right for success)
|
||||
// - You need type-safe transformations for both possible values
|
||||
//
|
||||
// Common patterns:
|
||||
// - Error handling: Either[Error, Value]
|
||||
// - Optional with reason: Either[Reason, Value]
|
||||
// - Validation results: Either[ValidationError, ValidatedData]
|
||||
func TranscodeEither[L, R, OL, OR, IL, IR any](leftItem Type[L, OL, IL], rightItem Type[R, OR, IR]) Type[either.Either[L, R], either.Either[OL, OR], either.Either[IL, IR]] {
|
||||
validate := validateEitherFromEither(leftItem, rightItem)
|
||||
is := isFromValidate(validate)
|
||||
name := fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name())
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
is,
|
||||
validate,
|
||||
either.Fold(F.Flow2(
|
||||
leftItem.Encode,
|
||||
either.Left[OR, OL],
|
||||
), F.Flow2(
|
||||
rightItem.Encode,
|
||||
either.Right[OL, OR],
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
func validateAlways[T any](is T) Decode[Context, T] {
|
||||
return reader.Of[Context](validation.Success(is))
|
||||
}
|
||||
|
||||
// Id creates an identity Type codec that performs no transformation or validation.
|
||||
//
|
||||
// An identity codec is a Type[T, T, T] where:
|
||||
// - Decode: Always succeeds and returns the input value unchanged
|
||||
// - Encode: Returns the input value unchanged (identity function)
|
||||
// - Validation: Always succeeds without any checks
|
||||
//
|
||||
// This is useful as:
|
||||
// - A building block for more complex codecs
|
||||
// - A no-op codec when you need a Type but don't want any transformation
|
||||
// - A starting point for codec composition
|
||||
// - Testing and debugging codec pipelines
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type that passes through unchanged
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[T, T, T] that performs identity operations on type T
|
||||
//
|
||||
// The codec:
|
||||
// - Name: Uses the type's string representation (e.g., "int", "string")
|
||||
// - Is: Checks if a value is of type T
|
||||
// - Validate: Always succeeds and returns the input value
|
||||
// - Encode: Identity function (returns input unchanged)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create an identity codec for strings
|
||||
// stringId := codec.Id[string]()
|
||||
//
|
||||
// // Decode always succeeds
|
||||
// result := stringId.Decode("hello") // Success: Right("hello")
|
||||
//
|
||||
// // Encode is identity
|
||||
// encoded := stringId.Encode("world") // Returns: "world"
|
||||
//
|
||||
// // Use in composition
|
||||
// arrayOfStrings := codec.TranscodeArray(stringId)
|
||||
// result := arrayOfStrings.Decode([]string{"a", "b", "c"})
|
||||
//
|
||||
// Use cases:
|
||||
// - When you need a Type but don't want any validation or transformation
|
||||
// - As a placeholder in generic code that requires a Type parameter
|
||||
// - Building blocks for TranscodeArray, TranscodeEither, etc.
|
||||
// - Testing codec composition without side effects
|
||||
//
|
||||
// Note: Unlike MakeSimpleType which validates the type, Id always succeeds
|
||||
// in validation. It only checks the type during the Is operation.
|
||||
func Id[T any]() Type[T, T, T] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("%T", *new(T)),
|
||||
Is[T](),
|
||||
validateAlways[T],
|
||||
F.Identity[T],
|
||||
)
|
||||
}
|
||||
|
||||
func validateFromRefinement[A, B any](refinement Refinement[A, B]) Validate[A, B] {
|
||||
|
||||
return func(a A) Decode[Context, B] {
|
||||
|
||||
return func(ctx Context) Validation[B] {
|
||||
return F.Pipe2(
|
||||
a,
|
||||
refinement.GetOption,
|
||||
either.FromOption[B](func() validation.Errors {
|
||||
return array.Of(&validation.ValidationError{
|
||||
Value: a,
|
||||
Context: ctx,
|
||||
Messsage: fmt.Sprintf("type cannot be refined: %s", refinement),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isFromRefinement[A, B any](refinement Refinement[A, B]) ReaderResult[any, B] {
|
||||
|
||||
isA := Is[A]()
|
||||
isB := Is[B]()
|
||||
|
||||
err := fmt.Errorf("type cannot be refined: %s", refinement)
|
||||
|
||||
isAtoB := F.Flow2(
|
||||
isA,
|
||||
result.ChainOptionK[A, B](lazy.Of(err))(refinement.GetOption),
|
||||
)
|
||||
|
||||
return F.Pipe1(
|
||||
isAtoB,
|
||||
readereither.ChainLeft(reader.Of[error](isB)),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// FromRefinement creates a Type codec from a Refinement (Prism).
|
||||
//
|
||||
// A Refinement[A, B] represents the concept that B is a specialized/refined version of A.
|
||||
// For example, PositiveInt is a refinement of int, or NonEmptyString is a refinement of string.
|
||||
// This function converts a Prism[A, B] into a Type[B, A, A] codec that can validate and transform
|
||||
// between the base type A and the refined type B.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The base/broader type (e.g., int, string)
|
||||
// - B: The refined/specialized type (e.g., PositiveInt, NonEmptyString)
|
||||
//
|
||||
// Parameters:
|
||||
// - refinement: A Refinement[A, B] (which is a Prism[A, B]) that defines:
|
||||
// - GetOption: A → Option[B] - attempts to refine A to B (may fail if refinement conditions aren't met)
|
||||
// - ReverseGet: B → A - converts refined type back to base type (always succeeds)
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[B, A, A] codec where:
|
||||
// - Decode: A → Validation[B] - validates that A satisfies refinement conditions and produces B
|
||||
// - Encode: B → A - converts refined type back to base type using ReverseGet
|
||||
// - Is: Checks if a value is of type B
|
||||
// - Name: Descriptive name including the refinement's string representation
|
||||
//
|
||||
// The codec:
|
||||
// - Uses the refinement's GetOption for validation during decoding
|
||||
// - Returns validation errors if the refinement conditions are not met
|
||||
// - Uses the refinement's ReverseGet for encoding (always succeeds)
|
||||
// - Provides context-aware error messages indicating why refinement failed
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Define a refinement for positive integers
|
||||
// positiveIntPrism := prism.MakePrismWithName(
|
||||
// func(n int) option.Option[int] {
|
||||
// if n > 0 {
|
||||
// return option.Some(n)
|
||||
// }
|
||||
// return option.None[int]()
|
||||
// },
|
||||
// func(n int) int { return n },
|
||||
// "PositiveInt",
|
||||
// )
|
||||
//
|
||||
// // Create a codec from the refinement
|
||||
// positiveIntCodec := codec.FromRefinement[int, int](positiveIntPrism)
|
||||
//
|
||||
// // Decode: validates the refinement condition
|
||||
// result := positiveIntCodec.Decode(42) // Success: Right(42)
|
||||
// result = positiveIntCodec.Decode(-5) // Failure: validation error
|
||||
// result = positiveIntCodec.Decode(0) // Failure: validation error
|
||||
//
|
||||
// // Encode: converts back to base type
|
||||
// encoded := positiveIntCodec.Encode(42) // Returns: 42
|
||||
//
|
||||
// Use cases:
|
||||
// - Creating codecs for refined types (positive numbers, non-empty strings, etc.)
|
||||
// - Validating that values meet specific constraints
|
||||
// - Building type-safe APIs with refined types
|
||||
// - Composing refinements with other codecs using Pipe
|
||||
//
|
||||
// Common refinement patterns:
|
||||
// - Numeric constraints: PositiveInt, NonNegativeFloat, BoundedInt
|
||||
// - String constraints: NonEmptyString, EmailAddress, URL
|
||||
// - Collection constraints: NonEmptyArray, UniqueElements
|
||||
// - Domain-specific constraints: ValidAge, ValidZipCode, ValidCreditCard
|
||||
//
|
||||
// Note: The refinement's GetOption returning None will result in a validation error
|
||||
// with a message indicating the type cannot be refined. For more specific error messages,
|
||||
// consider using MakeType directly with custom validation logic.
|
||||
func FromRefinement[A, B any](refinement Refinement[A, B]) Type[B, A, A] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("FromRefinement(%s)", refinement),
|
||||
isFromRefinement(refinement),
|
||||
validateFromRefinement(refinement),
|
||||
refinement.ReverseGet,
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
129
v2/optics/codec/decode/monad.go
Normal file
129
v2/optics/codec/decode/monad.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package decode
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Of creates a Decode that always succeeds with the given value.
|
||||
// This is the pointed functor operation that lifts a pure value into the Decode context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// decoder := decode.Of[string](42)
|
||||
// result := decoder("any input") // Always returns validation.Success(42)
|
||||
func Of[I, A any](a A) Decode[I, A] {
|
||||
return reader.Of[I](validation.Of(a))
|
||||
}
|
||||
|
||||
// MonadChain sequences two decode operations, passing the result of the first to the second.
|
||||
// This is the monadic bind operation that enables sequential composition of decoders.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// decoder1 := decode.Of[string](42)
|
||||
// decoder2 := decode.MonadChain(decoder1, func(n int) Decode[string, string] {
|
||||
// return decode.Of[string](fmt.Sprintf("Number: %d", n))
|
||||
// })
|
||||
func MonadChain[I, A, B any](fa Decode[I, A], f Kleisli[I, A, B]) Decode[I, B] {
|
||||
return readert.MonadChain(
|
||||
validation.MonadChain,
|
||||
fa,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Chain creates an operator that sequences decode operations.
|
||||
// This is the curried version of MonadChain, useful for composition pipelines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// chainOp := decode.Chain(func(n int) Decode[string, string] {
|
||||
// return decode.Of[string](fmt.Sprintf("Number: %d", n))
|
||||
// })
|
||||
// decoder := chainOp(decode.Of[string](42))
|
||||
func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
|
||||
return readert.Chain[Decode[I, A]](
|
||||
validation.Chain,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadMap transforms the decoded value using the provided function.
|
||||
// This is the functor map operation that applies a transformation to successful decode results.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// decoder := decode.Of[string](42)
|
||||
// mapped := decode.MonadMap(decoder, func(n int) string {
|
||||
// return fmt.Sprintf("Number: %d", n)
|
||||
// })
|
||||
func MonadMap[I, A, B any](fa Decode[I, A], f func(A) B) Decode[I, B] {
|
||||
return readert.MonadMap[
|
||||
Decode[I, A],
|
||||
Decode[I, B]](
|
||||
validation.MonadMap,
|
||||
fa,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Map creates an operator that transforms decoded values.
|
||||
// This is the curried version of MonadMap, useful for composition pipelines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mapOp := decode.Map(func(n int) string {
|
||||
// return fmt.Sprintf("Number: %d", n)
|
||||
// })
|
||||
// decoder := mapOp(decode.Of[string](42))
|
||||
func Map[I, A, B any](f func(A) B) Operator[I, A, B] {
|
||||
return readert.Map[
|
||||
Decode[I, A],
|
||||
Decode[I, B]](
|
||||
validation.Map,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAp applies a decoder containing a function to a decoder containing a value.
|
||||
// This is the applicative apply operation that enables parallel composition of decoders.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// decoderFn := decode.Of[string](func(n int) string {
|
||||
// return fmt.Sprintf("Number: %d", n)
|
||||
// })
|
||||
// decoderVal := decode.Of[string](42)
|
||||
// result := decode.MonadAp(decoderFn, decoderVal)
|
||||
func MonadAp[B, I, A any](fab Decode[I, func(A) B], fa Decode[I, A]) Decode[I, B] {
|
||||
return readert.MonadAp[
|
||||
Decode[I, A],
|
||||
Decode[I, B],
|
||||
Decode[I, func(A) B], I, A](
|
||||
validation.MonadAp[B, A],
|
||||
fab,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// Ap creates an operator that applies a function decoder to a value decoder.
|
||||
// This is the curried version of MonadAp, useful for composition pipelines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// apOp := decode.Ap[string](decode.Of[string](42))
|
||||
// decoderFn := decode.Of[string](func(n int) string {
|
||||
// return fmt.Sprintf("Number: %d", n)
|
||||
// })
|
||||
// result := apOp(decoderFn)
|
||||
func Ap[B, I, A any](fa Decode[I, A]) Operator[I, func(A) B, B] {
|
||||
return readert.Ap[
|
||||
Decode[I, A],
|
||||
Decode[I, B],
|
||||
Decode[I, func(A) B], I, A](
|
||||
validation.Ap[B, A],
|
||||
fa,
|
||||
)
|
||||
}
|
||||
384
v2/optics/codec/decode/monad_test.go
Normal file
384
v2/optics/codec/decode/monad_test.go
Normal file
@@ -0,0 +1,384 @@
|
||||
package decode
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestOf tests the Of function
|
||||
func TestOf(t *testing.T) {
|
||||
t.Run("creates decoder that always succeeds", func(t *testing.T) {
|
||||
decoder := Of[string](42)
|
||||
res := decoder("any input")
|
||||
|
||||
assert.Equal(t, validation.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("works with different input types", func(t *testing.T) {
|
||||
decoder := Of[int]("hello")
|
||||
res := decoder(123)
|
||||
|
||||
assert.Equal(t, validation.Of("hello"), res)
|
||||
})
|
||||
|
||||
t.Run("works with complex types", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
decoder := Of[string](person)
|
||||
res := decoder("input")
|
||||
|
||||
assert.Equal(t, validation.Of(person), res)
|
||||
})
|
||||
|
||||
t.Run("ignores input value", func(t *testing.T) {
|
||||
decoder := Of[string](100)
|
||||
|
||||
res1 := decoder("input1")
|
||||
res2 := decoder("input2")
|
||||
|
||||
assert.Equal(t, res1, res2)
|
||||
assert.Equal(t, validation.Of(100), res1)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadChain tests the MonadChain function
|
||||
func TestMonadChain(t *testing.T) {
|
||||
t.Run("chains successful decoders", func(t *testing.T) {
|
||||
decoder1 := Of[string](42)
|
||||
decoder2 := MonadChain(decoder1, func(n int) Decode[string, string] {
|
||||
return Of[string](fmt.Sprintf("Number: %d", n))
|
||||
})
|
||||
|
||||
res := decoder2("input")
|
||||
assert.Equal(t, validation.Of("Number: 42"), res)
|
||||
})
|
||||
|
||||
t.Run("chains multiple operations", func(t *testing.T) {
|
||||
decoder1 := Of[string](10)
|
||||
decoder2 := MonadChain(decoder1, func(n int) Decode[string, int] {
|
||||
return Of[string](n * 2)
|
||||
})
|
||||
decoder3 := MonadChain(decoder2, func(n int) Decode[string, string] {
|
||||
return Of[string](fmt.Sprintf("Result: %d", n))
|
||||
})
|
||||
|
||||
res := decoder3("input")
|
||||
assert.Equal(t, validation.Of("Result: 20"), res)
|
||||
})
|
||||
|
||||
t.Run("propagates validation errors", func(t *testing.T) {
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "decode failed"},
|
||||
})
|
||||
}
|
||||
|
||||
decoder1 := failingDecoder
|
||||
decoder2 := MonadChain(decoder1, func(n int) Decode[string, string] {
|
||||
return Of[string](fmt.Sprintf("Number: %d", n))
|
||||
})
|
||||
|
||||
res := decoder2("input")
|
||||
assert.True(t, either.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("short-circuits on first error", func(t *testing.T) {
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "first error"},
|
||||
})
|
||||
}
|
||||
|
||||
chainCalled := false
|
||||
decoder := MonadChain(failingDecoder, func(n int) Decode[string, string] {
|
||||
chainCalled = true
|
||||
return Of[string]("should not be called")
|
||||
})
|
||||
|
||||
res := decoder("input")
|
||||
assert.True(t, either.IsLeft(res))
|
||||
assert.False(t, chainCalled, "Chain function should not be called on error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestChain tests the Chain function
|
||||
func TestChain(t *testing.T) {
|
||||
t.Run("creates chainable operator", func(t *testing.T) {
|
||||
chainOp := Chain(func(n int) Decode[string, string] {
|
||||
return Of[string](fmt.Sprintf("Number: %d", n))
|
||||
})
|
||||
|
||||
decoder := chainOp(Of[string](42))
|
||||
res := decoder("input")
|
||||
|
||||
assert.Equal(t, validation.Of("Number: 42"), res)
|
||||
})
|
||||
|
||||
t.Run("can be composed", func(t *testing.T) {
|
||||
double := Chain(func(n int) Decode[string, int] {
|
||||
return Of[string](n * 2)
|
||||
})
|
||||
|
||||
toString := Chain(func(n int) Decode[string, string] {
|
||||
return Of[string](fmt.Sprintf("Value: %d", n))
|
||||
})
|
||||
|
||||
decoder := toString(double(Of[string](21)))
|
||||
res := decoder("input")
|
||||
|
||||
assert.Equal(t, validation.Of("Value: 42"), res)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadMap tests the MonadMap function
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("maps successful decoder", func(t *testing.T) {
|
||||
decoder := Of[string](42)
|
||||
mapped := MonadMap(decoder, S.Format[int]("Number: %d"))
|
||||
|
||||
res := mapped("input")
|
||||
assert.Equal(t, validation.Of("Number: 42"), res)
|
||||
})
|
||||
|
||||
t.Run("transforms value type", func(t *testing.T) {
|
||||
decoder := Of[string]("hello")
|
||||
mapped := MonadMap(decoder, S.Size)
|
||||
|
||||
res := mapped("input")
|
||||
assert.Equal(t, validation.Of(5), res)
|
||||
})
|
||||
|
||||
t.Run("preserves validation errors", func(t *testing.T) {
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "decode failed"},
|
||||
})
|
||||
}
|
||||
|
||||
mapped := MonadMap(failingDecoder, S.Format[int]("Number: %d"))
|
||||
|
||||
res := mapped("input")
|
||||
assert.True(t, either.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("does not call function on error", func(t *testing.T) {
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error"},
|
||||
})
|
||||
}
|
||||
|
||||
mapCalled := false
|
||||
mapped := MonadMap(failingDecoder, func(n int) string {
|
||||
mapCalled = true
|
||||
return "should not be called"
|
||||
})
|
||||
|
||||
res := mapped("input")
|
||||
assert.True(t, either.IsLeft(res))
|
||||
assert.False(t, mapCalled, "Map function should not be called on error")
|
||||
})
|
||||
|
||||
t.Run("chains multiple maps", func(t *testing.T) {
|
||||
decoder := Of[string](10)
|
||||
mapped1 := MonadMap(decoder, N.Mul(2))
|
||||
mapped2 := MonadMap(mapped1, N.Add(5))
|
||||
mapped3 := MonadMap(mapped2, S.Format[int]("Result: %d"))
|
||||
|
||||
res := mapped3("input")
|
||||
assert.Equal(t, validation.Of("Result: 25"), res)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMap tests the Map function
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("creates mappable operator", func(t *testing.T) {
|
||||
mapOp := Map[string](S.Format[int]("Number: %d"))
|
||||
|
||||
decoder := mapOp(Of[string](42))
|
||||
res := decoder("input")
|
||||
|
||||
assert.Equal(t, validation.Of("Number: 42"), res)
|
||||
})
|
||||
|
||||
t.Run("can be composed", func(t *testing.T) {
|
||||
double := Map[string](N.Mul(2))
|
||||
toString := Map[string](S.Format[int]("Value: %d"))
|
||||
|
||||
decoder := toString(double(Of[string](21)))
|
||||
res := decoder("input")
|
||||
|
||||
assert.Equal(t, validation.Of("Value: 42"), res)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAp tests the MonadAp function
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("applies function decoder to value decoder", func(t *testing.T) {
|
||||
decoderFn := Of[string](S.Format[int]("Number: %d"))
|
||||
decoderVal := Of[string](42)
|
||||
|
||||
res := MonadAp(decoderFn, decoderVal)("input")
|
||||
assert.Equal(t, validation.Of("Number: 42"), res)
|
||||
})
|
||||
|
||||
t.Run("works with different transformations", func(t *testing.T) {
|
||||
decoderFn := Of[string](N.Mul(2))
|
||||
decoderVal := Of[string](21)
|
||||
|
||||
res := MonadAp(decoderFn, decoderVal)("input")
|
||||
assert.Equal(t, validation.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("propagates function decoder error", func(t *testing.T) {
|
||||
failingFnDecoder := func(input string) Validation[func(int) string] {
|
||||
return either.Left[func(int) string](validation.Errors{
|
||||
{Value: input, Messsage: "function decode failed"},
|
||||
})
|
||||
}
|
||||
decoderVal := Of[string](42)
|
||||
|
||||
res := MonadAp(failingFnDecoder, decoderVal)("input")
|
||||
assert.True(t, either.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("propagates value decoder error", func(t *testing.T) {
|
||||
decoderFn := Of[string](S.Format[int]("Number: %d"))
|
||||
failingValDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "value decode failed"},
|
||||
})
|
||||
}
|
||||
|
||||
res := MonadAp(decoderFn, failingValDecoder)("input")
|
||||
assert.True(t, either.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("combines multiple values", func(t *testing.T) {
|
||||
// Create a function that takes two arguments
|
||||
decoderFn := Of[string](N.Add[int])
|
||||
decoderVal1 := Of[string](10)
|
||||
decoderVal2 := Of[string](32)
|
||||
|
||||
// Apply first value
|
||||
partial := MonadAp(decoderFn, decoderVal1)
|
||||
// Apply second value
|
||||
result := MonadAp(partial, decoderVal2)
|
||||
|
||||
res := result("input")
|
||||
assert.Equal(t, validation.Of(42), res)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAp tests the Ap function
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("creates applicable operator", func(t *testing.T) {
|
||||
decoderVal := Of[string](42)
|
||||
apOp := Ap[string](decoderVal)
|
||||
|
||||
decoderFn := Of[string](S.Format[int]("Number: %d"))
|
||||
|
||||
res := apOp(decoderFn)("input")
|
||||
assert.Equal(t, validation.Of("Number: 42"), res)
|
||||
})
|
||||
|
||||
t.Run("can be composed", func(t *testing.T) {
|
||||
val1 := Of[string](10)
|
||||
val2 := Of[string](32)
|
||||
|
||||
apOp1 := Ap[func(int) int](val1)
|
||||
apOp2 := Ap[int](val2)
|
||||
|
||||
fnDecoder := Of[string](N.Add[int])
|
||||
|
||||
result := apOp2(apOp1(fnDecoder))
|
||||
res := result("input")
|
||||
|
||||
assert.Equal(t, validation.Of(42), res)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadLaws tests that the monad operations satisfy monad laws
|
||||
func TestMonadLaws(t *testing.T) {
|
||||
t.Run("left identity: Of(a) >>= f === f(a)", func(t *testing.T) {
|
||||
a := 42
|
||||
f := func(n int) Decode[string, string] {
|
||||
return Of[string](fmt.Sprintf("Number: %d", n))
|
||||
}
|
||||
|
||||
left := MonadChain(Of[string](a), f)
|
||||
right := f(a)
|
||||
|
||||
input := "test"
|
||||
assert.Equal(t, right(input), left(input))
|
||||
})
|
||||
|
||||
t.Run("right identity: m >>= Of === m", func(t *testing.T) {
|
||||
m := Of[string](42)
|
||||
|
||||
left := MonadChain(m, func(a int) Decode[string, int] {
|
||||
return Of[string](a)
|
||||
})
|
||||
|
||||
input := "test"
|
||||
assert.Equal(t, m(input), left(input))
|
||||
})
|
||||
|
||||
t.Run("associativity: (m >>= f) >>= g === m >>= (\\x -> f(x) >>= g)", func(t *testing.T) {
|
||||
m := Of[string](10)
|
||||
f := func(n int) Decode[string, int] {
|
||||
return Of[string](n * 2)
|
||||
}
|
||||
g := func(n int) Decode[string, string] {
|
||||
return Of[string](fmt.Sprintf("Result: %d", n))
|
||||
}
|
||||
|
||||
// (m >>= f) >>= g
|
||||
left := MonadChain(MonadChain(m, f), g)
|
||||
|
||||
// m >>= (\x -> f(x) >>= g)
|
||||
right := MonadChain(m, func(x int) Decode[string, string] {
|
||||
return MonadChain(f(x), g)
|
||||
})
|
||||
|
||||
input := "test"
|
||||
assert.Equal(t, right(input), left(input))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFunctorLaws tests that the functor operations satisfy functor laws
|
||||
func TestFunctorLaws(t *testing.T) {
|
||||
t.Run("identity: map(id) === id", func(t *testing.T) {
|
||||
decoder := Of[string](42)
|
||||
mapped := MonadMap(decoder, func(a int) int { return a })
|
||||
|
||||
input := "test"
|
||||
assert.Equal(t, decoder(input), mapped(input))
|
||||
})
|
||||
|
||||
t.Run("composition: map(f . g) === map(f) . map(g)", func(t *testing.T) {
|
||||
decoder := Of[string](10)
|
||||
f := N.Mul(2)
|
||||
g := N.Add(5)
|
||||
|
||||
// map(f . g)
|
||||
left := MonadMap(decoder, func(n int) int {
|
||||
return f(g(n))
|
||||
})
|
||||
|
||||
// map(f) . map(g)
|
||||
right := MonadMap(MonadMap(decoder, g), f)
|
||||
|
||||
input := "test"
|
||||
assert.Equal(t, right(input), left(input))
|
||||
})
|
||||
}
|
||||
30
v2/optics/codec/decode/types.go
Normal file
30
v2/optics/codec/decode/types.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package decode
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Validation represents the result of a validation operation that may contain
|
||||
// validation errors or a successfully validated value of type A.
|
||||
Validation[A any] = validation.Validation[A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// Decode is a function that decodes input I to type A with validation.
|
||||
// It returns a Validation result directly.
|
||||
Decode[I, A any] = Reader[I, Validation[A]]
|
||||
|
||||
// Kleisli represents a function from A to a decoded B given input type I.
|
||||
// It's a Reader that takes an input A and produces a Decode[I, B] function.
|
||||
// This enables composition of decoding operations in a functional style.
|
||||
Kleisli[I, A, B any] = Reader[A, Decode[I, B]]
|
||||
|
||||
// Operator represents a decoding transformation that takes a decoded A and produces a decoded B.
|
||||
// It's a specialized Kleisli arrow for composing decode operations where the input is already decoded.
|
||||
// This allows chaining multiple decode transformations together.
|
||||
Operator[I, A, B any] = Kleisli[I, Decode[I, A], B]
|
||||
)
|
||||
84
v2/optics/codec/format.go
Normal file
84
v2/optics/codec/format.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
)
|
||||
|
||||
// String implements the fmt.Stringer interface for typeImpl.
|
||||
// It returns the name of the type, which is used for simple string representation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// stringType := codec.String()
|
||||
// fmt.Println(stringType) // Output: "string"
|
||||
func (t *typeImpl[A, O, I]) String() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
// Format implements the fmt.Formatter interface for typeImpl.
|
||||
// It provides custom formatting based on the format verb:
|
||||
// - %s, %v: Returns the type name
|
||||
// - %q: Returns the type name in quotes
|
||||
// - %#v: Returns a detailed Go-syntax representation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intType := codec.Int()
|
||||
// fmt.Printf("%s\n", intType) // Output: int
|
||||
// fmt.Printf("%q\n", intType) // Output: "int"
|
||||
// fmt.Printf("%#v\n", intType) // Output: codec.Type[int, int, any]{name: "int"}
|
||||
func (t *typeImpl[A, O, I]) Format(f fmt.State, verb rune) {
|
||||
formatting.FmtString(t, f, verb)
|
||||
}
|
||||
|
||||
// GoString implements the fmt.GoStringer interface for typeImpl.
|
||||
// It returns a Go-syntax representation of the type that could be used
|
||||
// to recreate the type (though not executable due to function values).
|
||||
//
|
||||
// This is called when using the %#v format verb with fmt.Printf.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// stringType := codec.String()
|
||||
// fmt.Printf("%#v\n", stringType)
|
||||
// // Output: codec.Type[string, string, any]{name: "string"}
|
||||
func (t *typeImpl[A, O, I]) GoString() string {
|
||||
return fmt.Sprintf("codec.Type[%s, %s, %s]{name: %q}",
|
||||
typeNameOf[A](), typeNameOf[O](), typeNameOf[I](), t.name)
|
||||
}
|
||||
|
||||
// LogValue implements the slog.LogValuer interface for typeImpl.
|
||||
// It provides structured logging representation of the codec type.
|
||||
// Returns a slog.Value containing the type information as a group with
|
||||
// the codec name and type parameters.
|
||||
//
|
||||
// This method is called automatically when logging a codec with slog.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// stringType := codec.String()
|
||||
// slog.Info("codec created", "codec", stringType)
|
||||
// // Logs: codec={name=string type_a=string type_o=string type_i=interface {}}
|
||||
func (t *typeImpl[A, O, I]) LogValue() slog.Value {
|
||||
return slog.GroupValue(
|
||||
slog.String("name", t.name),
|
||||
slog.String("type_a", typeNameOf[A]()),
|
||||
slog.String("type_o", typeNameOf[O]()),
|
||||
slog.String("type_i", typeNameOf[I]()),
|
||||
)
|
||||
}
|
||||
|
||||
// typeNameOf returns a string representation of the type T.
|
||||
// It handles the special case where T is 'any' (interface{}).
|
||||
func typeNameOf[T any]() string {
|
||||
var zero T
|
||||
typeName := fmt.Sprintf("%T", zero)
|
||||
// Handle the case where %T prints "<nil>" for interface{} types
|
||||
if typeName == "<nil>" {
|
||||
return "interface {}"
|
||||
}
|
||||
return typeName
|
||||
}
|
||||
216
v2/optics/codec/format_test.go
Normal file
216
v2/optics/codec/format_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTypeImplStringer tests the String() method implementation
|
||||
func TestTypeImplStringer(t *testing.T) {
|
||||
t.Run("String codec", func(t *testing.T) {
|
||||
codec := String().(*typeImpl[string, string, any])
|
||||
result := codec.String()
|
||||
assert.Equal(t, "string", result)
|
||||
})
|
||||
|
||||
t.Run("Int codec", func(t *testing.T) {
|
||||
codec := Int().(*typeImpl[int, int, any])
|
||||
result := codec.String()
|
||||
assert.Equal(t, "int", result)
|
||||
})
|
||||
|
||||
t.Run("Bool codec", func(t *testing.T) {
|
||||
codec := Bool().(*typeImpl[bool, bool, any])
|
||||
result := codec.String()
|
||||
assert.Equal(t, "bool", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTypeImplFormat tests the Format() method implementation
|
||||
func TestTypeImplFormat(t *testing.T) {
|
||||
t.Run("String codec with %s", func(t *testing.T) {
|
||||
codec := String().(*typeImpl[string, string, any])
|
||||
result := fmt.Sprintf("%s", codec)
|
||||
assert.Equal(t, "string", result)
|
||||
})
|
||||
|
||||
t.Run("String codec with %v", func(t *testing.T) {
|
||||
codec := String().(*typeImpl[string, string, any])
|
||||
result := fmt.Sprintf("%v", codec)
|
||||
assert.Equal(t, "string", result)
|
||||
})
|
||||
|
||||
t.Run("String codec with %q", func(t *testing.T) {
|
||||
codec := String().(*typeImpl[string, string, any])
|
||||
result := fmt.Sprintf("%q", codec)
|
||||
assert.Equal(t, `"string"`, result)
|
||||
})
|
||||
|
||||
t.Run("Int codec with %s", func(t *testing.T) {
|
||||
codec := Int().(*typeImpl[int, int, any])
|
||||
result := fmt.Sprintf("%s", codec)
|
||||
assert.Equal(t, "int", result)
|
||||
})
|
||||
|
||||
t.Run("Int codec with %#v", func(t *testing.T) {
|
||||
codec := Int().(*typeImpl[int, int, any])
|
||||
result := fmt.Sprintf("%#v", codec)
|
||||
assert.Equal(t, `codec.Type[int, int, interface {}]{name: "int"}`, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTypeImplGoString tests the GoString() method implementation
|
||||
func TestTypeImplGoString(t *testing.T) {
|
||||
t.Run("String codec", func(t *testing.T) {
|
||||
codec := String().(*typeImpl[string, string, any])
|
||||
result := codec.GoString()
|
||||
assert.Equal(t, `codec.Type[string, string, interface {}]{name: "string"}`, result)
|
||||
})
|
||||
|
||||
t.Run("Int codec", func(t *testing.T) {
|
||||
codec := Int().(*typeImpl[int, int, any])
|
||||
result := codec.GoString()
|
||||
assert.Equal(t, `codec.Type[int, int, interface {}]{name: "int"}`, result)
|
||||
})
|
||||
|
||||
t.Run("Bool codec", func(t *testing.T) {
|
||||
codec := Bool().(*typeImpl[bool, bool, any])
|
||||
result := codec.GoString()
|
||||
assert.Equal(t, `codec.Type[bool, bool, interface {}]{name: "bool"}`, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTypeImplFormatWithPrintf tests that %#v uses GoString
|
||||
func TestTypeImplFormatWithPrintf(t *testing.T) {
|
||||
stringCodec := String().(*typeImpl[string, string, any])
|
||||
|
||||
// Test that %#v calls GoString
|
||||
result := fmt.Sprintf("%#v", stringCodec)
|
||||
assert.Equal(t, `codec.Type[string, string, interface {}]{name: "string"}`, result)
|
||||
}
|
||||
|
||||
// TestComplexTypeFormatting tests formatting of more complex types
|
||||
func TestComplexTypeFormatting(t *testing.T) {
|
||||
// Create an array codec
|
||||
arrayCodec := Array(Int()).(*typeImpl[[]int, []int, any])
|
||||
|
||||
// Test String()
|
||||
name := arrayCodec.String()
|
||||
assert.Equal(t, "Array[int]", name)
|
||||
|
||||
// Test Format with %s
|
||||
formatted := fmt.Sprintf("%s", arrayCodec)
|
||||
assert.Equal(t, "Array[int]", formatted)
|
||||
|
||||
// Test GoString
|
||||
goString := arrayCodec.GoString()
|
||||
// Just verify it's not empty
|
||||
assert.NotEmpty(t, goString)
|
||||
}
|
||||
|
||||
// TestFormatterInterface verifies that typeImpl implements fmt.Formatter
|
||||
func TestFormatterInterface(t *testing.T) {
|
||||
var _ fmt.Formatter = (*typeImpl[int, int, any])(nil)
|
||||
}
|
||||
|
||||
// TestStringerInterface verifies that typeImpl implements fmt.Stringer
|
||||
func TestStringerInterface(t *testing.T) {
|
||||
var _ fmt.Stringer = (*typeImpl[int, int, any])(nil)
|
||||
}
|
||||
|
||||
// TestGoStringerInterface verifies that typeImpl implements fmt.GoStringer
|
||||
func TestGoStringerInterface(t *testing.T) {
|
||||
var _ fmt.GoStringer = (*typeImpl[int, int, any])(nil)
|
||||
}
|
||||
|
||||
// TestLogValuerInterface verifies that typeImpl implements slog.LogValuer
|
||||
func TestLogValuerInterface(t *testing.T) {
|
||||
var _ slog.LogValuer = (*typeImpl[int, int, any])(nil)
|
||||
}
|
||||
|
||||
// TestTypeImplLogValue tests the LogValue() method implementation
|
||||
func TestTypeImplLogValue(t *testing.T) {
|
||||
t.Run("String codec", func(t *testing.T) {
|
||||
codec := String().(*typeImpl[string, string, any])
|
||||
logValue := codec.LogValue()
|
||||
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract attributes from the group
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 4)
|
||||
|
||||
// Check that we have the expected attributes
|
||||
attrMap := make(map[string]string)
|
||||
for _, attr := range attrs {
|
||||
attrMap[attr.Key] = attr.Value.String()
|
||||
}
|
||||
|
||||
assert.Equal(t, "string", attrMap["name"])
|
||||
assert.Equal(t, "string", attrMap["type_a"])
|
||||
assert.Equal(t, "string", attrMap["type_o"])
|
||||
assert.Contains(t, attrMap["type_i"], "interface")
|
||||
})
|
||||
|
||||
t.Run("Int codec", func(t *testing.T) {
|
||||
codec := Int().(*typeImpl[int, int, any])
|
||||
logValue := codec.LogValue()
|
||||
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 4)
|
||||
|
||||
attrMap := make(map[string]string)
|
||||
for _, attr := range attrs {
|
||||
attrMap[attr.Key] = attr.Value.String()
|
||||
}
|
||||
|
||||
assert.Equal(t, "int", attrMap["name"])
|
||||
assert.Equal(t, "int", attrMap["type_a"])
|
||||
assert.Equal(t, "int", attrMap["type_o"])
|
||||
})
|
||||
|
||||
t.Run("Bool codec", func(t *testing.T) {
|
||||
codec := Bool().(*typeImpl[bool, bool, any])
|
||||
logValue := codec.LogValue()
|
||||
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 4)
|
||||
|
||||
attrMap := make(map[string]string)
|
||||
for _, attr := range attrs {
|
||||
attrMap[attr.Key] = attr.Value.String()
|
||||
}
|
||||
|
||||
assert.Equal(t, "bool", attrMap["name"])
|
||||
assert.Equal(t, "bool", attrMap["type_a"])
|
||||
})
|
||||
|
||||
t.Run("Array codec", func(t *testing.T) {
|
||||
codec := Array(Int()).(*typeImpl[[]int, []int, any])
|
||||
logValue := codec.LogValue()
|
||||
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 4)
|
||||
|
||||
attrMap := make(map[string]string)
|
||||
for _, attr := range attrs {
|
||||
attrMap[attr.Key] = attr.Value.String()
|
||||
}
|
||||
|
||||
assert.Equal(t, "Array[int]", attrMap["name"])
|
||||
})
|
||||
}
|
||||
|
||||
// TestFormattableInterface verifies that typeImpl implements formatting.Formattable
|
||||
func TestFormattableInterface(t *testing.T) {
|
||||
var _ Formattable = (*typeImpl[int, int, any])(nil)
|
||||
}
|
||||
81
v2/optics/codec/prism.go
Normal file
81
v2/optics/codec/prism.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
)
|
||||
|
||||
// TypeToPrism converts a Type codec into a Prism optic.
|
||||
//
|
||||
// A Type[A, S, S] represents a bidirectional codec that can decode S to A (with validation)
|
||||
// and encode A back to S. A Prism[S, A] is an optic that can optionally extract an A from S
|
||||
// and always construct an S from an A.
|
||||
//
|
||||
// This conversion bridges the codec and optics worlds, allowing you to use validation-based
|
||||
// codecs as prisms for functional optics composition.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source/encoded type (both input and output)
|
||||
// - A: The decoded/focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - t: A Type[A, S, S] codec where:
|
||||
// - Decode: S → Validation[A] (may fail with validation errors)
|
||||
// - Encode: A → S (always succeeds)
|
||||
// - Name: Provides a descriptive name for the type
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[S, A] where:
|
||||
// - GetOption: S → Option[A] (Some if decode succeeds, None if validation fails)
|
||||
// - ReverseGet: A → S (uses the codec's Encode function)
|
||||
// - Name: Inherited from the Type's name
|
||||
//
|
||||
// The conversion works as follows:
|
||||
// - GetOption: Decodes the value and converts validation result to Option
|
||||
// (Right(a) → Some(a), Left(errors) → None)
|
||||
// - ReverseGet: Directly uses the Type's Encode function
|
||||
// - Name: Preserves the Type's descriptive name
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for positive integers
|
||||
// positiveInt := codec.MakeType[int, int, int](
|
||||
// "PositiveInt",
|
||||
// func(i any) result.Result[int] { ... },
|
||||
// func(i int) codec.Validate[int] {
|
||||
// if i <= 0 {
|
||||
// return validation.FailureWithMessage(i, "must be positive")
|
||||
// }
|
||||
// return validation.Success(i)
|
||||
// },
|
||||
// func(i int) int { return i },
|
||||
// )
|
||||
//
|
||||
// // Convert to prism
|
||||
// prism := codec.TypeToPrism(positiveInt)
|
||||
//
|
||||
// // Use as prism
|
||||
// value := prism.GetOption(42) // Some(42) - validation succeeds
|
||||
// value = prism.GetOption(-5) // None - validation fails
|
||||
// result := prism.ReverseGet(10) // 10 - encoding always succeeds
|
||||
//
|
||||
// Use cases:
|
||||
// - Composing codecs with other optics (lenses, prisms, traversals)
|
||||
// - Using validation logic in optics pipelines
|
||||
// - Building complex data transformations with functional composition
|
||||
// - Integrating type-safe parsing with optics-based data access
|
||||
//
|
||||
// Note: The prism's GetOption will return None for any validation failure,
|
||||
// discarding the specific error details. If you need error information,
|
||||
// use the Type's Decode method directly instead.
|
||||
func TypeToPrism[S, A any](t Type[A, S, S]) Prism[S, A] {
|
||||
return prism.MakePrismWithName(
|
||||
F.Flow2(
|
||||
t.Decode,
|
||||
either.ToOption,
|
||||
),
|
||||
t.Encode,
|
||||
t.Name(),
|
||||
)
|
||||
}
|
||||
327
v2/optics/codec/prism_test.go
Normal file
327
v2/optics/codec/prism_test.go
Normal file
@@ -0,0 +1,327 @@
|
||||
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/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTypeToPrismBasic tests basic TypeToPrism functionality
|
||||
func TestTypeToPrismBasic(t *testing.T) {
|
||||
// Create a simple string identity type
|
||||
stringType := Id[string]()
|
||||
|
||||
prism := TypeToPrism(stringType)
|
||||
|
||||
t.Run("GetOption returns Some for valid value", func(t *testing.T) {
|
||||
result := prism.GetOption("hello")
|
||||
assert.True(t, option.IsSome(result), "Expected Some for valid string")
|
||||
|
||||
value := option.GetOrElse(F.Constant(""))(result)
|
||||
assert.Equal(t, "hello", value)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet encodes value correctly", func(t *testing.T) {
|
||||
encoded := prism.ReverseGet("world")
|
||||
assert.Equal(t, "world", encoded)
|
||||
})
|
||||
|
||||
t.Run("Name is preserved from Type", func(t *testing.T) {
|
||||
assert.Equal(t, stringType.Name(), prism.String())
|
||||
})
|
||||
|
||||
t.Run("Round trip preserves value", func(t *testing.T) {
|
||||
original := "test value"
|
||||
encoded := prism.ReverseGet(original)
|
||||
decoded := prism.GetOption(encoded)
|
||||
|
||||
assert.True(t, option.IsSome(decoded))
|
||||
value := option.GetOrElse(F.Constant(""))(decoded)
|
||||
assert.Equal(t, original, value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTypeToPrismValidationLogic tests TypeToPrism with validation logic
|
||||
func TestTypeToPrismValidationLogic(t *testing.T) {
|
||||
// Create a type that validates positive integers
|
||||
positiveIntType := MakeType(
|
||||
"PositiveInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i <= 0 {
|
||||
return either.Left[int](assert.AnError)
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i <= 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
prism := TypeToPrism(positiveIntType)
|
||||
|
||||
t.Run("GetOption returns Some for valid positive integer", func(t *testing.T) {
|
||||
result := prism.GetOption(42)
|
||||
assert.True(t, option.IsSome(result))
|
||||
|
||||
value := option.GetOrElse(F.Constant(0))(result)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None for negative integer", func(t *testing.T) {
|
||||
result := prism.GetOption(-5)
|
||||
assert.True(t, option.IsNone(result), "Expected None for negative integer")
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None for zero", func(t *testing.T) {
|
||||
result := prism.GetOption(0)
|
||||
assert.True(t, option.IsNone(result), "Expected None for zero")
|
||||
})
|
||||
|
||||
t.Run("GetOption returns Some for boundary value", func(t *testing.T) {
|
||||
result := prism.GetOption(1)
|
||||
assert.True(t, option.IsSome(result))
|
||||
|
||||
value := option.GetOrElse(F.Constant(0))(result)
|
||||
assert.Equal(t, 1, value)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet does not validate", func(t *testing.T) {
|
||||
// ReverseGet should encode without validation
|
||||
encoded := prism.ReverseGet(-10)
|
||||
assert.Equal(t, -10, encoded, "ReverseGet should not validate")
|
||||
})
|
||||
|
||||
t.Run("Name reflects validation purpose", func(t *testing.T) {
|
||||
assert.Equal(t, "PositiveInt", prism.String())
|
||||
})
|
||||
}
|
||||
|
||||
// TestTypeToPrismWithComplexValidation tests more complex validation scenarios
|
||||
func TestTypeToPrismWithComplexValidation(t *testing.T) {
|
||||
// Create a type that validates strings with length constraints
|
||||
boundedStringType := MakeType(
|
||||
"BoundedString",
|
||||
func(u any) either.Either[error, string] {
|
||||
s, ok := u.(string)
|
||||
if !ok {
|
||||
return either.Left[string](assert.AnError)
|
||||
}
|
||||
return either.Of[error](s)
|
||||
},
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) < 3 {
|
||||
return validation.FailureWithMessage[string](s, "must be at least 3 characters")(c)
|
||||
}
|
||||
if len(s) > 10 {
|
||||
return validation.FailureWithMessage[string](s, "must be at most 10 characters")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
prism := TypeToPrism(boundedStringType)
|
||||
|
||||
t.Run("GetOption returns Some for valid length", func(t *testing.T) {
|
||||
result := prism.GetOption("hello")
|
||||
assert.True(t, option.IsSome(result))
|
||||
|
||||
value := option.GetOrElse(F.Constant(""))(result)
|
||||
assert.Equal(t, "hello", value)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None for too short string", func(t *testing.T) {
|
||||
result := prism.GetOption("ab")
|
||||
assert.True(t, option.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("GetOption returns None for too long string", func(t *testing.T) {
|
||||
result := prism.GetOption("this is way too long")
|
||||
assert.True(t, option.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("GetOption returns Some for minimum length", func(t *testing.T) {
|
||||
result := prism.GetOption("abc")
|
||||
assert.True(t, option.IsSome(result))
|
||||
})
|
||||
|
||||
t.Run("GetOption returns Some for maximum length", func(t *testing.T) {
|
||||
result := prism.GetOption("1234567890")
|
||||
assert.True(t, option.IsSome(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestTypeToPrismWithNumericTypes tests TypeToPrism with different numeric types
|
||||
func TestTypeToPrismWithNumericTypes(t *testing.T) {
|
||||
t.Run("Float64 type", func(t *testing.T) {
|
||||
floatType := Id[float64]()
|
||||
|
||||
prism := TypeToPrism(floatType)
|
||||
|
||||
result := prism.GetOption(3.14)
|
||||
assert.True(t, option.IsSome(result))
|
||||
|
||||
value := option.GetOrElse(F.Constant(0.0))(result)
|
||||
assert.Equal(t, 3.14, value)
|
||||
})
|
||||
|
||||
t.Run("Int64 type", func(t *testing.T) {
|
||||
int64Type := Id[int64]()
|
||||
|
||||
prism := TypeToPrism(int64Type)
|
||||
|
||||
result := prism.GetOption(int64(9223372036854775807))
|
||||
assert.True(t, option.IsSome(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestTypeToPrismWithBooleanType tests TypeToPrism with boolean type
|
||||
func TestTypeToPrismWithBooleanType(t *testing.T) {
|
||||
boolType := Id[bool]()
|
||||
|
||||
prism := TypeToPrism(boolType)
|
||||
|
||||
t.Run("GetOption returns Some for true", func(t *testing.T) {
|
||||
result := prism.GetOption(true)
|
||||
assert.True(t, option.IsSome(result))
|
||||
|
||||
value := option.GetOrElse(F.Constant(false))(result)
|
||||
assert.True(t, value)
|
||||
})
|
||||
|
||||
t.Run("GetOption returns Some for false", func(t *testing.T) {
|
||||
result := prism.GetOption(false)
|
||||
assert.True(t, option.IsSome(result))
|
||||
|
||||
value := option.GetOrElse(F.Constant(true))(result)
|
||||
assert.False(t, value)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet preserves boolean values", func(t *testing.T) {
|
||||
assert.True(t, prism.ReverseGet(true))
|
||||
assert.False(t, prism.ReverseGet(false))
|
||||
})
|
||||
}
|
||||
|
||||
// TestTypeToPrismEdgeCases tests edge cases and special scenarios
|
||||
func TestTypeToPrismEdgeCases(t *testing.T) {
|
||||
t.Run("Empty string validation", func(t *testing.T) {
|
||||
nonEmptyStringType := MakeType(
|
||||
"NonEmptyString",
|
||||
func(u any) either.Either[error, string] {
|
||||
s, ok := u.(string)
|
||||
if !ok {
|
||||
return either.Left[string](assert.AnError)
|
||||
}
|
||||
return either.Of[error](s)
|
||||
},
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s == "" {
|
||||
return validation.FailureWithMessage[string](s, "must not be empty")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
prism := TypeToPrism(nonEmptyStringType)
|
||||
|
||||
emptyResult := prism.GetOption("")
|
||||
assert.True(t, option.IsNone(emptyResult), "Empty string should fail validation")
|
||||
|
||||
nonEmptyResult := prism.GetOption("a")
|
||||
assert.True(t, option.IsSome(nonEmptyResult))
|
||||
})
|
||||
|
||||
t.Run("Multiple validation failures", func(t *testing.T) {
|
||||
strictIntType := MakeType(
|
||||
"StrictInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok {
|
||||
return either.Left[int](assert.AnError)
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i < 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be non-negative")(c)
|
||||
}
|
||||
if i > 100 {
|
||||
return validation.FailureWithMessage[int](i, "must be at most 100")(c)
|
||||
}
|
||||
if i%2 != 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be even")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
prism := TypeToPrism(strictIntType)
|
||||
|
||||
// Valid value
|
||||
validResult := prism.GetOption(42)
|
||||
assert.True(t, option.IsSome(validResult))
|
||||
|
||||
// Various invalid values
|
||||
assert.True(t, option.IsNone(prism.GetOption(-1)), "Negative should fail")
|
||||
assert.True(t, option.IsNone(prism.GetOption(101)), "Too large should fail")
|
||||
assert.True(t, option.IsNone(prism.GetOption(43)), "Odd should fail")
|
||||
})
|
||||
}
|
||||
|
||||
// TestTypeToPrismNamePreservation tests that prism names are correctly preserved
|
||||
func TestTypeToPrismNamePreservation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
typeName string
|
||||
}{
|
||||
{"Simple name", "SimpleType"},
|
||||
{"Descriptive name", "PositiveIntegerValidator"},
|
||||
{"With spaces", "Type With Spaces"},
|
||||
{"With special chars", "Type_With-Special.Chars"},
|
||||
{"Unicode name", "类型名称"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
stringType := MakeType(
|
||||
tc.typeName,
|
||||
func(u any) either.Either[error, string] {
|
||||
s, ok := u.(string)
|
||||
if !ok {
|
||||
return either.Left[string](assert.AnError)
|
||||
}
|
||||
return either.Of[error](s)
|
||||
},
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
prism := TypeToPrism(stringType)
|
||||
assert.Equal(t, tc.typeName, prism.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/decode"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"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/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -15,6 +18,12 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Formattable represents a type that can be formatted as a string representation.
|
||||
// It provides a way to obtain a human-readable description of a type or value.
|
||||
Formattable = formatting.Formattable
|
||||
|
||||
// ReaderResult represents a computation that depends on an environment R,
|
||||
// produces a value A, and may fail with an error.
|
||||
ReaderResult[R, A any] = readerresult.ReaderResult[R, A]
|
||||
|
||||
// Lazy represents a lazily evaluated value.
|
||||
@@ -26,9 +35,6 @@ type (
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Either represents a value that can be one of two types: Left (error) or Right (success).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Result represents a computation that may fail with an error.
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
@@ -39,17 +45,21 @@ type (
|
||||
Encode encoder.Encoder[O, A]
|
||||
}
|
||||
|
||||
// Validation represents the result of a validation operation that may contain
|
||||
// validation errors or a successfully validated value of type A.
|
||||
Validation[A any] = validation.Validation[A]
|
||||
|
||||
// Context provides contextual information for validation operations,
|
||||
// such as the current path in a nested structure.
|
||||
Context = validation.Context
|
||||
|
||||
// Validate is a function that validates input I to produce type A.
|
||||
// It takes an input and returns a Reader that depends on the validation Context.
|
||||
Validate[I, A any] = Reader[I, Reader[Context, Validation[A]]]
|
||||
Validate[I, A any] = validate.Validate[I, A]
|
||||
|
||||
// Decode is a function that decodes input I to type A with validation.
|
||||
// It returns a Validation result directly.
|
||||
Decode[I, A any] = Reader[I, Validation[A]]
|
||||
Decode[I, A any] = decode.Decode[I, A]
|
||||
|
||||
// Encode is a function that encodes type A to output O.
|
||||
Encode[A, O any] = Reader[A, O]
|
||||
@@ -57,7 +67,7 @@ type (
|
||||
// Decoder is an interface for types that can decode and validate input.
|
||||
Decoder[I, A any] interface {
|
||||
Name() string
|
||||
Validate(I) Reader[Context, Validation[A]]
|
||||
Validate(I) Decode[Context, A]
|
||||
Decode(I) Validation[A]
|
||||
}
|
||||
|
||||
@@ -70,6 +80,7 @@ type (
|
||||
// and type checking capabilities. It represents a complete specification of
|
||||
// how to work with a particular type.
|
||||
Type[A, O, I any] interface {
|
||||
Formattable
|
||||
Decoder[I, A]
|
||||
Encoder[A, O]
|
||||
AsDecoder() Decoder[I, A]
|
||||
@@ -77,7 +88,17 @@ type (
|
||||
Is(any) Result[A]
|
||||
}
|
||||
|
||||
// Endomorphism represents a function from type A to itself (A -> A).
|
||||
// It forms a monoid under function composition.
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Pair represents a tuple of two values of types L and R.
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
|
||||
// Prism is an optic that focuses on a part of a sum type S that may or may not
|
||||
// contain a value of type A. It provides a way to preview and review values.
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
// Refinement represents the concept that B is a specialized type of A
|
||||
Refinement[A, B any] = Prism[A, B]
|
||||
)
|
||||
|
||||
124
v2/optics/codec/validate/monoid.go
Normal file
124
v2/optics/codec/validate/monoid.go
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// ApplicativeMonoid creates a Monoid instance for Validate[I, A] given a Monoid[A].
|
||||
//
|
||||
// This function lifts a monoid operation on values of type A to work with validators
|
||||
// that produce values of type A. It uses the applicative functor structure of the
|
||||
// nested Reader types to combine validators while preserving their validation context.
|
||||
//
|
||||
// The resulting monoid allows you to:
|
||||
// - Combine multiple validators that produce monoidal values
|
||||
// - Run validators in parallel and merge their results using the monoid operation
|
||||
// - Build complex validators compositionally from simpler ones
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type that validators accept
|
||||
// - A: The output type that validators produce (must have a Monoid instance)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: A Monoid[A] that defines how to combine values of type A
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Monoid[Validate[I, A]] that can combine validators using the applicative structure.
|
||||
//
|
||||
// # How It Works
|
||||
//
|
||||
// The function composes three layers of applicative monoids:
|
||||
// 1. The innermost layer uses validation.ApplicativeMonoid(m) to combine Validation[A] values
|
||||
// 2. The middle layer wraps this in reader.ApplicativeMonoid for the Context dependency
|
||||
// 3. The outer layer wraps everything in reader.ApplicativeMonoid for the input I dependency
|
||||
//
|
||||
// This creates a monoid that:
|
||||
// - Takes the same input I for both validators
|
||||
// - Threads the same Context through both validators
|
||||
// - Combines successful results using the monoid operation on A
|
||||
// - Accumulates validation errors from both validators if either fails
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// Combining string validators using string concatenation:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/monoid"
|
||||
// "github.com/IBM/fp-go/v2/string"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// )
|
||||
//
|
||||
// // Create a monoid for string validators
|
||||
// stringMonoid := string.Monoid
|
||||
// validatorMonoid := validate.ApplicativeMonoid[string, string](stringMonoid)
|
||||
//
|
||||
// // Define two validators that extract different parts
|
||||
// validator1 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success("Hello ")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// validator2 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success("World")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Combine them - results will be concatenated
|
||||
// combined := validatorMonoid.Concat(validator1, validator2)
|
||||
// // When run, produces validation.Success("Hello World")
|
||||
//
|
||||
// Combining numeric validators using addition:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// // Create a monoid for int validators using addition
|
||||
// intMonoid := number.MonoidSum[int]()
|
||||
// validatorMonoid := validate.ApplicativeMonoid[string, int](intMonoid)
|
||||
//
|
||||
// // Validators that extract and validate different numeric fields
|
||||
// // Results will be summed together
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Both validators receive the same input value I
|
||||
// - If either validator fails, all errors are accumulated
|
||||
// - If both succeed, their results are combined using the monoid operation
|
||||
// - The empty element of the monoid serves as the identity for the Concat operation
|
||||
// - This follows the applicative functor laws for combining effectful computations
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - validation.ApplicativeMonoid: The underlying monoid for validation results
|
||||
// - reader.ApplicativeMonoid: The monoid for reader computations
|
||||
// - Monoid[A]: The monoid instance for the result type
|
||||
func ApplicativeMonoid[I, A any](m Monoid[A]) Monoid[Validate[I, A]] {
|
||||
return monoid.ApplicativeMonoid[A, Validate[I, A]](
|
||||
Of,
|
||||
MonadMap,
|
||||
MonadAp,
|
||||
m,
|
||||
)
|
||||
}
|
||||
475
v2/optics/codec/validate/monoid_test.go
Normal file
475
v2/optics/codec/validate/monoid_test.go
Normal file
@@ -0,0 +1,475 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
intAddMonoid = N.MonoidSum[int]()
|
||||
strMonoid = S.Monoid
|
||||
)
|
||||
|
||||
// Helper function to create a successful validator
|
||||
func successValidator[I, A any](value A) Validate[I, A] {
|
||||
return func(input I) Reader[validation.Context, validation.Validation[A]] {
|
||||
return func(ctx validation.Context) validation.Validation[A] {
|
||||
return validation.Success(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create a failing validator
|
||||
func failureValidator[I, A any](message string) Validate[I, A] {
|
||||
return func(input I) Reader[validation.Context, validation.Validation[A]] {
|
||||
return validation.FailureWithMessage[A](input, message)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create a validator that uses the input
|
||||
func inputDependentValidator[A any](f func(A) A) Validate[A, A] {
|
||||
return func(input A) Reader[validation.Context, validation.Validation[A]] {
|
||||
return func(ctx validation.Context) validation.Validation[A] {
|
||||
return validation.Success(f(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_EmptyElement tests the empty element of the monoid
|
||||
func TestApplicativeMonoid_EmptyElement(t *testing.T) {
|
||||
t.Run("int addition monoid", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
empty := m.Empty()
|
||||
|
||||
result := empty("test")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(0), result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation monoid", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[int](strMonoid)
|
||||
empty := m.Empty()
|
||||
|
||||
result := empty(42)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ConcatSuccesses tests concatenating two successful validators
|
||||
func TestApplicativeMonoid_ConcatSuccesses(t *testing.T) {
|
||||
t.Run("int addition", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](5)
|
||||
v2 := successValidator[string](3)
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(8), result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
m := ApplicativeMonoid[int](strMonoid)
|
||||
|
||||
v1 := successValidator[int]("Hello")
|
||||
v2 := successValidator[int](" World")
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined(42)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("Hello World"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ConcatWithFailure tests concatenating validators where one fails
|
||||
func TestApplicativeMonoid_ConcatWithFailure(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
t.Run("left failure", func(t *testing.T) {
|
||||
v1 := failureValidator[string, int]("left error")
|
||||
v2 := successValidator[string](5)
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "left error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("right failure", func(t *testing.T) {
|
||||
v1 := successValidator[string](5)
|
||||
v2 := failureValidator[string, int]("right error")
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "right error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("both failures", func(t *testing.T) {
|
||||
v1 := failureValidator[string, int]("left error")
|
||||
v2 := failureValidator[string, int]("right error")
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
// Note: The current implementation returns the first error encountered
|
||||
assert.GreaterOrEqual(t, len(errors), 1)
|
||||
// At least one of the errors should be present
|
||||
hasError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "left error" || err.Messsage == "right error" {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasError, "Should contain at least one validation error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_LeftIdentity tests the left identity law
|
||||
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v := successValidator[string](42)
|
||||
|
||||
// empty <> v == v
|
||||
combined := m.Concat(m.Empty(), v)
|
||||
|
||||
resultCombined := combined("test")(nil)
|
||||
resultOriginal := v("test")(nil)
|
||||
|
||||
assert.Equal(t, resultOriginal, resultCombined)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RightIdentity tests the right identity law
|
||||
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v := successValidator[string](42)
|
||||
|
||||
// v <> empty == v
|
||||
combined := m.Concat(v, m.Empty())
|
||||
|
||||
resultCombined := combined("test")(nil)
|
||||
resultOriginal := v("test")(nil)
|
||||
|
||||
assert.Equal(t, resultOriginal, resultCombined)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Associativity tests the associativity law
|
||||
func TestApplicativeMonoid_Associativity(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](1)
|
||||
v2 := successValidator[string](2)
|
||||
v3 := successValidator[string](3)
|
||||
|
||||
// (v1 <> v2) <> v3 == v1 <> (v2 <> v3)
|
||||
left := m.Concat(m.Concat(v1, v2), v3)
|
||||
right := m.Concat(v1, m.Concat(v2, v3))
|
||||
|
||||
resultLeft := left("test")(nil)
|
||||
resultRight := right("test")(nil)
|
||||
|
||||
assert.Equal(t, resultRight, resultLeft)
|
||||
|
||||
// Both should equal 6
|
||||
assert.Equal(t, validation.Of(6), resultLeft)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_AssociativityWithFailures tests associativity with failures
|
||||
func TestApplicativeMonoid_AssociativityWithFailures(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](1)
|
||||
v2 := failureValidator[string, int]("error 2")
|
||||
v3 := successValidator[string](3)
|
||||
|
||||
// (v1 <> v2) <> v3 == v1 <> (v2 <> v3)
|
||||
left := m.Concat(m.Concat(v1, v2), v3)
|
||||
right := m.Concat(v1, m.Concat(v2, v3))
|
||||
|
||||
resultLeft := left("test")(nil)
|
||||
resultRight := right("test")(nil)
|
||||
|
||||
// Both should fail with the same error
|
||||
assert.True(t, E.IsLeft(resultLeft))
|
||||
assert.True(t, E.IsLeft(resultRight))
|
||||
|
||||
_, errorsLeft := E.Unwrap(resultLeft)
|
||||
_, errorsRight := E.Unwrap(resultRight)
|
||||
|
||||
assert.Len(t, errorsLeft, 1)
|
||||
assert.Len(t, errorsRight, 1)
|
||||
assert.Equal(t, "error 2", errorsLeft[0].Messsage)
|
||||
assert.Equal(t, "error 2", errorsRight[0].Messsage)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_MultipleValidators tests combining multiple validators
|
||||
func TestApplicativeMonoid_MultipleValidators(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](10)
|
||||
v2 := successValidator[string](20)
|
||||
v3 := successValidator[string](30)
|
||||
v4 := successValidator[string](40)
|
||||
|
||||
// Chain multiple concat operations
|
||||
combined := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(v1, v2),
|
||||
v3,
|
||||
),
|
||||
v4,
|
||||
)
|
||||
|
||||
result := combined("test")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(100), result)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_InputDependent tests validators that depend on input
|
||||
func TestApplicativeMonoid_InputDependent(t *testing.T) {
|
||||
m := ApplicativeMonoid[int](intAddMonoid)
|
||||
|
||||
// Validator that doubles the input
|
||||
v1 := inputDependentValidator(N.Mul(2))
|
||||
// Validator that adds 10 to the input
|
||||
v2 := inputDependentValidator(N.Add(10))
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined(5)(nil)
|
||||
|
||||
// (5 * 2) + (5 + 10) = 10 + 15 = 25
|
||||
assert.Equal(t, validation.Of(25), result)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ContextPropagation tests that context is properly propagated
|
||||
func TestApplicativeMonoid_ContextPropagation(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
// Create a validator that captures the context
|
||||
var capturedContext validation.Context
|
||||
v1 := func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
capturedContext = ctx
|
||||
return validation.Success(5)
|
||||
}
|
||||
}
|
||||
|
||||
v2 := successValidator[string](3)
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
|
||||
// Create a context with some entries
|
||||
ctx := validation.Context{
|
||||
{Key: "field1", Type: "int"},
|
||||
{Key: "field2", Type: "string"},
|
||||
}
|
||||
|
||||
result := combined("test")(ctx)
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
assert.Equal(t, ctx, capturedContext)
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ErrorAccumulation tests that errors are accumulated
|
||||
func TestApplicativeMonoid_ErrorAccumulation(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := failureValidator[string, int]("error 1")
|
||||
v2 := failureValidator[string, int]("error 2")
|
||||
v3 := failureValidator[string, int]("error 3")
|
||||
|
||||
combined := m.Concat(m.Concat(v1, v2), v3)
|
||||
result := combined("test")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
|
||||
// Note: The current implementation returns the first error encountered
|
||||
// At least one error should be present
|
||||
assert.GreaterOrEqual(t, len(errors), 1)
|
||||
hasError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "error 1" || err.Messsage == "error 2" || err.Messsage == "error 3" {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasError, "Should contain at least one validation error")
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_MixedSuccessFailure tests mixing successes and failures
|
||||
func TestApplicativeMonoid_MixedSuccessFailure(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
v1 := successValidator[string](10)
|
||||
v2 := failureValidator[string, int]("error in v2")
|
||||
v3 := successValidator[string](20)
|
||||
v4 := failureValidator[string, int]("error in v4")
|
||||
|
||||
combined := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(v1, v2),
|
||||
v3,
|
||||
),
|
||||
v4,
|
||||
)
|
||||
|
||||
result := combined("test")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
|
||||
// Note: The current implementation returns the first error encountered
|
||||
// At least one error should be present
|
||||
assert.GreaterOrEqual(t, len(errors), 1)
|
||||
hasError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "error in v2" || err.Messsage == "error in v4" {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasError, "Should contain at least one validation error")
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_DifferentInputTypes tests with different input types
|
||||
func TestApplicativeMonoid_DifferentInputTypes(t *testing.T) {
|
||||
t.Run("struct input", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
Timeout int
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid[Config](intAddMonoid)
|
||||
|
||||
v1 := func(cfg Config) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.Success(cfg.Port)
|
||||
}
|
||||
}
|
||||
|
||||
v2 := func(cfg Config) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.Success(cfg.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
combined := m.Concat(v1, v2)
|
||||
result := combined(Config{Port: 8080, Timeout: 30})(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(8110), result) // 8080 + 30
|
||||
})
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_StringConcatenation tests string concatenation scenarios
|
||||
func TestApplicativeMonoid_StringConcatenation(t *testing.T) {
|
||||
m := ApplicativeMonoid[string](strMonoid)
|
||||
|
||||
t.Run("build sentence", func(t *testing.T) {
|
||||
v1 := successValidator[string]("The")
|
||||
v2 := successValidator[string](" quick")
|
||||
v3 := successValidator[string](" brown")
|
||||
v4 := successValidator[string](" fox")
|
||||
|
||||
combined := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(v1, v2),
|
||||
v3,
|
||||
),
|
||||
v4,
|
||||
)
|
||||
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("The quick brown fox"), result)
|
||||
})
|
||||
|
||||
t.Run("with empty strings", func(t *testing.T) {
|
||||
v1 := successValidator[string]("Hello")
|
||||
v2 := successValidator[string]("")
|
||||
v3 := successValidator[string]("World")
|
||||
|
||||
combined := m.Concat(m.Concat(v1, v2), v3)
|
||||
result := combined("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("HelloWorld"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkApplicativeMonoid_ConcatSuccesses(b *testing.B) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
v1 := successValidator[string](5)
|
||||
v2 := successValidator[string](3)
|
||||
combined := m.Concat(v1, v2)
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
_ = combined("test")(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApplicativeMonoid_ConcatFailures(b *testing.B) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
v1 := failureValidator[string, int]("error 1")
|
||||
v2 := failureValidator[string, int]("error 2")
|
||||
combined := m.Concat(v1, v2)
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
_ = combined("test")(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkApplicativeMonoid_MultipleConcat(b *testing.B) {
|
||||
m := ApplicativeMonoid[string](intAddMonoid)
|
||||
|
||||
validators := make([]Validate[string, int], 10)
|
||||
for i := range validators {
|
||||
validators[i] = successValidator[string](i)
|
||||
}
|
||||
|
||||
// Chain all validators
|
||||
combined := validators[0]
|
||||
for i := 1; i < len(validators); i++ {
|
||||
combined = m.Concat(combined, validators[i])
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
_ = combined("test")(nil)
|
||||
}
|
||||
}
|
||||
177
v2/optics/codec/validate/types.go
Normal file
177
v2/optics/codec/validate/types.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/decode"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Monoid represents an algebraic structure with an associative binary operation
|
||||
// and an identity element. Used for combining values of type A.
|
||||
//
|
||||
// A Monoid[A] must satisfy:
|
||||
// - Associativity: Concat(Concat(a, b), c) == Concat(a, Concat(b, c))
|
||||
// - Identity: Concat(Empty(), a) == a == Concat(a, Empty())
|
||||
//
|
||||
// Common examples:
|
||||
// - Numbers with addition (identity: 0)
|
||||
// - Numbers with multiplication (identity: 1)
|
||||
// - Strings with concatenation (identity: "")
|
||||
// - Lists with concatenation (identity: [])
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
//
|
||||
// Reader[R, A] is a function type: func(R) A
|
||||
//
|
||||
// The Reader pattern is used to:
|
||||
// - Thread configuration or context through computations
|
||||
// - Implement dependency injection in a functional way
|
||||
// - Defer computation until the environment is available
|
||||
// - Compose computations that share the same environment
|
||||
//
|
||||
// Example:
|
||||
// type Config struct { Port int }
|
||||
// getPort := func(cfg Config) int { return cfg.Port }
|
||||
// // getPort is a Reader[Config, int]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// Validation represents the result of a validation operation that may contain
|
||||
// validation errors or a successfully validated value of type A.
|
||||
//
|
||||
// Validation[A] is an Either[Errors, A], where:
|
||||
// - Left(errors): Validation failed with one or more errors
|
||||
// - Right(value): Validation succeeded with value of type A
|
||||
//
|
||||
// The Validation type supports:
|
||||
// - Error accumulation: Multiple validation errors can be collected
|
||||
// - Applicative composition: Parallel validations with error aggregation
|
||||
// - Monadic composition: Sequential validations with short-circuiting
|
||||
//
|
||||
// Example:
|
||||
// success := validation.Success(42) // Right(42)
|
||||
// failure := validation.Failure[int](errors) // Left(errors)
|
||||
Validation[A any] = validation.Validation[A]
|
||||
|
||||
// Context provides contextual information for validation operations,
|
||||
// tracking the path through nested data structures.
|
||||
//
|
||||
// Context is a slice of ContextEntry values, where each entry represents
|
||||
// a level in the nested structure being validated. This enables detailed
|
||||
// error messages that show exactly where validation failed.
|
||||
//
|
||||
// Example context path for nested validation:
|
||||
// Context{
|
||||
// {Key: "user", Type: "User"},
|
||||
// {Key: "address", Type: "Address"},
|
||||
// {Key: "zipCode", Type: "string"},
|
||||
// }
|
||||
// // Represents: user.address.zipCode
|
||||
//
|
||||
// The context is used to generate error messages like:
|
||||
// "at user.address.zipCode: expected string, got number"
|
||||
Context = validation.Context
|
||||
|
||||
Decode[I, A any] = decode.Decode[I, A]
|
||||
|
||||
// Validate is a function that validates input I to produce type A with full context tracking.
|
||||
//
|
||||
// Type structure:
|
||||
// Validate[I, A] = Reader[I, Decode[Context, A]]
|
||||
//
|
||||
// This means:
|
||||
// 1. Takes an input of type I
|
||||
// 2. Returns a Reader that depends on validation Context
|
||||
// 3. That Reader produces a Validation[A] (Either[Errors, A])
|
||||
//
|
||||
// The layered structure enables:
|
||||
// - Access to the input value being validated
|
||||
// - Context tracking through nested structures
|
||||
// - Error accumulation with detailed paths
|
||||
// - Composition with other validators
|
||||
//
|
||||
// Example usage:
|
||||
// validatePositive := func(n int) Reader[Context, Validation[int]] {
|
||||
// return func(ctx Context) Validation[int] {
|
||||
// if n > 0 {
|
||||
// return validation.Success(n)
|
||||
// }
|
||||
// return validation.FailureWithMessage[int](n, "must be positive")(ctx)
|
||||
// }
|
||||
// }
|
||||
// // validatePositive is a Validate[int, int]
|
||||
//
|
||||
// The Validate type forms:
|
||||
// - A Functor: Can map over successful results
|
||||
// - An Applicative: Can combine validators in parallel
|
||||
// - A Monad: Can chain dependent validations
|
||||
Validate[I, A any] = Reader[I, Decode[Context, A]]
|
||||
|
||||
// Errors is a collection of validation errors that occurred during validation.
|
||||
//
|
||||
// Each error in the collection contains:
|
||||
// - The value that failed validation
|
||||
// - The context path where the error occurred
|
||||
// - A human-readable error message
|
||||
// - An optional underlying cause error
|
||||
//
|
||||
// Errors can be accumulated from multiple validation failures, allowing
|
||||
// all problems to be reported at once rather than failing fast.
|
||||
Errors = validation.Errors
|
||||
|
||||
// Kleisli represents a Kleisli arrow for the Validate monad.
|
||||
//
|
||||
// A Kleisli arrow is a function from A to a monadic value Validate[I, B].
|
||||
// It's used for composing computations that produce monadic results.
|
||||
//
|
||||
// Type: Kleisli[I, A, B] = func(A) Validate[I, B]
|
||||
//
|
||||
// Kleisli arrows can be composed using the Chain function, enabling
|
||||
// sequential validation where later validators depend on earlier results.
|
||||
//
|
||||
// Example:
|
||||
// parseString := func(s string) Validate[string, int] {
|
||||
// // Parse string to int with validation
|
||||
// }
|
||||
// checkPositive := func(n int) Validate[string, int] {
|
||||
// // Validate that int is positive
|
||||
// }
|
||||
// // Both are Kleisli arrows that can be composed
|
||||
Kleisli[I, A, B any] = Reader[A, Validate[I, B]]
|
||||
|
||||
// Operator represents a transformation operator for validators.
|
||||
//
|
||||
// An Operator transforms a Validate[I, A] into a Validate[I, B].
|
||||
// It's a specialized Kleisli arrow where the input is itself a validator.
|
||||
//
|
||||
// Type: Operator[I, A, B] = func(Validate[I, A]) Validate[I, B]
|
||||
//
|
||||
// Operators are used to:
|
||||
// - Transform validation results (Map)
|
||||
// - Chain dependent validations (Chain)
|
||||
// - Apply function validators to value validators (Ap)
|
||||
//
|
||||
// Example:
|
||||
// toUpper := Map[string, string, string](strings.ToUpper)
|
||||
// // toUpper is an Operator[string, string, string]
|
||||
// // It can be applied to any string validator to uppercase the result
|
||||
Operator[I, A, B any] = Kleisli[I, Validate[I, A], B]
|
||||
)
|
||||
411
v2/optics/codec/validate/validate.go
Normal file
411
v2/optics/codec/validate/validate.go
Normal file
@@ -0,0 +1,411 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package validate provides functional validation primitives for building composable validators.
|
||||
//
|
||||
// This package implements a validation framework based on functional programming principles,
|
||||
// allowing you to build complex validators from simple, composable pieces. It uses the
|
||||
// Reader monad pattern to thread validation context through nested structures.
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// The validate package is built around several key types:
|
||||
//
|
||||
// - Validate[I, A]: A validator that transforms input I to output A with validation context
|
||||
// - Validation[A]: The result of validation, either errors or a valid value A
|
||||
// - Context: Tracks the path through nested structures for detailed error messages
|
||||
//
|
||||
// # Type Structure
|
||||
//
|
||||
// A Validate[I, A] is defined as:
|
||||
//
|
||||
// Reader[I, Decode[A]]]
|
||||
//
|
||||
// This means:
|
||||
// 1. It takes an input of type I
|
||||
// 2. Returns a Reader that depends on validation Context
|
||||
// 3. That Reader produces a Validation[A] (Either[Errors, A])
|
||||
//
|
||||
// This layered structure allows validators to:
|
||||
// - Access the input value
|
||||
// - Track validation context (path in nested structures)
|
||||
// - Accumulate multiple validation errors
|
||||
// - Compose with other validators
|
||||
//
|
||||
// # Validation Context
|
||||
//
|
||||
// The Context type tracks the path through nested data structures during validation.
|
||||
// Each ContextEntry contains:
|
||||
// - Key: The field name or map key
|
||||
// - Type: The expected type name
|
||||
// - Actual: The actual value being validated
|
||||
//
|
||||
// This provides detailed error messages like "at user.address.zipCode: expected string, got number".
|
||||
//
|
||||
// # Monoid Operations
|
||||
//
|
||||
// The package provides ApplicativeMonoid for combining validators using monoid operations.
|
||||
// This allows you to:
|
||||
// - Combine multiple validators that produce monoidal values
|
||||
// - Accumulate results from parallel validations
|
||||
// - Build complex validators from simpler ones
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// Basic validation structure:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// )
|
||||
//
|
||||
// // A validator that checks if a string is non-empty
|
||||
// func nonEmptyString(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// if input == "" {
|
||||
// return validation.FailureWithMessage[string](input, "string must not be empty")
|
||||
// }
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success(input)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Create a Validate function
|
||||
// var validateNonEmpty validate.Validate[string, string] = func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return nonEmptyString(input)
|
||||
// }
|
||||
//
|
||||
// Combining validators with monoids:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/monoid"
|
||||
// "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// // Combine string validators using string concatenation monoid
|
||||
// stringMonoid := string.Monoid
|
||||
// validatorMonoid := validate.ApplicativeMonoid[string, string](stringMonoid)
|
||||
//
|
||||
// // Now you can combine validators that produce strings
|
||||
// combined := validatorMonoid.Concat(validator1, validator2)
|
||||
//
|
||||
// # Integration with Codec
|
||||
//
|
||||
// This package is designed to work with the optics/codec package for building
|
||||
// type-safe encoders and decoders with validation. Validators can be composed
|
||||
// into codecs that handle serialization, deserialization, and validation in a
|
||||
// unified way.
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// Validation errors are accumulated using the Either monad's applicative instance.
|
||||
// This means:
|
||||
// - Multiple validation errors can be collected in a single pass
|
||||
// - Errors include full context path for debugging
|
||||
// - Errors can be formatted for logging or user display
|
||||
//
|
||||
// See the validation package for error types and formatting options.
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/decode"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Of creates a Validate that always succeeds with the given value.
|
||||
//
|
||||
// This is the "pure" or "return" operation for the Validate monad. It lifts a plain
|
||||
// value into the validation context without performing any actual validation.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type (not used, but required for type consistency)
|
||||
// - A: The type of the value to wrap
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - a: The value to wrap in a successful validation
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, A] that ignores its input and always returns a successful validation
|
||||
// containing the value a.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Create a validator that always succeeds with value 42
|
||||
// alwaysValid := validate.Of[string, int](42)
|
||||
// result := alwaysValid("any input")(nil)
|
||||
// // result is validation.Success(42)
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - This is useful for lifting pure values into the validation context
|
||||
// - The input type I is ignored; the validator succeeds regardless of input
|
||||
// - This satisfies the monad laws: Of is the left and right identity for Chain
|
||||
func Of[I, A any](a A) Validate[I, A] {
|
||||
return reader.Of[I](decode.Of[Context](a))
|
||||
}
|
||||
|
||||
// MonadMap applies a function to the successful result of a validation.
|
||||
//
|
||||
// This is the functor map operation for Validate. It transforms the success value
|
||||
// without affecting the validation logic or error handling. If the validation fails,
|
||||
// the function is not applied and errors are preserved.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the current validation result
|
||||
// - B: The type after applying the transformation
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fa: The validator to transform
|
||||
// - f: The transformation function to apply to successful results
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A new Validate[I, B] that applies f to the result if validation succeeds.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Transform a string validator to uppercase
|
||||
// validateString := func(s string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// return validation.Success(s)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// upperValidator := validate.MonadMap(validateString, strings.ToUpper)
|
||||
// result := upperValidator("hello")(nil)
|
||||
// // result is validation.Success("HELLO")
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Preserves validation errors unchanged
|
||||
// - Only applies the function to successful validations
|
||||
// - Satisfies the functor laws: composition and identity
|
||||
func MonadMap[I, A, B any](fa Validate[I, A], f func(A) B) Validate[I, B] {
|
||||
return readert.MonadMap[
|
||||
Validate[I, A],
|
||||
Validate[I, B]](
|
||||
decode.MonadMap,
|
||||
fa,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Map creates an operator that transforms validation results.
|
||||
//
|
||||
// This is the curried version of MonadMap, returning a function that can be applied
|
||||
// to validators. It's useful for creating reusable transformation pipelines.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the current validation result
|
||||
// - B: The type after applying the transformation
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: The transformation function to apply to successful results
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[I, A, B] that transforms Validate[I, A] to Validate[I, B].
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Create a reusable transformation
|
||||
// toUpper := validate.Map[string, string, string](strings.ToUpper)
|
||||
//
|
||||
// // Apply it to different validators
|
||||
// validator1 := toUpper(someStringValidator)
|
||||
// validator2 := toUpper(anotherStringValidator)
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - This is the point-free style version of MonadMap
|
||||
// - Useful for building transformation pipelines
|
||||
// - Can be composed with other operators
|
||||
func Map[I, A, B any](f func(A) B) Operator[I, A, B] {
|
||||
return readert.Map[
|
||||
Validate[I, A],
|
||||
Validate[I, B]](
|
||||
decode.Map,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Chain sequences two validators, where the second depends on the result of the first.
|
||||
//
|
||||
// This is the monadic bind operation for Validate. It allows you to create validators
|
||||
// that depend on the results of previous validations, enabling complex validation logic
|
||||
// that builds on earlier results.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the first validation result
|
||||
// - B: The type of the second validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow that takes a value of type A and returns a Validate[I, B]
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[I, A, B] that sequences the validations.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // First validate that a string is non-empty, then validate its length
|
||||
// validateNonEmpty := func(s string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if s == "" {
|
||||
// return validation.FailureWithMessage[string](s, "must not be empty")(ctx)
|
||||
// }
|
||||
// return validation.Success(s)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// validateLength := func(s string) validate.Validate[string, int] {
|
||||
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// if len(s) < 3 {
|
||||
// return validation.FailureWithMessage[int](len(s), "too short")(ctx)
|
||||
// }
|
||||
// return validation.Success(len(s))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Chain them together
|
||||
// chained := validate.Chain(validateLength)(validateNonEmpty)
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - If the first validation fails, the second is not executed
|
||||
// - Errors from the first validation are preserved
|
||||
// - This enables dependent validation logic
|
||||
// - Satisfies the monad laws: associativity and identity
|
||||
func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
|
||||
return readert.Chain[Validate[I, A]](
|
||||
decode.Chain,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAp applies a validator containing a function to a validator containing a value.
|
||||
//
|
||||
// This is the applicative apply operation for Validate. It allows you to apply
|
||||
// functions wrapped in validation context to values wrapped in validation context,
|
||||
// accumulating errors from both if either fails.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - B: The result type after applying the function
|
||||
// - I: The input type
|
||||
// - A: The type of the value to which the function is applied
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fab: A validator that produces a function from A to B
|
||||
// - fa: A validator that produces a value of type A
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, B] that applies the function to the value if both validations succeed.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Create a validator that produces a function
|
||||
// validateFunc := validate.Of[string, func(int) int](func(x int) int { return x * 2 })
|
||||
//
|
||||
// // Create a validator that produces a value
|
||||
// validateValue := validate.Of[string, int](21)
|
||||
//
|
||||
// // Apply them
|
||||
// result := validate.MonadAp(validateFunc, validateValue)
|
||||
// // When run, produces validation.Success(42)
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Both validators receive the same input
|
||||
// - If either validation fails, all errors are accumulated
|
||||
// - If both succeed, the function is applied to the value
|
||||
// - This enables parallel validation with error accumulation
|
||||
// - Satisfies the applicative functor laws
|
||||
func MonadAp[B, I, A any](fab Validate[I, func(A) B], fa Validate[I, A]) Validate[I, B] {
|
||||
return readert.MonadAp[
|
||||
Validate[I, A],
|
||||
Validate[I, B],
|
||||
Validate[I, func(A) B], I, A](
|
||||
decode.MonadAp[B, Context, A],
|
||||
fab,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// Ap creates an operator that applies a function validator to a value validator.
|
||||
//
|
||||
// This is the curried version of MonadAp, returning a function that can be applied
|
||||
// to function validators. It's useful for creating reusable applicative patterns.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - B: The result type after applying the function
|
||||
// - I: The input type
|
||||
// - A: The type of the value to which the function is applied
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fa: A validator that produces a value of type A
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[I, func(A) B, B] that applies function validators to the value validator.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Create a value validator
|
||||
// validateValue := validate.Of[string, int](21)
|
||||
//
|
||||
// // Create an applicative operator
|
||||
// applyTo21 := validate.Ap[int, string, int](validateValue)
|
||||
//
|
||||
// // Create a function validator
|
||||
// validateDouble := validate.Of[string, func(int) int](func(x int) int { return x * 2 })
|
||||
//
|
||||
// // Apply it
|
||||
// result := applyTo21(validateDouble)
|
||||
// // When run, produces validation.Success(42)
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - This is the point-free style version of MonadAp
|
||||
// - Useful for building applicative pipelines
|
||||
// - Enables parallel validation with error accumulation
|
||||
// - Can be composed with other applicative operators
|
||||
func Ap[B, I, A any](fa Validate[I, A]) Operator[I, func(A) B, B] {
|
||||
return readert.Ap[
|
||||
Validate[I, A],
|
||||
Validate[I, B],
|
||||
Validate[I, func(A) B], I, A](
|
||||
decode.Ap[B, Context, A],
|
||||
fa,
|
||||
)
|
||||
}
|
||||
851
v2/optics/codec/validate/validate_test.go
Normal file
851
v2/optics/codec/validate/validate_test.go
Normal file
@@ -0,0 +1,851 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestValidateType tests the Validate type structure
|
||||
func TestValidateType(t *testing.T) {
|
||||
t.Run("basic validate function", func(t *testing.T) {
|
||||
// Create a simple validator that checks if a number is positive
|
||||
validatePositive := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if n > 0 {
|
||||
return validation.Success(n)
|
||||
}
|
||||
return validation.FailureWithMessage[int](n, "must be positive")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Test with positive number
|
||||
result := validatePositive(42)(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
|
||||
// Test with negative number
|
||||
result = validatePositive(-5)(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "must be positive", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("validate with context", func(t *testing.T) {
|
||||
validateWithContext := func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
if s == "" {
|
||||
return validation.FailureWithMessage[string](s, "empty string")(ctx)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := validation.Context{
|
||||
{Key: "username", Type: "string"},
|
||||
}
|
||||
|
||||
result := validateWithContext("")(ctx)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, ctx, errors[0].Context)
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateComposition tests composing validators
|
||||
func TestValidateComposition(t *testing.T) {
|
||||
t.Run("sequential validation", func(t *testing.T) {
|
||||
// First validator: check if string is not empty
|
||||
validateNotEmpty := func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
if s == "" {
|
||||
return validation.FailureWithMessage[string](s, "must not be empty")(ctx)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Second validator: check if string has minimum length
|
||||
validateMinLength := func(minLen int) func(string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
if len(s) < minLen {
|
||||
return validation.FailureWithMessage[string](s, "too short")(ctx)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test with valid input
|
||||
input := "hello"
|
||||
result1 := validateNotEmpty(input)(nil)
|
||||
assert.Equal(t, validation.Of("hello"), result1)
|
||||
|
||||
result2 := validateMinLength(3)(input)(nil)
|
||||
assert.Equal(t, validation.Of("hello"), result2)
|
||||
|
||||
// Test with invalid input
|
||||
shortInput := "hi"
|
||||
result3 := validateMinLength(5)(shortInput)(nil)
|
||||
assert.True(t, E.IsLeft(result3))
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateWithDifferentTypes tests validators with various input/output types
|
||||
func TestValidateWithDifferentTypes(t *testing.T) {
|
||||
t.Run("string to int conversion", func(t *testing.T) {
|
||||
// Validator that parses string to int
|
||||
validateParseInt := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
// Simple parsing logic for testing
|
||||
if s == "42" {
|
||||
return validation.Success(42)
|
||||
}
|
||||
return validation.FailureWithMessage[int](s, "invalid integer")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
result := validateParseInt("42")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
|
||||
result = validateParseInt("abc")(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("struct validation", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
validateUser := func(u User) Reader[validation.Context, validation.Validation[User]] {
|
||||
return func(ctx validation.Context) validation.Validation[User] {
|
||||
if u.Name == "" {
|
||||
return validation.FailureWithMessage[User](u, "name is required")(ctx)
|
||||
}
|
||||
if u.Age < 0 {
|
||||
return validation.FailureWithMessage[User](u, "age must be non-negative")(ctx)
|
||||
}
|
||||
if u.Email == "" {
|
||||
return validation.FailureWithMessage[User](u, "email is required")(ctx)
|
||||
}
|
||||
return validation.Success(u)
|
||||
}
|
||||
}
|
||||
|
||||
validUser := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
result := validateUser(validUser)(nil)
|
||||
assert.Equal(t, validation.Of(validUser), result)
|
||||
|
||||
invalidUser := User{Name: "", Age: 30, Email: "alice@example.com"}
|
||||
result = validateUser(invalidUser)(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateContextTracking tests context tracking through nested structures
|
||||
func TestValidateContextTracking(t *testing.T) {
|
||||
t.Run("nested context", func(t *testing.T) {
|
||||
validateField := func(value string, fieldName string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
// Add field to context
|
||||
newCtx := append(ctx, validation.ContextEntry{
|
||||
Key: fieldName,
|
||||
Type: "string",
|
||||
})
|
||||
|
||||
if value == "" {
|
||||
return validation.FailureWithMessage[string](value, "field is empty")(newCtx)
|
||||
}
|
||||
return validation.Success(value)
|
||||
}
|
||||
}
|
||||
|
||||
baseCtx := validation.Context{
|
||||
{Key: "user", Type: "User"},
|
||||
}
|
||||
|
||||
result := validateField("", "email")(baseCtx)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
|
||||
// Check that context includes both user and email
|
||||
assert.Len(t, errors[0].Context, 2)
|
||||
assert.Equal(t, "user", errors[0].Context[0].Key)
|
||||
assert.Equal(t, "email", errors[0].Context[1].Key)
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateErrorMessages tests error message generation
|
||||
func TestValidateErrorMessages(t *testing.T) {
|
||||
t.Run("custom error messages", func(t *testing.T) {
|
||||
validateRange := func(min, max int) func(int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if n < min {
|
||||
return validation.FailureWithMessage[int](n, "value too small")(ctx)
|
||||
}
|
||||
if n > max {
|
||||
return validation.FailureWithMessage[int](n, "value too large")(ctx)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := validateRange(0, 100)(150)(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Equal(t, "value too large", errors[0].Messsage)
|
||||
|
||||
result = validateRange(0, 100)(-10)(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors = E.Unwrap(result)
|
||||
assert.Equal(t, "value too small", errors[0].Messsage)
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateTransformations tests validators that transform values
|
||||
func TestValidateTransformations(t *testing.T) {
|
||||
t.Run("normalize and validate", func(t *testing.T) {
|
||||
// Validator that normalizes (trims) and validates
|
||||
validateAndNormalize := func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
// Simple trim simulation - trim all leading and trailing spaces
|
||||
normalized := s
|
||||
// Trim leading spaces
|
||||
for len(normalized) > 0 && normalized[0] == ' ' {
|
||||
normalized = normalized[1:]
|
||||
}
|
||||
// Trim trailing spaces
|
||||
for len(normalized) > 0 && normalized[len(normalized)-1] == ' ' {
|
||||
normalized = normalized[:len(normalized)-1]
|
||||
}
|
||||
|
||||
if normalized == "" {
|
||||
return validation.FailureWithMessage[string](s, "empty after normalization")(ctx)
|
||||
}
|
||||
return validation.Success(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
result := validateAndNormalize(" hello ")(nil)
|
||||
assert.Equal(t, validation.Of("hello"), result)
|
||||
|
||||
result = validateAndNormalize(" ")(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateChaining tests chaining multiple validators
|
||||
func TestValidateChaining(t *testing.T) {
|
||||
t.Run("chain validators manually", func(t *testing.T) {
|
||||
// First validator
|
||||
v1 := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if n < 0 {
|
||||
return validation.FailureWithMessage[int](n, "must be non-negative")(ctx)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
}
|
||||
|
||||
// Second validator (depends on first)
|
||||
v2 := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if n > 100 {
|
||||
return validation.FailureWithMessage[int](n, "must be <= 100")(ctx)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
}
|
||||
|
||||
// Test valid value
|
||||
input := 50
|
||||
result1 := v1(input)(nil)
|
||||
assert.Equal(t, validation.Of(50), result1)
|
||||
|
||||
result2 := v2(input)(nil)
|
||||
assert.Equal(t, validation.Of(50), result2)
|
||||
|
||||
// Test invalid value (too large)
|
||||
input = 150
|
||||
result1 = v1(input)(nil)
|
||||
assert.Equal(t, validation.Of(150), result1)
|
||||
|
||||
result2 = v2(input)(nil)
|
||||
assert.True(t, E.IsLeft(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateComplexScenarios tests real-world validation scenarios
|
||||
func TestValidateComplexScenarios(t *testing.T) {
|
||||
t.Run("email validation", func(t *testing.T) {
|
||||
validateEmail := func(email string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
// Simple email validation for testing
|
||||
hasAt := false
|
||||
hasDot := false
|
||||
for _, c := range email {
|
||||
if c == '@' {
|
||||
hasAt = true
|
||||
}
|
||||
if c == '.' {
|
||||
hasDot = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAt || !hasDot {
|
||||
return validation.FailureWithMessage[string](email, "invalid email format")(ctx)
|
||||
}
|
||||
return validation.Success(email)
|
||||
}
|
||||
}
|
||||
|
||||
result := validateEmail("user@example.com")(nil)
|
||||
assert.Equal(t, validation.Of("user@example.com"), result)
|
||||
|
||||
result = validateEmail("invalid-email")(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
result = validateEmail("no-domain@")(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("password strength validation", func(t *testing.T) {
|
||||
validatePassword := func(pwd string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
if len(pwd) < 8 {
|
||||
return validation.FailureWithMessage[string](pwd, "password too short")(ctx)
|
||||
}
|
||||
|
||||
hasUpper := false
|
||||
hasLower := false
|
||||
hasDigit := false
|
||||
|
||||
for _, c := range pwd {
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
hasUpper = true
|
||||
}
|
||||
if c >= 'a' && c <= 'z' {
|
||||
hasLower = true
|
||||
}
|
||||
if c >= '0' && c <= '9' {
|
||||
hasDigit = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasUpper || !hasLower || !hasDigit {
|
||||
return validation.FailureWithMessage[string](pwd, "password must contain upper, lower, and digit")(ctx)
|
||||
}
|
||||
|
||||
return validation.Success(pwd)
|
||||
}
|
||||
}
|
||||
|
||||
result := validatePassword("StrongPass123")(nil)
|
||||
assert.Equal(t, validation.Of("StrongPass123"), result)
|
||||
|
||||
result = validatePassword("weak")(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
result = validatePassword("nouppercase123")(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkValidate_Success(b *testing.B) {
|
||||
validate := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if n > 0 {
|
||||
return validation.Success(n)
|
||||
}
|
||||
return validation.FailureWithMessage[int](n, "must be positive")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validate(42)(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidate_Failure(b *testing.B) {
|
||||
validate := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if n > 0 {
|
||||
return validation.Success(n)
|
||||
}
|
||||
return validation.FailureWithMessage[int](n, "must be positive")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validate(-1)(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidate_WithContext(b *testing.B) {
|
||||
validate := func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
if s == "" {
|
||||
return validation.FailureWithMessage[string](s, "empty string")(ctx)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := validation.Context{
|
||||
{Key: "field1", Type: "string"},
|
||||
{Key: "field2", Type: "string"},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validate("test")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOf tests the Of function
|
||||
func TestOf(t *testing.T) {
|
||||
t.Run("creates successful validation with value", func(t *testing.T) {
|
||||
validator := Of[string](42)
|
||||
result := validator("any input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("ignores input value", func(t *testing.T) {
|
||||
validator := Of[string]("success")
|
||||
|
||||
result1 := validator("input1")(nil)
|
||||
result2 := validator("input2")(nil)
|
||||
result3 := validator("")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("success"), result1)
|
||||
assert.Equal(t, validation.Of("success"), result2)
|
||||
assert.Equal(t, validation.Of("success"), result3)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
validator := Of[int](user)
|
||||
result := validator(123)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(user), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadMap tests the MonadMap function
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("transforms successful validation", func(t *testing.T) {
|
||||
validator := Of[string](21)
|
||||
doubled := MonadMap(validator, N.Mul(2))
|
||||
|
||||
result := doubled("input")(nil)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("preserves validation errors", func(t *testing.T) {
|
||||
failingValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "validation failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
mapped := MonadMap(failingValidator, N.Mul(2))
|
||||
result := mapped("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "validation failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("chains multiple transformations", func(t *testing.T) {
|
||||
validator := Of[string](10)
|
||||
transformed := MonadMap(
|
||||
MonadMap(
|
||||
MonadMap(validator, N.Add(5)),
|
||||
N.Mul(2),
|
||||
),
|
||||
N.Sub(10),
|
||||
)
|
||||
|
||||
result := transformed("input")(nil)
|
||||
assert.Equal(t, validation.Of(20), result) // (10 + 5) * 2 - 10 = 20
|
||||
})
|
||||
|
||||
t.Run("transforms between different types", func(t *testing.T) {
|
||||
validator := Of[string](42)
|
||||
toString := MonadMap(validator, func(x int) string {
|
||||
return "value: " + string(rune(x+'0'))
|
||||
})
|
||||
|
||||
result := toString("input")(nil)
|
||||
assert.True(t, E.IsRight(result))
|
||||
if E.IsRight(result) {
|
||||
value, _ := E.Unwrap(result)
|
||||
assert.Contains(t, value, "value:")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMap tests the Map function
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("creates reusable transformation", func(t *testing.T) {
|
||||
double := Map[string](N.Mul(2))
|
||||
|
||||
validator1 := Of[string](21)
|
||||
validator2 := Of[string](10)
|
||||
|
||||
result1 := double(validator1)("input")(nil)
|
||||
result2 := double(validator2)("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result1)
|
||||
assert.Equal(t, validation.Of(20), result2)
|
||||
})
|
||||
|
||||
t.Run("preserves errors in transformation", func(t *testing.T) {
|
||||
increment := Map[string](func(x int) int { return x + 1 })
|
||||
|
||||
failingValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "error")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
result := increment(failingValidator)("input")(nil)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("composes with other operators", func(t *testing.T) {
|
||||
addFive := Map[string](N.Add(5))
|
||||
double := Map[string](N.Mul(2))
|
||||
|
||||
validator := Of[string](10)
|
||||
composed := double(addFive(validator))
|
||||
|
||||
result := composed("input")(nil)
|
||||
assert.Equal(t, validation.Of(30), result) // (10 + 5) * 2 = 30
|
||||
})
|
||||
}
|
||||
|
||||
// TestChain tests the Chain function
|
||||
func TestChain(t *testing.T) {
|
||||
t.Run("sequences dependent validations", func(t *testing.T) {
|
||||
// First validator: parse string to int
|
||||
parseValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if s == "42" {
|
||||
return validation.Success(42)
|
||||
}
|
||||
return validation.FailureWithMessage[int](s, "invalid number")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Second validator: check if number is positive
|
||||
checkPositive := func(n int) Validate[string, string] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
if n > 0 {
|
||||
return validation.Success("positive")
|
||||
}
|
||||
return validation.FailureWithMessage[string](n, "not positive")(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chained := Chain(checkPositive)(parseValidator)
|
||||
result := chained("42")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("positive"), result)
|
||||
})
|
||||
|
||||
t.Run("stops on first validation failure", func(t *testing.T) {
|
||||
failingValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "first failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
neverCalled := func(n int) Validate[string, string] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
// This should never be reached
|
||||
t.Error("Second validator should not be called")
|
||||
return validation.Success("should not reach")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chained := Chain(neverCalled)(failingValidator)
|
||||
result := chained("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Equal(t, "first failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("propagates second validation failure", func(t *testing.T) {
|
||||
successValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.Success(42)
|
||||
}
|
||||
}
|
||||
|
||||
failingSecond := func(n int) Validate[string, string] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](n, "second failed")(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chained := Chain(failingSecond)(successValidator)
|
||||
result := chained("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Equal(t, "second failed", errors[0].Messsage)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAp tests the MonadAp function
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("applies function to value when both succeed", func(t *testing.T) {
|
||||
funcValidator := Of[string](N.Mul(2))
|
||||
valueValidator := Of[string](21)
|
||||
|
||||
result := MonadAp(funcValidator, valueValidator)("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when function validator fails", func(t *testing.T) {
|
||||
failingFunc := func(s string) Reader[validation.Context, validation.Validation[func(int) int]] {
|
||||
return func(ctx validation.Context) validation.Validation[func(int) int] {
|
||||
return validation.FailureWithMessage[func(int) int](s, "func failed")(ctx)
|
||||
}
|
||||
}
|
||||
valueValidator := Of[string](21)
|
||||
|
||||
result := MonadAp(failingFunc, valueValidator)("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "func failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors when value validator fails", func(t *testing.T) {
|
||||
funcValidator := Of[string](N.Mul(2))
|
||||
failingValue := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "value failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
result := MonadAp(funcValidator, failingValue)("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "value failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("returns error when both validators fail", func(t *testing.T) {
|
||||
failingFunc := func(s string) Reader[validation.Context, validation.Validation[func(int) int]] {
|
||||
return func(ctx validation.Context) validation.Validation[func(int) int] {
|
||||
return validation.FailureWithMessage[func(int) int](s, "func failed")(ctx)
|
||||
}
|
||||
}
|
||||
failingValue := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "value failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
result := MonadAp(failingFunc, failingValue)("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
// Note: The current implementation returns the first error encountered
|
||||
assert.GreaterOrEqual(t, len(errors), 1)
|
||||
// At least one of the errors should be present
|
||||
hasError := false
|
||||
for _, err := range errors {
|
||||
if err.Messsage == "func failed" || err.Messsage == "value failed" {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasError, "Should contain at least one validation error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAp tests the Ap function
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("creates reusable applicative operator", func(t *testing.T) {
|
||||
valueValidator := Of[string](21)
|
||||
applyTo21 := Ap[int](valueValidator)
|
||||
|
||||
double := Of[string](N.Mul(2))
|
||||
triple := Of[string](func(x int) int { return x * 3 })
|
||||
|
||||
result1 := applyTo21(double)("input")(nil)
|
||||
result2 := applyTo21(triple)("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result1)
|
||||
assert.Equal(t, validation.Of(63), result2)
|
||||
})
|
||||
|
||||
t.Run("preserves errors from value validator", func(t *testing.T) {
|
||||
failingValue := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "value error")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
applyToFailing := Ap[int](failingValue)
|
||||
funcValidator := Of[string](N.Mul(2))
|
||||
|
||||
result := applyToFailing(funcValidator)("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Equal(t, "value error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("preserves errors from function validator", func(t *testing.T) {
|
||||
valueValidator := Of[string](21)
|
||||
applyTo21 := Ap[int](valueValidator)
|
||||
|
||||
failingFunc := func(s string) Reader[validation.Context, validation.Validation[func(int) int]] {
|
||||
return func(ctx validation.Context) validation.Validation[func(int) int] {
|
||||
return validation.FailureWithMessage[func(int) int](s, "func error")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
result := applyTo21(failingFunc)("input")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Equal(t, "func error", errors[0].Messsage)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadLaws tests that the monad laws hold for Validate
|
||||
func TestMonadLaws(t *testing.T) {
|
||||
t.Run("left identity: Of(a) >>= f === f(a)", func(t *testing.T) {
|
||||
a := 42
|
||||
f := func(x int) Validate[string, string] {
|
||||
return Of[string]("value: " + string(rune(x+'0')))
|
||||
}
|
||||
|
||||
// Of(a) >>= f
|
||||
left := Chain(f)(Of[string](a))
|
||||
// f(a)
|
||||
right := f(a)
|
||||
|
||||
leftResult := left("input")(nil)
|
||||
rightResult := right("input")(nil)
|
||||
|
||||
assert.Equal(t, E.IsRight(leftResult), E.IsRight(rightResult))
|
||||
if E.IsRight(leftResult) {
|
||||
leftVal, _ := E.Unwrap(leftResult)
|
||||
rightVal, _ := E.Unwrap(rightResult)
|
||||
assert.Equal(t, leftVal, rightVal)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("right identity: m >>= Of === m", func(t *testing.T) {
|
||||
m := Of[string](42)
|
||||
|
||||
// m >>= Of
|
||||
chained := Chain(func(x int) Validate[string, int] {
|
||||
return Of[string](x)
|
||||
})(m)
|
||||
|
||||
mResult := m("input")(nil)
|
||||
chainedResult := chained("input")(nil)
|
||||
|
||||
assert.Equal(t, E.IsRight(mResult), E.IsRight(chainedResult))
|
||||
if E.IsRight(mResult) {
|
||||
mVal, _ := E.Unwrap(mResult)
|
||||
chainedVal, _ := E.Unwrap(chainedResult)
|
||||
assert.Equal(t, mVal, chainedVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFunctorLaws tests that the functor laws hold for Validate
|
||||
func TestFunctorLaws(t *testing.T) {
|
||||
t.Run("identity: map(id) === id", func(t *testing.T) {
|
||||
validator := Of[string](42)
|
||||
identity := func(x int) int { return x }
|
||||
|
||||
mapped := MonadMap(validator, identity)
|
||||
|
||||
origResult := validator("input")(nil)
|
||||
mappedResult := mapped("input")(nil)
|
||||
|
||||
assert.Equal(t, E.IsRight(origResult), E.IsRight(mappedResult))
|
||||
if E.IsRight(origResult) {
|
||||
origVal, _ := E.Unwrap(origResult)
|
||||
mappedVal, _ := E.Unwrap(mappedResult)
|
||||
assert.Equal(t, origVal, mappedVal)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("composition: map(f . g) === map(f) . map(g)", func(t *testing.T) {
|
||||
validator := Of[string](10)
|
||||
f := N.Mul(2)
|
||||
g := N.Add(5)
|
||||
|
||||
// map(f . g)
|
||||
composed := MonadMap(validator, func(x int) int { return f(g(x)) })
|
||||
|
||||
// map(f) . map(g)
|
||||
separate := MonadMap(MonadMap(validator, g), f)
|
||||
|
||||
composedResult := composed("input")(nil)
|
||||
separateResult := separate("input")(nil)
|
||||
|
||||
assert.Equal(t, E.IsRight(composedResult), E.IsRight(separateResult))
|
||||
if E.IsRight(composedResult) {
|
||||
composedVal, _ := E.Unwrap(composedResult)
|
||||
separateVal, _ := E.Unwrap(separateResult)
|
||||
assert.Equal(t, composedVal, separateVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -3,19 +3,18 @@ package codec
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/errors"
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func onTypeError(expType string) func(any) error {
|
||||
return func(u any) error {
|
||||
return fmt.Errorf("expecting type [%s] but got [%T]", expType, u)
|
||||
}
|
||||
return errors.OnSome[any](fmt.Sprintf("expecting type [%s] but got [%%T]", expType))
|
||||
}
|
||||
|
||||
// Is checks if a value can be converted to type T.
|
||||
// Returns Some(value) if the conversion succeeds, None otherwise.
|
||||
// This is a type-safe cast operation.
|
||||
func Is[T any]() func(any) Result[T] {
|
||||
var zero T
|
||||
return result.ToType[T](onTypeError(fmt.Sprintf("%T", zero)))
|
||||
func Is[T any]() ReaderResult[any, T] {
|
||||
return result.ToType[T](onTypeError(formatting.TypeInfo(*new(T))))
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ func Ap[B, A any](fa Validation[A]) Operator[func(A) B, B] {
|
||||
return either.ApV[B, A](ErrorsMonoid())(fa)
|
||||
}
|
||||
|
||||
func MonadAp[B, A any](fab Validation[func(A) B], fa Validation[A]) Validation[B] {
|
||||
return either.MonadApV[B, A](ErrorsMonoid())(fab, fa)
|
||||
}
|
||||
|
||||
// Map transforms the value inside a successful validation using the provided function.
|
||||
// If the validation is a failure, the errors are preserved unchanged.
|
||||
// This is the functor map operation for Validation.
|
||||
@@ -43,6 +47,18 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return either.Map[Errors](f)
|
||||
}
|
||||
|
||||
func MonadMap[A, B any](fa Validation[A], f func(A) B) Validation[B] {
|
||||
return either.MonadMap(fa, f)
|
||||
}
|
||||
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return either.Chain(f)
|
||||
}
|
||||
|
||||
func MonadChain[A, B any](fa Validation[A], f Kleisli[A, B]) Validation[B] {
|
||||
return either.MonadChain(fa, f)
|
||||
}
|
||||
|
||||
// Applicative creates an Applicative instance for Validation with error accumulation.
|
||||
//
|
||||
// This returns a lawful Applicative that accumulates validation errors using the Errors monoid.
|
||||
|
||||
@@ -109,7 +109,7 @@ func TestAp(t *testing.T) {
|
||||
funcValidation := Of(double)
|
||||
valueValidation := Of(21)
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
result := Ap[int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
@@ -126,7 +126,7 @@ func TestAp(t *testing.T) {
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
result := Ap[int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
@@ -143,7 +143,7 @@ func TestAp(t *testing.T) {
|
||||
})
|
||||
valueValidation := Of(21)
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
result := Ap[int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
@@ -162,7 +162,7 @@ func TestAp(t *testing.T) {
|
||||
&ValidationError{Messsage: "value error"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
result := Ap[int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
@@ -180,7 +180,7 @@ func TestAp(t *testing.T) {
|
||||
funcValidation := Of(toUpper)
|
||||
valueValidation := Of("hello")
|
||||
|
||||
result := Ap[string, string](valueValidation)(funcValidation)
|
||||
result := Ap[string](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
@@ -199,7 +199,7 @@ func TestAp(t *testing.T) {
|
||||
&ValidationError{Messsage: "value error 1"},
|
||||
})
|
||||
|
||||
result := Ap[int, int](valueValidation)(funcValidation)
|
||||
result := Ap[int](valueValidation)(funcValidation)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
@@ -242,7 +242,7 @@ func TestMonadLaws(t *testing.T) {
|
||||
t.Run("applicative identity law", func(t *testing.T) {
|
||||
// Ap(v)(Of(id)) == v
|
||||
v := Of(42)
|
||||
result := Ap[int, int](v)(Of(F.Identity[int]))
|
||||
result := Ap[int](v)(Of(F.Identity[int]))
|
||||
|
||||
assert.Equal(t, v, result)
|
||||
})
|
||||
@@ -252,7 +252,7 @@ func TestMonadLaws(t *testing.T) {
|
||||
f := func(x int) int { return x * 2 }
|
||||
x := 21
|
||||
|
||||
left := Ap[int, int](Of(x))(Of(f))
|
||||
left := Ap[int](Of(x))(Of(f))
|
||||
right := Of(f(x))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
@@ -285,7 +285,7 @@ func TestMapWithOperator(t *testing.T) {
|
||||
func TestApWithOperator(t *testing.T) {
|
||||
t.Run("Ap returns an Operator", func(t *testing.T) {
|
||||
valueValidation := Of(21)
|
||||
operator := Ap[int, int](valueValidation)
|
||||
operator := Ap[int](valueValidation)
|
||||
|
||||
// Operator can be applied to different function validations
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user