1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-29 10:36:04 +02:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Obed Tetteh
da0344f9bd feat(iterator): add Last function with Option return type (#155)
- Add Last function to retrieve the final element from an iterator,
  returning Some(element) for non-empty sequences and None for empty ones.
- Includes tests covering simple types and  complex types
- Add documentation including example code
2026-01-26 09:04:51 +01:00
Dr. Carsten Leue
cd79dd56b9 fix: simplify tests a bit
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 17:56:28 +01:00
Dr. Carsten Leue
df07599a9e fix: some docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:40:45 +01:00
Dr. Carsten Leue
30ad0e4dd8 doc: add validation docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:26:53 +01:00
Dr. Carsten Leue
2374d7f1e4 fix: support unexported fields for lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:18:44 +01:00
Dr. Carsten Leue
eafc008798 fix: doc for lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:00:11 +01:00
Dr. Carsten Leue
46bf065e34 fix: migrate CLI to github.com/urfave/v3
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 12:56:23 +01:00
renovate[bot]
b4e303423b chore(deps): update actions/checkout action to v6.0.2 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 12:39:53 +01:00
renovate[bot]
7afc098f58 fix(deps): update go dependencies (major) (#144)
* fix(deps): update go dependencies

* fix: fix renovate config

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

---------

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 11:28:56 +01:00
Dr. Carsten Leue
617e43de19 fix: improve codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 11:12:40 +01:00
61 changed files with 5785 additions and 307 deletions

View File

@@ -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
View File

@@ -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=

View File

@@ -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": [

View File

@@ -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

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -16,7 +16,7 @@
package cli
import (
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func Commands() []*C.Command {

View File

@@ -16,7 +16,7 @@
package cli
import (
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
const (

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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")
}

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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),
)
},
}

View File

@@ -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
)

View File

@@ -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=

54
v2/iterator/iter/last.go Normal file
View File

@@ -0,0 +1,54 @@
package iter
import (
"github.com/IBM/fp-go/v2/option"
)
// Last returns the last element from an [Iterator] wrapped in an [Option].
//
// This function retrieves the last element from the iterator by consuming the entire
// sequence. If the iterator contains at least one element, it returns Some(element).
// If the iterator is empty, it returns None.
//
// RxJS Equivalent: [last] - https://rxjs.dev/api/operators/last
//
// Type Parameters:
// - U: The type of elements in the iterator
//
// Parameters:
// - it: The input iterator to get the last element from
//
// Returns:
// - Option[U]: Some(last element) if the iterator is non-empty, None otherwise
//
// Example with non-empty sequence:
//
// seq := iter.From(1, 2, 3, 4, 5)
// last := iter.Last(seq)
// // Returns: Some(5)
//
// Example with empty sequence:
//
// seq := iter.Empty[int]()
// last := iter.Last(seq)
// // Returns: None
//
// Example with filtered sequence:
//
// seq := iter.From(1, 2, 3, 4, 5)
// filtered := iter.Filter(func(x int) bool { return x < 4 })(seq)
// last := iter.Last(filtered)
// // Returns: Some(3)
func Last[U any](it Seq[U]) Option[U] {
var last U
found := false
for last = range it {
found = true
}
if !found {
return option.None[U]()
}
return option.Some(last)
}

View File

@@ -0,0 +1,305 @@
package iter
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestLast test getting the last element from a non-empty sequence
func TestLastSimple(t *testing.T) {
t.Run("returns last element from integer sequence", func(t *testing.T) {
seq := From(1, 2, 3)
last := Last(seq)
assert.Equal(t, O.Of(3), last)
})
t.Run("returns last element from string sequence", func(t *testing.T) {
seq := From("a", "b", "c")
last := Last(seq)
assert.Equal(t, O.Of("c"), last)
})
t.Run("returns last element from single element sequence", func(t *testing.T) {
seq := From(42)
last := Last(seq)
assert.Equal(t, O.Of(42), last)
})
t.Run("returns last element from large sequence", func(t *testing.T) {
seq := From(100, 200, 300, 400, 500)
last := Last(seq)
assert.Equal(t, O.Of(500), last)
})
}
// TestLastEmpty tests getting the last element from an empty sequence
func TestLastEmpty(t *testing.T) {
t.Run("returns None for empty integer sequence", func(t *testing.T) {
seq := Empty[int]()
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
t.Run("returns None for empty string sequence", func(t *testing.T) {
seq := Empty[string]()
last := Last(seq)
assert.Equal(t, O.None[string](), last)
})
t.Run("returns None for empty struct sequence", func(t *testing.T) {
type TestStruct struct {
Value int
}
seq := Empty[TestStruct]()
last := Last(seq)
assert.Equal(t, O.None[TestStruct](), last)
})
t.Run("returns None for empty sequence of functions", func(t *testing.T) {
type TestFunc func(int)
seq := Empty[TestFunc]()
last := Last(seq)
assert.Equal(t, O.None[TestFunc](), last)
})
}
// TestLastWithComplex tests Last with complex types
func TestLastWithComplex(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("returns last person", func(t *testing.T) {
seq := From(
Person{"Alice", 30},
Person{"Bob", 25},
Person{"Charlie", 35},
)
last := Last(seq)
expected := O.Of(Person{"Charlie", 35})
assert.Equal(t, expected, last)
})
t.Run("returns last pointer", func(t *testing.T) {
p1 := &Person{"Alice", 30}
p2 := &Person{"Bob", 25}
seq := From(p1, p2)
last := Last(seq)
assert.Equal(t, O.Of(p2), last)
})
}
func TestLastWithFunctions(t *testing.T) {
t.Run("return function", func(t *testing.T) {
want := "last"
f1 := function.Constant("first")
f2 := function.Constant("last")
seq := From(f1, f2)
getLast := function.Flow2(
Last,
O.Map(funcReader),
)
assert.Equal(t, O.Of(want), getLast(seq))
})
}
func funcReader(f func() string) string {
return f()
}
// TestLastWithChan tests Last with channels
func TestLastWithChan(t *testing.T) {
t.Run("return function", func(t *testing.T) {
want := 30
seq := From(intChan(10),
intChan(20),
intChan(want))
getLast := function.Flow2(
Last,
O.Map(chanReader[int]),
)
assert.Equal(t, O.Of(want), getLast(seq))
})
}
func chanReader[T any](c <-chan T) T {
return <-c
}
func intChan(val int) <-chan int {
ch := make(chan int, 1)
ch <- val
close(ch)
return ch
}
// TestLastWithChainedOperations tests Last with multiple chained operations
func TestLastWithChainedOperations(t *testing.T) {
t.Run("chains filter, map, and last", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, N.MoreThan(5))
mapped := MonadMap(filtered, N.Mul(10))
result := Last(mapped)
assert.Equal(t, O.Of(100), result)
})
t.Run("chains map and filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
mapped := MonadMap(seq, N.Mul(2))
filtered := MonadFilter(mapped, N.MoreThan(5))
result := Last(filtered)
assert.Equal(t, O.Of(10), result)
})
}
// TestLastWithReplicate tests Last with replicated values
func TestLastWithReplicate(t *testing.T) {
t.Run("returns last from replicated sequence", func(t *testing.T) {
seq := Replicate(5, 42)
last := Last(seq)
assert.Equal(t, O.Of(42), last)
})
t.Run("returns None from zero replications", func(t *testing.T) {
seq := Replicate(0, 42)
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithMakeBy tests Last with MakeBy
func TestLastWithMakeBy(t *testing.T) {
t.Run("returns last generated element", func(t *testing.T) {
seq := MakeBy(5, func(i int) int { return i * i })
last := Last(seq)
assert.Equal(t, O.Of(16), last)
})
t.Run("returns None for zero elements", func(t *testing.T) {
seq := MakeBy(0, F.Identity[int])
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithPrepend tests Last with Prepend
func TestLastWithPrepend(t *testing.T) {
t.Run("returns last element, not prepended", func(t *testing.T) {
seq := From(2, 3, 4)
prepended := Prepend(1)(seq)
last := Last(prepended)
assert.Equal(t, O.Of(4), last)
})
t.Run("returns prepended element from empty sequence", func(t *testing.T) {
seq := Empty[int]()
prepended := Prepend(42)(seq)
last := Last(prepended)
assert.Equal(t, O.Of(42), last)
})
}
// TestLastWithAppend tests Last with Append
func TestLastWithAppend(t *testing.T) {
t.Run("returns appended element", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(4)(seq)
last := Last(appended)
assert.Equal(t, O.Of(4), last)
})
t.Run("returns appended element from empty sequence", func(t *testing.T) {
seq := Empty[int]()
appended := Append(42)(seq)
last := Last(appended)
assert.Equal(t, O.Of(42), last)
})
}
// TestLastWithChain tests Last with Chain (flatMap)
func TestLastWithChain(t *testing.T) {
t.Run("returns last from chained sequence", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return From(x, x*10)
})
last := Last(chained)
assert.Equal(t, O.Of(30), last)
})
t.Run("returns None when chain produces empty", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return Empty[int]()
})
last := Last(chained)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithFlatten tests Last with Flatten
func TestLastWithFlatten(t *testing.T) {
t.Run("returns last from flattened sequence", func(t *testing.T) {
nested := From(From(1, 2), From(3, 4), From(5))
flattened := Flatten(nested)
last := Last(flattened)
assert.Equal(t, O.Of(5), last)
})
t.Run("returns None from empty nested sequence", func(t *testing.T) {
nested := Empty[Seq[int]]()
flattened := Flatten(nested)
last := Last(flattened)
assert.Equal(t, O.None[int](), last)
})
}
// Example tests for documentation
func ExampleLast() {
seq := From(1, 2, 3, 4, 5)
last := Last(seq)
if value, ok := O.Unwrap(last); ok {
fmt.Printf("Last element: %d\n", value)
}
// Output: Last element: 5
}
func ExampleLast_empty() {
seq := Empty[int]()
last := Last(seq)
if _, ok := O.Unwrap(last); !ok {
fmt.Println("Sequence is empty")
}
// Output: Sequence is empty
}
func ExampleLast_functions() {
f1 := function.Constant("first")
f2 := function.Constant("middle")
f3 := function.Constant("last")
seq := From(f1, f2, f3)
last := Last(seq)
if fn, ok := O.Unwrap(last); ok {
result := fn()
fmt.Printf("Last function result: %s\n", result)
}
// Output: Last function result: last
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -55,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)
}
@@ -145,7 +145,7 @@ func validateFromIs[A, I any](
is ReaderResult[I, A],
msg string,
) Validate[I, A] {
return func(i I) Reader[Context, Validation[A]] {
return func(i I) Decode[Context, A] {
return F.Pipe2(
i,
is,
@@ -284,7 +284,7 @@ func validateArrayFromArray[T, O, I any](item Type[T, O, I]) Validate[[]I, []T]
zero := pair.Zero[validation.Errors, []T]()
return func(is []I) Reader[Context, Validation[[]T]] {
return func(is []I) Decode[Context, []T] {
return func(c Context) Validation[[]T] {
@@ -318,7 +318,7 @@ func validateArray[T, O any](item Type[T, O, any]) Validate[any, []T] {
zero := pair.Zero[validation.Errors, []T]()
return func(i any) Reader[Context, Validation[[]T]] {
return func(i any) Decode[Context, []T] {
res, ok := i.([]T)
if ok {
@@ -471,7 +471,7 @@ func validateEitherFromEither[L, R, OL, OR, IL, IR any](
// leftName := left.Name()
// rightName := right.Name()
return func(is either.Either[IL, IR]) Reader[Context, Validation[either.Either[L, R]]] {
return func(is either.Either[IL, IR]) Decode[Context, either.Either[L, R]] {
return either.MonadFold(
is,
@@ -570,7 +570,7 @@ func TranscodeEither[L, R, OL, OR, IL, IR any](leftItem Type[L, OL, IL], rightIt
)
}
func validateAlways[T any](is T) Reader[Context, Validation[T]] {
func validateAlways[T any](is T) Decode[Context, T] {
return reader.Of[Context](validation.Success(is))
}
@@ -633,7 +633,7 @@ func Id[T any]() Type[T, T, T] {
func validateFromRefinement[A, B any](refinement Refinement[A, B]) Validate[A, B] {
return func(a A) Reader[Context, Validation[B]] {
return func(a A) Decode[Context, B] {
return func(ctx Context) Validation[B] {
return F.Pipe2(

View File

@@ -465,7 +465,7 @@ func TestTranscodeArrayWithTransformation(t *testing.T) {
// Simple conversion: length of string
return either.Of[error](len(s))
},
func(s string) Reader[Context, Validation[int]] {
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
// Transform string to its length
return validation.Success(len(s))
@@ -520,7 +520,7 @@ func TestTranscodeArrayValidation(t *testing.T) {
}
return either.Of[error](i)
},
func(i int) Reader[Context, Validation[int]] {
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
if i <= 0 {
return validation.FailureWithMessage[int](i, "must be positive")(c)
@@ -721,7 +721,7 @@ func TestTranscodeEitherWithTransformation(t *testing.T) {
}
return either.Of[error](len(s))
},
func(s string) Reader[Context, Validation[int]] {
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(len(s))
}
@@ -741,7 +741,7 @@ func TestTranscodeEitherWithTransformation(t *testing.T) {
}
return either.Of[error](i * 2)
},
func(i int) Reader[Context, Validation[int]] {
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(i * 2)
}
@@ -944,7 +944,7 @@ func TestTypeToPrism(t *testing.T) {
}
return either.Of[error](s)
},
func(s string) Reader[Context, Validation[string]] {
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
return validation.Success(s)
}
@@ -1001,7 +1001,7 @@ func TestTypeToPrismWithValidation(t *testing.T) {
}
return either.Of[error](i)
},
func(i int) Reader[Context, Validation[int]] {
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
if i <= 0 {
return validation.FailureWithMessage[int](i, "must be positive")(c)
@@ -1053,7 +1053,7 @@ func TestTypeToPrismWithArrays(t *testing.T) {
}
return either.Of[error](i)
},
func(i int) Reader[Context, Validation[int]] {
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(i)
}
@@ -1093,7 +1093,7 @@ func TestTypeToPrismWithEither(t *testing.T) {
}
return either.Of[error](s)
},
func(s string) Reader[Context, Validation[string]] {
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
return validation.Success(s)
}
@@ -1110,7 +1110,7 @@ func TestTypeToPrismWithEither(t *testing.T) {
}
return either.Of[error](i)
},
func(i int) Reader[Context, Validation[int]] {
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(i)
}
@@ -1158,7 +1158,7 @@ func TestTypeToPrismComposition(t *testing.T) {
}
return either.Of[error](s)
},
func(s string) Reader[Context, Validation[string]] {
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
return validation.Success(s)
}
@@ -1193,7 +1193,7 @@ func TestTypeToPrismIntegration(t *testing.T) {
}
return either.Of[error](i)
},
func(i int) Reader[Context, Validation[int]] {
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(i)
}

View 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,
)
}

View 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))
})
}

View 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
View 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
}

View 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)
}

View 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())
})
}
}

View File

@@ -2,7 +2,10 @@ package codec
import (
"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"
@@ -15,6 +18,10 @@ 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]
@@ -48,11 +55,11 @@ type (
// 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]
@@ -60,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]
}
@@ -73,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]

