mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-06 13:23:39 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1657569f1d | ||
|
|
545876d013 | ||
|
|
9492c5d994 |
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 }}
|
||||
|
||||
|
||||
@@ -479,7 +479,7 @@ 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],
|
||||
|
||||
@@ -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