1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-03-04 13:21:02 +02:00

Compare commits

..

5 Commits

Author SHA1 Message Date
Dr. Carsten Leue
e4dd1169c4 fix: recursion in Errors()
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-04 11:12:22 +01:00
Dr. Carsten Leue
1657569f1d Merge branch 'main' of github.com:IBM/fp-go 2026-03-04 10:31:04 +01:00
Dr. Carsten Leue
545876d013 fix: add bool codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-04 10:30:55 +01:00
renovate[bot]
9492c5d994 chore(deps): update actions/setup-node action to v6.3.0 (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 09:20:04 +00:00
Dr. Carsten Leue
94b1ea30d1 fix: improved doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-02 13:23:59 +01:00
7 changed files with 614 additions and 66 deletions

View File

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

View File

@@ -20,13 +20,89 @@ import (
F "github.com/IBM/fp-go/v2/function"
"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/option"
"github.com/IBM/fp-go/v2/reader"
"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,
@@ -403,48 +479,26 @@ func ApSO[S, T, O, I any](
// See also:
// - ApSL: Applicative sequencing with a fixed lens codec (error accumulation)
// - Kleisli: The function type from S to Type[T, O, I]
// - decode.Bind: The underlying decode-level bind combinator
// - 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 := reader.Curry(Type[T, O, I].Validate)
rm := reader.ApplicativeMonoid[S](m)
val := F.Curry2(Type[T, O, I].Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
func(i I) Decode[validate.Context, S] {
bind := decode.Bind(
l.Set,
F.Flow2(
f,
val(i),
),
)
return F.Pipe2(
i,
t.Validate,
bind,
)
},
F.Pipe1(
t.Validate,
validate.Bind(l.Set, F.Flow2(f, val)),
),
func(s S) O {
fa := f(s)
encConcat := F.Pipe1(
F.Flow2(
l.Get,
fa.Encode,
),
semigroup.AppendTo(rm),
)
return encConcat(t.Encode)(s)
return m.Concat(t.Encode(s), f(s).Encode(l.Get(s)))
},
)
}

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package validation
import (
"fmt"
"log/slog"
"strings"
A "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/either"
@@ -12,6 +13,11 @@ import (
// Returns a generic error message indicating this is a validation error.
// For detailed error information, use String() or Format() methods.
// toError converts the validation error to the error interface
func toError(v *ValidationError) error {
return v
}
// Error implements the error interface for ValidationError.
// Returns a generic error message.
func (v *ValidationError) Error() string {
@@ -34,44 +40,45 @@ func (v *ValidationError) String() string {
// It includes the context path, message, and optionally the cause error.
// Supports verbs: %s, %v, %+v (with additional details)
func (v *ValidationError) Format(s fmt.State, verb rune) {
// Build the context path
path := ""
for i, entry := range v.Context {
if i > 0 {
path += "."
}
if entry.Key != "" {
path += entry.Key
} else {
path += entry.Type
}
}
var result strings.Builder
// Start with the path if available
result := ""
if path != "" {
result = fmt.Sprintf("at %s: ", path)
// Build the context path
if len(v.Context) > 0 {
var path strings.Builder
for i, entry := range v.Context {
if i > 0 {
path.WriteString(".")
}
if entry.Key != "" {
path.WriteString(entry.Key)
} else {
path.WriteString(entry.Type)
}
}
result.WriteString("at ")
result.WriteString(path.String())
result.WriteString(": ")
}
// Add the message
result += v.Messsage
result.WriteString(v.Messsage)
// Add the cause if present
if v.Cause != nil {
if s.Flag('+') && verb == 'v' {
// Verbose format with detailed cause
result += fmt.Sprintf("\n caused by: %+v", v.Cause)
fmt.Fprintf(&result, "\n caused by: %+v", v.Cause)
} else {
result += fmt.Sprintf(" (caused by: %v)", v.Cause)
fmt.Fprintf(&result, " (caused by: %v)", v.Cause)
}
}
// Add value information for verbose format
if s.Flag('+') && verb == 'v' {
result += fmt.Sprintf("\n value: %#v", v.Value)
fmt.Fprintf(&result, "\n value: %#v", v.Value)
}
fmt.Fprint(s, result)
fmt.Fprint(s, result.String())
}
// LogValue implements the slog.LogValuer interface for ValidationError.
@@ -94,18 +101,18 @@ func (v *ValidationError) LogValue() slog.Value {
// Add context path if available
if len(v.Context) > 0 {
path := ""
var path strings.Builder
for i, entry := range v.Context {
if i > 0 {
path += "."
path.WriteString(".")
}
if entry.Key != "" {
path += entry.Key
path.WriteString(entry.Key)
} else {
path += entry.Type
path.WriteString(entry.Type)
}
}
attrs = append(attrs, slog.String("path", path))
attrs = append(attrs, slog.String("path", path.String()))
}
// Add cause if present
@@ -119,13 +126,14 @@ func (v *ValidationError) LogValue() slog.Value {
// 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 {
switch len(ve.errors) {
case 0:
return "ValidationErrors: no errors"
}
if len(ve.errors) == 1 {
case 1:
return "ValidationErrors: 1 error"
default:
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.
@@ -134,6 +142,33 @@ func (ve *validationErrors) Unwrap() error {
return ve.cause
}
// Errors implements the ErrorsProvider interface for validationErrors.
// It converts the internal collection of ValidationError pointers to a slice of error interfaces.
// This method enables uniform error extraction from validation error collections.
//
// The returned slice contains the same errors as the internal errors field,
// but typed as error interface values for compatibility with standard Go error handling.
//
// Returns:
// - A slice of error interfaces, one for each ValidationError in the collection
//
// Example:
//
// ve := &validationErrors{
// errors: Errors{
// &ValidationError{Messsage: "invalid email"},
// &ValidationError{Messsage: "age must be positive"},
// },
// }
// errs := ve.Errors()
// // errs is []error with 2 elements, each implementing the error interface
// for _, err := range errs {
// fmt.Println(err.Error()) // "ValidationError"
// }
func (ve *validationErrors) Errors() []error {
return A.MonadMap(ve.errors, toError)
}
// 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 {
@@ -141,16 +176,17 @@ func (ve *validationErrors) String() string {
return "ValidationErrors: no errors"
}
result := fmt.Sprintf("ValidationErrors (%d):\n", len(ve.errors))
var result strings.Builder
fmt.Fprintf(&result, "ValidationErrors (%d):\n", len(ve.errors))
for i, err := range ve.errors {
result += fmt.Sprintf(" [%d] %s\n", i, err.String())
fmt.Fprintf(&result, " [%d] %s\n", i, err.String())
}
if ve.cause != nil {
result += fmt.Sprintf(" caused by: %v\n", ve.cause)
fmt.Fprintf(&result, " caused by: %v\n", ve.cause)
}
return result
return result.String()
}
// Format implements fmt.Formatter for custom formatting of ValidationErrors.

View File

@@ -846,3 +846,142 @@ func TestLogValuerInterface(t *testing.T) {
var _ slog.LogValuer = (*validationErrors)(nil)
})
}
// TestValidationErrors_Errors tests the Errors() method implementation
func TestValidationErrors_Errors(t *testing.T) {
t.Run("returns empty slice for no errors", func(t *testing.T) {
ve := &validationErrors{
errors: Errors{},
}
errs := ve.Errors()
assert.Empty(t, errs)
assert.NotNil(t, errs)
})
t.Run("converts single ValidationError to error interface", func(t *testing.T) {
ve := &validationErrors{
errors: Errors{
&ValidationError{Value: "test", Messsage: "invalid value"},
},
}
errs := ve.Errors()
require.Len(t, errs, 1)
assert.Equal(t, "ValidationError", errs[0].Error())
})
t.Run("converts multiple ValidationErrors to error interfaces", 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"},
},
}
errs := ve.Errors()
require.Len(t, errs, 3)
for _, err := range errs {
assert.Equal(t, "ValidationError", err.Error())
}
})
t.Run("preserves error details in converted errors", func(t *testing.T) {
originalErr := &ValidationError{
Value: "abc",
Context: []ContextEntry{{Key: "field"}},
Messsage: "invalid format",
Cause: errors.New("parse error"),
}
ve := &validationErrors{
errors: Errors{originalErr},
}
errs := ve.Errors()
require.Len(t, errs, 1)
// Verify the error can be type-asserted back to ValidationError
validationErr, ok := errs[0].(*ValidationError)
require.True(t, ok)
assert.Equal(t, "abc", validationErr.Value)
assert.Equal(t, "invalid format", validationErr.Messsage)
assert.NotNil(t, validationErr.Cause)
assert.Len(t, validationErr.Context, 1)
})
t.Run("implements ErrorsProvider interface", func(t *testing.T) {
ve := &validationErrors{
errors: Errors{
&ValidationError{Messsage: "error 1"},
&ValidationError{Messsage: "error 2"},
},
}
// Verify it implements ErrorsProvider
var provider ErrorsProvider = ve
errs := provider.Errors()
assert.Len(t, errs, 2)
})
t.Run("returned errors are usable with standard error handling", func(t *testing.T) {
cause := errors.New("underlying error")
ve := &validationErrors{
errors: Errors{
&ValidationError{
Value: "test",
Messsage: "validation failed",
Cause: cause,
},
},
}
errs := ve.Errors()
require.Len(t, errs, 1)
// Test with errors.Is
assert.True(t, errors.Is(errs[0], cause))
// Test with errors.As
var validationErr *ValidationError
assert.True(t, errors.As(errs[0], &validationErr))
assert.Equal(t, "validation failed", validationErr.Messsage)
})
t.Run("does not modify original errors slice", func(t *testing.T) {
originalErrors := Errors{
&ValidationError{Value: "test1", Messsage: "error 1"},
&ValidationError{Value: "test2", Messsage: "error 2"},
}
ve := &validationErrors{
errors: originalErrors,
}
errs := ve.Errors()
require.Len(t, errs, 2)
// Original should be unchanged
assert.Len(t, ve.errors, 2)
assert.Equal(t, originalErrors, ve.errors)
})
t.Run("each error in slice is independent", func(t *testing.T) {
ve := &validationErrors{
errors: Errors{
&ValidationError{Value: "test1", Messsage: "error 1"},
&ValidationError{Value: "test2", Messsage: "error 2"},
},
}
errs := ve.Errors()
require.Len(t, errs, 2)
// Verify each error is distinct
err1, ok1 := errs[0].(*ValidationError)
err2, ok2 := errs[1].(*ValidationError)
require.True(t, ok1)
require.True(t, ok2)
assert.NotEqual(t, err1.Messsage, err2.Messsage)
assert.NotEqual(t, err1.Value, err2.Value)
})
}