View 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,
)
}

View 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)
}
}

View 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]
)

View 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,
)
}

View 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)
}
})
}

View File

@@ -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]() ReaderResult[any, T] {
var zero T
return result.ToType[T](onTypeError(fmt.Sprintf("%T", zero)))
return result.ToType[T](onTypeError(formatting.TypeInfo(*new(T))))
}

View File

@@ -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.

View File

@@ -8,6 +8,7 @@ import (
)
type (
// Result represents a computation that may succeed with a value of type A or fail with an error.
Result[A any] = result.Result[A]
// Either represents a value that can be one of two types: Left (error) or Right (success).
@@ -36,9 +37,11 @@ type (
// Errors is a collection of validation errors.
Errors = []*ValidationError
ValidationErrors struct {
Errors Errors
Cause error
// validationErrors wraps a collection of validation errors with an optional root cause.
// It provides structured error information for validation failures.
validationErrors struct {
errors Errors
cause error
}
// Validation represents the result of a validation operation.
@@ -48,9 +51,14 @@ type (
// Reader represents a computation that depends on an environment R and produces a value A.
Reader[R, A any] = reader.Reader[R, A]
// Kleisli represents a function from A to a validated B.
// It's a Reader that takes an input A and produces a Validation[B].
Kleisli[A, B any] = Reader[A, Validation[B]]
// Operator represents a validation transformation that takes a validated A and produces a validated B.
// It's a specialized Kleisli arrow for composing validation operations.
Operator[A, B any] = Kleisli[Validation[A], B]
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
Monoid[A any] = monoid.Monoid[A]
)

View File

@@ -2,6 +2,7 @@ package validation
import (
"fmt"
"log/slog"
A "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/either"
@@ -73,38 +74,80 @@ func (v *ValidationError) Format(s fmt.State, verb rune) {
fmt.Fprint(s, result)
}
// LogValue implements the slog.LogValuer interface for ValidationError.
// It provides structured logging representation of the validation error.
// Returns a slog.Value containing the error details as a group with
// message, value, context path, and optional cause.
//
// This method is called automatically when logging a ValidationError with slog.
//
// Example:
//
// err := &ValidationError{Value: "abc", Messsage: "expected number"}
// slog.Error("validation failed", "error", err)
// // Logs: error={message="expected number" value="abc"}
func (v *ValidationError) LogValue() slog.Value {
attrs := []slog.Attr{
slog.String("message", v.Messsage),
slog.Any("value", v.Value),
}
// Add context path if available
if len(v.Context) > 0 {
path := ""
for i, entry := range v.Context {
if i > 0 {
path += "."
}
if entry.Key != "" {
path += entry.Key
} else {
path += entry.Type
}
}
attrs = append(attrs, slog.String("path", path))
}
// Add cause if present
if v.Cause != nil {
attrs = append(attrs, slog.Any("cause", v.Cause))
}
return slog.GroupValue(attrs...)
}
// Error implements the error interface for ValidationErrors.
// Returns a generic error message indicating validation errors occurred.
func (ve *ValidationErrors) Error() string {
if len(ve.Errors) == 0 {
func (ve *validationErrors) Error() string {
if len(ve.errors) == 0 {
return "ValidationErrors: no errors"
}
if len(ve.Errors) == 1 {
if len(ve.errors) == 1 {
return "ValidationErrors: 1 error"
}
return fmt.Sprintf("ValidationErrors: %d errors", len(ve.Errors))
return fmt.Sprintf("ValidationErrors: %d errors", len(ve.errors))
}
// Unwrap returns the underlying cause error if present.
// This allows ValidationErrors to work with errors.Is and errors.As.
func (ve *ValidationErrors) Unwrap() error {
return ve.Cause
func (ve *validationErrors) Unwrap() error {
return ve.cause
}
// String returns a simple string representation of all validation errors.
// Each error is listed on a separate line with its index.
func (ve *ValidationErrors) String() string {
if len(ve.Errors) == 0 {
func (ve *validationErrors) String() string {
if len(ve.errors) == 0 {
return "ValidationErrors: no errors"
}
result := fmt.Sprintf("ValidationErrors (%d):\n", len(ve.Errors))
for i, err := range ve.Errors {
result := fmt.Sprintf("ValidationErrors (%d):\n", len(ve.errors))
for i, err := range ve.errors {
result += fmt.Sprintf(" [%d] %s\n", i, err.String())
}
if ve.Cause != nil {
result += fmt.Sprintf(" caused by: %v\n", ve.Cause)
if ve.cause != nil {
result += fmt.Sprintf(" caused by: %v\n", ve.cause)
}
return result
@@ -114,37 +157,70 @@ func (ve *ValidationErrors) String() string {
// Supports verbs: %s, %v, %+v (with additional details)
// %s and %v: compact format with error count
// %+v: verbose format with all error details
func (ve *ValidationErrors) Format(s fmt.State, verb rune) {
if len(ve.Errors) == 0 {
func (ve *validationErrors) Format(s fmt.State, verb rune) {
if len(ve.errors) == 0 {
fmt.Fprint(s, "ValidationErrors: no errors")
return
}
// For simple format, just show the count
if verb == 's' || (verb == 'v' && !s.Flag('+')) {
if len(ve.Errors) == 1 {
if len(ve.errors) == 1 {
fmt.Fprint(s, "ValidationErrors: 1 error")
} else {
fmt.Fprintf(s, "ValidationErrors: %d errors", len(ve.Errors))
fmt.Fprintf(s, "ValidationErrors: %d errors", len(ve.errors))
}
return
}
// Verbose format with all details
if s.Flag('+') && verb == 'v' {
fmt.Fprintf(s, "ValidationErrors (%d):\n", len(ve.Errors))
for i, err := range ve.Errors {
fmt.Fprintf(s, "ValidationErrors (%d):\n", len(ve.errors))
for i, err := range ve.errors {
fmt.Fprintf(s, " [%d] ", i)
err.Format(s, verb)
fmt.Fprint(s, "\n")
}
if ve.Cause != nil {
fmt.Fprintf(s, " root cause: %+v\n", ve.Cause)
if ve.cause != nil {
fmt.Fprintf(s, " root cause: %+v\n", ve.cause)
}
}
}
// LogValue implements the slog.LogValuer interface for ValidationErrors.
// It provides structured logging representation of multiple validation errors.
// Returns a slog.Value containing the error count and individual errors as a group.
//
// This method is called automatically when logging ValidationErrors with slog.
//
// Example:
//
// errors := &ValidationErrors{Errors: []*ValidationError{{Messsage: "error1"}, {Messsage: "error2"}}}
// slog.Error("validation failed", "errors", errors)
// // Logs: errors={count=2 errors=[...]}
func (ve *validationErrors) LogValue() slog.Value {
attrs := []slog.Attr{
slog.Int("count", len(ve.errors)),
}
// Add individual errors as a group
if len(ve.errors) > 0 {
errorAttrs := make([]slog.Attr, len(ve.errors))
for i, err := range ve.errors {
errorAttrs[i] = slog.Any(fmt.Sprintf("error_%d", i), err)
}
attrs = append(attrs, slog.Any("errors", slog.GroupValue(errorAttrs...)))
}
// Add cause if present
if ve.cause != nil {
attrs = append(attrs, slog.Any("cause", ve.cause))
}
return slog.GroupValue(attrs...)
}
// Failures creates a validation failure from a collection of errors.
// Returns a Left Either containing the errors.
func Failures[T any](err Errors) Validation[T] {
@@ -215,7 +291,7 @@ func Success[T any](value T) Validation[T] {
// err := MakeValidationErrors(errors)
// fmt.Println(err) // Output: ValidationErrors: 2 errors
func MakeValidationErrors(errors Errors) error {
return &ValidationErrors{Errors: errors}
return &validationErrors{errors: errors}
}
// ToResult converts a Validation[T] to a Result[T].

View File

@@ -3,6 +3,7 @@ package validation
import (
"errors"
"fmt"
"log/slog"
"testing"
"github.com/IBM/fp-go/v2/either"
@@ -430,10 +431,10 @@ func TestMakeValidationErrors(t *testing.T) {
assert.Equal(t, "ValidationErrors: 1 error", err.Error())
// Verify it's a ValidationErrors type
ve, ok := err.(*ValidationErrors)
ve, ok := err.(*validationErrors)
require.True(t, ok)
assert.Len(t, ve.Errors, 1)
assert.Equal(t, "invalid value", ve.Errors[0].Messsage)
assert.Len(t, ve.errors, 1)
assert.Equal(t, "invalid value", ve.errors[0].Messsage)
})
t.Run("creates error from multiple validation errors", func(t *testing.T) {
@@ -448,9 +449,9 @@ func TestMakeValidationErrors(t *testing.T) {
require.NotNil(t, err)
assert.Equal(t, "ValidationErrors: 3 errors", err.Error())
ve, ok := err.(*ValidationErrors)
ve, ok := err.(*validationErrors)
require.True(t, ok)
assert.Len(t, ve.Errors, 3)
assert.Len(t, ve.errors, 3)
})
t.Run("creates error from empty errors slice", func(t *testing.T) {
@@ -461,9 +462,9 @@ func TestMakeValidationErrors(t *testing.T) {
require.NotNil(t, err)
assert.Equal(t, "ValidationErrors: no errors", err.Error())
ve, ok := err.(*ValidationErrors)
ve, ok := err.(*validationErrors)
require.True(t, ok)
assert.Len(t, ve.Errors, 0)
assert.Len(t, ve.errors, 0)
})
t.Run("preserves error details", func(t *testing.T) {
@@ -479,13 +480,13 @@ func TestMakeValidationErrors(t *testing.T) {
err := MakeValidationErrors(errs)
ve, ok := err.(*ValidationErrors)
ve, ok := err.(*validationErrors)
require.True(t, ok)
require.Len(t, ve.Errors, 1)
assert.Equal(t, "abc", ve.Errors[0].Value)
assert.Equal(t, "invalid format", ve.Errors[0].Messsage)
assert.Equal(t, cause, ve.Errors[0].Cause)
assert.Len(t, ve.Errors[0].Context, 1)
require.Len(t, ve.errors, 1)
assert.Equal(t, "abc", ve.errors[0].Value)
assert.Equal(t, "invalid format", ve.errors[0].Messsage)
assert.Equal(t, cause, ve.errors[0].Cause)
assert.Len(t, ve.errors[0].Context, 1)
})
t.Run("error can be formatted", func(t *testing.T) {
@@ -536,10 +537,10 @@ func TestToResult(t *testing.T) {
assert.Equal(t, "ValidationErrors: 1 error", err.Error())
// Verify it's a ValidationErrors type
ve, ok := err.(*ValidationErrors)
ve, ok := err.(*validationErrors)
require.True(t, ok)
assert.Len(t, ve.Errors, 1)
assert.Equal(t, "expected number", ve.Errors[0].Messsage)
assert.Len(t, ve.errors, 1)
assert.Equal(t, "expected number", ve.errors[0].Messsage)
})
t.Run("converts multiple validation errors to result", func(t *testing.T) {
@@ -559,9 +560,9 @@ func TestToResult(t *testing.T) {
require.NotNil(t, err)
assert.Equal(t, "ValidationErrors: 2 errors", err.Error())
ve, ok := err.(*ValidationErrors)
ve, ok := err.(*validationErrors)
require.True(t, ok)
assert.Len(t, ve.Errors, 2)
assert.Len(t, ve.errors, 2)
})
t.Run("works with different types", func(t *testing.T) {
@@ -625,10 +626,10 @@ func TestToResult(t *testing.T) {
F.Identity[error],
func(int) error { return nil },
)
ve, ok := err.(*ValidationErrors)
ve, ok := err.(*validationErrors)
require.True(t, ok)
require.Len(t, ve.Errors, 1)
assert.True(t, errors.Is(ve.Errors[0], cause))
require.Len(t, ve.errors, 1)
assert.True(t, errors.Is(ve.errors[0], cause))
})
t.Run("result error implements error interface", func(t *testing.T) {
@@ -650,3 +651,213 @@ func TestToResult(t *testing.T) {
assert.Contains(t, stdErr.Error(), "ValidationErrors")
})
}
// TestValidationError_LogValue tests the LogValue() method implementation
func TestValidationError_LogValue(t *testing.T) {
t.Run("simple error without context", func(t *testing.T) {
err := &ValidationError{
Value: "test",
Messsage: "invalid value",
}
logValue := err.LogValue()
assert.Equal(t, slog.KindGroup, logValue.Kind())
attrs := logValue.Group()
assert.GreaterOrEqual(t, len(attrs), 2)
attrMap := make(map[string]string)
for _, attr := range attrs {
attrMap[attr.Key] = attr.Value.String()
}
assert.Equal(t, "invalid value", attrMap["message"])
assert.Contains(t, attrMap["value"], "test")
})
t.Run("error with context path", func(t *testing.T) {
err := &ValidationError{
Value: "test",
Context: []ContextEntry{{Key: "user"}, {Key: "name"}},
Messsage: "must not be empty",
}
logValue := err.LogValue()
attrs := logValue.Group()
attrMap := make(map[string]string)
for _, attr := range attrs {
attrMap[attr.Key] = attr.Value.String()
}
assert.Equal(t, "must not be empty", attrMap["message"])
assert.Equal(t, "user.name", attrMap["path"])
})
t.Run("error with cause", func(t *testing.T) {
cause := errors.New("parse error")
err := &ValidationError{
Value: "abc",
Messsage: "invalid number",
Cause: cause,
}
logValue := err.LogValue()
attrs := logValue.Group()
attrMap := make(map[string]any)
for _, attr := range attrs {
attrMap[attr.Key] = attr.Value.Any()
}
assert.Equal(t, "invalid number", attrMap["message"])
assert.NotNil(t, attrMap["cause"])
})
t.Run("error with context using type", func(t *testing.T) {
err := &ValidationError{
Value: 123,
Context: []ContextEntry{{Type: "User"}, {Key: "age"}},
Messsage: "must be positive",
}
logValue := err.LogValue()
attrs := logValue.Group()
attrMap := make(map[string]string)
for _, attr := range attrs {
attrMap[attr.Key] = attr.Value.String()
}
assert.Equal(t, "User.age", attrMap["path"])
})
t.Run("complex context path", func(t *testing.T) {
err := &ValidationError{
Value: "invalid",
Context: []ContextEntry{
{Key: "user"},
{Key: "address"},
{Key: "zipCode"},
},
Messsage: "invalid format",
}
logValue := err.LogValue()
attrs := logValue.Group()
attrMap := make(map[string]string)
for _, attr := range attrs {
attrMap[attr.Key] = attr.Value.String()
}
assert.Equal(t, "user.address.zipCode", attrMap["path"])
})
}
// TestValidationErrors_LogValue tests the LogValue() method implementation
func TestValidationErrors_LogValue(t *testing.T) {
t.Run("empty errors", func(t *testing.T) {
ve := &validationErrors{errors: Errors{}}
logValue := ve.LogValue()
assert.Equal(t, slog.KindGroup, logValue.Kind())
attrs := logValue.Group()
attrMap := make(map[string]any)
for _, attr := range attrs {
attrMap[attr.Key] = attr.Value.Any()
}
assert.Equal(t, int64(0), attrMap["count"])
})
t.Run("single error", func(t *testing.T) {
ve := &validationErrors{
errors: Errors{
&ValidationError{Value: "test", Messsage: "error 1"},
},
}
logValue := ve.LogValue()
attrs := logValue.Group()
attrMap := make(map[string]any)
for _, attr := range attrs {
attrMap[attr.Key] = attr.Value.Any()
}
assert.Equal(t, int64(1), attrMap["count"])
assert.NotNil(t, attrMap["errors"])
})
t.Run("multiple errors", func(t *testing.T) {
ve := &validationErrors{
errors: Errors{
&ValidationError{Value: "test1", Messsage: "error 1"},
&ValidationError{Value: "test2", Messsage: "error 2"},
&ValidationError{Value: "test3", Messsage: "error 3"},
},
}
logValue := ve.LogValue()
attrs := logValue.Group()
attrMap := make(map[string]any)
for _, attr := range attrs {
attrMap[attr.Key] = attr.Value.Any()
}
assert.Equal(t, int64(3), attrMap["count"])
assert.NotNil(t, attrMap["errors"])
})
t.Run("with cause", func(t *testing.T) {
cause := errors.New("underlying error")
ve := &validationErrors{
errors: Errors{
&ValidationError{Value: "test", Messsage: "error"},
},
cause: cause,
}
logValue := ve.LogValue()
attrs := logValue.Group()
attrMap := make(map[string]any)
for _, attr := range attrs {
attrMap[attr.Key] = attr.Value.Any()
}
assert.NotNil(t, attrMap["cause"])
})
t.Run("preserves error details", func(t *testing.T) {
ve := &validationErrors{
errors: Errors{
&ValidationError{
Value: "abc",
Context: []ContextEntry{{Key: "field"}},
Messsage: "invalid format",
},
},
}
logValue := ve.LogValue()
assert.Equal(t, slog.KindGroup, logValue.Kind())
attrs := logValue.Group()
assert.GreaterOrEqual(t, len(attrs), 2)
})
}
// TestLogValuerInterface verifies that ValidationError and ValidationErrors implement slog.LogValuer
func TestLogValuerInterface(t *testing.T) {
t.Run("ValidationError implements slog.LogValuer", func(t *testing.T) {
var _ slog.LogValuer = (*ValidationError)(nil)
})
t.Run("ValidationErrors implements slog.LogValuer", func(t *testing.T) {
var _ slog.LogValuer = (*validationErrors)(nil)
})
}

View File

@@ -0,0 +1,370 @@
package codec
import (
"testing"
"github.com/IBM/fp-go/v2/either"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestIsWithPrimitiveTypes tests the Is function with primitive types
func TestIsWithPrimitiveTypes(t *testing.T) {
t.Run("string type succeeds with string value", func(t *testing.T) {
isString := Is[string]()
res := isString("hello")
assert.Equal(t, R.Of("hello"), res)
})
t.Run("string type fails with int value", func(t *testing.T) {
isString := Is[string]()
res := isString(42)
assert.True(t, either.IsLeft(res), "Expected Left for invalid type")
})
t.Run("int type succeeds with int value", func(t *testing.T) {
isInt := Is[int]()
res := isInt(42)
assert.Equal(t, R.Of(42), res)
})
t.Run("int type fails with string value", func(t *testing.T) {
isInt := Is[int]()
res := isInt("42")
assert.True(t, either.IsLeft(res))
})
t.Run("bool type succeeds with bool value", func(t *testing.T) {
isBool := Is[bool]()
res := isBool(true)
assert.Equal(t, R.Of(true), res)
})
t.Run("bool type fails with int value", func(t *testing.T) {
isBool := Is[bool]()
res := isBool(1)
assert.True(t, either.IsLeft(res))
})
t.Run("float64 type succeeds with float64 value", func(t *testing.T) {
isFloat := Is[float64]()
res := isFloat(3.14)
assert.Equal(t, R.Of(3.14), res)
})
t.Run("float64 type fails with int value", func(t *testing.T) {
isFloat := Is[float64]()
res := isFloat(42)
assert.True(t, either.IsLeft(res))
})
}
// TestIsWithNumericTypes tests Is with different numeric types
func TestIsWithNumericTypes(t *testing.T) {
t.Run("int8 type", func(t *testing.T) {
isInt8 := Is[int8]()
res := isInt8(int8(127))
assert.Equal(t, R.Of(int8(127)), res)
// Fails with regular int
res = isInt8(127)
assert.True(t, either.IsLeft(res))
})
t.Run("int16 type", func(t *testing.T) {
isInt16 := Is[int16]()
res := isInt16(int16(32767))
assert.Equal(t, R.Of(int16(32767)), res)
})
t.Run("int32 type", func(t *testing.T) {
isInt32 := Is[int32]()
res := isInt32(int32(2147483647))
assert.Equal(t, R.Of(int32(2147483647)), res)
})
t.Run("int64 type", func(t *testing.T) {
isInt64 := Is[int64]()
res := isInt64(int64(9223372036854775807))
assert.Equal(t, R.Of(int64(9223372036854775807)), res)
})
t.Run("uint type", func(t *testing.T) {
isUint := Is[uint]()
res := isUint(uint(42))
assert.Equal(t, R.Of(uint(42)), res)
// Fails with int
res = isUint(42)
assert.True(t, either.IsLeft(res))
})
t.Run("float32 type", func(t *testing.T) {
isFloat32 := Is[float32]()
res := isFloat32(float32(3.14))
assert.Equal(t, R.Of(float32(3.14)), res)
// Fails with float64
res = isFloat32(3.14)
assert.True(t, either.IsLeft(res))
})
}
// TestIsWithComplexTypes tests Is with complex and composite types
func TestIsWithComplexTypes(t *testing.T) {
t.Run("slice type succeeds with slice", func(t *testing.T) {
isSlice := Is[[]int]()
res := isSlice([]int{1, 2, 3})
assert.Equal(t, R.Of([]int{1, 2, 3}), res)
})
t.Run("slice type fails with array", func(t *testing.T) {
isSlice := Is[[]int]()
res := isSlice([3]int{1, 2, 3})
assert.True(t, either.IsLeft(res))
})
t.Run("map type succeeds with map", func(t *testing.T) {
isMap := Is[map[string]int]()
testMap := map[string]int{"a": 1, "b": 2}
res := isMap(testMap)
assert.Equal(t, R.Of(testMap), res)
})
t.Run("map type fails with wrong key type", func(t *testing.T) {
isMap := Is[map[string]int]()
wrongMap := map[int]int{1: 1, 2: 2}
res := isMap(wrongMap)
assert.True(t, either.IsLeft(res))
})
t.Run("array type succeeds with array", func(t *testing.T) {
isArray := Is[[3]int]()
res := isArray([3]int{1, 2, 3})
assert.Equal(t, R.Of([3]int{1, 2, 3}), res)
})
t.Run("array type fails with different size", func(t *testing.T) {
isArray := Is[[3]int]()
res := isArray([4]int{1, 2, 3, 4})
assert.True(t, either.IsLeft(res))
})
}
// TestIsWithStructTypes tests Is with struct types
func TestIsWithStructTypes(t *testing.T) {
type Person struct {
Name string
Age int
}
type Employee struct {
Name string
Salary float64
}
t.Run("struct type succeeds with matching struct", func(t *testing.T) {
isPerson := Is[Person]()
person := Person{Name: "Alice", Age: 30}
res := isPerson(person)
assert.Equal(t, R.Of(person), res)
})
t.Run("struct type fails with different struct", func(t *testing.T) {
isPerson := Is[Person]()
employee := Employee{Name: "Bob", Salary: 50000}
res := isPerson(employee)
assert.True(t, either.IsLeft(res))
})
t.Run("struct type fails with primitive", func(t *testing.T) {
isPerson := Is[Person]()
res := isPerson("not a person")
assert.True(t, either.IsLeft(res))
})
}
// TestIsWithPointerTypes tests Is with pointer types
func TestIsWithPointerTypes(t *testing.T) {
t.Run("pointer type succeeds with pointer", func(t *testing.T) {
isStringPtr := Is[*string]()
str := "hello"
res := isStringPtr(&str)
assert.Equal(t, R.Of(&str), res)
})
t.Run("pointer type fails with non-pointer", func(t *testing.T) {
isStringPtr := Is[*string]()
res := isStringPtr("hello")
assert.True(t, either.IsLeft(res))
})
t.Run("pointer type succeeds with nil pointer", func(t *testing.T) {
isStringPtr := Is[*string]()
var nilPtr *string = nil
res := isStringPtr(nilPtr)
assert.Equal(t, R.Of(nilPtr), res)
})
t.Run("non-pointer type fails with pointer", func(t *testing.T) {
isString := Is[string]()
str := "hello"
res := isString(&str)
assert.True(t, either.IsLeft(res))
})
}
// TestIsWithEmptyValues tests Is with empty/zero values
func TestIsWithEmptyValues(t *testing.T) {
t.Run("empty string", func(t *testing.T) {
isString := Is[string]()
res := isString("")
assert.Equal(t, R.Of(""), res)
})
t.Run("zero int", func(t *testing.T) {
isInt := Is[int]()
res := isInt(0)
assert.Equal(t, R.Of(0), res)
})
t.Run("false bool", func(t *testing.T) {
isBool := Is[bool]()
res := isBool(false)
assert.Equal(t, R.Of(false), res)
})
t.Run("nil slice", func(t *testing.T) {
isSlice := Is[[]int]()
var nilSlice []int = nil
res := isSlice(nilSlice)
assert.Equal(t, R.Of(nilSlice), res)
})
t.Run("empty slice", func(t *testing.T) {
isSlice := Is[[]int]()
emptySlice := []int{}
res := isSlice(emptySlice)
assert.Equal(t, R.Of(emptySlice), res)
})
t.Run("nil map", func(t *testing.T) {
isMap := Is[map[string]int]()
var nilMap map[string]int = nil
res := isMap(nilMap)
assert.Equal(t, R.Of(nilMap), res)
})
}
// TestIsWithChannelTypes tests Is with channel types
func TestIsWithChannelTypes(t *testing.T) {
t.Run("channel type succeeds with channel", func(t *testing.T) {
isChan := Is[chan int]()
ch := make(chan int)
defer close(ch)
res := isChan(ch)
assert.Equal(t, R.Of(ch), res)
})
t.Run("channel type fails with wrong channel type", func(t *testing.T) {
isChan := Is[chan int]()
ch := make(chan string)
defer close(ch)
res := isChan(ch)
assert.True(t, either.IsLeft(res))
})
t.Run("bidirectional vs unidirectional channels", func(t *testing.T) {
isSendChan := Is[chan<- int]()
ch := make(chan int)
defer close(ch)
// Bidirectional channel can be used as send-only
sendCh := chan<- int(ch)
res := isSendChan(sendCh)
assert.Equal(t, R.Of(sendCh), res)
})
}
// TestIsWithFunctionTypes tests Is with function types
func TestIsWithFunctionTypes(t *testing.T) {
t.Run("function type succeeds with matching function", func(t *testing.T) {
isFunc := Is[func(int) int]()
fn := func(x int) int { return x * 2 }
res := isFunc(fn)
// Functions can't be compared for equality, so just check it's Right
assert.True(t, either.IsRight(res))
})
t.Run("function type fails with different signature", func(t *testing.T) {
isFunc := Is[func(int) int]()
fn := func(x string) string { return x }
res := isFunc(fn)
assert.True(t, either.IsLeft(res))
})
t.Run("function type fails with non-function", func(t *testing.T) {
isFunc := Is[func(int) int]()
res := isFunc(42)
assert.True(t, either.IsLeft(res))
})
}
// TestIsErrorMessages tests that Is produces appropriate error messages
func TestIsErrorMessages(t *testing.T) {
t.Run("error message for type mismatch", func(t *testing.T) {
isString := Is[string]()
res := isString(42)
assert.True(t, either.IsLeft(res), "Expected Left for type mismatch")
})
t.Run("error for struct type mismatch", func(t *testing.T) {
type CustomType struct {
Field string
}
isCustom := Is[CustomType]()
res := isCustom("not a custom type")
assert.True(t, either.IsLeft(res), "Expected Left for struct type mismatch")
})
}

View File

@@ -134,7 +134,7 @@ func TestComposeBasicFunctionality(t *testing.T) {
pgPrism := postgresqlPrism()
// Compose connection lens with PostgreSQL prism
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := Compose[Config](pgPrism)(connLens)
config := Config{
Connection: PostgreSQL{Host: "localhost", Port: 5432},
@@ -153,7 +153,7 @@ func TestComposeBasicFunctionality(t *testing.T) {
connLens := connectionLens()
pgPrism := postgresqlPrism()
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := Compose[Config](pgPrism)(connLens)
config := Config{
Connection: MySQL{Host: "localhost", Port: 3306},
@@ -168,7 +168,7 @@ func TestComposeBasicFunctionality(t *testing.T) {
connLens := connectionLens()
pgPrism := postgresqlPrism()
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := Compose[Config](pgPrism)(connLens)
config := Config{
Connection: PostgreSQL{Host: "localhost", Port: 5432},
@@ -194,7 +194,7 @@ func TestComposeBasicFunctionality(t *testing.T) {
connLens := connectionLens()
pgPrism := postgresqlPrism()
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := Compose[Config](pgPrism)(connLens)
config := Config{
Connection: MySQL{Host: "localhost", Port: 3306},
@@ -222,7 +222,7 @@ func TestComposeBasicFunctionality(t *testing.T) {
func TestComposeOptionalLaws(t *testing.T) {
connLens := connectionLens()
pgPrism := postgresqlPrism()
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := Compose[Config](pgPrism)(connLens)
t.Run("SetGet Law: GetOption(Set(b)(s)) = Some(b) when GetOption(s) = Some(_)", func(t *testing.T) {
// Start with a config that has PostgreSQL
@@ -301,7 +301,7 @@ func TestComposeMultipleVariants(t *testing.T) {
t.Run("PostgreSQL variant", func(t *testing.T) {
pgPrism := postgresqlPrism()
optional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
optional := Compose[Config](pgPrism)(connLens)
config := Config{
Connection: PostgreSQL{Host: "pg.example.com", Port: 5432},
@@ -313,7 +313,7 @@ func TestComposeMultipleVariants(t *testing.T) {
t.Run("MySQL variant", func(t *testing.T) {
myPrism := mysqlPrism()
optional := Compose[Config, ConnectionType, MySQL](myPrism)(connLens)
optional := Compose[Config](myPrism)(connLens)
config := Config{
Connection: MySQL{Host: "mysql.example.com", Port: 3306},
@@ -325,7 +325,7 @@ func TestComposeMultipleVariants(t *testing.T) {
t.Run("MongoDB variant", func(t *testing.T) {
mgPrism := mongodbPrism()
optional := Compose[Config, ConnectionType, MongoDB](mgPrism)(connLens)
optional := Compose[Config](mgPrism)(connLens)
config := Config{
Connection: MongoDB{Host: "mongo.example.com", Port: 27017},
@@ -338,7 +338,7 @@ func TestComposeMultipleVariants(t *testing.T) {
t.Run("Cross-variant no-op", func(t *testing.T) {
// Try to use PostgreSQL optional on MySQL config
pgPrism := postgresqlPrism()
optional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
optional := Compose[Config](pgPrism)(connLens)
config := Config{
Connection: MySQL{Host: "mysql.example.com", Port: 3306},
@@ -365,7 +365,7 @@ func TestComposeEdgeCases(t *testing.T) {
)
pgPrism := postgresqlPrism()
optional := Compose[ConnectionType, ConnectionType, PostgreSQL](pgPrism)(idLens)
optional := Compose[ConnectionType](pgPrism)(idLens)
conn := ConnectionType(PostgreSQL{Host: "localhost", Port: 5432})
result := optional.GetOption(conn)
@@ -378,7 +378,7 @@ func TestComposeEdgeCases(t *testing.T) {
t.Run("Multiple sets preserve structure", func(t *testing.T) {
connLens := connectionLens()
pgPrism := postgresqlPrism()
optional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
optional := Compose[Config](pgPrism)(connLens)
config := Config{
Connection: PostgreSQL{Host: "host1", Port: 5432},
@@ -431,7 +431,7 @@ func TestComposeDocumentationExample(t *testing.T) {
)
// Compose to create Optional[Config, PostgreSQL]
configPgOptional := Compose[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := Compose[Config](pgPrism)(connLens)
config := Config{Connection: PostgreSQL{Host: "localhost"}}
host := configPgOptional.GetOption(config) // Some(PostgreSQL{Host: "localhost"})
@@ -459,7 +459,7 @@ func TestComposeRefBasicFunctionality(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := ComposeRef[Config](pgPrism)(connLens)
config := &Config{
Connection: PostgreSQL{Host: "localhost", Port: 5432},
@@ -478,7 +478,7 @@ func TestComposeRefBasicFunctionality(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := ComposeRef[Config](pgPrism)(connLens)
var config *Config = nil
@@ -490,7 +490,7 @@ func TestComposeRefBasicFunctionality(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := ComposeRef[Config](pgPrism)(connLens)
config := &Config{
Connection: MySQL{Host: "localhost", Port: 3306},
@@ -505,7 +505,7 @@ func TestComposeRefBasicFunctionality(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := ComposeRef[Config](pgPrism)(connLens)
original := &Config{
Connection: PostgreSQL{Host: "localhost", Port: 5432},
@@ -541,7 +541,7 @@ func TestComposeRefBasicFunctionality(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := ComposeRef[Config](pgPrism)(connLens)
var config *Config = nil
@@ -558,7 +558,7 @@ func TestComposeRefBasicFunctionality(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := ComposeRef[Config](pgPrism)(connLens)
original := &Config{
Connection: MySQL{Host: "localhost", Port: 3306},
@@ -585,7 +585,7 @@ func TestComposeRefBasicFunctionality(t *testing.T) {
func TestComposeRefOptionalLaws(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := ComposeRef[Config](pgPrism)(connLens)
t.Run("SetGet Law: GetOption(Set(b)(s)) = Some(b) when GetOption(s) = Some(_)", func(t *testing.T) {
// Start with a config that has PostgreSQL
@@ -686,7 +686,7 @@ func TestComposeRefImmutability(t *testing.T) {
t.Run("Set creates a new pointer, doesn't modify original", func(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
optional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
optional := ComposeRef[Config](pgPrism)(connLens)
original := &Config{
Connection: PostgreSQL{Host: "original", Port: 5432},
@@ -731,7 +731,7 @@ func TestComposeRefImmutability(t *testing.T) {
t.Run("Multiple operations on nil preserve nil", func(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
optional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
optional := ComposeRef[Config](pgPrism)(connLens)
var config *Config = nil
@@ -756,7 +756,7 @@ func TestComposeRefNilPointerEdgeCases(t *testing.T) {
t.Run("GetOption on nil returns None", func(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
optional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
optional := ComposeRef[Config](pgPrism)(connLens)
var config *Config = nil
result := optional.GetOption(config)
@@ -767,7 +767,7 @@ func TestComposeRefNilPointerEdgeCases(t *testing.T) {
t.Run("Set on nil with matching prism returns nil", func(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
optional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
optional := ComposeRef[Config](pgPrism)(connLens)
var config *Config = nil
newPg := PostgreSQL{Host: "remote", Port: 5432}
@@ -781,7 +781,7 @@ func TestComposeRefNilPointerEdgeCases(t *testing.T) {
t.Run("Chaining operations starting from nil", func(t *testing.T) {
connLens := connectionLensRef()
pgPrism := postgresqlPrism()
optional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
optional := ComposeRef[Config](pgPrism)(connLens)
var config *Config = nil
@@ -818,7 +818,7 @@ func TestComposeRefDocumentationExample(t *testing.T) {
)
// Compose to create Optional[*Config, PostgreSQL]
configPgOptional := ComposeRef[Config, ConnectionType, PostgreSQL](pgPrism)(connLens)
configPgOptional := ComposeRef[Config](pgPrism)(connLens)
// Works with non-nil pointers
config := &Config{Connection: PostgreSQL{Host: "localhost"}}

View File

@@ -605,7 +605,7 @@ func TestOptionalComposition(t *testing.T) {
)
// Compose them
composed := Compose[Person, string, rune](firstCharOptional)(nameOptional)
composed := Compose[Person](firstCharOptional)(nameOptional)
person := Person{Name: "Alice", Age: 30}
result := composed.GetOption(person)
@@ -639,7 +639,7 @@ func TestOptionalNoOpBehavior(t *testing.T) {
assert.True(t, O.IsNone(initial))
// Try to modify - should return None
modifyResult := ModifyOption[Person, string](func(name string) string {
modifyResult := ModifyOption[Person](func(name string) string {
return "Bob"
})(optional)(person)
@@ -672,7 +672,7 @@ func TestOptionalNoOpBehavior(t *testing.T) {
assert.True(t, O.IsSome(initial))
// Modify should return Some with updated value
modifyResult := ModifyOption[Person, string](func(name string) string {
modifyResult := ModifyOption[Person](func(name string) string {
return name + " Smith"
})(optional)(person)
@@ -721,7 +721,7 @@ func TestOptionalNoOpBehaviorRef(t *testing.T) {
assert.True(t, O.IsNone(initial))
// Try to modify - should return None
modifyResult := ModifyOption[*Person, string](func(name string) string {
modifyResult := ModifyOption[*Person](func(name string) string {
return "Bob"
})(optional)(person)
@@ -736,7 +736,7 @@ func TestOptionalNoOpBehaviorRef(t *testing.T) {
assert.True(t, O.IsNone(initial))
// Try to modify - should return None
modifyResult := ModifyOption[*Person, string](func(name string) string {
modifyResult := ModifyOption[*Person](func(name string) string {
return "Bob"
})(optional)(person)
@@ -966,7 +966,7 @@ func TestSetOptionNoOpBehavior(t *testing.T) {
assert.True(t, O.IsNone(initial))
// SetOption should return None
result := SetOption[Person, string]("Bob")(optional)(person)
result := SetOption[Person]("Bob")(optional)(person)
assert.True(t, O.IsNone(result))
})
@@ -979,7 +979,7 @@ func TestSetOptionNoOpBehavior(t *testing.T) {
assert.True(t, O.IsSome(initial))
// SetOption should return Some with updated value
result := SetOption[Person, string]("Bob")(optional)(person)
result := SetOption[Person]("Bob")(optional)(person)
assert.True(t, O.IsSome(result))
updatedPerson := O.GetOrElse(F.Constant(person))(result)

View File

@@ -1,11 +1,25 @@
@echo off
setlocal enabledelayedexpansion
REM Get the directory to scan from parameter or use current directory
set "SCAN_DIR=%~1"
if "%SCAN_DIR%"=="" set "SCAN_DIR=."
REM Convert to absolute path
pushd "%SCAN_DIR%" 2>nul
if errorlevel 1 (
echo Error: Directory "%SCAN_DIR%" does not exist
exit /b 1
)
set "SCAN_DIR=%CD%"
popd
echo Finding and fixing unnecessary type arguments...
echo Scanning directory: %SCAN_DIR%
echo.
REM Find all Go files recursively
for /r %%f in (*.go) do (
REM Find all Go files recursively in the specified directory
for /r "%SCAN_DIR%" %%f in (*.go) do (
echo Checking %%f...
REM Run gopls check and capture output

View File

@@ -0,0 +1,235 @@
# 🏗️ Builder Pattern with fp-go
This package demonstrates a functional builder pattern using fp-go's optics library. It shows how to construct and validate objects using lenses, prisms, and codecs, separating the building phase from validation.
## 📋 Overview
The builder pattern here uses two key types:
- **`PartialPerson`** 🚧: An intermediate type with unvalidated fields (raw `string` and `int`)
- **`Person`** ✅: A validated type with refined fields (`NonEmptyString` and `AdultAge`)
The pattern provides two approaches for validation:
1. **Prism-based validation** 🔍 (simple, no error messages)
2. **Codec-based validation** 📝 (detailed error reporting)
## 🎯 Core Concepts
### 1. 🔧 Auto-Generated Lenses
The `fp-go:Lens` directive in `types.go` generates lens accessors for both types:
```go
// fp-go:Lens
type PartialPerson struct {
name string
age int
}
// fp-go:Lens
type Person struct {
Name NonEmptyString
Age AdultAge
}
```
This generates:
- `partialPersonLenses` with `.name` and `.age` lenses
- `personLenses` with `.Name` and `.Age` lenses
### 2. 🎁 Exporting Setters as `WithXXX` Methods
The lens setters are exported as builder methods:
```go
// WithName sets the Name field of a PartialPerson
WithName = partialPersonLenses.name.Set
// WithAge sets the Age field of a PartialPerson
WithAge = partialPersonLenses.age.Set
```
These return `Endomorphism[*PartialPerson]` functions that can be composed:
```go
builder := F.Pipe1(
A.From(
WithName("Alice"),
WithAge(25),
),
allOfPartialPerson,
)
partial := builder(&PartialPerson{})
```
Or use the convenience function:
```go
builder := MakePerson("Alice", 25)
```
## 🔍 Approach 1: Prism-Based Validation (No Error Messages)
### Creating Validation Prisms
Define prisms that validate individual fields:
> 💡 **Tip**: The `optics/prism` package provides many helpful out-of-the-box prisms for common validations, including:
> - `NonEmptyString()` - validates non-empty strings
> - `ParseInt()`, `ParseInt64()` - parses integers from strings
> - `ParseFloat32()`, `ParseFloat64()` - parses floats from strings
> - `ParseBool()` - parses booleans from strings
> - `ParseDate(layout)` - parses dates with custom layouts
> - `ParseURL()` - parses URLs
> - `FromZero()`, `FromNonZero()` - validates zero/non-zero values
> - `RegexMatcher()`, `RegexNamedMatcher()` - regex-based validation
> - `FromOption()`, `FromEither()`, `FromResult()` - extracts from monadic types
> - And many more! Check `optics/prism/prisms.go` for the full list.
>
> For custom validation logic, create your own prisms:
```go
namePrism = prism.MakePrismWithName(
func(s string) Option[NonEmptyString] {
if S.IsEmpty(s) {
return option.None[NonEmptyString]()
}
return option.Of(NonEmptyString(s))
},
func(ns NonEmptyString) string {
return string(ns)
},
"NonEmptyString",
)
agePrism = prism.MakePrismWithName(
func(a int) Option[AdultAge] {
if a < 18 {
return option.None[AdultAge]()
}
return option.Of(AdultAge(a))
},
func(aa AdultAge) int {
return int(aa)
},
"AdultAge",
)
```
### 🎭 Creating the PersonPrism
The `PersonPrism` converts between a builder and a validated `Person`:
```go
PersonPrism = prism.MakePrismWithName(
buildPerson(), // Forward: builder -> Option[*Person]
buildEndomorphism(), // Reverse: *Person -> builder
"Person",
)
```
**Forward direction** ➡️ (`buildPerson`):
1. Applies the builder to an empty `PartialPerson`
2. Validates each field using field prisms
3. Returns `Some(*Person)` if all validations pass, `None` otherwise
**Reverse direction** ⬅️ (`buildEndomorphism`):
1. Extracts validated fields from `Person`
2. Converts them back to raw types
3. Returns a builder that reconstructs the `PartialPerson`
### 💡 Usage Example
```go
// Create a builder
builder := MakePerson("Alice", 25)
// Validate and convert to Person
maybePerson := PersonPrism.GetOption(builder)
// maybePerson is Option[*Person]
// - Some(*Person) if validation succeeds ✅
// - None if validation fails (no error details) ❌
```
## 📝 Approach 2: Codec-Based Validation (With Error Messages)
### Creating Field Codecs
Convert prisms to codecs for detailed validation:
```go
nameCodec = codec.FromRefinement(namePrism)
ageCodec = codec.FromRefinement(agePrism)
```
### 🎯 Creating the PersonCodec
The `PersonCodec` provides bidirectional transformation with validation:
```go
func makePersonCodec() PersonCodec {
return codec.MakeType(
"Person",
codec.Is[*Person](),
makePersonValidate(), // Validation with error reporting
buildEndomorphism(), // Encoding (same as prism)
)
}
```
The `makePersonValidate` function:
1. Applies the builder to an empty `PartialPerson`
2. Validates each field using field codecs
3. Accumulates validation errors using applicative composition 📚
4. Returns `Validation[*Person]` (either errors or a valid `Person`)
### 💡 Usage Example
```go
// Create a builder
builder := MakePerson("", 15) // Invalid: empty name, age < 18
// Validate with detailed errors
personCodec := makePersonCodec()
validation := personCodec.Validate(builder)
// validation is Validation[*Person]
// - Right(*Person) if validation succeeds ✅
// - Left(ValidationErrors) with detailed error messages if validation fails ❌
```
## ⚖️ Key Differences
| Feature | Prism-Based 🔍 | Codec-Based 📝 |
|---------|-------------|-------------|
| Error Messages | No (returns `None`) ❌ | Yes (returns detailed errors) ✅ |
| Complexity | Simpler 🟢 | More complex 🟡 |
| Use Case | Simple validation | Production validation with user feedback |
| Return Type | `Option[*Person]` | `Validation[*Person]` |
## 📝 Pattern Summary
1. **Define types** 📐: Create `PartialPerson` (unvalidated) and `Person` (validated)
2. **Generate lenses** 🔧: Use `fp-go:Lens` directive
3. **Export setters** 🎁: Create `WithXXX` methods from lens setters
4. **Create validation prisms** 🎭: Define validation rules for each field
5. **Choose validation approach** ⚖️:
- **Simple** 🔍: Create a `Prism` for quick validation without errors
- **Detailed** 📝: Create a `Codec` for validation with error reporting
## ✨ Benefits
- **Type Safety** 🛡️: Validated types guarantee business rules at compile time
- **Composability** 🧩: Builders can be composed using monoid operations
- **Bidirectional** ↔️: Both prisms and codecs support encoding and decoding
- **Separation of Concerns** 🎯: Building and validation are separate phases
- **Functional** 🔄: Pure functions, no mutation, easy to test
## 📁 Files
- `types.go`: Type definitions and lens generation directives
- `builder.go`: Prism-based builder implementation
- `codec.go`: Codec-based validation implementation
- `codec_test.go`: Tests demonstrating usage patterns

View File

@@ -27,10 +27,10 @@ var (
personLenses = MakePersonRefLenses()
// emptyPartialPerson is a zero-value PartialPerson used as a starting point for building.
emptyPartialPerson = &PartialPerson{}
emptyPartialPerson = F.Zero[*PartialPerson]()
// emptyPerson is a zero-value Person used as a starting point for validation.
emptyPerson = &Person{}
emptyPerson = F.Zero[*Person]()
// monoidPartialPerson is a monoid for composing endomorphisms on PartialPerson.
// Allows combining multiple builder operations.
@@ -90,7 +90,7 @@ var (
// Example:
// builder := WithName("Alice")
// person := builder(&PartialPerson{})
WithName = partialPersonLenses.Name.Set
WithName = partialPersonLenses.name.Set
// WithAge is a builder function that sets the Age field of a PartialPerson.
// It returns an endomorphism that can be composed with other builder operations.
@@ -98,7 +98,7 @@ var (
// Example:
// builder := WithAge(25)
// person := builder(&PartialPerson{})
WithAge = partialPersonLenses.Age.Set
WithAge = partialPersonLenses.age.Set
// PersonPrism is a prism that converts between a builder pattern (Endomorphism[*PartialPerson])
// and a validated Person in both directions.
@@ -143,27 +143,6 @@ func MakePerson(name string, age int) Endomorphism[*PartialPerson] {
allOfPartialPerson)
}
func buildGeneric[S, A, T any](
src Prism[Endomorphism[S], Endomorphism[A]],
) Prism[Endomorphism[S], A] {
var emptyA A
x := F.Pipe1(
src.GetOption,
readeroption.Map[Endomorphism[S]](reader.Read[A](emptyA)),
)
y := F.Pipe1(
src.ReverseGet,
reader.Local[Endomorphism[S]](reader.Of[A, A]),
)
return prism.MakePrism(
x,
y,
)
}
// buildPerson constructs the forward direction of PersonPrism.
// It takes a builder (Endomorphism[*PartialPerson]) and attempts to create
// a validated Person by:
@@ -181,7 +160,7 @@ func buildPerson() ReaderOption[Endomorphism[*PartialPerson], *Person] {
// maybeName extracts the name from PartialPerson, validates it,
// and creates a setter for the Person's Name field if valid
maybeName := F.Flow3(
partialPersonLenses.Name.Get,
partialPersonLenses.name.Get,
namePrism.GetOption,
option.Map(personLenses.Name.Set),
)
@@ -189,7 +168,7 @@ func buildPerson() ReaderOption[Endomorphism[*PartialPerson], *Person] {
// maybeAge extracts the age from PartialPerson, validates it,
// and creates a setter for the Person's Age field if valid
maybeAge := F.Flow3(
partialPersonLenses.Age.Get,
partialPersonLenses.age.Get,
agePrism.GetOption,
option.Map(personLenses.Age.Set),
)
@@ -221,7 +200,7 @@ func buildEndomorphism() Reader[*Person, Endomorphism[*PartialPerson]] {
name := F.Flow3(
personLenses.Name.Get,
namePrism.ReverseGet,
partialPersonLenses.Name.Set,
partialPersonLenses.name.Set,
)
// age extracts the validated age, converts it to int,
@@ -229,7 +208,7 @@ func buildEndomorphism() Reader[*Person, Endomorphism[*PartialPerson]] {
age := F.Flow3(
personLenses.Age.Get,
agePrism.ReverseGet,
partialPersonLenses.Age.Set,
partialPersonLenses.age.Set,
)
// Combine the field extractors into a single builder

131
v2/samples/builder/codec.go Normal file
View File

@@ -0,0 +1,131 @@
// Package builder demonstrates codec-based validation and encoding/decoding
// for Person objects using fp-go's optics and validation framework.
//
// This file extends the builder pattern with codec functionality, enabling:
// - Bidirectional transformation between PartialPerson and Person
// - Validation with detailed error reporting
// - Type-safe encoding and decoding operations
package builder
import (
A "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/identity"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/codec"
"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"
)
type (
// PersonCodec is a codec type that handles bidirectional transformation
// between Person and PartialPerson using endomorphisms.
//
// Type parameters:
// - A: *Person - The validated target type
// - O: Endomorphism[*PartialPerson] - The output encoding type (builder)
// - I: Endomorphism[*PartialPerson] - The input decoding type (builder)
//
// This codec enables:
// - Validation: Converting a PartialPerson builder to a validated Person
// - Encoding: Converting a Person back to a PartialPerson builder
PersonCodec = Type[*Person, Endomorphism[*PartialPerson], Endomorphism[*PartialPerson]]
)
var (
// nameCodec is a codec for validating and transforming name fields.
// It uses namePrism to ensure names are non-empty strings.
//
// Validation: string -> Result[NonEmptyString]
// Encoding: NonEmptyString -> string
nameCodec = codec.FromRefinement(namePrism)
// ageCodec is a codec for validating and transforming age fields.
// It uses agePrism to ensure ages meet adult criteria (>= 18).
//
// Validation: int -> Result[AdultAge]
// Encoding: AdultAge -> int
ageCodec = codec.FromRefinement(agePrism)
)
// makePersonValidate creates a validation function that transforms a PartialPerson
// builder (endomorphism) into a validated Person.
//
// The validation process:
// 1. Applies the builder endomorphism to an empty PartialPerson
// 2. Extracts and validates the Name field using nameCodec
// 3. Extracts and validates the Age field using ageCodec
// 4. Combines all validations using applicative composition
// 5. Returns either a validated Person or a collection of validation errors
//
// This function uses the Reader monad to thread validation context through
// the computation, and ReaderEither to accumulate validation errors.
//
// Returns:
//
// A Validate function that takes a PartialPerson builder and returns
// a Reader that produces a Validation result (either errors or a Person)
func makePersonValidate() Validate[Endomorphism[*PartialPerson], *Person] {
// Create a monoid for combining validation operations
// This allows multiple field validations to be composed together
rdrMonoid := validate.ApplicativeMonoid[*PartialPerson](endomorphism.Monoid[*Person]())
// allOfRdr combines an array of validation readers into a single reader
allOfRdr := monoid.ConcatAll(rdrMonoid)
// valName validates the Name field:
// 1. Extract name from PartialPerson
// 2. Validate using nameCodec (ensures non-empty)
// 3. Map to a Person name setter if valid
valName := F.Flow3(
partialPersonLenses.name.Get,
nameCodec.Validate,
decode.Map[validation.Context](personLenses.Name.Set),
)
// valAge validates the Age field:
// 1. Extract age from PartialPerson
// 2. Validate using ageCodec (ensures >= 18)
// 3. Map to a Person age setter if valid
valAge := F.Flow3(
partialPersonLenses.age.Get,
ageCodec.Validate,
decode.Map[validation.Context](personLenses.Age.Set),
)
// Collect all field validators
vals := A.From(valName, valAge)
// Combine all validations and apply to an empty Person
return F.Flow3(
identity.Flap[*PartialPerson](emptyPartialPerson),
allOfRdr(vals),
decode.Map[validation.Context](identity.Flap[*Person](emptyPerson)),
)
}
// makePersonCodec creates a complete codec for Person objects.
//
// The codec provides:
// - Type checking: Verifies if a value is a *Person
// - Validation: Converts PartialPerson builders to validated Person instances
// - Encoding: Converts Person instances back to PartialPerson builders
//
// This enables bidirectional transformation with validation:
// - Decode: Endomorphism[*PartialPerson] -> Validation[*Person]
// - Encode: *Person -> Endomorphism[*PartialPerson]
//
// Returns:
//
// A PersonCodec that can validate, encode, and decode Person objects
func makePersonCodec() PersonCodec {
return codec.MakeType(
"Person",
codec.Is[*Person](),
makePersonValidate(),
buildEndomorphism(),
)
}

View File

@@ -0,0 +1,331 @@
package builder
import (
"testing"
A "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestMakePersonValidate_ValidPerson tests validation of a valid person
func TestMakePersonValidate_ValidPerson(t *testing.T) {
// Arrange
validate := makePersonValidate()
builder := MakePerson("Alice", 25)
ctx := A.Of(validation.ContextEntry{Type: "Person", Actual: builder})
// Act
result := validate(builder)(ctx)
// Assert
assert.True(t, either.IsRight(result), "Expected validation to succeed")
person, _ := either.Unwrap(result)
require.NotNil(t, person, "Expected to unwrap person")
assert.Equal(t, NonEmptyString("Alice"), person.Name)
assert.Equal(t, AdultAge(25), person.Age)
}
// TestMakePersonValidate_EmptyName tests validation failure for empty name
func TestMakePersonValidate_EmptyName(t *testing.T) {
// Arrange
validate := makePersonValidate()
builder := MakePerson("", 25)
ctx := A.Of(validation.ContextEntry{Type: "Person", Actual: builder})
// Act
result := validate(builder)(ctx)
// Assert
assert.True(t, either.IsLeft(result), "Expected validation to fail for empty name")
_, errors := either.Unwrap(result)
assert.NotEmpty(t, errors, "Expected validation errors")
}
// TestMakePersonValidate_InvalidAge tests validation failure for age < 18
func TestMakePersonValidate_InvalidAge(t *testing.T) {
// Arrange
validate := makePersonValidate()
builder := MakePerson("Bob", 15)
ctx := A.Of(validation.ContextEntry{Type: "Person", Actual: builder})
// Act
result := validate(builder)(ctx)
// Assert
assert.True(t, either.IsLeft(result), "Expected validation to fail for age < 18")
_, errors := either.Unwrap(result)
assert.NotEmpty(t, errors, "Expected validation errors")
}
// TestMakePersonValidate_MultipleErrors tests validation with multiple errors
func TestMakePersonValidate_MultipleErrors(t *testing.T) {
// Arrange
validate := makePersonValidate()
builder := MakePerson("", 10) // Both empty name and invalid age
ctx := A.Of(validation.ContextEntry{Type: "Person", Actual: builder})
// Act
result := validate(builder)(ctx)
// Assert
assert.True(t, either.IsLeft(result), "Expected validation to fail")
_, errors := either.Unwrap(result)
assert.Len(t, errors, 2, "Expected two validation errors")
}
// TestMakePersonValidate_BoundaryAge tests validation at age boundary (18)
func TestMakePersonValidate_BoundaryAge(t *testing.T) {
// Arrange
validate := makePersonValidate()
builder := MakePerson("Charlie", 18)
ctx := A.Of(validation.ContextEntry{Type: "Person", Actual: builder})
// Act
result := validate(builder)(ctx)
// Assert
assert.True(t, either.IsRight(result), "Expected validation to succeed for age 18")
person, _ := either.Unwrap(result)
require.NotNil(t, person, "Expected to unwrap person")
assert.Equal(t, AdultAge(18), person.Age)
}
// TestMakePersonCodec_Decode tests the codec's Decode method
func TestMakePersonCodec_Decode(t *testing.T) {
// Arrange
codec := makePersonCodec()
builder := MakePerson("Diana", 30)
// Act
result := codec.Decode(builder)
// Assert
assert.True(t, either.IsRight(result), "Expected decode to succeed")
person, _ := either.Unwrap(result)
require.NotNil(t, person, "Expected to unwrap person")
assert.Equal(t, NonEmptyString("Diana"), person.Name)
assert.Equal(t, AdultAge(30), person.Age)
}
// TestMakePersonCodec_Decode_Invalid tests the codec's Decode method with invalid data
func TestMakePersonCodec_Decode_Invalid(t *testing.T) {
// Arrange
codec := makePersonCodec()
builder := MakePerson("", 10) // Invalid name and age
// Act
result := codec.Decode(builder)
// Assert
assert.True(t, either.IsLeft(result), "Expected decode to fail")
_, errors := either.Unwrap(result)
assert.Len(t, errors, 2, "Expected two validation errors")
}
// TestMakePersonCodec_Encode tests the codec's Encode method
func TestMakePersonCodec_Encode(t *testing.T) {
// Arrange
codec := makePersonCodec()
person := &Person{
Name: NonEmptyString("Eve"),
Age: AdultAge(28),
}
// Act
builder := codec.Encode(person)
// Apply the builder to get a PartialPerson
partial := builder(emptyPartialPerson)
// Assert
assert.Equal(t, "Eve", partial.name)
assert.Equal(t, 28, partial.age)
}
// TestMakePersonCodec_RoundTrip tests encoding and decoding round-trip
func TestMakePersonCodec_RoundTrip(t *testing.T) {
// Arrange
codec := makePersonCodec()
originalPerson := &Person{
Name: NonEmptyString("Frank"),
Age: AdultAge(35),
}
// Act - Encode to builder
builder := codec.Encode(originalPerson)
// Decode back to person
result := codec.Decode(builder)
// Assert
assert.True(t, either.IsRight(result), "Expected round-trip to succeed")
decodedPerson, _ := either.Unwrap(result)
require.NotNil(t, decodedPerson, "Expected to unwrap person")
assert.Equal(t, originalPerson.Name, decodedPerson.Name)
assert.Equal(t, originalPerson.Age, decodedPerson.Age)
}
// TestMakePersonCodec_Name tests the codec's Name method
func TestMakePersonCodec_Name(t *testing.T) {
// Arrange
codec := makePersonCodec()
// Act
name := codec.Name()
// Assert
assert.Equal(t, "Person", name)
}
// TestNameCodec_Validate tests the name codec validation
func TestNameCodec_Validate(t *testing.T) {
tests := []struct {
name string
input string
wantValid bool
}{
{
name: "valid name",
input: "Alice",
wantValid: true,
},
{
name: "empty name",
input: "",
wantValid: false,
},
{
name: "whitespace name",
input: " ",
wantValid: true, // Non-empty string, even if whitespace
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Arrange
ctx := A.Of(validation.ContextEntry{Type: "Name", Actual: tt.input})
// Act
result := nameCodec.Validate(tt.input)(ctx)
// Assert
if tt.wantValid {
assert.True(t, either.IsRight(result), "Expected validation to succeed")
} else {
assert.True(t, either.IsLeft(result), "Expected validation to fail")
}
})
}
}
// TestAgeCodec_Validate tests the age codec validation
func TestAgeCodec_Validate(t *testing.T) {
tests := []struct {
name string
input int
wantValid bool
}{
{
name: "valid adult age",
input: 25,
wantValid: true,
},
{
name: "boundary age 18",
input: 18,
wantValid: true,
},
{
name: "minor age",
input: 17,
wantValid: false,
},
{
name: "zero age",
input: 0,
wantValid: false,
},
{
name: "negative age",
input: -5,
wantValid: false,
},
{
name: "very old age",
input: 120,
wantValid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Arrange
ctx := A.Of(validation.ContextEntry{Type: "Age", Actual: tt.input})
// Act
result := ageCodec.Validate(tt.input)(ctx)
// Assert
if tt.wantValid {
assert.True(t, either.IsRight(result), "Expected validation to succeed")
} else {
assert.True(t, either.IsLeft(result), "Expected validation to fail")
}
})
}
}
// TestMakePersonCodec_WithComposedBuilders tests codec with composed builders
func TestMakePersonCodec_WithComposedBuilders(t *testing.T) {
// Arrange
codec := makePersonCodec()
// Create a builder by composing individual field setters
builder := endomorphism.Chain(
WithAge(40),
)(WithName("Grace"))
// Act
result := codec.Decode(builder)
// Assert
assert.True(t, either.IsRight(result), "Expected decode to succeed")
person, _ := either.Unwrap(result)
require.NotNil(t, person, "Expected to unwrap person")
assert.Equal(t, NonEmptyString("Grace"), person.Name)
assert.Equal(t, AdultAge(40), person.Age)
}
// TestMakePersonCodec_PartialBuilder tests codec with partial builder (missing fields)
func TestMakePersonCodec_PartialBuilder(t *testing.T) {
// Arrange
codec := makePersonCodec()
// Create a builder that only sets name
builder := WithName("Henry")
// Act
result := codec.Decode(builder)
// Assert
// Should fail because age is 0 (< 18)
assert.True(t, either.IsLeft(result), "Expected decode to fail for missing age")
}

View File

@@ -2,7 +2,7 @@ package builder
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2026-01-21 17:40:56.5217758 +0100 CET m=+0.003738101
// 2026-01-23 16:15:30.703391 +0100 CET m=+0.003782501
import (
__lens "github.com/IBM/fp-go/v2/optics/lens"
@@ -15,105 +15,105 @@ import (
// PartialPersonLenses provides lenses for accessing fields of PartialPerson
type PartialPersonLenses struct {
// mandatory fields
Name __lens.Lens[PartialPerson, string]
Age __lens.Lens[PartialPerson, int]
name __lens.Lens[PartialPerson, string]
age __lens.Lens[PartialPerson, int]
// optional fields
NameO __lens_option.LensO[PartialPerson, string]
AgeO __lens_option.LensO[PartialPerson, int]
nameO __lens_option.LensO[PartialPerson, string]
ageO __lens_option.LensO[PartialPerson, int]
}
// PartialPersonRefLenses provides lenses for accessing fields of PartialPerson via a reference to PartialPerson
type PartialPersonRefLenses struct {
// mandatory fields
Name __lens.Lens[*PartialPerson, string]
Age __lens.Lens[*PartialPerson, int]
name __lens.Lens[*PartialPerson, string]
age __lens.Lens[*PartialPerson, int]
// optional fields
NameO __lens_option.LensO[*PartialPerson, string]
AgeO __lens_option.LensO[*PartialPerson, int]
nameO __lens_option.LensO[*PartialPerson, string]
ageO __lens_option.LensO[*PartialPerson, int]
// prisms
NameP __prism.Prism[*PartialPerson, string]
AgeP __prism.Prism[*PartialPerson, int]
nameP __prism.Prism[*PartialPerson, string]
ageP __prism.Prism[*PartialPerson, int]
}
// PartialPersonPrisms provides prisms for accessing fields of PartialPerson
type PartialPersonPrisms struct {
Name __prism.Prism[PartialPerson, string]
Age __prism.Prism[PartialPerson, int]
name __prism.Prism[PartialPerson, string]
age __prism.Prism[PartialPerson, int]
}
// MakePartialPersonLenses creates a new PartialPersonLenses with lenses for all fields
func MakePartialPersonLenses() PartialPersonLenses {
// mandatory lenses
lensName := __lens.MakeLensWithName(
func(s PartialPerson) string { return s.Name },
func(s PartialPerson, v string) PartialPerson { s.Name = v; return s },
"PartialPerson.Name",
lensname := __lens.MakeLensWithName(
func(s PartialPerson) string { return s.name },
func(s PartialPerson, v string) PartialPerson { s.name = v; return s },
"PartialPerson.name",
)
lensAge := __lens.MakeLensWithName(
func(s PartialPerson) int { return s.Age },
func(s PartialPerson, v int) PartialPerson { s.Age = v; return s },
"PartialPerson.Age",
lensage := __lens.MakeLensWithName(
func(s PartialPerson) int { return s.age },
func(s PartialPerson, v int) PartialPerson { s.age = v; return s },
"PartialPerson.age",
)
// optional lenses
lensNameO := __lens_option.FromIso[PartialPerson](__iso_option.FromZero[string]())(lensName)
lensAgeO := __lens_option.FromIso[PartialPerson](__iso_option.FromZero[int]())(lensAge)
lensnameO := __lens_option.FromIso[PartialPerson](__iso_option.FromZero[string]())(lensname)
lensageO := __lens_option.FromIso[PartialPerson](__iso_option.FromZero[int]())(lensage)
return PartialPersonLenses{
// mandatory lenses
Name: lensName,
Age: lensAge,
name: lensname,
age: lensage,
// optional lenses
NameO: lensNameO,
AgeO: lensAgeO,
nameO: lensnameO,
ageO: lensageO,
}
}
// MakePartialPersonRefLenses creates a new PartialPersonRefLenses with lenses for all fields
func MakePartialPersonRefLenses() PartialPersonRefLenses {
// mandatory lenses
lensName := __lens.MakeLensStrictWithName(
func(s *PartialPerson) string { return s.Name },
func(s *PartialPerson, v string) *PartialPerson { s.Name = v; return s },
"(*PartialPerson).Name",
lensname := __lens.MakeLensStrictWithName(
func(s *PartialPerson) string { return s.name },
func(s *PartialPerson, v string) *PartialPerson { s.name = v; return s },
"(*PartialPerson).name",
)
lensAge := __lens.MakeLensStrictWithName(
func(s *PartialPerson) int { return s.Age },
func(s *PartialPerson, v int) *PartialPerson { s.Age = v; return s },
"(*PartialPerson).Age",
lensage := __lens.MakeLensStrictWithName(
func(s *PartialPerson) int { return s.age },
func(s *PartialPerson, v int) *PartialPerson { s.age = v; return s },
"(*PartialPerson).age",
)
// optional lenses
lensNameO := __lens_option.FromIso[*PartialPerson](__iso_option.FromZero[string]())(lensName)
lensAgeO := __lens_option.FromIso[*PartialPerson](__iso_option.FromZero[int]())(lensAge)
lensnameO := __lens_option.FromIso[*PartialPerson](__iso_option.FromZero[string]())(lensname)
lensageO := __lens_option.FromIso[*PartialPerson](__iso_option.FromZero[int]())(lensage)
return PartialPersonRefLenses{
// mandatory lenses
Name: lensName,
Age: lensAge,
name: lensname,
age: lensage,
// optional lenses
NameO: lensNameO,
AgeO: lensAgeO,
nameO: lensnameO,
ageO: lensageO,
}
}
// MakePartialPersonPrisms creates a new PartialPersonPrisms with prisms for all fields
func MakePartialPersonPrisms() PartialPersonPrisms {
_fromNonZeroName := __option.FromNonZero[string]()
_prismName := __prism.MakePrismWithName(
func(s PartialPerson) __option.Option[string] { return _fromNonZeroName(s.Name) },
_fromNonZeroname := __option.FromNonZero[string]()
_prismname := __prism.MakePrismWithName(
func(s PartialPerson) __option.Option[string] { return _fromNonZeroname(s.name) },
func(v string) PartialPerson {
return PartialPerson{ Name: v }
return PartialPerson{ name: v }
},
"PartialPerson.Name",
"PartialPerson.name",
)
_fromNonZeroAge := __option.FromNonZero[int]()
_prismAge := __prism.MakePrismWithName(
func(s PartialPerson) __option.Option[int] { return _fromNonZeroAge(s.Age) },
_fromNonZeroage := __option.FromNonZero[int]()
_prismage := __prism.MakePrismWithName(
func(s PartialPerson) __option.Option[int] { return _fromNonZeroage(s.age) },
func(v int) PartialPerson {
return PartialPerson{ Age: v }
return PartialPerson{ age: v }
},
"PartialPerson.Age",
"PartialPerson.age",
)
return PartialPersonPrisms {
Name: _prismName,
Age: _prismAge,
name: _prismname,
age: _prismage,
}
}

View File

@@ -4,6 +4,9 @@ package builder
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/codec"
"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/lens"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
@@ -35,8 +38,37 @@ type (
// a value of type A. It is an alias for reader.Reader[R, A].
Reader[R, A any] = reader.Reader[R, A]
// Prism represents an optic that focuses on a subset of values of type S that can be
// converted to type A. It provides bidirectional transformation with validation.
// It is an alias for prism.Prism[S, A].
Prism[S, A any] = prism.Prism[S, A]
Lens[S, A any] = lens.Lens[S, A]
// Lens represents an optic that focuses on a field of type A within a structure of type S.
// It provides getter and setter operations for immutable updates.
// It is an alias for lens.Lens[S, A].
Lens[S, A any] = lens.Lens[S, A]
// Type represents a codec that handles bidirectional transformation between types.
// A: The validated target type
// O: The output encoding type
// I: The input decoding type
// It is an alias for codec.Type[A, O, I].
Type[A, O, I any] = codec.Type[A, O, I]
// Validate represents a validation function that transforms input I into a validated result A.
// It returns a Validation that contains either the validated value or validation errors.
// It is an alias for validate.Validate[I, A].
Validate[I, A any] = validate.Validate[I, A]
// Validation represents the result of a validation operation.
// It contains either a validated value of type A (Right) or validation errors (Left).
// It is an alias for validation.Validation[A].
Validation[A any] = validation.Validation[A]
// Encode represents an encoding function that transforms a value of type A into type O.
// It is used in codecs for the reverse direction of validation.
// It is an alias for codec.Encode[A, O].
Encode[A, O any] = codec.Encode[A, O]
// NonEmptyString is a string type that represents a validated non-empty string.
// It is used to ensure that string fields contain meaningful data.
@@ -56,11 +88,11 @@ type (
//
// fp-go:Lens
type PartialPerson struct {
// Name is the person's name as a raw string, which may be empty or invalid.
Name string
// name is the person's name as a raw string, which may be empty or invalid.
name string
// Age is the person's age as a raw integer, which may be negative or otherwise invalid.
Age int
// age is the person's age as a raw integer, which may be negative or otherwise invalid.
age int
}
// Person represents a person record with validated fields.

View File

@@ -107,7 +107,7 @@ func TestHeterogeneousHttpRequests(t *testing.T) {
// BenchmarkHeterogeneousHttpRequests shows how to execute multiple HTTP requests in parallel when
// the response structure of these requests is different. We use [R.TraverseTuple2] to account for the different types
func BenchmarkHeterogeneousHttpRequests(b *testing.B) {
for n := 0; n < b.N; n++ {
heterogeneousHTTPRequests()(context.Background())()
for b.Loop() {
heterogeneousHTTPRequests()(b.Context())()
}
}

View File

@@ -64,3 +64,9 @@ type WithGeneric[T any] struct {
Name string
Value T
}
// fp-go:Lens
type DataBuilder struct {
name string
value string
}

View File

@@ -21,6 +21,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
@@ -341,3 +342,125 @@ func TestCompanyRefLensesOptionalIdempotent(t *testing.T) {
assert.Equal(t, &newWebsiteValue, differentWebsite.Website)
assert.Equal(t, &websiteValue, company.Website, "Original should be unchanged")
}
func TestDataBuilderLensWithUnexportedFields(t *testing.T) {
// Test that lenses can access and modify unexported fields
// This demonstrates that the lens generator now supports unexported fields
// Create a DataBuilder with unexported fields
builder := DataBuilder{
name: "initial-name",
value: "initial-value",
}
// Create lenses
lenses := MakeDataBuilderLenses()
// Test Get on unexported fields
assert.Equal(t, "initial-name", lenses.name.Get(builder))
assert.Equal(t, "initial-value", lenses.value.Get(builder))
// Test Set on unexported fields
updatedName := lenses.name.Set("updated-name")(builder)
assert.Equal(t, "updated-name", updatedName.name)
assert.Equal(t, "initial-value", updatedName.value) // Other field unchanged
assert.Equal(t, "initial-name", builder.name) // Original unchanged
updatedValue := lenses.value.Set("updated-value")(builder)
assert.Equal(t, "initial-name", updatedValue.name) // Other field unchanged
assert.Equal(t, "updated-value", updatedValue.value)
assert.Equal(t, "initial-value", builder.value) // Original unchanged
// Test Modify on unexported fields
modifyName := F.Pipe1(
lenses.name,
L.Modify[DataBuilder](S.Append("-modified")),
)
modified := modifyName(builder)
assert.Equal(t, "initial-name-modified", modified.name)
assert.Equal(t, "initial-name", builder.name) // Original unchanged
// Test composition of modifications
updatedBoth := F.Pipe2(
builder,
lenses.name.Set("new-name"),
lenses.value.Set("new-value"),
)
assert.Equal(t, "new-name", updatedBoth.name)
assert.Equal(t, "new-value", updatedBoth.value)
assert.Equal(t, "initial-name", builder.name) // Original unchanged
assert.Equal(t, "initial-value", builder.value) // Original unchanged
}
func TestDataBuilderRefLensesWithUnexportedFields(t *testing.T) {
// Test that ref lenses work with unexported fields and maintain idempotency
builder := &DataBuilder{
name: "test-name",
value: "test-value",
}
refLenses := MakeDataBuilderRefLenses()
// Test Get on unexported fields
assert.Equal(t, "test-name", refLenses.name.Get(builder))
assert.Equal(t, "test-value", refLenses.value.Get(builder))
// Test idempotency - setting same value should return same pointer
sameName := refLenses.name.Set("test-name")(builder)
assert.Same(t, builder, sameName, "Setting name to same value should return identical pointer")
sameValue := refLenses.value.Set("test-value")(builder)
assert.Same(t, builder, sameValue, "Setting value to same value should return identical pointer")
// Test that setting different value creates new pointer
differentName := refLenses.name.Set("different-name")(builder)
assert.NotSame(t, builder, differentName, "Setting name to different value should return new pointer")
assert.Equal(t, "different-name", differentName.name)
assert.Equal(t, "test-name", builder.name, "Original should be unchanged")
differentValue := refLenses.value.Set("different-value")(builder)
assert.NotSame(t, builder, differentValue, "Setting value to different value should return new pointer")
assert.Equal(t, "different-value", differentValue.value)
assert.Equal(t, "test-value", builder.value, "Original should be unchanged")
}
func TestDataBuilderOptionalLensesWithUnexportedFields(t *testing.T) {
// Test optional lenses (LensO) with unexported fields
builder := DataBuilder{
name: "test",
value: "data",
}
lenses := MakeDataBuilderLenses()
// Test getting non-zero values as Some
nameOpt := lenses.nameO.Get(builder)
assert.True(t, O.IsSome(nameOpt))
assert.Equal(t, "test", O.GetOrElse(F.Zero[string])(nameOpt))
valueOpt := lenses.valueO.Get(builder)
assert.True(t, O.IsSome(valueOpt))
assert.Equal(t, "data", O.GetOrElse(F.Zero[string])(valueOpt))
// Test setting to Some
updatedName := lenses.nameO.Set(O.Some("new-test"))(builder)
assert.Equal(t, "new-test", updatedName.name)
// Test setting to None (zero value for string is "")
clearedName := lenses.nameO.Set(O.None[string]())(builder)
assert.Equal(t, "", clearedName.name)
// Test with zero value
emptyBuilder := DataBuilder{
name: "",
value: "",
}
emptyNameOpt := lenses.nameO.Get(emptyBuilder)
assert.True(t, O.IsNone(emptyNameOpt), "Empty string should be None")
emptyValueOpt := lenses.valueO.Get(emptyBuilder)
assert.True(t, O.IsNone(emptyValueOpt), "Empty string should be None")
}

View File

@@ -2,7 +2,7 @@ package lens
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2025-12-16 15:41:28.7198645 +0100 CET m=+0.003889201
// 2026-01-23 16:09:35.747264 +0100 CET m=+0.003865601
import (
__lens "github.com/IBM/fp-go/v2/optics/lens"
@@ -946,3 +946,108 @@ func MakeWithGenericPrisms[T any]() WithGenericPrisms[T] {
Value: _prismValue,
}
}
// DataBuilderLenses provides lenses for accessing fields of DataBuilder
type DataBuilderLenses struct {
// mandatory fields
name __lens.Lens[DataBuilder, string]
value __lens.Lens[DataBuilder, string]
// optional fields
nameO __lens_option.LensO[DataBuilder, string]
valueO __lens_option.LensO[DataBuilder, string]
}
// DataBuilderRefLenses provides lenses for accessing fields of DataBuilder via a reference to DataBuilder
type DataBuilderRefLenses struct {
// mandatory fields
name __lens.Lens[*DataBuilder, string]
value __lens.Lens[*DataBuilder, string]
// optional fields
nameO __lens_option.LensO[*DataBuilder, string]
valueO __lens_option.LensO[*DataBuilder, string]
// prisms
nameP __prism.Prism[*DataBuilder, string]
valueP __prism.Prism[*DataBuilder, string]
}
// DataBuilderPrisms provides prisms for accessing fields of DataBuilder
type DataBuilderPrisms struct {
name __prism.Prism[DataBuilder, string]
value __prism.Prism[DataBuilder, string]
}
// MakeDataBuilderLenses creates a new DataBuilderLenses with lenses for all fields
func MakeDataBuilderLenses() DataBuilderLenses {
// mandatory lenses
lensname := __lens.MakeLensWithName(
func(s DataBuilder) string { return s.name },
func(s DataBuilder, v string) DataBuilder { s.name = v; return s },
"DataBuilder.name",
)
lensvalue := __lens.MakeLensWithName(
func(s DataBuilder) string { return s.value },
func(s DataBuilder, v string) DataBuilder { s.value = v; return s },
"DataBuilder.value",
)
// optional lenses
lensnameO := __lens_option.FromIso[DataBuilder](__iso_option.FromZero[string]())(lensname)
lensvalueO := __lens_option.FromIso[DataBuilder](__iso_option.FromZero[string]())(lensvalue)
return DataBuilderLenses{
// mandatory lenses
name: lensname,
value: lensvalue,
// optional lenses
nameO: lensnameO,
valueO: lensvalueO,
}
}
// MakeDataBuilderRefLenses creates a new DataBuilderRefLenses with lenses for all fields
func MakeDataBuilderRefLenses() DataBuilderRefLenses {
// mandatory lenses
lensname := __lens.MakeLensStrictWithName(
func(s *DataBuilder) string { return s.name },
func(s *DataBuilder, v string) *DataBuilder { s.name = v; return s },
"(*DataBuilder).name",
)
lensvalue := __lens.MakeLensStrictWithName(
func(s *DataBuilder) string { return s.value },
func(s *DataBuilder, v string) *DataBuilder { s.value = v; return s },
"(*DataBuilder).value",
)
// optional lenses
lensnameO := __lens_option.FromIso[*DataBuilder](__iso_option.FromZero[string]())(lensname)
lensvalueO := __lens_option.FromIso[*DataBuilder](__iso_option.FromZero[string]())(lensvalue)
return DataBuilderRefLenses{
// mandatory lenses
name: lensname,
value: lensvalue,
// optional lenses
nameO: lensnameO,
valueO: lensvalueO,
}
}
// MakeDataBuilderPrisms creates a new DataBuilderPrisms with prisms for all fields
func MakeDataBuilderPrisms() DataBuilderPrisms {
_fromNonZeroname := __option.FromNonZero[string]()
_prismname := __prism.MakePrismWithName(
func(s DataBuilder) __option.Option[string] { return _fromNonZeroname(s.name) },
func(v string) DataBuilder {
return DataBuilder{ name: v }
},
"DataBuilder.name",
)
_fromNonZerovalue := __option.FromNonZero[string]()
_prismvalue := __prism.MakePrismWithName(
func(s DataBuilder) __option.Option[string] { return _fromNonZerovalue(s.value) },
func(v string) DataBuilder {
return DataBuilder{ value: v }
},
"DataBuilder.value",
)
return DataBuilderPrisms {
name: _prismname,
value: _prismvalue,
}
}