mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-24 12:57:26 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a276f3acff | ||
|
|
8c656a4297 | ||
|
|
bd9a642e93 |
99
v2/llms.txt
Normal file
99
v2/llms.txt
Normal file
@@ -0,0 +1,99 @@
|
||||
# fp-go
|
||||
|
||||
> A comprehensive functional programming library for Go, bringing type-safe monads, functors, applicatives, optics, and composable abstractions inspired by fp-ts and Haskell to the Go ecosystem. Created by IBM, licensed under Apache-2.0.
|
||||
|
||||
fp-go v2 requires Go 1.24+ and leverages generic type aliases for a cleaner API.
|
||||
|
||||
Key concepts: `Option` for nullable values, `Either`/`Result` for error handling, `IO` for lazy side effects, `Reader` for dependency injection, `IOResult` for effectful error handling, `ReaderIOResult` for the full monad stack, and `Optics` (lens, prism, traversal, iso) for immutable data manipulation.
|
||||
|
||||
## Core Documentation
|
||||
|
||||
- [API Reference (pkg.go.dev)](https://pkg.go.dev/github.com/IBM/fp-go/v2): Complete API documentation for all packages
|
||||
- [README](https://github.com/IBM/fp-go/blob/main/v2/README.md): Overview, quick start, installation, and migration guide from v1 to v2
|
||||
- [Design Decisions](https://github.com/IBM/fp-go/blob/main/v2/DESIGN.md): Key design principles and patterns
|
||||
- [Functional I/O Guide](https://github.com/IBM/fp-go/blob/main/v2/FUNCTIONAL_IO.md): Understanding Context, errors, and the Reader pattern for I/O operations
|
||||
- [Idiomatic vs Standard Comparison](https://github.com/IBM/fp-go/blob/main/v2/IDIOMATIC_COMPARISON.md): Performance comparison and when to use each approach
|
||||
- [Optics README](https://github.com/IBM/fp-go/blob/main/v2/optics/README.md): Guide to lens, prism, optional, and traversal optics
|
||||
|
||||
## Standard Packages (struct-based)
|
||||
|
||||
- [option](https://pkg.go.dev/github.com/IBM/fp-go/v2/option): Option monad — represent optional values without nil
|
||||
- [either](https://pkg.go.dev/github.com/IBM/fp-go/v2/either): Either monad — type-safe error handling with Left/Right values
|
||||
- [result](https://pkg.go.dev/github.com/IBM/fp-go/v2/result): Result monad — simplified Either with `error` as Left type (recommended for error handling)
|
||||
- [io](https://pkg.go.dev/github.com/IBM/fp-go/v2/io): IO monad — lazy evaluation and side effect management
|
||||
- [iooption](https://pkg.go.dev/github.com/IBM/fp-go/v2/iooption): IOOption — IO combined with Option
|
||||
- [ioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/ioeither): IOEither — IO combined with Either for effectful error handling
|
||||
- [ioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/ioresult): IOResult — IO combined with Result (recommended over IOEither)
|
||||
- [reader](https://pkg.go.dev/github.com/IBM/fp-go/v2/reader): Reader monad — dependency injection pattern
|
||||
- [readeroption](https://pkg.go.dev/github.com/IBM/fp-go/v2/readeroption): ReaderOption — Reader combined with Option
|
||||
- [readeriooption](https://pkg.go.dev/github.com/IBM/fp-go/v2/readeriooption): ReaderIOOption — Reader + IO + Option
|
||||
- [readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/readerioresult): ReaderIOResult — Reader + IO + Result for complex workflows
|
||||
- [readerioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/readerioeither): ReaderIOEither — Reader + IO + Either
|
||||
- [statereaderioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/statereaderioeither): StateReaderIOEither — State + Reader + IO + Either
|
||||
|
||||
## Idiomatic Packages (tuple-based, high performance)
|
||||
|
||||
- [idiomatic/option](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/option): Option using native Go `(value, bool)` tuples
|
||||
- [idiomatic/result](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/result): Result using native Go `(value, error)` tuples
|
||||
- [idiomatic/ioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/ioresult): IOResult using `func() (value, error)`
|
||||
- [idiomatic/readerresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/readerresult): ReaderResult with tuple-based results
|
||||
- [idiomatic/readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/readerioresult): ReaderIOResult with tuple-based results
|
||||
|
||||
## Context Packages (context.Context specializations)
|
||||
|
||||
- [context/readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult): ReaderIOResult specialized for context.Context
|
||||
- [context/readerioresult/http](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult/http): Functional HTTP client utilities
|
||||
- [context/readerioresult/http/builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult/http/builder): Functional HTTP request builder
|
||||
- [context/statereaderioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/statereaderioresult): State + Reader + IO + Result for context.Context
|
||||
|
||||
## Optics
|
||||
|
||||
- [optics](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics): Core optics package
|
||||
- [optics/lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens): Lenses for focusing on fields in product types
|
||||
- [optics/prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism): Prisms for focusing on variants in sum types
|
||||
- [optics/iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso): Isomorphisms for bidirectional transformations
|
||||
- [optics/optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional): Optionals for values that may not exist
|
||||
- [optics/traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal): Traversals for focusing on multiple values
|
||||
- [optics/codec](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/codec): Codecs for encoding/decoding with validation
|
||||
|
||||
## Utility Packages
|
||||
|
||||
- [array](https://pkg.go.dev/github.com/IBM/fp-go/v2/array): Functional array/slice operations (map, filter, fold, etc.)
|
||||
- [record](https://pkg.go.dev/github.com/IBM/fp-go/v2/record): Functional operations for maps
|
||||
- [function](https://pkg.go.dev/github.com/IBM/fp-go/v2/function): Function composition, pipe, flow, curry, identity
|
||||
- [pair](https://pkg.go.dev/github.com/IBM/fp-go/v2/pair): Strongly-typed pair/tuple data structure
|
||||
- [tuple](https://pkg.go.dev/github.com/IBM/fp-go/v2/tuple): Type-safe heterogeneous tuples
|
||||
- [predicate](https://pkg.go.dev/github.com/IBM/fp-go/v2/predicate): Predicate combinators (and, or, not, etc.)
|
||||
- [endomorphism](https://pkg.go.dev/github.com/IBM/fp-go/v2/endomorphism): Endomorphism operations (compose, chain)
|
||||
- [eq](https://pkg.go.dev/github.com/IBM/fp-go/v2/eq): Type-safe equality comparisons
|
||||
- [ord](https://pkg.go.dev/github.com/IBM/fp-go/v2/ord): Total ordering type class
|
||||
- [semigroup](https://pkg.go.dev/github.com/IBM/fp-go/v2/semigroup): Semigroup algebraic structure
|
||||
- [monoid](https://pkg.go.dev/github.com/IBM/fp-go/v2/monoid): Monoid algebraic structure
|
||||
- [number](https://pkg.go.dev/github.com/IBM/fp-go/v2/number): Algebraic structures for numeric types
|
||||
- [string](https://pkg.go.dev/github.com/IBM/fp-go/v2/string): Functional string utilities
|
||||
- [boolean](https://pkg.go.dev/github.com/IBM/fp-go/v2/boolean): Functional boolean utilities
|
||||
- [bytes](https://pkg.go.dev/github.com/IBM/fp-go/v2/bytes): Functional byte slice utilities
|
||||
- [json](https://pkg.go.dev/github.com/IBM/fp-go/v2/json): Functional JSON encoding/decoding
|
||||
- [lazy](https://pkg.go.dev/github.com/IBM/fp-go/v2/lazy): Lazy evaluation without side effects
|
||||
- [identity](https://pkg.go.dev/github.com/IBM/fp-go/v2/identity): Identity monad
|
||||
- [retry](https://pkg.go.dev/github.com/IBM/fp-go/v2/retry): Retry policies with configurable backoff
|
||||
- [tailrec](https://pkg.go.dev/github.com/IBM/fp-go/v2/tailrec): Trampoline for tail-call optimization
|
||||
- [di](https://pkg.go.dev/github.com/IBM/fp-go/v2/di): Dependency injection utilities
|
||||
- [effect](https://pkg.go.dev/github.com/IBM/fp-go/v2/effect): Functional effect system
|
||||
- [circuitbreaker](https://pkg.go.dev/github.com/IBM/fp-go/v2/circuitbreaker): Circuit breaker error types
|
||||
- [builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/builder): Generic builder pattern with validation
|
||||
|
||||
## Code Samples
|
||||
|
||||
- [samples/builder](https://github.com/IBM/fp-go/tree/main/v2/samples/builder): Functional builder pattern example
|
||||
- [samples/http](https://github.com/IBM/fp-go/tree/main/v2/samples/http): HTTP client examples
|
||||
- [samples/lens](https://github.com/IBM/fp-go/tree/main/v2/samples/lens): Optics/lens examples
|
||||
- [samples/mostly-adequate](https://github.com/IBM/fp-go/tree/main/v2/samples/mostly-adequate): Examples adapted from "Mostly Adequate Guide to Functional Programming"
|
||||
- [samples/tuples](https://github.com/IBM/fp-go/tree/main/v2/samples/tuples): Tuple usage examples
|
||||
|
||||
## Optional
|
||||
|
||||
- [Source Code](https://github.com/IBM/fp-go): GitHub repository
|
||||
- [Issues](https://github.com/IBM/fp-go/issues): Bug reports and feature requests
|
||||
- [Go Report Card](https://goreportcard.com/report/github.com/IBM/fp-go/v2): Code quality report
|
||||
- [Coverage](https://coveralls.io/github/IBM/fp-go?branch=main): Test coverage report
|
||||
480
v2/optics/codec/alt.go
Normal file
480
v2/optics/codec/alt.go
Normal file
@@ -0,0 +1,480 @@
|
||||
// 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 codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// validateAlt creates a validation function that tries the first codec's validation,
|
||||
// and if it fails, tries the second codec's validation as a fallback.
|
||||
//
|
||||
// This is an internal helper function that implements the Alternative pattern for
|
||||
// codec validation. It combines two codec validators using the validate.Alt operation,
|
||||
// which provides error recovery and fallback logic.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that both codecs decode to
|
||||
// - O: The output type that both codecs encode to
|
||||
// - I: The input type that both codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - first: The primary codec whose validation is tried first
|
||||
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
|
||||
// first validation fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, A] function that tries the first codec's validation, falling back
|
||||
// to the second if needed. If both fail, errors from both are aggregated.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The second codec is lazily evaluated for efficiency
|
||||
// - This function is used internally by MonadAlt and Alt
|
||||
// - The validation context is threaded through both validators
|
||||
// - Errors are accumulated using the validation error monoid
|
||||
func validateAlt[A, O, I any](
|
||||
first Type[A, O, I],
|
||||
second Lazy[Type[A, O, I]],
|
||||
) Validate[I, A] {
|
||||
|
||||
return F.Pipe1(
|
||||
first.Validate,
|
||||
validate.Alt(F.Pipe1(
|
||||
second,
|
||||
lazy.Map(F.Flip(reader.Curry(Type[A, O, I].Validate))),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAlt creates a new codec that tries the first codec, and if it fails during
|
||||
// validation, tries the second codec as a fallback.
|
||||
//
|
||||
// This function implements the Alternative typeclass pattern for codecs, enabling
|
||||
// "try this codec, or else try that codec" logic. It's particularly useful for:
|
||||
// - Handling multiple valid input formats
|
||||
// - Providing backward compatibility with legacy formats
|
||||
// - Implementing graceful degradation in parsing
|
||||
// - Supporting union types or polymorphic data
|
||||
//
|
||||
// The resulting codec uses the first codec's encoder and combines both validators
|
||||
// using the Alternative pattern. If both validations fail, errors from both are
|
||||
// aggregated for comprehensive error reporting.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that both codecs decode to
|
||||
// - O: The output type that both codecs encode to
|
||||
// - I: The input type that both codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - first: The primary codec to try first. Its encoder is used for the result.
|
||||
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
|
||||
// first validation fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A new Type[A, O, I] that combines both codecs with Alternative semantics.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// **Validation**:
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
//
|
||||
// **Encoding**:
|
||||
// - Always uses the first codec's encoder
|
||||
// - This assumes both codecs encode to the same output format
|
||||
//
|
||||
// **Type Checking**:
|
||||
// - Uses the generic Is[A]() type checker
|
||||
// - Validates that values are of type A
|
||||
//
|
||||
// # Example: Multiple Input Formats
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// )
|
||||
//
|
||||
// // Accept integers as either strings or numbers
|
||||
// intFromString := codec.IntFromString()
|
||||
// intFromNumber := codec.Int()
|
||||
//
|
||||
// // Try parsing as string first, fall back to number
|
||||
// flexibleInt := codec.MonadAlt(
|
||||
// intFromString,
|
||||
// func() codec.Type[int, any, any] { return intFromNumber },
|
||||
// )
|
||||
//
|
||||
// // Can now decode both "42" and 42
|
||||
// result1 := flexibleInt.Decode("42") // Success(42)
|
||||
// result2 := flexibleInt.Decode(42) // Success(42)
|
||||
//
|
||||
// # Example: Backward Compatibility
|
||||
//
|
||||
// // Support both old and new configuration formats
|
||||
// newConfigCodec := codec.Struct(/* new format */)
|
||||
// oldConfigCodec := codec.Struct(/* old format */)
|
||||
//
|
||||
// // Try new format first, fall back to old format
|
||||
// configCodec := codec.MonadAlt(
|
||||
// newConfigCodec,
|
||||
// func() codec.Type[Config, any, any] { return oldConfigCodec },
|
||||
// )
|
||||
//
|
||||
// // Automatically handles both formats
|
||||
// config := configCodec.Decode(input)
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// // Both validations will fail for invalid input
|
||||
// result := flexibleInt.Decode("not a number")
|
||||
// // Result contains errors from both string and number parsing attempts
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The second codec is lazily evaluated for efficiency
|
||||
// - First success short-circuits evaluation (second not called)
|
||||
// - Errors are aggregated when both fail
|
||||
// - The resulting codec's name is "Alt[<first codec name>]"
|
||||
// - Both codecs must have compatible input and output types
|
||||
// - The first codec's encoder is always used
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Alt: The curried, point-free version
|
||||
// - validate.MonadAlt: The underlying validation operation
|
||||
// - Either: For codecs that decode to Either[L, R] types
|
||||
func MonadAlt[A, O, I any](first Type[A, O, I], second Lazy[Type[A, O, I]]) Type[A, O, I] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("Alt[%s]", first.Name()),
|
||||
Is[A](),
|
||||
validateAlt(first, second),
|
||||
first.Encode,
|
||||
)
|
||||
}
|
||||
|
||||
// Alt creates an operator that adds alternative fallback logic to a codec.
|
||||
//
|
||||
// This is the curried, point-free version of MonadAlt. It returns a function that
|
||||
// can be applied to codecs to add fallback behavior. This style is particularly
|
||||
// useful for building codec transformation pipelines using function composition.
|
||||
//
|
||||
// Alt implements the Alternative typeclass pattern, enabling "try this codec, or
|
||||
// else try that codec" logic in a composable way.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that both codecs decode to
|
||||
// - O: The output type that both codecs encode to
|
||||
// - I: The input type that both codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
|
||||
// first codec's validation fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[A, A, O, I] that transforms codecs by adding alternative fallback logic.
|
||||
// This operator can be applied to any Type[A, O, I] to create a new codec with
|
||||
// fallback behavior.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// When the returned operator is applied to a codec:
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
//
|
||||
// # Example: Point-Free Style
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// )
|
||||
//
|
||||
// // Create a reusable fallback operator
|
||||
// withNumberFallback := codec.Alt(func() codec.Type[int, any, any] {
|
||||
// return codec.Int()
|
||||
// })
|
||||
//
|
||||
// // Apply it to different codecs
|
||||
// flexibleInt1 := withNumberFallback(codec.IntFromString())
|
||||
// flexibleInt2 := withNumberFallback(codec.IntFromHex())
|
||||
//
|
||||
// # Example: Pipeline Composition
|
||||
//
|
||||
// // Build a codec pipeline with multiple fallbacks
|
||||
// flexibleCodec := F.Pipe2(
|
||||
// primaryCodec,
|
||||
// codec.Alt(func() codec.Type[T, O, I] { return fallback1 }),
|
||||
// codec.Alt(func() codec.Type[T, O, I] { return fallback2 }),
|
||||
// )
|
||||
// // Tries primary, then fallback1, then fallback2
|
||||
//
|
||||
// # Example: Reusable Transformations
|
||||
//
|
||||
// // Create a transformation that adds JSON fallback
|
||||
// withJSONFallback := codec.Alt(func() codec.Type[Config, string, string] {
|
||||
// return codec.JSONCodec[Config]()
|
||||
// })
|
||||
//
|
||||
// // Apply to multiple codecs
|
||||
// yamlWithFallback := withJSONFallback(yamlCodec)
|
||||
// tomlWithFallback := withJSONFallback(tomlCodec)
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - This is the point-free style version of MonadAlt
|
||||
// - Useful for building transformation pipelines with F.Pipe
|
||||
// - The second codec is lazily evaluated for efficiency
|
||||
// - First success short-circuits evaluation
|
||||
// - Errors are aggregated when both fail
|
||||
// - Can be composed with other codec operators
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - MonadAlt: The direct application version
|
||||
// - validate.Alt: The underlying validation operation
|
||||
// - F.Pipe: For composing multiple operators
|
||||
func Alt[A, O, I any](second Lazy[Type[A, O, I]]) Operator[A, A, O, I] {
|
||||
return F.Bind2nd(MonadAlt, second)
|
||||
}
|
||||
|
||||
// AltMonoid creates a Monoid instance for Type[A, O, I] using alternative semantics
|
||||
// with a provided zero/default codec.
|
||||
//
|
||||
// This function creates a monoid where:
|
||||
// 1. The first successful codec wins (no result combination)
|
||||
// 2. If the first fails during validation, the second is tried as a fallback
|
||||
// 3. If both fail, errors are aggregated
|
||||
// 4. The provided zero codec serves as the identity element
|
||||
//
|
||||
// Unlike other monoid patterns, AltMonoid does NOT combine successful results - it always
|
||||
// returns the first success. This makes it ideal for building fallback chains with default
|
||||
// codecs, configuration loading from multiple sources, and parser combinators with alternatives.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that all codecs decode to
|
||||
// - O: The output type that all codecs encode to
|
||||
// - I: The input type that all codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - zero: A lazy Type[A, O, I] that serves as the identity element. This is typically
|
||||
// a codec that always succeeds with a default value, but can also be a failing
|
||||
// codec if no default is appropriate.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Monoid[Type[A, O, I]] that combines codecs using alternative semantics where
|
||||
// the first success wins.
|
||||
//
|
||||
// # Behavior Details
|
||||
//
|
||||
// The AltMonoid implements a "first success wins" strategy:
|
||||
//
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
// - **Concat with Empty**: The zero codec is used as fallback
|
||||
// - **Encoding**: Always uses the first codec's encoder
|
||||
//
|
||||
// # Example: Configuration Loading with Fallbacks
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// "github.com/IBM/fp-go/v2/array"
|
||||
// )
|
||||
//
|
||||
// // Create a monoid with a default configuration
|
||||
// m := codec.AltMonoid(func() codec.Type[Config, string, string] {
|
||||
// return codec.MakeType(
|
||||
// "DefaultConfig",
|
||||
// codec.Is[Config](),
|
||||
// func(s string) codec.Decode[codec.Context, Config] {
|
||||
// return func(c codec.Context) codec.Validation[Config] {
|
||||
// return validation.Success(defaultConfig)
|
||||
// }
|
||||
// },
|
||||
// encodeConfig,
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// // Define codecs for different sources
|
||||
// fileCodec := loadFromFile("config.json")
|
||||
// envCodec := loadFromEnv()
|
||||
// defaultCodec := m.Empty()
|
||||
//
|
||||
// // Try file, then env, then default
|
||||
// configCodec := array.MonadFold(
|
||||
// []codec.Type[Config, string, string]{fileCodec, envCodec, defaultCodec},
|
||||
// m.Empty(),
|
||||
// m.Concat,
|
||||
// )
|
||||
//
|
||||
// // Load configuration - tries each source in order
|
||||
// result := configCodec.Decode(input)
|
||||
//
|
||||
// # Example: Parser with Multiple Formats
|
||||
//
|
||||
// // Create a monoid for parsing dates in multiple formats
|
||||
// m := codec.AltMonoid(func() codec.Type[time.Time, string, string] {
|
||||
// return codec.Date(time.RFC3339) // default format
|
||||
// })
|
||||
//
|
||||
// // Define parsers for different date formats
|
||||
// iso8601 := codec.Date("2006-01-02")
|
||||
// usFormat := codec.Date("01/02/2006")
|
||||
// euroFormat := codec.Date("02/01/2006")
|
||||
//
|
||||
// // Combine: try ISO 8601, then US, then European, then RFC3339
|
||||
// flexibleDate := m.Concat(
|
||||
// m.Concat(
|
||||
// m.Concat(iso8601, usFormat),
|
||||
// euroFormat,
|
||||
// ),
|
||||
// m.Empty(),
|
||||
// )
|
||||
//
|
||||
// // Can parse any of these formats
|
||||
// result1 := flexibleDate.Decode("2024-03-15") // ISO 8601
|
||||
// result2 := flexibleDate.Decode("03/15/2024") // US format
|
||||
// result3 := flexibleDate.Decode("15/03/2024") // European format
|
||||
//
|
||||
// # Example: Integer Parsing with Default
|
||||
//
|
||||
// // Create a monoid with default value of 0
|
||||
// m := codec.AltMonoid(func() codec.Type[int, string, string] {
|
||||
// return codec.MakeType(
|
||||
// "DefaultZero",
|
||||
// codec.Is[int](),
|
||||
// func(s string) codec.Decode[codec.Context, int] {
|
||||
// return func(c codec.Context) codec.Validation[int] {
|
||||
// return validation.Success(0)
|
||||
// }
|
||||
// },
|
||||
// strconv.Itoa,
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// // Try parsing as int, fall back to 0
|
||||
// intOrZero := m.Concat(codec.IntFromString(), m.Empty())
|
||||
//
|
||||
// result1 := intOrZero.Decode("42") // Success(42)
|
||||
// result2 := intOrZero.Decode("invalid") // Success(0) - uses default
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// // Both codecs fail - errors are aggregated
|
||||
// m := codec.AltMonoid(func() codec.Type[int, string, string] {
|
||||
// return codec.MakeType(
|
||||
// "NoDefault",
|
||||
// codec.Is[int](),
|
||||
// func(s string) codec.Decode[codec.Context, int] {
|
||||
// return func(c codec.Context) codec.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](s, "no default available")(c)
|
||||
// }
|
||||
// },
|
||||
// strconv.Itoa,
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// failing1 := codec.MakeType(
|
||||
// "Failing1",
|
||||
// codec.Is[int](),
|
||||
// func(s string) codec.Decode[codec.Context, int] {
|
||||
// return func(c codec.Context) codec.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](s, "error 1")(c)
|
||||
// }
|
||||
// },
|
||||
// strconv.Itoa,
|
||||
// )
|
||||
//
|
||||
// failing2 := codec.MakeType(
|
||||
// "Failing2",
|
||||
// codec.Is[int](),
|
||||
// func(s string) codec.Decode[codec.Context, int] {
|
||||
// return func(c codec.Context) codec.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](s, "error 2")(c)
|
||||
// }
|
||||
// },
|
||||
// strconv.Itoa,
|
||||
// )
|
||||
//
|
||||
// combined := m.Concat(failing1, failing2)
|
||||
// result := combined.Decode("input")
|
||||
// // result contains errors: "error 1", "error 2", and "no default available"
|
||||
//
|
||||
// # Monoid Laws
|
||||
//
|
||||
// AltMonoid satisfies the monoid laws:
|
||||
//
|
||||
// 1. **Left Identity**: m.Concat(m.Empty(), codec) ≡ codec
|
||||
// 2. **Right Identity**: m.Concat(codec, m.Empty()) ≡ codec (tries codec first, falls back to zero)
|
||||
// 3. **Associativity**: m.Concat(m.Concat(a, b), c) ≡ m.Concat(a, m.Concat(b, c))
|
||||
//
|
||||
// Note: Due to the "first success wins" behavior, right identity means the zero is only
|
||||
// used if the codec fails.
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Configuration loading with multiple sources (file, env, default)
|
||||
// - Parsing data in multiple formats with fallbacks
|
||||
// - API versioning (try v2, fall back to v1, then default)
|
||||
// - Content negotiation (try JSON, then XML, then plain text)
|
||||
// - Validation with default values
|
||||
// - Parser combinators with alternative branches
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The zero codec is lazily evaluated, only when needed
|
||||
// - First success short-circuits evaluation (subsequent codecs not tried)
|
||||
// - Error aggregation ensures all validation failures are reported
|
||||
// - Encoding always uses the first codec's encoder
|
||||
// - This follows the alternative functor laws
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - MonadAlt: The underlying alternative operation for two codecs
|
||||
// - Alt: The curried version for pipeline composition
|
||||
// - validate.AltMonoid: The validation-level alternative monoid
|
||||
// - decode.AltMonoid: The decode-level alternative monoid
|
||||
func AltMonoid[A, O, I any](zero Lazy[Type[A, O, I]]) Monoid[Type[A, O, I]] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[A, O, I],
|
||||
)
|
||||
}
|
||||
921
v2/optics/codec/alt_test.go
Normal file
921
v2/optics/codec/alt_test.go
Normal file
@@ -0,0 +1,921 @@
|
||||
// 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 codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"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/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMonadAltBasicFunctionality tests the basic behavior of MonadAlt
|
||||
func TestMonadAltBasicFunctionality(t *testing.T) {
|
||||
t.Run("uses first codec when it succeeds", func(t *testing.T) {
|
||||
// Create two codecs that both work with strings
|
||||
stringCodec := Id[string]()
|
||||
|
||||
// Create another string codec that only accepts uppercase
|
||||
uppercaseOnly := MakeType(
|
||||
"UppercaseOnly",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
for _, r := range s {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return validation.FailureWithMessage[string](s, "must be uppercase")(c)
|
||||
}
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Create alt codec that tries uppercase first, then any string
|
||||
altCodec := MonadAlt(
|
||||
uppercaseOnly,
|
||||
func() Type[string, string, string] { return stringCodec },
|
||||
)
|
||||
|
||||
// Test with uppercase string - should succeed with first codec
|
||||
result := altCodec.Decode("HELLO")
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode with first codec")
|
||||
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
|
||||
assert.Equal(t, "HELLO", value)
|
||||
})
|
||||
|
||||
t.Run("falls back to second codec when first fails", func(t *testing.T) {
|
||||
// Create a codec that only accepts positive integers
|
||||
positiveInt := MakeType(
|
||||
"PositiveInt",
|
||||
Is[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)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Create a codec that accepts any integer (with same input type)
|
||||
anyInt := MakeType(
|
||||
"AnyInt",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
positiveInt,
|
||||
func() Type[int, int, int] { return anyInt },
|
||||
)
|
||||
|
||||
// Test with negative number - first fails, second succeeds
|
||||
result := altCodec.Decode(-5)
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode with second codec")
|
||||
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
|
||||
assert.Equal(t, -5, value)
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when both codecs fail", func(t *testing.T) {
|
||||
// Create two codecs that will both fail
|
||||
positiveInt := MakeType(
|
||||
"PositiveInt",
|
||||
Is[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)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
evenInt := MakeType(
|
||||
"EvenInt",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i%2 != 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be even")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
positiveInt,
|
||||
func() Type[int, int, int] { return evenInt },
|
||||
)
|
||||
|
||||
// Test with -3 (negative and odd) - both should fail
|
||||
result := altCodec.Decode(-3)
|
||||
|
||||
assert.True(t, either.IsLeft(result), "should fail when both codecs fail")
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
// Should have errors from both validation attempts
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "should have errors from both codecs")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAltNaming tests that the codec name is correctly generated
|
||||
func TestMonadAltNaming(t *testing.T) {
|
||||
t.Run("generates correct name", func(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
anotherStringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] { return anotherStringCodec },
|
||||
)
|
||||
|
||||
assert.Equal(t, "Alt[string]", altCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAltEncoding tests that encoding uses the first codec's encoder
|
||||
func TestMonadAltEncoding(t *testing.T) {
|
||||
t.Run("uses first codec's encoder", func(t *testing.T) {
|
||||
// Create a codec that encodes ints as strings with prefix
|
||||
prefixedInt := MakeType(
|
||||
"PrefixedInt",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
var n int
|
||||
_, err := fmt.Sscanf(s, "NUM:%d", &n)
|
||||
if err != nil {
|
||||
return validation.FailureWithError[int](s, "expected NUM:n format")(err)(c)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
},
|
||||
func(n int) string {
|
||||
return fmt.Sprintf("NUM:%d", n)
|
||||
},
|
||||
)
|
||||
|
||||
// Create a standard int from string codec
|
||||
standardInt := IntFromString()
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
prefixedInt,
|
||||
func() Type[int, string, string] { return standardInt },
|
||||
)
|
||||
|
||||
// Encode should use first codec's encoder
|
||||
encoded := altCodec.Encode(42)
|
||||
assert.Equal(t, "NUM:42", encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltOperator tests the curried Alt function
|
||||
func TestAltOperator(t *testing.T) {
|
||||
t.Run("creates reusable operator", func(t *testing.T) {
|
||||
// Create a fallback operator that accepts any string
|
||||
withStringFallback := Alt(func() Type[string, string, string] {
|
||||
return Id[string]()
|
||||
})
|
||||
|
||||
// Create a codec that only accepts "hello"
|
||||
helloOnly := MakeType(
|
||||
"HelloOnly",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s != "hello" {
|
||||
return validation.FailureWithMessage[string](s, "must be 'hello'")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply fallback to the codec
|
||||
altCodec := withStringFallback(helloOnly)
|
||||
|
||||
// Test that it works
|
||||
result1 := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
|
||||
result2 := altCodec.Decode("world")
|
||||
assert.True(t, either.IsRight(result2))
|
||||
})
|
||||
|
||||
t.Run("works in pipeline with F.Pipe", func(t *testing.T) {
|
||||
// Create a codec pipeline with multiple fallbacks
|
||||
baseCodec := MakeType(
|
||||
"StrictInt",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if s != "42" {
|
||||
return validation.FailureWithMessage[int](s, "must be exactly '42'")(c)
|
||||
}
|
||||
return validation.Success(42)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
fallback1 := MakeType(
|
||||
"Fallback1",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if s != "100" {
|
||||
return validation.FailureWithMessage[int](s, "must be exactly '100'")(c)
|
||||
}
|
||||
return validation.Success(100)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
fallback2 := MakeType(
|
||||
"AnyInt",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return validation.FailureWithError[int](s, "not an integer")(err)(c)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
// Build pipeline with multiple alternatives
|
||||
pipeline := F.Pipe2(
|
||||
baseCodec,
|
||||
Alt(func() Type[int, string, string] { return fallback1 }),
|
||||
Alt(func() Type[int, string, string] { return fallback2 }),
|
||||
)
|
||||
|
||||
// Test with "42" - should use base codec
|
||||
result1 := pipeline.Decode("42")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
value1 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result1)
|
||||
assert.Equal(t, 42, value1)
|
||||
|
||||
// Test with "100" - should use fallback1
|
||||
result2 := pipeline.Decode("100")
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result2)
|
||||
assert.Equal(t, 100, value2)
|
||||
|
||||
// Test with "999" - should use fallback2
|
||||
result3 := pipeline.Decode("999")
|
||||
assert.True(t, either.IsRight(result3))
|
||||
value3 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result3)
|
||||
assert.Equal(t, 999, value3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltLazyEvaluation tests that the second codec is only evaluated when needed
|
||||
func TestAltLazyEvaluation(t *testing.T) {
|
||||
t.Run("does not evaluate second codec when first succeeds", func(t *testing.T) {
|
||||
evaluated := false
|
||||
|
||||
stringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] {
|
||||
evaluated = true
|
||||
return Id[string]()
|
||||
},
|
||||
)
|
||||
|
||||
// Decode with first codec succeeding
|
||||
result := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Second codec should not have been evaluated
|
||||
assert.False(t, evaluated, "second codec should not be evaluated when first succeeds")
|
||||
})
|
||||
|
||||
t.Run("evaluates second codec when first fails", func(t *testing.T) {
|
||||
evaluated := false
|
||||
|
||||
// Create a codec that always fails
|
||||
failingCodec := MakeType(
|
||||
"Failing",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
return validation.FailureWithMessage[string](s, "always fails")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
altCodec := MonadAlt(
|
||||
failingCodec,
|
||||
func() Type[string, string, string] {
|
||||
evaluated = true
|
||||
return Id[string]()
|
||||
},
|
||||
)
|
||||
|
||||
// Decode with first codec failing
|
||||
result := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Second codec should have been evaluated
|
||||
assert.True(t, evaluated, "second codec should be evaluated when first fails")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltWithComplexTypes tests Alt with more complex codec scenarios
|
||||
func TestAltWithComplexTypes(t *testing.T) {
|
||||
t.Run("works with string length validation", func(t *testing.T) {
|
||||
// Create codec that accepts strings of length 5
|
||||
length5 := MakeType(
|
||||
"Length5",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) != 5 {
|
||||
return validation.FailureWithMessage[string](s, "must be length 5")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Create codec that accepts any string
|
||||
anyString := Id[string]()
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
length5,
|
||||
func() Type[string, string, string] { return anyString },
|
||||
)
|
||||
|
||||
// Test with length 5 - should use first codec
|
||||
result1 := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
|
||||
// Test with different length - should fall back to second codec
|
||||
result2 := altCodec.Decode("hi")
|
||||
assert.True(t, either.IsRight(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltTypeChecking tests that type checking works correctly
|
||||
func TestAltTypeChecking(t *testing.T) {
|
||||
t.Run("type checking uses generic Is", func(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
anotherStringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] { return anotherStringCodec },
|
||||
)
|
||||
|
||||
// Test Is with valid type
|
||||
result1 := altCodec.Is("hello")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
|
||||
// Test Is with invalid type
|
||||
result2 := altCodec.Is(42)
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltRoundTrip tests encoding and decoding round trips
|
||||
func TestAltRoundTrip(t *testing.T) {
|
||||
t.Run("round-trip with first codec", func(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
anotherStringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] { return anotherStringCodec },
|
||||
)
|
||||
|
||||
original := "hello"
|
||||
|
||||
// Decode
|
||||
decodeResult := altCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := altCodec.Encode(decoded)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip with second codec", func(t *testing.T) {
|
||||
// Create a codec that only accepts "hello"
|
||||
helloOnly := MakeType(
|
||||
"HelloOnly",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s != "hello" {
|
||||
return validation.FailureWithMessage[string](s, "must be 'hello'")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
anyString := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
helloOnly,
|
||||
func() Type[string, string, string] { return anyString },
|
||||
)
|
||||
|
||||
original := "world"
|
||||
|
||||
// Decode (will use second codec)
|
||||
decodeResult := altCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
|
||||
|
||||
// Encode (uses first codec's encoder, which is identity)
|
||||
encoded := altCodec.Encode(decoded)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltErrorMessages tests that error messages are informative
|
||||
func TestAltErrorMessages(t *testing.T) {
|
||||
t.Run("provides detailed error context", func(t *testing.T) {
|
||||
// Create two codecs with specific error messages
|
||||
codec1 := MakeType(
|
||||
"Codec1",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](i, "codec1 error")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
codec2 := MakeType(
|
||||
"Codec2",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](i, "codec2 error")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
altCodec := MonadAlt(
|
||||
codec1,
|
||||
func() Type[int, int, int] { return codec2 },
|
||||
)
|
||||
|
||||
result := altCodec.Decode(42)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
require.GreaterOrEqual(t, len(errors), 2)
|
||||
|
||||
// Check that both error messages are present
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
|
||||
hasCodec1Error := false
|
||||
hasCodec2Error := false
|
||||
for _, msg := range messages {
|
||||
if msg == "codec1 error" {
|
||||
hasCodec1Error = true
|
||||
}
|
||||
if msg == "codec2 error" {
|
||||
hasCodec2Error = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasCodec1Error, "should have error from first codec")
|
||||
assert.True(t, hasCodec2Error, "should have error from second codec")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltMonoid tests the AltMonoid function
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
t.Run("with default value as zero", func(t *testing.T) {
|
||||
// Create a monoid with a default value of 0
|
||||
m := AltMonoid(func() Type[int, string, string] {
|
||||
return MakeType(
|
||||
"DefaultZero",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(0)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
})
|
||||
|
||||
// Create codecs
|
||||
intFromString := IntFromString()
|
||||
failing := MakeType(
|
||||
"Failing",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "always fails")(c)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
t.Run("first success wins", func(t *testing.T) {
|
||||
// Combine two successful codecs - first should win
|
||||
codec1 := MakeType(
|
||||
"Returns10",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(10)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
codec2 := MakeType(
|
||||
"Returns20",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(20)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
combined := m.Concat(codec1, codec2)
|
||||
result := combined.Decode("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
|
||||
assert.Equal(t, 10, value, "first success should win")
|
||||
})
|
||||
|
||||
t.Run("falls back to second when first fails", func(t *testing.T) {
|
||||
combined := m.Concat(failing, intFromString)
|
||||
result := combined.Decode("42")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("uses zero when both fail", func(t *testing.T) {
|
||||
combined := m.Concat(failing, m.Empty())
|
||||
result := combined.Decode("invalid")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
|
||||
assert.Equal(t, 0, value, "should use default zero value")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with failing zero", func(t *testing.T) {
|
||||
// Create a monoid with a failing zero
|
||||
m := AltMonoid(func() Type[int, string, string] {
|
||||
return MakeType(
|
||||
"NoDefault",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "no default available")(c)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
})
|
||||
|
||||
failing1 := MakeType(
|
||||
"Failing1",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "error 1")(c)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
failing2 := MakeType(
|
||||
"Failing2",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "error 2")(c)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
t.Run("aggregates all errors when all fail", func(t *testing.T) {
|
||||
combined := m.Concat(m.Concat(failing1, failing2), m.Empty())
|
||||
result := combined.Decode("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
// Should have errors from all three: failing1, failing2, and zero
|
||||
assert.GreaterOrEqual(t, len(errors), 3)
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
|
||||
hasError1 := false
|
||||
hasError2 := false
|
||||
hasNoDefault := false
|
||||
for _, msg := range messages {
|
||||
if msg == "error 1" {
|
||||
hasError1 = true
|
||||
}
|
||||
if msg == "error 2" {
|
||||
hasError2 = true
|
||||
}
|
||||
if msg == "no default available" {
|
||||
hasNoDefault = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasError1, "should have error from failing1")
|
||||
assert.True(t, hasError2, "should have error from failing2")
|
||||
assert.True(t, hasNoDefault, "should have error from zero")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("chaining multiple fallbacks", func(t *testing.T) {
|
||||
m := AltMonoid(func() Type[string, string, string] {
|
||||
return MakeType(
|
||||
"Default",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
return validation.Success("default")
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
})
|
||||
|
||||
primary := MakeType(
|
||||
"Primary",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s == "primary" {
|
||||
return validation.Success("from primary")
|
||||
}
|
||||
return validation.FailureWithMessage[string](s, "not primary")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
secondary := MakeType(
|
||||
"Secondary",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s == "secondary" {
|
||||
return validation.Success("from secondary")
|
||||
}
|
||||
return validation.FailureWithMessage[string](s, "not secondary")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Chain: try primary, then secondary, then default
|
||||
combined := m.Concat(m.Concat(primary, secondary), m.Empty())
|
||||
|
||||
t.Run("uses primary when it succeeds", func(t *testing.T) {
|
||||
result := combined.Decode("primary")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
|
||||
assert.Equal(t, "from primary", value)
|
||||
})
|
||||
|
||||
t.Run("uses secondary when primary fails", func(t *testing.T) {
|
||||
result := combined.Decode("secondary")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
|
||||
assert.Equal(t, "from secondary", value)
|
||||
})
|
||||
|
||||
t.Run("uses default when both fail", func(t *testing.T) {
|
||||
result := combined.Decode("other")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
|
||||
assert.Equal(t, "default", value)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := AltMonoid(func() Type[int, string, string] {
|
||||
return MakeType(
|
||||
"DefaultZero",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(0)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
})
|
||||
|
||||
codec1 := MakeType(
|
||||
"Codec1",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(10)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
codec2 := MakeType(
|
||||
"Codec2",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(20)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
codec3 := MakeType(
|
||||
"Codec3",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(30)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// m.Concat(m.Empty(), codec) should behave like codec
|
||||
// But with AltMonoid, if codec fails, it falls back to empty
|
||||
combined := m.Concat(m.Empty(), codec1)
|
||||
result := combined.Decode("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
|
||||
// Empty (0) comes first, so it wins
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// m.Concat(codec, m.Empty()) tries codec first, falls back to empty
|
||||
combined := m.Concat(codec1, m.Empty())
|
||||
result := combined.Decode("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
|
||||
assert.Equal(t, 10, value, "codec1 should win")
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
// For AltMonoid, first success wins
|
||||
left := m.Concat(m.Concat(codec1, codec2), codec3)
|
||||
right := m.Concat(codec1, m.Concat(codec2, codec3))
|
||||
|
||||
resultLeft := left.Decode("input")
|
||||
resultRight := right.Decode("input")
|
||||
|
||||
assert.True(t, either.IsRight(resultLeft))
|
||||
assert.True(t, either.IsRight(resultRight))
|
||||
|
||||
valueLeft := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultLeft)
|
||||
valueRight := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultRight)
|
||||
|
||||
// Both should return 10 (first success)
|
||||
assert.Equal(t, valueLeft, valueRight)
|
||||
assert.Equal(t, 10, valueLeft)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("encoding uses first codec", func(t *testing.T) {
|
||||
m := AltMonoid(func() Type[int, string, string] {
|
||||
return MakeType(
|
||||
"Default",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(0)
|
||||
}
|
||||
},
|
||||
func(n int) string { return "DEFAULT" },
|
||||
)
|
||||
})
|
||||
|
||||
codec1 := MakeType(
|
||||
"Codec1",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(42)
|
||||
}
|
||||
},
|
||||
func(n int) string { return fmt.Sprintf("FIRST:%d", n) },
|
||||
)
|
||||
|
||||
codec2 := MakeType(
|
||||
"Codec2",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(100)
|
||||
}
|
||||
},
|
||||
func(n int) string { return fmt.Sprintf("SECOND:%d", n) },
|
||||
)
|
||||
|
||||
combined := m.Concat(codec1, codec2)
|
||||
|
||||
// Encoding should use first codec's encoder
|
||||
encoded := combined.Encode(42)
|
||||
assert.Equal(t, "FIRST:42", encoded)
|
||||
})
|
||||
}
|
||||
@@ -18,11 +18,10 @@ package codec
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
)
|
||||
|
||||
// encodeEither creates an encoder for Either[A, B] values.
|
||||
@@ -151,33 +150,20 @@ func validateEither[A, B, O, I any](
|
||||
rightItem Type[B, O, I],
|
||||
) Validate[I, either.Either[A, B]] {
|
||||
|
||||
// F.Pipe1(
|
||||
// leftItem.Decode,
|
||||
// decode.OrElse()
|
||||
// )
|
||||
valRight := F.Pipe1(
|
||||
rightItem.Validate,
|
||||
validate.Map[I, B](either.Right[A]),
|
||||
)
|
||||
|
||||
return func(i I) Decode[Context, either.Either[A, B]] {
|
||||
valRight := rightItem.Validate(i)
|
||||
valLeft := leftItem.Validate(i)
|
||||
valLeft := F.Pipe1(
|
||||
leftItem.Validate,
|
||||
validate.Map[I, A](either.Left[B]),
|
||||
)
|
||||
|
||||
return func(ctx Context) Validation[either.Either[A, B]] {
|
||||
|
||||
resRight := valRight(ctx)
|
||||
|
||||
return either.Fold(
|
||||
func(rightErrors validate.Errors) Validation[either.Either[A, B]] {
|
||||
resLeft := valLeft(ctx)
|
||||
return either.Fold(
|
||||
func(leftErrors validate.Errors) Validation[either.Either[A, B]] {
|
||||
return validation.Failures[either.Either[A, B]](array.Concat(leftErrors)(rightErrors))
|
||||
},
|
||||
F.Flow2(either.Left[B, A], validation.Of),
|
||||
)(resLeft)
|
||||
},
|
||||
F.Flow2(either.Right[A, B], validation.Of),
|
||||
)(resRight)
|
||||
}
|
||||
}
|
||||
return F.Pipe1(
|
||||
valRight,
|
||||
validate.Alt(lazy.Of(valLeft)),
|
||||
)
|
||||
}
|
||||
|
||||
// Either creates a codec for Either[A, B] values.
|
||||
@@ -270,12 +256,9 @@ func Either[A, B, O, I any](
|
||||
leftItem Type[A, O, I],
|
||||
rightItem Type[B, O, I],
|
||||
) Type[either.Either[A, B], O, I] {
|
||||
name := fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name())
|
||||
isEither := Is[either.Either[A, B]]()
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
isEither,
|
||||
fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name()),
|
||||
Is[either.Either[A, B]](),
|
||||
validateEither(leftItem, rightItem),
|
||||
encodeEither(leftItem, rightItem),
|
||||
)
|
||||
|
||||
@@ -342,6 +342,27 @@ func TestEitherErrorAccumulation(t *testing.T) {
|
||||
|
||||
require.NotNil(t, errors)
|
||||
// Should have errors from both string and int validation attempts
|
||||
assert.NotEmpty(t, errors)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should have at least 2 errors (one from Right validation, one from Left validation)")
|
||||
|
||||
// Verify we have errors from both validation attempts
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
|
||||
// Check that we have errors related to both validations
|
||||
hasIntError := false
|
||||
hasStringError := false
|
||||
for _, msg := range messages {
|
||||
if msg == "expected integer string" || msg == "must be positive" {
|
||||
hasIntError = true
|
||||
}
|
||||
if msg == "must not be empty" {
|
||||
hasStringError = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasIntError, "Should have error from integer validation (Right branch)")
|
||||
assert.True(t, hasStringError, "Should have error from string validation (Left branch)")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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/monoid"
|
||||
"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"
|
||||
@@ -40,6 +41,27 @@ type (
|
||||
|
||||
// Codec combines a Decoder and an Encoder for bidirectional transformations.
|
||||
// It can decode input I to type A and encode type A to output O.
|
||||
//
|
||||
// This is a simple struct that pairs a decoder with an encoder, providing
|
||||
// the basic building blocks for bidirectional data transformation. Unlike
|
||||
// the Type interface, Codec is a concrete struct without validation context
|
||||
// or type checking capabilities.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - I: The input type to decode from
|
||||
// - O: The output type to encode to
|
||||
// - A: The intermediate type (decoded to, encoded from)
|
||||
//
|
||||
// Fields:
|
||||
// - Decode: A decoder that transforms I to A
|
||||
// - Encode: An encoder that transforms A to O
|
||||
//
|
||||
// Example:
|
||||
// A Codec[string, string, int] can decode strings to integers and
|
||||
// encode integers back to strings.
|
||||
//
|
||||
// Note: For most use cases, prefer using the Type interface which provides
|
||||
// additional validation and type checking capabilities.
|
||||
Codec[I, O, A any] struct {
|
||||
Decode decoder.Decoder[I, A]
|
||||
Encode encoder.Encoder[O, A]
|
||||
@@ -55,16 +77,82 @@ 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.
|
||||
//
|
||||
// The Validate type is the core validation abstraction, defined as:
|
||||
// Reader[I, Decode[Context, 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
|
||||
//
|
||||
// Example:
|
||||
// A Validate[string, int] takes a string and returns a context-aware
|
||||
// function that validates and converts it to an integer.
|
||||
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.
|
||||
//
|
||||
// The Decode type is defined as:
|
||||
// Reader[I, Validation[A]]
|
||||
//
|
||||
// This is simpler than Validate as it doesn't require explicit context passing.
|
||||
// The context is typically created automatically when the decoder is invoked.
|
||||
//
|
||||
// Decode is used when:
|
||||
// - You don't need to manually manage validation context
|
||||
// - You want a simpler API for basic validation
|
||||
// - You're working at the top level of validation
|
||||
//
|
||||
// Example:
|
||||
// A Decode[string, int] takes a string and returns a Validation[int]
|
||||
// which is Either[Errors, int].
|
||||
Decode[I, A any] = decode.Decode[I, A]
|
||||
|
||||
// Encode is a function that encodes type A to output O.
|
||||
//
|
||||
// Encode is simply a Reader[A, O], which is a function from A to O.
|
||||
// Encoders are pure functions with no error handling - they assume
|
||||
// the input is valid.
|
||||
//
|
||||
// Encoding is the inverse of decoding:
|
||||
// - Decoding: I -> Validation[A] (may fail)
|
||||
// - Encoding: A -> O (always succeeds)
|
||||
//
|
||||
// Example:
|
||||
// An Encode[int, string] takes an integer and returns its string
|
||||
// representation.
|
||||
Encode[A, O any] = Reader[A, O]
|
||||
|
||||
// Decoder is an interface for types that can decode and validate input.
|
||||
//
|
||||
// A Decoder transforms input of type I into a validated value of type A,
|
||||
// providing detailed error information when validation fails. It supports
|
||||
// both context-aware validation (via Validate) and direct decoding (via Decode).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - I: The input type to decode from
|
||||
// - A: The target type to decode to
|
||||
//
|
||||
// Methods:
|
||||
// - Name(): Returns a descriptive name for this decoder (used in error messages)
|
||||
// - Validate(I): Returns a context-aware validation function that can track
|
||||
// the path through nested structures
|
||||
// - Decode(I): Directly decodes input to a Validation result with a fresh context
|
||||
//
|
||||
// The Validate method is more flexible as it returns a Reader that can be called
|
||||
// with different contexts, while Decode is a convenience method that creates a
|
||||
// new context automatically.
|
||||
//
|
||||
// Example:
|
||||
// A Decoder[string, int] can decode strings to integers with validation.
|
||||
Decoder[I, A any] interface {
|
||||
Name() string
|
||||
Validate(I) Decode[Context, A]
|
||||
@@ -72,13 +160,76 @@ type (
|
||||
}
|
||||
|
||||
// Encoder is an interface for types that can encode values.
|
||||
//
|
||||
// An Encoder transforms values of type A into output format O. This is the
|
||||
// inverse operation of decoding, allowing bidirectional transformations.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The source type to encode from
|
||||
// - O: The output type to encode to
|
||||
//
|
||||
// Methods:
|
||||
// - Encode(A): Transforms a value of type A into output format O
|
||||
//
|
||||
// Encoders are pure functions with no validation or error handling - they
|
||||
// assume the input is valid. Validation should be performed during decoding.
|
||||
//
|
||||
// Example:
|
||||
// An Encoder[int, string] can encode integers to their string representation.
|
||||
Encoder[A, O any] interface {
|
||||
// Encode transforms a value of type A into output format O.
|
||||
Encode(A) O
|
||||
}
|
||||
|
||||
// Type is a bidirectional codec that combines encoding, decoding, validation,
|
||||
// and type checking capabilities. It represents a complete specification of
|
||||
// how to work with a particular type.
|
||||
//
|
||||
// Type is the central abstraction in the codec package, providing:
|
||||
// - Decoding: Transform input I to validated type A
|
||||
// - Encoding: Transform type A to output O
|
||||
// - Validation: Context-aware validation with detailed error reporting
|
||||
// - Type Checking: Runtime type verification via Is()
|
||||
// - Formatting: Human-readable type descriptions via Name()
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type (what we decode to and encode from)
|
||||
// - O: The output type (what we encode to)
|
||||
// - I: The input type (what we decode from)
|
||||
//
|
||||
// Common patterns:
|
||||
// - Type[A, A, A]: Identity codec (no transformation)
|
||||
// - Type[A, string, string]: String-based serialization
|
||||
// - Type[A, any, any]: Generic codec accepting any input/output
|
||||
// - Type[A, JSON, JSON]: JSON codec
|
||||
//
|
||||
// Methods:
|
||||
// - Name(): Returns the codec's descriptive name
|
||||
// - Validate(I): Returns context-aware validation function
|
||||
// - Decode(I): Decodes input with automatic context creation
|
||||
// - Encode(A): Encodes value to output format
|
||||
// - AsDecoder(): Returns this Type as a Decoder interface
|
||||
// - AsEncoder(): Returns this Type as an Encoder interface
|
||||
// - Is(any): Checks if a value can be converted to type A
|
||||
//
|
||||
// Example usage:
|
||||
// intCodec := codec.Int() // Type[int, int, any]
|
||||
// stringCodec := codec.String() // Type[string, string, any]
|
||||
// intFromString := codec.IntFromString() // Type[int, string, string]
|
||||
//
|
||||
// // Decode
|
||||
// result := intFromString.Decode("42") // Validation[int]
|
||||
//
|
||||
// // Encode
|
||||
// str := intFromString.Encode(42) // "42"
|
||||
//
|
||||
// // Type check
|
||||
// isInt := intCodec.Is(42) // Right(42)
|
||||
// notInt := intCodec.Is("42") // Left(error)
|
||||
//
|
||||
// Composition:
|
||||
// Types can be composed using operators like Alt, Map, Chain, and Pipe
|
||||
// to build complex codecs from simpler ones.
|
||||
Type[A, O, I any] interface {
|
||||
Formattable
|
||||
Decoder[I, A]
|
||||
@@ -99,6 +250,92 @@ type (
|
||||
// contain a value of type A. It provides a way to preview and review values.
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
// Refinement represents the concept that B is a specialized type of A
|
||||
// Refinement represents the concept that B is a specialized type of A.
|
||||
// It's an alias for Prism[A, B], providing a semantic name for type refinement operations.
|
||||
//
|
||||
// A refinement allows you to:
|
||||
// - Preview: Try to extract a B from an A (may fail if A is not a B)
|
||||
// - Review: Inject a B back into an A
|
||||
//
|
||||
// This is useful for working with subtypes, validated types, or constrained types.
|
||||
//
|
||||
// Example:
|
||||
// - Refinement[int, PositiveInt] - refines int to positive integers only
|
||||
// - Refinement[string, NonEmptyString] - refines string to non-empty strings
|
||||
// - Refinement[any, User] - refines any to User type
|
||||
Refinement[A, B any] = Prism[A, B]
|
||||
|
||||
// Kleisli represents a Kleisli arrow in the codec context.
|
||||
// It's a function that takes a value of type A and returns a codec Type[B, O, I].
|
||||
//
|
||||
// This is the fundamental building block for codec transformations and compositions.
|
||||
// Kleisli arrows allow you to:
|
||||
// - Chain codec operations
|
||||
// - Build dependent codecs (where the next codec depends on the previous result)
|
||||
// - Create codec pipelines
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input type to the function
|
||||
// - B: The target type that the resulting codec decodes to
|
||||
// - O: The output type that the resulting codec encodes to
|
||||
// - I: The input type that the resulting codec decodes from
|
||||
//
|
||||
// Example:
|
||||
// A Kleisli[string, int, string, string] takes a string and returns a codec
|
||||
// that can decode strings to ints and encode ints to strings.
|
||||
Kleisli[A, B, O, I any] = Reader[A, Type[B, O, I]]
|
||||
|
||||
// Operator is a specialized Kleisli arrow that transforms codecs.
|
||||
// It takes a codec Type[A, O, I] and returns a new codec Type[B, O, I].
|
||||
//
|
||||
// Operators are the primary way to build codec transformation pipelines.
|
||||
// They enable functional composition of codec transformations using F.Pipe.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The source type that the input codec decodes to
|
||||
// - B: The target type that the output codec decodes to
|
||||
// - O: The output type (same for both input and output codecs)
|
||||
// - I: The input type (same for both input and output codecs)
|
||||
//
|
||||
// Common operators include:
|
||||
// - Map: Transforms the decoded value
|
||||
// - Chain: Sequences dependent codec operations
|
||||
// - Alt: Provides alternative fallback codecs
|
||||
// - Refine: Adds validation constraints
|
||||
//
|
||||
// Example:
|
||||
// An Operator[int, PositiveInt, int, any] transforms a codec that decodes
|
||||
// to int into a codec that decodes to PositiveInt (with validation).
|
||||
//
|
||||
// Usage with F.Pipe:
|
||||
// codec := F.Pipe2(
|
||||
// baseCodec,
|
||||
// operator1, // Operator[A, B, O, I]
|
||||
// operator2, // Operator[B, C, O, I]
|
||||
// )
|
||||
Operator[A, B, O, I any] = Kleisli[Type[A, O, I], B, O, I]
|
||||
|
||||
// Monoid represents an algebraic structure with an associative binary operation
|
||||
// and an identity element.
|
||||
//
|
||||
// A Monoid[A] provides:
|
||||
// - Empty(): Returns the identity element
|
||||
// - Concat(A, A): Combines two values associatively
|
||||
//
|
||||
// Monoid laws:
|
||||
// 1. Left Identity: Concat(Empty(), a) = a
|
||||
// 2. Right Identity: Concat(a, Empty()) = a
|
||||
// 3. Associativity: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
|
||||
//
|
||||
// In the codec context, monoids are used to:
|
||||
// - Combine multiple codecs with specific semantics
|
||||
// - Build codec chains with fallback behavior (AltMonoid)
|
||||
// - Aggregate validation results (ApplicativeMonoid)
|
||||
// - Compose codec transformations
|
||||
//
|
||||
// Example monoids for codecs:
|
||||
// - AltMonoid: First success wins (alternative semantics)
|
||||
// - ApplicativeMonoid: Combines successful results using inner monoid
|
||||
// - AlternativeMonoid: Combines applicative and alternative behaviors
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
)
|
||||
|
||||
@@ -486,16 +486,17 @@ func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
|
||||
// - Building validation pipelines with fallback logic
|
||||
// - Implementing optional validation with defaults
|
||||
//
|
||||
// **Key behavior**: Unlike error accumulation in [MonadAp], MonadAlt does NOT accumulate errors.
|
||||
// When falling back to the second validation, the first validation's errors are discarded.
|
||||
// This is the standard Alt behavior - it's about choosing alternatives, not combining errors.
|
||||
// **Key behavior**: When both validations fail, MonadAlt DOES accumulate errors from both
|
||||
// validations using the Errors monoid. This is different from standard Either Alt behavior.
|
||||
// The error accumulation happens through the underlying ChainLeft/chainErrors mechanism.
|
||||
//
|
||||
// The second parameter is lazy (Lazy[Validation[A]]) to avoid unnecessary computation when
|
||||
// the first validation succeeds. The second validation is only evaluated if needed.
|
||||
//
|
||||
// Behavior:
|
||||
// - First succeeds: returns first validation (second is not evaluated)
|
||||
// - First fails: evaluates and returns second validation (first errors discarded)
|
||||
// - First fails, second succeeds: returns second validation
|
||||
// - Both fail: aggregates errors from both validations
|
||||
//
|
||||
// This is useful for:
|
||||
// - Fallback values: provide defaults when primary validation fails
|
||||
@@ -547,7 +548,7 @@ func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
|
||||
// )
|
||||
// // Tries: env var → file → default (uses first that succeeds)
|
||||
//
|
||||
// Example - No error accumulation:
|
||||
// Example - Error accumulation when both fail:
|
||||
//
|
||||
// v1 := Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "error 1"},
|
||||
@@ -559,8 +560,8 @@ func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
|
||||
// })
|
||||
// }
|
||||
// result := MonadAlt(v1, v2)
|
||||
// // Result: Failures with only ["error 3"]
|
||||
// // The errors from v1 are discarded (not accumulated)
|
||||
// // Result: Failures with ALL errors ["error 1", "error 2", "error 3"]
|
||||
// // The errors from v1 are aggregated with errors from v2
|
||||
func MonadAlt[A any](first Validation[A], second Lazy[Validation[A]]) Validation[A] {
|
||||
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user