mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-04 13:21:02 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1657569f1d | ||
|
|
545876d013 | ||
|
|
9492c5d994 | ||
|
|
94b1ea30d1 | ||
|
|
a77d61f632 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -134,7 +134,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
|
||||
@@ -26,6 +26,83 @@ import (
|
||||
"github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// Do creates the initial empty codec to be used as the starting point for
|
||||
// do-notation style codec construction.
|
||||
//
|
||||
// This is the entry point for building up a struct codec field-by-field using
|
||||
// the applicative and monadic sequencing operators ApSL, ApSO, and Bind.
|
||||
// It wraps Empty and lifts a lazily-evaluated default Pair[O, A] into a
|
||||
// Type[A, O, I] that ignores its input and always succeeds with the default value.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type for decoding (what the codec reads from)
|
||||
// - A: The target struct type being built up (what the codec decodes to)
|
||||
// - O: The output type for encoding (what the codec writes to)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - e: A Lazy[Pair[O, A]] providing the initial default values:
|
||||
// - pair.Head(e()): The default encoded output O (e.g. an empty monoid value)
|
||||
// - pair.Tail(e()): The initial zero value of the struct A (e.g. MyStruct{})
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Type[A, O, I] that always decodes to the default A and encodes to the
|
||||
// default O, regardless of input. This is then transformed by chaining
|
||||
// ApSL, ApSO, or Bind operators to add fields one by one.
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// Building a struct codec using do-notation style:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/lazy"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// "github.com/IBM/fp-go/v2/optics/lens"
|
||||
// "github.com/IBM/fp-go/v2/pair"
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// nameLens := lens.MakeLens(
|
||||
// func(p Person) string { return p.Name },
|
||||
// func(p Person, name string) Person { p.Name = name; return p },
|
||||
// )
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(p Person) int { return p.Age },
|
||||
// func(p Person, age int) Person { p.Age = age; return p },
|
||||
// )
|
||||
//
|
||||
// personCodec := F.Pipe2(
|
||||
// codec.Do[any, Person, string](lazy.Of(pair.MakePair("", Person{}))),
|
||||
// codec.ApSL(S.Monoid, nameLens, codec.String()),
|
||||
// codec.ApSL(S.Monoid, ageLens, codec.Int()),
|
||||
// )
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Do is typically the first call in a codec pipeline, followed by ApSL, ApSO, or Bind
|
||||
// - The lazy pair should use the monoid's empty value for O and the zero value for A
|
||||
// - For convenience, use Struct to create the initial codec for named struct types
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Empty: The underlying codec constructor that Do delegates to
|
||||
// - ApSL: Applicative sequencing for required struct fields via Lens
|
||||
// - ApSO: Applicative sequencing for optional struct fields via Optional
|
||||
// - Bind: Monadic sequencing for context-dependent field codecs
|
||||
//
|
||||
//go:inline
|
||||
func Do[I, A, O any](e Lazy[Pair[O, A]]) Type[A, O, I] {
|
||||
return Empty[I](e)
|
||||
}
|
||||
|
||||
// ApSL creates an applicative sequencing operator for codecs using a lens.
|
||||
//
|
||||
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs,
|
||||
@@ -297,3 +374,132 @@ func ApSO[S, T, O, I any](
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Bind creates a monadic sequencing operator for codecs using a lens and a Kleisli arrow.
|
||||
//
|
||||
// This function implements the "Bind" (monadic bind / chain) pattern for codecs,
|
||||
// allowing you to build up complex codecs where the codec for a field depends on
|
||||
// the current decoded value of the struct. Unlike ApSL which uses a fixed field
|
||||
// codec, Bind accepts a Kleisli arrow — a function from the current struct value S
|
||||
// to a Type[T, O, I] — enabling context-sensitive codec construction.
|
||||
//
|
||||
// The function combines:
|
||||
// - Encoding: Evaluates the Kleisli arrow f on the current struct value s to obtain
|
||||
// the field codec, extracts the field T using the lens, encodes it with that codec,
|
||||
// and combines it with the base encoding using the monoid.
|
||||
// - Validation: Validates the base struct first (monadic sequencing), then uses the
|
||||
// Kleisli arrow to obtain the field codec for the decoded struct value, and validates
|
||||
// the field through the lens. Errors are propagated but NOT accumulated (fail-fast
|
||||
// semantics, unlike ApSL which accumulates errors).
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - S: The source struct type (what we're building a codec for)
|
||||
// - T: The field type accessed by the lens
|
||||
// - O: The output type for encoding (must have a monoid)
|
||||
// - I: The input type for decoding
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: A Monoid[O] for combining encoded outputs
|
||||
// - l: A Lens[S, T] that focuses on a specific field in S
|
||||
// - f: A Kleisli[S, T, O, I] — a function from S to Type[T, O, I] — that produces
|
||||
// the field codec based on the current struct value
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[S, S, O, I] that transforms a base codec by adding the field
|
||||
// specified by the lens, where the field codec is determined by the Kleisli arrow.
|
||||
//
|
||||
// # How It Works
|
||||
//
|
||||
// 1. **Encoding**: When encoding a value of type S:
|
||||
// - Evaluate f(s) to obtain the field codec fa
|
||||
// - Extract the field T using l.Get
|
||||
// - Encode T to O using fa.Encode
|
||||
// - Combine with the base encoding using the monoid
|
||||
//
|
||||
// 2. **Validation**: When validating input I:
|
||||
// - Run the base validation to obtain a decoded S (fail-fast: stop on base failure)
|
||||
// - For the decoded S, evaluate f(s) to obtain the field codec fa
|
||||
// - Validate the input I using fa.Validate
|
||||
// - Set the validated T into S using l.Set
|
||||
//
|
||||
// 3. **Type Checking**: Preserves the base type checker
|
||||
//
|
||||
// # Difference from ApSL
|
||||
//
|
||||
// Unlike ApSL which uses a fixed field codec:
|
||||
// - ApSL: Field codec is fixed at construction time; errors are accumulated
|
||||
// - Bind: Field codec depends on the current struct value (Kleisli arrow); validation
|
||||
// uses monadic sequencing (fail-fast on base failure)
|
||||
// - Bind is more powerful but less parallel than ApSL
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// "github.com/IBM/fp-go/v2/optics/lens"
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// type Config struct {
|
||||
// Mode string
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// modeLens := lens.MakeLens(
|
||||
// func(c Config) string { return c.Mode },
|
||||
// func(c Config, mode string) Config { c.Mode = mode; return c },
|
||||
// )
|
||||
//
|
||||
// // Build a Config codec where the Value codec depends on the Mode
|
||||
// configCodec := F.Pipe1(
|
||||
// codec.Struct[Config]("Config"),
|
||||
// codec.Bind(S.Monoid, modeLens, func(c Config) codec.Type[string, string, any] {
|
||||
// return codec.String()
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Building codecs where a field's codec depends on another field's value
|
||||
// - Implementing discriminated unions or tagged variants
|
||||
// - Context-sensitive validation (e.g., validate field B differently based on field A)
|
||||
// - Dependent type-like patterns in codec construction
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The monoid determines how encoded outputs are combined
|
||||
// - The lens must be total (handle all cases safely)
|
||||
// - Validation uses monadic (fail-fast) sequencing: if the base codec fails,
|
||||
// the Kleisli arrow is never evaluated
|
||||
// - The name is automatically generated for debugging purposes
|
||||
//
|
||||
// See also:
|
||||
// - ApSL: Applicative sequencing with a fixed lens codec (error accumulation)
|
||||
// - Kleisli: The function type from S to Type[T, O, I]
|
||||
// - validate.Bind: The underlying validate-level bind combinator
|
||||
func Bind[S, T, O, I any](
|
||||
m Monoid[O],
|
||||
l Lens[S, T],
|
||||
f Kleisli[S, T, O, I],
|
||||
) Operator[S, S, O, I] {
|
||||
name := fmt.Sprintf("Bind[%s]", l)
|
||||
val := F.Curry2(Type[T, O, I].Validate)
|
||||
|
||||
return func(t Type[S, O, I]) Type[S, O, I] {
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
t.Is,
|
||||
F.Pipe1(
|
||||
t.Validate,
|
||||
validate.Bind(l.Set, F.Flow2(f, val)),
|
||||
),
|
||||
func(s S) O {
|
||||
return m.Concat(t.Encode(s), f(s).Encode(l.Get(s)))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -814,3 +814,588 @@ func TestApSO_ErrorAccumulation(t *testing.T) {
|
||||
assert.NotEmpty(t, errors, "Should have validation errors")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind_EncodingCombination verifies that Bind combines the base encoding with
|
||||
// the field encoding produced by the Kleisli arrow using the monoid.
|
||||
func TestBind_EncodingCombination(t *testing.T) {
|
||||
t.Run("combines base and field encodings using monoid", func(t *testing.T) {
|
||||
// Lens for Person.Name
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
// Base codec encodes to "Person:"
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected Person"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "Person:" },
|
||||
)
|
||||
|
||||
// Kleisli arrow: always returns a string identity codec regardless of struct value
|
||||
kleisli := func(p Person) Type[string, string, any] {
|
||||
return MakeType(
|
||||
"Name",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected string"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
}
|
||||
|
||||
operator := Bind(S.Monoid, nameLens, kleisli)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
encoded := enhancedCodec.Encode(person)
|
||||
|
||||
// Encoding should include both the base prefix and the field value
|
||||
assert.Contains(t, encoded, "Person:")
|
||||
assert.Contains(t, encoded, "Alice")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind_KleisliArrowReceivesCurrentValue verifies that the Kleisli arrow f
|
||||
// receives the current struct value when producing the field codec.
|
||||
func TestBind_KleisliArrowReceivesCurrentValue(t *testing.T) {
|
||||
t.Run("kleisli arrow receives current struct value during encoding", func(t *testing.T) {
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected Person"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
// Kleisli arrow that uses the struct value to produce a prefix in the encoding
|
||||
var capturedPerson Person
|
||||
kleisli := func(p Person) Type[string, string, any] {
|
||||
capturedPerson = p
|
||||
return MakeType(
|
||||
"Name",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected string"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
}
|
||||
|
||||
operator := Bind(S.Monoid, nameLens, kleisli)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
person := Person{Name: "Bob", Age: 25}
|
||||
enhancedCodec.Encode(person)
|
||||
|
||||
// The Kleisli arrow should have been called with the actual struct value
|
||||
assert.Equal(t, person, capturedPerson)
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind_ValidationSuccess verifies that Bind correctly validates and decodes
|
||||
// a struct when both the base and field validations succeed.
|
||||
func TestBind_ValidationSuccess(t *testing.T) {
|
||||
t.Run("succeeds when base and field validations pass", func(t *testing.T) {
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected Person"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
// The field codec receives the same input I (any = Person struct).
|
||||
// It must extract the Name field from the Person input.
|
||||
kleisli := func(p Person) Type[string, string, any] {
|
||||
return MakeType(
|
||||
"Name",
|
||||
func(i any) validation.Result[string] {
|
||||
if person, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(person.Name))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected Person"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if person, ok := i.(Person); ok {
|
||||
return validation.Success(person.Name)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
}
|
||||
|
||||
operator := Bind(S.Monoid, nameLens, kleisli)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
person := Person{Name: "Carol", Age: 28}
|
||||
result := enhancedCodec.Decode(person)
|
||||
|
||||
assert.True(t, either.IsRight(result), "Should succeed when both validations pass")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind_ValidationFailsOnBaseFailure verifies that Bind uses fail-fast (monadic)
|
||||
// semantics: if the base codec fails, the Kleisli arrow is never evaluated.
|
||||
func TestBind_ValidationFailsOnBaseFailure(t *testing.T) {
|
||||
t.Run("fails fast when base validation fails", func(t *testing.T) {
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
// Base codec always fails
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "base always fails"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
return validation.FailureWithMessage[Person](i, "base always fails")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
kleisliCalled := false
|
||||
kleisli := func(p Person) Type[string, string, any] {
|
||||
kleisliCalled = true
|
||||
return MakeType(
|
||||
"Name",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected string"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
}
|
||||
|
||||
operator := Bind(S.Monoid, nameLens, kleisli)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
person := Person{Name: "Dave", Age: 40}
|
||||
result := enhancedCodec.Decode(person)
|
||||
|
||||
assert.True(t, either.IsLeft(result), "Should fail when base validation fails")
|
||||
assert.False(t, kleisliCalled, "Kleisli arrow should NOT be called when base fails")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind_ValidationFailsOnFieldFailure verifies that Bind propagates field
|
||||
// validation errors when the Kleisli arrow's codec fails.
|
||||
func TestBind_ValidationFailsOnFieldFailure(t *testing.T) {
|
||||
t.Run("fails when field validation from kleisli codec fails", func(t *testing.T) {
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
// Base codec succeeds
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected Person"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
// Kleisli arrow returns a codec that always fails regardless of input
|
||||
kleisli := func(p Person) Type[string, string, any] {
|
||||
return MakeType(
|
||||
"Name",
|
||||
func(i any) validation.Result[string] {
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "field always fails"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](i, "field always fails")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
}
|
||||
|
||||
operator := Bind(S.Monoid, nameLens, kleisli)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// The field codec receives the same input (Person) and always fails
|
||||
person := Person{Name: "Eve", Age: 22}
|
||||
result := enhancedCodec.Decode(person)
|
||||
|
||||
assert.True(t, either.IsLeft(result), "Should fail when field validation fails")
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(Person) validation.Errors { return nil },
|
||||
)
|
||||
assert.NotEmpty(t, errors, "Should have validation errors from field codec")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind_TypeCheckingPreserved verifies that Bind preserves the base type checker.
|
||||
func TestBind_TypeCheckingPreserved(t *testing.T) {
|
||||
t.Run("preserves base type checker", func(t *testing.T) {
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected Person"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
kleisli := func(p Person) Type[string, string, any] {
|
||||
return MakeType(
|
||||
"Name",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected string"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
}
|
||||
|
||||
operator := Bind(S.Monoid, nameLens, kleisli)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Valid type
|
||||
person := Person{Name: "Frank", Age: 35}
|
||||
isResult := enhancedCodec.Is(person)
|
||||
assert.True(t, either.IsRight(isResult), "Should accept Person type")
|
||||
|
||||
// Invalid type
|
||||
invalidResult := enhancedCodec.Is("not a person")
|
||||
assert.True(t, either.IsLeft(invalidResult), "Should reject non-Person type")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind_Naming verifies that Bind generates a descriptive name for the codec.
|
||||
func TestBind_Naming(t *testing.T) {
|
||||
t.Run("generates descriptive name containing Bind and lens info", func(t *testing.T) {
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected Person"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
kleisli := func(p Person) Type[string, string, any] {
|
||||
return MakeType(
|
||||
"Name",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected string"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
}
|
||||
|
||||
operator := Bind(S.Monoid, nameLens, kleisli)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
name := enhancedCodec.Name()
|
||||
assert.Contains(t, name, "Bind", "Name should contain 'Bind'")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind_DependentFieldCodec verifies that the Kleisli arrow can produce
|
||||
// different codecs based on the current struct value (the key differentiator
|
||||
// from ApSL).
|
||||
//
|
||||
// The field codec Type[T, O, I] receives the same input I as the base codec.
|
||||
// It must extract the field value from that input. The Kleisli arrow f(s)
|
||||
// produces a different codec depending on the already-decoded struct value s.
|
||||
func TestBind_DependentFieldCodec(t *testing.T) {
|
||||
t.Run("kleisli arrow produces different codecs based on struct value", func(t *testing.T) {
|
||||
// Lens for Person.Name
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
// Base codec succeeds for any Person
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected Person"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
// Kleisli arrow: the field codec receives the same input I (any = Person).
|
||||
// It extracts the Name from the Person input.
|
||||
// If the decoded struct's Age > 18, accept any name (including empty).
|
||||
// If Age <= 18, reject empty names.
|
||||
kleisli := func(p Person) Type[string, string, any] {
|
||||
if p.Age > 18 {
|
||||
// Adult: accept any name extracted from the Person input
|
||||
return MakeType(
|
||||
"AnyName",
|
||||
func(i any) validation.Result[string] {
|
||||
if person, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(person.Name))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected Person"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if person, ok := i.(Person); ok {
|
||||
return validation.Success(person.Name)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
}
|
||||
// Minor: reject empty names
|
||||
return MakeType(
|
||||
"NonEmptyName",
|
||||
func(i any) validation.Result[string] {
|
||||
if person, ok := i.(Person); ok {
|
||||
if person.Name != "" {
|
||||
return validation.ToResult(validation.Success(person.Name))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{Value: person.Name, Messsage: "name must not be empty for minors"},
|
||||
}))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{Value: i, Messsage: "expected Person"},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if person, ok := i.(Person); ok {
|
||||
if person.Name != "" {
|
||||
return validation.Success(person.Name)
|
||||
}
|
||||
return validation.FailureWithMessage[string](person.Name, "name must not be empty for minors")(ctx)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
}
|
||||
|
||||
operator := Bind(S.Monoid, nameLens, kleisli)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Adult (Age=30) with empty name: should succeed (adult codec accepts any name)
|
||||
adultPerson := Person{Name: "", Age: 30}
|
||||
adultResult := enhancedCodec.Decode(adultPerson)
|
||||
assert.True(t, either.IsRight(adultResult), "Adult should accept empty name")
|
||||
|
||||
// Minor (Age=15) with empty name: should fail (minor codec rejects empty names)
|
||||
minorPerson := Person{Name: "", Age: 15}
|
||||
minorResult := enhancedCodec.Decode(minorPerson)
|
||||
assert.True(t, either.IsLeft(minorResult), "Minor with empty name should fail")
|
||||
|
||||
// Minor (Age=15) with non-empty name: should succeed
|
||||
minorWithName := Person{Name: "Junior", Age: 15}
|
||||
minorWithNameResult := enhancedCodec.Decode(minorWithName)
|
||||
assert.True(t, either.IsRight(minorWithNameResult), "Minor with non-empty name should succeed")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -343,6 +343,61 @@ func Int64FromString() Type[int64, string, string] {
|
||||
)
|
||||
}
|
||||
|
||||
// BoolFromString creates a bidirectional codec for parsing boolean values from strings.
|
||||
// This codec converts string representations of booleans to bool values and vice versa.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string to a bool using strconv.ParseBool
|
||||
// - Encodes: Converts a bool to its string representation using strconv.FormatBool
|
||||
// - Validates: Ensures the string contains a valid boolean value
|
||||
//
|
||||
// The codec accepts the following string values (case-insensitive):
|
||||
// - true: "1", "t", "T", "true", "TRUE", "True"
|
||||
// - false: "0", "f", "F", "false", "FALSE", "False"
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[bool, string, string] codec that handles bool/string conversions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// boolCodec := BoolFromString()
|
||||
//
|
||||
// // Decode valid boolean strings
|
||||
// validation := boolCodec.Decode("true")
|
||||
// // validation is Right(true)
|
||||
//
|
||||
// validation := boolCodec.Decode("1")
|
||||
// // validation is Right(true)
|
||||
//
|
||||
// validation := boolCodec.Decode("false")
|
||||
// // validation is Right(false)
|
||||
//
|
||||
// validation := boolCodec.Decode("0")
|
||||
// // validation is Right(false)
|
||||
//
|
||||
// // Encode a boolean to string
|
||||
// str := boolCodec.Encode(true)
|
||||
// // str is "true"
|
||||
//
|
||||
// str := boolCodec.Encode(false)
|
||||
// // str is "false"
|
||||
//
|
||||
// // Invalid boolean string fails validation
|
||||
// validation := boolCodec.Decode("yes")
|
||||
// // validation is Left(ValidationError{...})
|
||||
//
|
||||
// // Case variations are accepted
|
||||
// validation := boolCodec.Decode("TRUE")
|
||||
// // validation is Right(true)
|
||||
func BoolFromString() Type[bool, string, string] {
|
||||
return MakeType(
|
||||
"BoolFromString",
|
||||
Is[bool](),
|
||||
validateFromParser(strconv.ParseBool),
|
||||
strconv.FormatBool,
|
||||
)
|
||||
}
|
||||
|
||||
func decodeJSON[T any](dec json.Unmarshaler) ReaderResult[[]byte, T] {
|
||||
return func(b []byte) Result[T] {
|
||||
var t T
|
||||
|
||||
@@ -461,6 +461,233 @@ func TestInt64FromString_Name(t *testing.T) {
|
||||
assert.Equal(t, "Int64FromString", Int64FromString().Name())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BoolFromString
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestBoolFromString_Decode_Success(t *testing.T) {
|
||||
t.Run("decodes 'true' string", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("true")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'false' string", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("false")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
|
||||
t.Run("decodes '1' as true", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("1")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes '0' as false", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("0")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 't' as true", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("t")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'f' as false", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("f")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'T' as true", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("T")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'F' as false", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("F")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'TRUE' as true", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("TRUE")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'FALSE' as false", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("FALSE")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'True' as true", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("True")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'False' as false", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("False")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBoolFromString_Decode_Failure(t *testing.T) {
|
||||
t.Run("fails on 'yes'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("yes")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on 'no'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("no")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on empty string", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on numeric string other than 0 or 1", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("2")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on arbitrary text", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("not a boolean")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on whitespace", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode(" ")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on 'true' with leading/trailing spaces", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode(" true ")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBoolFromString_Encode(t *testing.T) {
|
||||
t.Run("encodes true to 'true'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
assert.Equal(t, "true", c.Encode(true))
|
||||
})
|
||||
|
||||
t.Run("encodes false to 'false'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
assert.Equal(t, "false", c.Encode(false))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode 'true' then encode", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("true")
|
||||
require.True(t, either.IsRight(result))
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return false }, func(b bool) bool { return b })
|
||||
assert.Equal(t, "true", c.Encode(b))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode 'false' then encode", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("false")
|
||||
require.True(t, either.IsRight(result))
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return true }, func(b bool) bool { return b })
|
||||
assert.Equal(t, "false", c.Encode(b))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode '1' encodes as 'true'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("1")
|
||||
require.True(t, either.IsRight(result))
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return false }, func(b bool) bool { return b })
|
||||
// Note: strconv.FormatBool always returns "true" or "false", not "1" or "0"
|
||||
assert.Equal(t, "true", c.Encode(b))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode '0' encodes as 'false'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("0")
|
||||
require.True(t, either.IsRight(result))
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return true }, func(b bool) bool { return b })
|
||||
assert.Equal(t, "false", c.Encode(b))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBoolFromString_EdgeCases(t *testing.T) {
|
||||
t.Run("case sensitivity variations", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
cases := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"true", true},
|
||||
{"True", true},
|
||||
{"TRUE", true},
|
||||
{"false", false},
|
||||
{"False", false},
|
||||
{"FALSE", false},
|
||||
{"t", true},
|
||||
{"T", true},
|
||||
{"f", false},
|
||||
{"F", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
result := c.Decode(tc.input)
|
||||
require.True(t, either.IsRight(result), "expected success for %s", tc.input)
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return !tc.expected }, func(b bool) bool { return b })
|
||||
assert.Equal(t, tc.expected, b, "input: %s", tc.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBoolFromString_Name(t *testing.T) {
|
||||
assert.Equal(t, "BoolFromString", BoolFromString().Name())
|
||||
}
|
||||
|
||||
func TestBoolFromString_Integration(t *testing.T) {
|
||||
t.Run("decodes and encodes multiple boolean values", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
cases := []struct {
|
||||
str string
|
||||
val bool
|
||||
}{
|
||||
{"true", true},
|
||||
{"false", false},
|
||||
{"1", true},
|
||||
{"0", false},
|
||||
{"T", true},
|
||||
{"F", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
result := c.Decode(tc.str)
|
||||
require.True(t, either.IsRight(result), "expected success for %s", tc.str)
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return !tc.val }, func(b bool) bool { return b })
|
||||
assert.Equal(t, tc.val, b)
|
||||
// Note: encoding always produces "true" or "false", not the original input
|
||||
if tc.val {
|
||||
assert.Equal(t, "true", c.Encode(b))
|
||||
} else {
|
||||
assert.Equal(t, "false", c.Encode(b))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MarshalJSON
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -259,5 +259,42 @@ type (
|
||||
// result := LetL(lens, double)(Success(21)) // Success(42)
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Lazy represents a lazily-evaluated value of type A.
|
||||
// This is an alias for lazy.Lazy[A], which defers computation until the value is needed.
|
||||
//
|
||||
// In the validation context, Lazy is used to defer expensive validation operations
|
||||
// or to break circular dependencies in validation logic.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lazyValidation := lazy.Of(func() Validation[int] {
|
||||
// // Expensive validation logic here
|
||||
// return Success(42)
|
||||
// })
|
||||
// // Validation is not executed until lazyValidation() is called
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// ErrorsProvider is an interface for types that can provide a collection of errors.
|
||||
// This interface allows validation errors to be extracted from various error types
|
||||
// in a uniform way, supporting error aggregation and reporting.
|
||||
//
|
||||
// Types implementing this interface can be unwrapped to access their underlying
|
||||
// error collection, enabling consistent error handling across different error types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type MyErrors struct {
|
||||
// errs []error
|
||||
// }
|
||||
//
|
||||
// func (e *MyErrors) Errors() []error {
|
||||
// return e.errs
|
||||
// }
|
||||
//
|
||||
// // Usage
|
||||
// var provider ErrorsProvider = &MyErrors{errs: []error{...}}
|
||||
// allErrors := provider.Errors()
|
||||
ErrorsProvider interface {
|
||||
Errors() []error
|
||||
}
|
||||
)
|
||||
|
||||
@@ -134,6 +134,10 @@ func (ve *validationErrors) Unwrap() error {
|
||||
return ve.cause
|
||||
}
|
||||
|
||||
func (ve *validationErrors) Errors() []error {
|
||||
return ve.Errors()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user