mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-28 13:12:03 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77a8cc6b09 | ||
|
|
bc8743fdfc | ||
|
|
1837d3f86d |
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -388,8 +387,8 @@ func generateApplyHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -266,8 +265,8 @@ func generateBindHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n", pkg)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -189,8 +188,8 @@ func generateDIHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -148,8 +147,8 @@ func generateEitherHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func writePackage(f *os.File, pkg string) {
|
||||
@@ -26,6 +25,6 @@ func writePackage(f *os.File, pkg string) {
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -62,8 +61,8 @@ func generateIdentityHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v3"
|
||||
@@ -71,8 +70,8 @@ func generateIOHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v3"
|
||||
@@ -219,8 +218,8 @@ func generateIOEitherHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
@@ -234,8 +233,7 @@ import (
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(fg, "// This file was generated by robots at")
|
||||
fmt.Fprintf(fg, "// %s\n", time.Now())
|
||||
fmt.Fprintln(fg, "// This file was generated by robots.")
|
||||
|
||||
fmt.Fprintf(fg, "package generic\n\n")
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v3"
|
||||
@@ -76,8 +75,8 @@ func generateIOOptionHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -148,8 +147,8 @@ func generateOptionHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -378,8 +377,8 @@ func generatePipeHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n", pkg)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -118,8 +117,8 @@ func generateReaderHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
@@ -131,8 +130,7 @@ import (
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(fg, "// This file was generated by robots at")
|
||||
fmt.Fprintf(fg, "// %s\n", time.Now())
|
||||
fmt.Fprintln(fg, "// This file was generated by robots.")
|
||||
|
||||
fmt.Fprintf(fg, "package generic\n\n")
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -233,8 +232,8 @@ func generateReaderIOEitherHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
@@ -246,8 +245,7 @@ import (
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(fg, "// This file was generated by robots at")
|
||||
fmt.Fprintf(fg, "// %s\n", time.Now())
|
||||
fmt.Fprintln(fg, "// This file was generated by robots.")
|
||||
|
||||
fmt.Fprintf(fg, "package generic\n\n")
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -399,8 +398,8 @@ func generateTupleHelpers(filename string, count int) error {
|
||||
|
||||
// some header
|
||||
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
|
||||
fmt.Fprintln(f, "// This file was generated by robots at")
|
||||
fmt.Fprintf(f, "// %s\n\n", time.Now())
|
||||
fmt.Fprintln(f, "// This file was generated by robots.")
|
||||
fmt.Fprintln(f)
|
||||
|
||||
fmt.Fprintf(f, "package %s\n\n", pkg)
|
||||
|
||||
|
||||
299
v2/optics/codec/bind.go
Normal file
299
v2/optics/codec/bind.go
Normal file
@@ -0,0 +1,299 @@
|
||||
// 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/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// ApSL creates an applicative sequencing operator for codecs using a lens.
|
||||
//
|
||||
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs,
|
||||
// allowing you to build up complex codecs by combining a base codec with a field
|
||||
// accessed through a lens. It's particularly useful for building struct codecs
|
||||
// field-by-field in a composable way.
|
||||
//
|
||||
// The function combines:
|
||||
// - Encoding: Extracts the field value using the lens, encodes it with fa, and
|
||||
// combines it with the base encoding using the monoid
|
||||
// - Validation: Validates the field using the lens and combines the validation
|
||||
// with the base validation
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - S: The source struct type (what we're building a codec for)
|
||||
// - T: The field type accessed by the lens
|
||||
// - O: The output type for encoding (must have a monoid)
|
||||
// - I: The input type for decoding
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: A Monoid[O] for combining encoded outputs
|
||||
// - l: A Lens[S, T] that focuses on a specific field in S
|
||||
// - fa: A Type[T, O, I] codec for the field type T
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[S, S, O, I] that transforms a base codec by adding the field
|
||||
// specified by the lens.
|
||||
//
|
||||
// # How It Works
|
||||
//
|
||||
// 1. **Encoding**: When encoding a value of type S:
|
||||
// - Extract the field T using l.Get
|
||||
// - Encode T to O using fa.Encode
|
||||
// - Combine with the base encoding using the monoid
|
||||
//
|
||||
// 2. **Validation**: When validating input I:
|
||||
// - Validate the field using fa.Validate through the lens
|
||||
// - Combine with the base validation
|
||||
//
|
||||
// 3. **Type Checking**: Preserves the base type checker
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// "github.com/IBM/fp-go/v2/optics/lens"
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// // Lenses for Person fields
|
||||
// nameLens := lens.MakeLens(
|
||||
// func(p *Person) string { return p.Name },
|
||||
// func(p *Person, name string) *Person { p.Name = name; return p },
|
||||
// )
|
||||
//
|
||||
// // Build a Person codec field by field
|
||||
// personCodec := F.Pipe1(
|
||||
// codec.Struct[Person]("Person"),
|
||||
// codec.ApSL(S.Monoid, nameLens, codec.String),
|
||||
// // ... add more fields
|
||||
// )
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Building struct codecs incrementally
|
||||
// - Composing codecs for nested structures
|
||||
// - Creating type-safe serialization/deserialization
|
||||
// - Implementing Do-notation style codec construction
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The monoid determines how encoded outputs are combined
|
||||
// - The lens must be total (handle all cases safely)
|
||||
// - This is typically used with other ApS functions to build complete codecs
|
||||
// - The name is automatically generated for debugging purposes
|
||||
//
|
||||
// See also:
|
||||
// - validate.ApSL: The underlying validation combinator
|
||||
// - reader.ApplicativeMonoid: The monoid-based applicative instance
|
||||
// - Lens: The optic for accessing struct fields
|
||||
func ApSL[S, T, O, I any](
|
||||
m Monoid[O],
|
||||
l Lens[S, T],
|
||||
fa Type[T, O, I],
|
||||
) Operator[S, S, O, I] {
|
||||
name := fmt.Sprintf("ApS[%s x %s]", l, fa)
|
||||
rm := reader.ApplicativeMonoid[S](m)
|
||||
|
||||
encConcat := F.Pipe1(
|
||||
F.Flow2(
|
||||
l.Get,
|
||||
fa.Encode,
|
||||
),
|
||||
semigroup.AppendTo(rm),
|
||||
)
|
||||
|
||||
valConcat := validate.ApSL(l, fa.Validate)
|
||||
|
||||
return func(t Type[S, O, I]) Type[S, O, I] {
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
t.Is,
|
||||
F.Pipe1(
|
||||
t.Validate,
|
||||
valConcat,
|
||||
),
|
||||
encConcat(t.Encode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ApSO creates an applicative sequencing operator for codecs using an optional.
|
||||
//
|
||||
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs
|
||||
// with optional fields, allowing you to build up complex codecs by combining a base
|
||||
// codec with a field that may or may not be present. It's particularly useful for
|
||||
// building struct codecs with optional fields in a composable way.
|
||||
//
|
||||
// The function combines:
|
||||
// - Encoding: Attempts to extract the optional field value, encodes it if present,
|
||||
// and combines it with the base encoding using the monoid. If the field is absent,
|
||||
// only the base encoding is used.
|
||||
// - Validation: Validates the optional field and combines the validation with the
|
||||
// base validation using applicative semantics (error accumulation).
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - S: The source struct type (what we're building a codec for)
|
||||
// - T: The optional field type accessed by the optional
|
||||
// - O: The output type for encoding (must have a monoid)
|
||||
// - I: The input type for decoding
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: A Monoid[O] for combining encoded outputs
|
||||
// - o: An Optional[S, T] that focuses on a field in S that may not exist
|
||||
// - fa: A Type[T, O, I] codec for the optional field type T
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[S, S, O, I] that transforms a base codec by adding the optional field
|
||||
// specified by the optional.
|
||||
//
|
||||
// # How It Works
|
||||
//
|
||||
// 1. **Encoding**: When encoding a value of type S:
|
||||
// - Try to extract the optional field T using o.GetOption
|
||||
// - If present (Some(T)): Encode T to O using fa.Encode and combine with base using monoid
|
||||
// - If absent (None): Return only the base encoding unchanged
|
||||
//
|
||||
// 2. **Validation**: When validating input I:
|
||||
// - Validate the optional field using fa.Validate through o.Set
|
||||
// - Combine with the base validation using applicative semantics
|
||||
// - Accumulates all validation errors from both base and field
|
||||
//
|
||||
// 3. **Type Checking**: Preserves the base type checker
|
||||
//
|
||||
// # Difference from ApSL
|
||||
//
|
||||
// Unlike ApSL which works with required fields via Lens, ApSO handles optional fields:
|
||||
// - ApSL: Field always exists, always encoded
|
||||
// - ApSO: Field may not exist, only encoded when present
|
||||
// - ApSO uses Optional.GetOption which returns Option[T]
|
||||
// - ApSO gracefully handles missing fields without errors
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// "github.com/IBM/fp-go/v2/optics/optional"
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Nickname *string // Optional field
|
||||
// }
|
||||
//
|
||||
// // Optional for Person.Nickname
|
||||
// nicknameOpt := optional.MakeOptional(
|
||||
// func(p Person) option.Option[string] {
|
||||
// if p.Nickname != nil {
|
||||
// return option.Some(*p.Nickname)
|
||||
// }
|
||||
// return option.None[string]()
|
||||
// },
|
||||
// func(p Person, nick string) Person {
|
||||
// p.Nickname = &nick
|
||||
// return p
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Build a Person codec with optional nickname
|
||||
// personCodec := F.Pipe1(
|
||||
// codec.Struct[Person]("Person"),
|
||||
// codec.ApSO(S.Monoid, nicknameOpt, codec.String),
|
||||
// )
|
||||
//
|
||||
// // Encoding with nickname present
|
||||
// p1 := Person{Name: "Alice", Nickname: ptr("Ali")}
|
||||
// encoded1 := personCodec.Encode(p1) // Includes nickname
|
||||
//
|
||||
// // Encoding with nickname absent
|
||||
// p2 := Person{Name: "Bob", Nickname: nil}
|
||||
// encoded2 := personCodec.Encode(p2) // No nickname in output
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Building struct codecs with optional/nullable fields
|
||||
// - Handling pointer fields that may be nil
|
||||
// - Composing codecs for structures with optional nested data
|
||||
// - Creating flexible serialization that omits absent fields
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The monoid determines how encoded outputs are combined when field is present
|
||||
// - When the optional field is absent, encoding returns base encoding unchanged
|
||||
// - Validation still accumulates errors even for optional fields
|
||||
// - The name is automatically generated for debugging purposes
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ApSL: For required fields using Lens
|
||||
// - validate.ApS: The underlying validation combinator
|
||||
// - Optional: The optic for accessing optional fields
|
||||
func ApSO[S, T, O, I any](
|
||||
m Monoid[O],
|
||||
o Optional[S, T],
|
||||
fa Type[T, O, I],
|
||||
) Operator[S, S, O, I] {
|
||||
name := fmt.Sprintf("ApS[%s x %s]", o, fa)
|
||||
|
||||
encConcat := F.Flow2(
|
||||
o.GetOption,
|
||||
option.Map(F.Flow2(
|
||||
fa.Encode,
|
||||
semigroup.AppendTo(m),
|
||||
)),
|
||||
)
|
||||
|
||||
valConcat := validate.ApS(o.Set, fa.Validate)
|
||||
|
||||
return func(t Type[S, O, I]) Type[S, O, I] {
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
t.Is,
|
||||
F.Pipe1(
|
||||
t.Validate,
|
||||
valConcat,
|
||||
),
|
||||
func(s S) O {
|
||||
to := t.Encode(s)
|
||||
return F.Pipe2(
|
||||
encConcat(s),
|
||||
option.Flap[O](to),
|
||||
option.GetOrElse(lazy.Of(to)),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
818
v2/optics/codec/bind_test.go
Normal file
818
v2/optics/codec/bind_test.go
Normal file
@@ -0,0 +1,818 @@
|
||||
// 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 (
|
||||
"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/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for ApSL
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
func TestApSL_EncodingCombination(t *testing.T) {
|
||||
t.Run("combines encodings using monoid", func(t *testing.T) {
|
||||
// Create a lens for Person.Name
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
// Create base codec that encodes to "Person:"
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected Person",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "Person:" },
|
||||
)
|
||||
|
||||
// Create field codec for Name
|
||||
nameCodec := MakeType(
|
||||
"Name",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected string",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply ApSL to combine encodings
|
||||
operator := ApSL(S.Monoid, nameLens, nameCodec)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Test encoding - should concatenate base encoding with field encoding
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
encoded := enhancedCodec.Encode(person)
|
||||
|
||||
// The monoid concatenates: base encoding + field encoding
|
||||
// Note: The order depends on how the monoid is applied in ApSL
|
||||
assert.Contains(t, encoded, "Person:")
|
||||
assert.Contains(t, encoded, "Alice")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL_ValidationCombination(t *testing.T) {
|
||||
t.Run("validates field through lens", func(t *testing.T) {
|
||||
// Create a lens for Person.Age
|
||||
ageLens := lens.MakeLens(
|
||||
func(p Person) int { return p.Age },
|
||||
func(p Person, age int) Person {
|
||||
return Person{Name: p.Name, Age: age}
|
||||
},
|
||||
)
|
||||
|
||||
// Create base codec that always succeeds
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected Person",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
// Create field codec for Age that validates positive numbers
|
||||
ageCodec := MakeType(
|
||||
"Age",
|
||||
func(i any) validation.Result[int] {
|
||||
if n, ok := i.(int); ok {
|
||||
if n > 0 {
|
||||
return validation.ToResult(validation.Success(n))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: n,
|
||||
Messsage: "age must be positive",
|
||||
},
|
||||
}))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected int",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, int] {
|
||||
return func(ctx Context) validation.Validation[int] {
|
||||
if n, ok := i.(int); ok {
|
||||
if n > 0 {
|
||||
return validation.Success(n)
|
||||
}
|
||||
return validation.FailureWithMessage[int](n, "age must be positive")(ctx)
|
||||
}
|
||||
return validation.FailureWithMessage[int](i, "expected int")(ctx)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
// Apply ApSL
|
||||
operator := ApSL(S.Monoid, ageLens, ageCodec)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Test with invalid age (negative) - field validation should fail
|
||||
invalidPerson := Person{Name: "Charlie", Age: -5}
|
||||
invalidResult := enhancedCodec.Decode(invalidPerson)
|
||||
assert.True(t, either.IsLeft(invalidResult), "Should fail with negative age")
|
||||
|
||||
// Extract and verify we have errors
|
||||
errors := either.MonadFold(invalidResult,
|
||||
F.Identity[validation.Errors],
|
||||
func(Person) validation.Errors { return nil },
|
||||
)
|
||||
assert.NotEmpty(t, errors, "Should have validation errors")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL_TypeChecking(t *testing.T) {
|
||||
t.Run("preserves base type checker", func(t *testing.T) {
|
||||
// Create a lens for Person.Name
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
// Create base codec with type checker
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected Person",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
// Create field codec
|
||||
nameCodec := MakeType(
|
||||
"Name",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected string",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply ApSL
|
||||
operator := ApSL(S.Monoid, nameLens, nameCodec)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Test type checking with valid type
|
||||
person := Person{Name: "Eve", Age: 22}
|
||||
isResult := enhancedCodec.Is(person)
|
||||
assert.True(t, either.IsRight(isResult), "Should accept Person type")
|
||||
|
||||
// Test type checking with invalid type
|
||||
invalidResult := enhancedCodec.Is("not a person")
|
||||
assert.True(t, either.IsLeft(invalidResult), "Should reject non-Person type")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL_Naming(t *testing.T) {
|
||||
t.Run("generates descriptive name", func(t *testing.T) {
|
||||
// Create a lens for Person.Name
|
||||
nameLens := lens.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person {
|
||||
return Person{Name: name, Age: p.Age}
|
||||
},
|
||||
)
|
||||
|
||||
// Create base codec
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected Person",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
if p, ok := i.(Person); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
// Create field codec
|
||||
nameCodec := MakeType(
|
||||
"Name",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected string",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply ApSL
|
||||
operator := ApSL(S.Monoid, nameLens, nameCodec)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Check that the name includes ApS
|
||||
name := enhancedCodec.Name()
|
||||
assert.Contains(t, name, "ApS", "Name should contain 'ApS'")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL_ErrorAccumulation(t *testing.T) {
|
||||
t.Run("accumulates validation errors", func(t *testing.T) {
|
||||
// Create a lens for Person.Age
|
||||
ageLens := lens.MakeLens(
|
||||
func(p Person) int { return p.Age },
|
||||
func(p Person, age int) Person {
|
||||
return Person{Name: p.Name, Age: age}
|
||||
},
|
||||
)
|
||||
|
||||
// Create base codec that fails validation
|
||||
baseCodec := MakeType(
|
||||
"Person",
|
||||
func(i any) validation.Result[Person] {
|
||||
return validation.ToResult(validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "base validation error",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, Person] {
|
||||
return func(ctx Context) validation.Validation[Person] {
|
||||
return validation.FailureWithMessage[Person](i, "base validation error")(ctx)
|
||||
}
|
||||
},
|
||||
func(p Person) string { return "" },
|
||||
)
|
||||
|
||||
// Create field codec that also fails
|
||||
ageCodec := MakeType(
|
||||
"Age",
|
||||
func(i any) validation.Result[int] {
|
||||
return validation.ToResult(validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "age validation error",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, int] {
|
||||
return func(ctx Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](i, "age validation error")(ctx)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
// Apply ApSL
|
||||
operator := ApSL(S.Monoid, ageLens, ageCodec)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Test validation - should accumulate errors
|
||||
person := Person{Name: "Dave", Age: 30}
|
||||
result := enhancedCodec.Decode(person)
|
||||
|
||||
// Should fail
|
||||
assert.True(t, either.IsLeft(result), "Should fail validation")
|
||||
|
||||
// Extract errors
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(Person) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
// Should have errors from both base and field validation
|
||||
assert.NotEmpty(t, errors, "Should have validation errors")
|
||||
})
|
||||
}
|
||||
|
||||
// Test types for ApSO
|
||||
type PersonWithNickname struct {
|
||||
Name string
|
||||
Nickname *string
|
||||
}
|
||||
|
||||
func TestApSO_EncodingWithPresentField(t *testing.T) {
|
||||
t.Run("encodes optional field when present", func(t *testing.T) {
|
||||
// Create an optional for PersonWithNickname.Nickname
|
||||
nicknameOpt := optional.MakeOptional(
|
||||
func(p PersonWithNickname) option.Option[string] {
|
||||
if p.Nickname != nil {
|
||||
return option.Some(*p.Nickname)
|
||||
}
|
||||
return option.None[string]()
|
||||
},
|
||||
func(p PersonWithNickname, nick string) PersonWithNickname {
|
||||
p.Nickname = &nick
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
// Create base codec that encodes to "Person:"
|
||||
baseCodec := MakeType(
|
||||
"PersonWithNickname",
|
||||
func(i any) validation.Result[PersonWithNickname] {
|
||||
if p, ok := i.(PersonWithNickname); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected PersonWithNickname",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, PersonWithNickname] {
|
||||
return func(ctx Context) validation.Validation[PersonWithNickname] {
|
||||
if p, ok := i.(PersonWithNickname); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
|
||||
}
|
||||
},
|
||||
func(p PersonWithNickname) string { return "Person:" },
|
||||
)
|
||||
|
||||
// Create field codec for Nickname
|
||||
nicknameCodec := MakeType(
|
||||
"Nickname",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected string",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply ApSO to combine encodings
|
||||
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Test encoding with nickname present
|
||||
nickname := "Ali"
|
||||
person := PersonWithNickname{Name: "Alice", Nickname: &nickname}
|
||||
encoded := enhancedCodec.Encode(person)
|
||||
|
||||
// Should include both base and nickname
|
||||
assert.Contains(t, encoded, "Person:")
|
||||
assert.Contains(t, encoded, "Ali")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSO_EncodingWithAbsentField(t *testing.T) {
|
||||
t.Run("omits optional field when absent", func(t *testing.T) {
|
||||
// Create an optional for PersonWithNickname.Nickname
|
||||
nicknameOpt := optional.MakeOptional(
|
||||
func(p PersonWithNickname) option.Option[string] {
|
||||
if p.Nickname != nil {
|
||||
return option.Some(*p.Nickname)
|
||||
}
|
||||
return option.None[string]()
|
||||
},
|
||||
func(p PersonWithNickname, nick string) PersonWithNickname {
|
||||
p.Nickname = &nick
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
// Create base codec
|
||||
baseCodec := MakeType(
|
||||
"PersonWithNickname",
|
||||
func(i any) validation.Result[PersonWithNickname] {
|
||||
if p, ok := i.(PersonWithNickname); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected PersonWithNickname",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, PersonWithNickname] {
|
||||
return func(ctx Context) validation.Validation[PersonWithNickname] {
|
||||
if p, ok := i.(PersonWithNickname); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
|
||||
}
|
||||
},
|
||||
func(p PersonWithNickname) string { return "Person:Bob" },
|
||||
)
|
||||
|
||||
// Create field codec
|
||||
nicknameCodec := MakeType(
|
||||
"Nickname",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected string",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply ApSO
|
||||
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Test encoding with nickname absent
|
||||
person := PersonWithNickname{Name: "Bob", Nickname: nil}
|
||||
encoded := enhancedCodec.Encode(person)
|
||||
|
||||
// Should only have base encoding
|
||||
assert.Equal(t, "Person:Bob", encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSO_TypeChecking(t *testing.T) {
|
||||
t.Run("preserves base type checker", func(t *testing.T) {
|
||||
// Create an optional for PersonWithNickname.Nickname
|
||||
nicknameOpt := optional.MakeOptional(
|
||||
func(p PersonWithNickname) option.Option[string] {
|
||||
if p.Nickname != nil {
|
||||
return option.Some(*p.Nickname)
|
||||
}
|
||||
return option.None[string]()
|
||||
},
|
||||
func(p PersonWithNickname, nick string) PersonWithNickname {
|
||||
p.Nickname = &nick
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
// Create base codec with type checker
|
||||
baseCodec := MakeType(
|
||||
"PersonWithNickname",
|
||||
func(i any) validation.Result[PersonWithNickname] {
|
||||
if p, ok := i.(PersonWithNickname); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected PersonWithNickname",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, PersonWithNickname] {
|
||||
return func(ctx Context) validation.Validation[PersonWithNickname] {
|
||||
if p, ok := i.(PersonWithNickname); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
|
||||
}
|
||||
},
|
||||
func(p PersonWithNickname) string { return "" },
|
||||
)
|
||||
|
||||
// Create field codec
|
||||
nicknameCodec := MakeType(
|
||||
"Nickname",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected string",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply ApSO
|
||||
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Test type checking with valid type
|
||||
nickname := "Eve"
|
||||
person := PersonWithNickname{Name: "Eve", Nickname: &nickname}
|
||||
isResult := enhancedCodec.Is(person)
|
||||
assert.True(t, either.IsRight(isResult), "Should accept PersonWithNickname type")
|
||||
|
||||
// Test type checking with invalid type
|
||||
invalidResult := enhancedCodec.Is("not a person")
|
||||
assert.True(t, either.IsLeft(invalidResult), "Should reject non-PersonWithNickname type")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSO_Naming(t *testing.T) {
|
||||
t.Run("generates descriptive name", func(t *testing.T) {
|
||||
// Create an optional for PersonWithNickname.Nickname
|
||||
nicknameOpt := optional.MakeOptional(
|
||||
func(p PersonWithNickname) option.Option[string] {
|
||||
if p.Nickname != nil {
|
||||
return option.Some(*p.Nickname)
|
||||
}
|
||||
return option.None[string]()
|
||||
},
|
||||
func(p PersonWithNickname, nick string) PersonWithNickname {
|
||||
p.Nickname = &nick
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
// Create base codec
|
||||
baseCodec := MakeType(
|
||||
"PersonWithNickname",
|
||||
func(i any) validation.Result[PersonWithNickname] {
|
||||
if p, ok := i.(PersonWithNickname); ok {
|
||||
return validation.ToResult(validation.Success(p))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected PersonWithNickname",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, PersonWithNickname] {
|
||||
return func(ctx Context) validation.Validation[PersonWithNickname] {
|
||||
if p, ok := i.(PersonWithNickname); ok {
|
||||
return validation.Success(p)
|
||||
}
|
||||
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
|
||||
}
|
||||
},
|
||||
func(p PersonWithNickname) string { return "" },
|
||||
)
|
||||
|
||||
// Create field codec
|
||||
nicknameCodec := MakeType(
|
||||
"Nickname",
|
||||
func(i any) validation.Result[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.ToResult(validation.Success(s))
|
||||
}
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "expected string",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
if s, ok := i.(string); ok {
|
||||
return validation.Success(s)
|
||||
}
|
||||
return validation.FailureWithMessage[string](i, "expected string")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply ApSO
|
||||
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Check that the name includes ApS
|
||||
name := enhancedCodec.Name()
|
||||
assert.Contains(t, name, "ApS", "Name should contain 'ApS'")
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSO_ErrorAccumulation(t *testing.T) {
|
||||
t.Run("accumulates validation errors", func(t *testing.T) {
|
||||
// Create an optional for PersonWithNickname.Nickname
|
||||
nicknameOpt := optional.MakeOptional(
|
||||
func(p PersonWithNickname) option.Option[string] {
|
||||
if p.Nickname != nil {
|
||||
return option.Some(*p.Nickname)
|
||||
}
|
||||
return option.None[string]()
|
||||
},
|
||||
func(p PersonWithNickname, nick string) PersonWithNickname {
|
||||
p.Nickname = &nick
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
// Create base codec that fails validation
|
||||
baseCodec := MakeType(
|
||||
"PersonWithNickname",
|
||||
func(i any) validation.Result[PersonWithNickname] {
|
||||
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "base validation error",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, PersonWithNickname] {
|
||||
return func(ctx Context) validation.Validation[PersonWithNickname] {
|
||||
return validation.FailureWithMessage[PersonWithNickname](i, "base validation error")(ctx)
|
||||
}
|
||||
},
|
||||
func(p PersonWithNickname) string { return "" },
|
||||
)
|
||||
|
||||
// Create field codec that also fails
|
||||
nicknameCodec := MakeType(
|
||||
"Nickname",
|
||||
func(i any) validation.Result[string] {
|
||||
return validation.ToResult(validation.Failures[string](validation.Errors{
|
||||
&validation.ValidationError{
|
||||
Value: i,
|
||||
Messsage: "nickname validation error",
|
||||
},
|
||||
}))
|
||||
},
|
||||
func(i any) Decode[Context, string] {
|
||||
return func(ctx Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](i, "nickname validation error")(ctx)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply ApSO
|
||||
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
|
||||
enhancedCodec := operator(baseCodec)
|
||||
|
||||
// Test validation with present nickname - should accumulate errors
|
||||
nickname := "Dave"
|
||||
person := PersonWithNickname{Name: "Dave", Nickname: &nickname}
|
||||
result := enhancedCodec.Decode(person)
|
||||
|
||||
// Should fail
|
||||
assert.True(t, either.IsLeft(result), "Should fail validation")
|
||||
|
||||
// Extract errors
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(PersonWithNickname) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
// Should have errors from both base and field validation
|
||||
assert.NotEmpty(t, errors, "Should have validation errors")
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/decoder"
|
||||
"github.com/IBM/fp-go/v2/optics/encoder"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
@@ -338,4 +340,104 @@ type (
|
||||
// - ApplicativeMonoid: Combines successful results using inner monoid
|
||||
// - AlternativeMonoid: Combines applicative and alternative behaviors
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
|
||||
// Lens is an optic that focuses on a specific field within a product type S.
|
||||
// It provides a way to get and set a field of type A within a structure of type S.
|
||||
//
|
||||
// A Lens[S, A] represents a relationship between a source type S and a focus type A,
|
||||
// where the focus always exists (unlike Optional which may not exist).
|
||||
//
|
||||
// Lens operations:
|
||||
// - Get: Extract the field value A from structure S
|
||||
// - Set: Update the field value A in structure S, returning a new S
|
||||
//
|
||||
// Lens laws:
|
||||
// 1. GetSet: If you get a value and then set it back, nothing changes
|
||||
// Set(Get(s))(s) = s
|
||||
// 2. SetGet: If you set a value, you can get it back
|
||||
// Get(Set(a)(s)) = a
|
||||
// 3. SetSet: Setting twice is the same as setting once with the final value
|
||||
// Set(b)(Set(a)(s)) = Set(b)(s)
|
||||
//
|
||||
// In the codec context, lenses are used with ApSL to build codecs for struct fields:
|
||||
// - Extract field values for encoding
|
||||
// - Update field values during validation
|
||||
// - Compose codec operations on nested structures
|
||||
//
|
||||
// Example:
|
||||
// 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 },
|
||||
// )
|
||||
//
|
||||
// // Use with ApSL to build a codec
|
||||
// personCodec := F.Pipe1(
|
||||
// codec.Struct[Person]("Person"),
|
||||
// codec.ApSL(S.Monoid, nameLens, codec.String),
|
||||
// )
|
||||
//
|
||||
// See also:
|
||||
// - ApSL: Applicative sequencing with lens
|
||||
// - Optional: For fields that may not exist
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
// Optional is an optic that focuses on a field within a product type S that may not exist.
|
||||
// It provides a way to get and set an optional field of type A within a structure of type S.
|
||||
//
|
||||
// An Optional[S, A] represents a relationship between a source type S and a focus type A,
|
||||
// where the focus may or may not be present (unlike Lens where it always exists).
|
||||
//
|
||||
// Optional operations:
|
||||
// - GetOption: Try to extract the field value, returning Option[A]
|
||||
// - Set: Update the field value if it exists, returning a new S
|
||||
//
|
||||
// Optional laws:
|
||||
// 1. GetSet (No-op on None): If GetOption returns None, Set has no effect
|
||||
// GetOption(s) = None => Set(a)(s) = s
|
||||
// 2. SetGet (Get what you Set): If GetOption returns Some, you can get back what you set
|
||||
// GetOption(s) = Some(_) => GetOption(Set(a)(s)) = Some(a)
|
||||
// 3. SetSet (Last Set Wins): Setting twice is the same as setting once with the final value
|
||||
// Set(b)(Set(a)(s)) = Set(b)(s)
|
||||
//
|
||||
// In the codec context, optionals are used with ApSO to build codecs for optional fields:
|
||||
// - Extract optional field values for encoding (only if present)
|
||||
// - Update optional field values during validation
|
||||
// - Handle nullable or pointer fields gracefully
|
||||
// - Compose codec operations on structures with optional data
|
||||
//
|
||||
// Example:
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Nickname *string // Optional field
|
||||
// }
|
||||
//
|
||||
// nicknameOpt := optional.MakeOptional(
|
||||
// func(p Person) option.Option[string] {
|
||||
// if p.Nickname != nil {
|
||||
// return option.Some(*p.Nickname)
|
||||
// }
|
||||
// return option.None[string]()
|
||||
// },
|
||||
// func(p Person, nick string) Person {
|
||||
// p.Nickname = &nick
|
||||
// return p
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Use with ApSO to build a codec with optional field
|
||||
// personCodec := F.Pipe1(
|
||||
// codec.Struct[Person]("Person"),
|
||||
// codec.ApSO(S.Monoid, nicknameOpt, codec.String),
|
||||
// )
|
||||
//
|
||||
// // Encoding omits the field when absent
|
||||
// p1 := Person{Name: "Alice", Nickname: nil}
|
||||
// encoded := personCodec.Encode(p1) // No nickname in output
|
||||
//
|
||||
// See also:
|
||||
// - ApSO: Applicative sequencing with optional
|
||||
// - Lens: For fields that always exist
|
||||
Optional[S, A any] = optional.Optional[S, A]
|
||||
)
|
||||
|
||||
@@ -123,3 +123,78 @@ func Last[A any]() Semigroup[A] {
|
||||
func ToMagma[A any](s Semigroup[A]) M.Magma[A] {
|
||||
return s
|
||||
}
|
||||
|
||||
// ConcatWith creates a curried version of the Concat operation with the left argument fixed first.
|
||||
// It returns a function that takes the left operand and returns another function that takes
|
||||
// the right operand and performs the concatenation.
|
||||
//
|
||||
// This is useful for partial application and function composition patterns.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of elements in the semigroup
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - s: The semigroup to use for concatenation
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(A) func(A) A: A curried function that takes left then right operand
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
// sum := N.SemigroupSum[int]()
|
||||
// concatWith := ConcatWith(sum)
|
||||
// add5 := concatWith(5)
|
||||
// result := add5(3) // 5 + 3 = 8
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - AppendTo: Similar but fixes the right argument first
|
||||
func ConcatWith[A any](s Semigroup[A]) func(A) func(A) A {
|
||||
return func(l A) func(A) A {
|
||||
return func(r A) A {
|
||||
return s.Concat(l, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AppendTo creates a curried version of the Concat operation with the right argument fixed first.
|
||||
// It returns a function that takes the right operand and returns another function that takes
|
||||
// the left operand and performs the concatenation.
|
||||
//
|
||||
// This is useful for partial application where you want to fix the second argument first,
|
||||
// which is common in append-style operations.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of elements in the semigroup
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - s: The semigroup to use for concatenation
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(A) func(A) A: A curried function that takes right then left operand
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import S "github.com/IBM/fp-go/v2/string"
|
||||
// strConcat := S.Semigroup
|
||||
// appendTo := AppendTo(strConcat)
|
||||
// addSuffix := appendTo("!")
|
||||
// result := addSuffix("Hello") // "Hello" + "!" = "Hello!"
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ConcatWith: Similar but fixes the left argument first
|
||||
func AppendTo[A any](s Semigroup[A]) func(A) func(A) A {
|
||||
return func(r A) func(A) A {
|
||||
return func(l A) A {
|
||||
return s.Concat(l, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,3 +444,261 @@ func BenchmarkFunctionSemigroup(b *testing.B) {
|
||||
combined("hello")
|
||||
}
|
||||
}
|
||||
|
||||
// Test ConcatWith function
|
||||
func TestConcatWith(t *testing.T) {
|
||||
t.Run("with integer addition", func(t *testing.T) {
|
||||
add := MakeSemigroup(func(a, b int) int { return a + b })
|
||||
concatWith := ConcatWith(add)
|
||||
|
||||
// Fix left operand to 5
|
||||
add5 := concatWith(5)
|
||||
assert.Equal(t, 8, add5(3)) // 5 + 3 = 8
|
||||
assert.Equal(t, 15, add5(10)) // 5 + 10 = 15
|
||||
assert.Equal(t, 5, add5(0)) // 5 + 0 = 5
|
||||
})
|
||||
|
||||
t.Run("with string concatenation", func(t *testing.T) {
|
||||
concat := MakeSemigroup(func(a, b string) string { return a + b })
|
||||
concatWith := ConcatWith(concat)
|
||||
|
||||
// Fix left operand to "Hello, "
|
||||
greet := concatWith("Hello, ")
|
||||
assert.Equal(t, "Hello, World", greet("World"))
|
||||
assert.Equal(t, "Hello, Bob", greet("Bob"))
|
||||
assert.Equal(t, "Hello, ", greet(""))
|
||||
})
|
||||
|
||||
t.Run("with subtraction (non-commutative)", func(t *testing.T) {
|
||||
sub := MakeSemigroup(func(a, b int) int { return a - b })
|
||||
concatWith := ConcatWith(sub)
|
||||
|
||||
// Fix left operand to 10
|
||||
subtract10 := concatWith(10)
|
||||
assert.Equal(t, 7, subtract10(3)) // 10 - 3 = 7
|
||||
assert.Equal(t, 5, subtract10(5)) // 10 - 5 = 5
|
||||
assert.Equal(t, -5, subtract10(15)) // 10 - 15 = -5
|
||||
})
|
||||
|
||||
t.Run("with First semigroup", func(t *testing.T) {
|
||||
first := First[int]()
|
||||
concatWith := ConcatWith(first)
|
||||
|
||||
// Fix left operand to 42
|
||||
always42 := concatWith(42)
|
||||
assert.Equal(t, 42, always42(1))
|
||||
assert.Equal(t, 42, always42(100))
|
||||
assert.Equal(t, 42, always42(0))
|
||||
})
|
||||
|
||||
t.Run("with Last semigroup", func(t *testing.T) {
|
||||
last := Last[string]()
|
||||
concatWith := ConcatWith(last)
|
||||
|
||||
// Fix left operand to "first"
|
||||
alwaysSecond := concatWith("first")
|
||||
assert.Equal(t, "second", alwaysSecond("second"))
|
||||
assert.Equal(t, "other", alwaysSecond("other"))
|
||||
assert.Equal(t, "", alwaysSecond(""))
|
||||
})
|
||||
|
||||
t.Run("currying behavior", func(t *testing.T) {
|
||||
mul := MakeSemigroup(func(a, b int) int { return a * b })
|
||||
concatWith := ConcatWith(mul)
|
||||
|
||||
// Create multiple partially applied functions
|
||||
double := concatWith(2)
|
||||
triple := concatWith(3)
|
||||
quadruple := concatWith(4)
|
||||
|
||||
assert.Equal(t, 10, double(5)) // 2 * 5 = 10
|
||||
assert.Equal(t, 15, triple(5)) // 3 * 5 = 15
|
||||
assert.Equal(t, 20, quadruple(5)) // 4 * 5 = 20
|
||||
})
|
||||
|
||||
t.Run("with complex types", func(t *testing.T) {
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
pointAdd := MakeSemigroup(func(a, b Point) Point {
|
||||
return Point{X: a.X + b.X, Y: a.Y + b.Y}
|
||||
})
|
||||
concatWith := ConcatWith(pointAdd)
|
||||
|
||||
// Fix left operand to origin offset
|
||||
offset := concatWith(Point{X: 10, Y: 20})
|
||||
assert.Equal(t, Point{X: 15, Y: 25}, offset(Point{X: 5, Y: 5}))
|
||||
assert.Equal(t, Point{X: 10, Y: 20}, offset(Point{X: 0, Y: 0}))
|
||||
})
|
||||
}
|
||||
|
||||
// Test AppendTo function
|
||||
func TestAppendTo(t *testing.T) {
|
||||
t.Run("with integer addition", func(t *testing.T) {
|
||||
add := MakeSemigroup(func(a, b int) int { return a + b })
|
||||
appendTo := AppendTo(add)
|
||||
|
||||
// Fix right operand to 5
|
||||
addTo5 := appendTo(5)
|
||||
assert.Equal(t, 8, addTo5(3)) // 3 + 5 = 8
|
||||
assert.Equal(t, 15, addTo5(10)) // 10 + 5 = 15
|
||||
assert.Equal(t, 5, addTo5(0)) // 0 + 5 = 5
|
||||
})
|
||||
|
||||
t.Run("with string concatenation", func(t *testing.T) {
|
||||
concat := MakeSemigroup(func(a, b string) string { return a + b })
|
||||
appendTo := AppendTo(concat)
|
||||
|
||||
// Fix right operand to "!"
|
||||
addExclamation := appendTo("!")
|
||||
assert.Equal(t, "Hello!", addExclamation("Hello"))
|
||||
assert.Equal(t, "World!", addExclamation("World"))
|
||||
assert.Equal(t, "!", addExclamation(""))
|
||||
})
|
||||
|
||||
t.Run("with subtraction (non-commutative)", func(t *testing.T) {
|
||||
sub := MakeSemigroup(func(a, b int) int { return a - b })
|
||||
appendTo := AppendTo(sub)
|
||||
|
||||
// Fix right operand to 3
|
||||
subtract3 := appendTo(3)
|
||||
assert.Equal(t, 7, subtract3(10)) // 10 - 3 = 7
|
||||
assert.Equal(t, 2, subtract3(5)) // 5 - 3 = 2
|
||||
assert.Equal(t, -3, subtract3(0)) // 0 - 3 = -3
|
||||
assert.Equal(t, -8, subtract3(-5)) // -5 - 3 = -8
|
||||
})
|
||||
|
||||
t.Run("with First semigroup", func(t *testing.T) {
|
||||
first := First[string]()
|
||||
appendTo := AppendTo(first)
|
||||
|
||||
// Fix right operand to "second"
|
||||
alwaysFirst := appendTo("second")
|
||||
assert.Equal(t, "first", alwaysFirst("first"))
|
||||
assert.Equal(t, "other", alwaysFirst("other"))
|
||||
assert.Equal(t, "", alwaysFirst(""))
|
||||
})
|
||||
|
||||
t.Run("with Last semigroup", func(t *testing.T) {
|
||||
last := Last[int]()
|
||||
appendTo := AppendTo(last)
|
||||
|
||||
// Fix right operand to 42
|
||||
always42 := appendTo(42)
|
||||
assert.Equal(t, 42, always42(1))
|
||||
assert.Equal(t, 42, always42(100))
|
||||
assert.Equal(t, 42, always42(0))
|
||||
})
|
||||
|
||||
t.Run("currying behavior", func(t *testing.T) {
|
||||
mul := MakeSemigroup(func(a, b int) int { return a * b })
|
||||
appendTo := AppendTo(mul)
|
||||
|
||||
// Create multiple partially applied functions
|
||||
multiplyBy2 := appendTo(2)
|
||||
multiplyBy3 := appendTo(3)
|
||||
multiplyBy4 := appendTo(4)
|
||||
|
||||
assert.Equal(t, 10, multiplyBy2(5)) // 5 * 2 = 10
|
||||
assert.Equal(t, 15, multiplyBy3(5)) // 5 * 3 = 15
|
||||
assert.Equal(t, 20, multiplyBy4(5)) // 5 * 4 = 20
|
||||
})
|
||||
|
||||
t.Run("with complex types", func(t *testing.T) {
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
pointAdd := MakeSemigroup(func(a, b Point) Point {
|
||||
return Point{X: a.X + b.X, Y: a.Y + b.Y}
|
||||
})
|
||||
appendTo := AppendTo(pointAdd)
|
||||
|
||||
// Fix right operand to offset
|
||||
addOffset := appendTo(Point{X: 10, Y: 20})
|
||||
assert.Equal(t, Point{X: 15, Y: 25}, addOffset(Point{X: 5, Y: 5}))
|
||||
assert.Equal(t, Point{X: 10, Y: 20}, addOffset(Point{X: 0, Y: 0}))
|
||||
})
|
||||
}
|
||||
|
||||
// Test ConcatWith vs AppendTo difference
|
||||
func TestConcatWithVsAppendTo(t *testing.T) {
|
||||
t.Run("demonstrates order difference with non-commutative operation", func(t *testing.T) {
|
||||
sub := MakeSemigroup(func(a, b int) int { return a - b })
|
||||
|
||||
concatWith := ConcatWith(sub)
|
||||
appendTo := AppendTo(sub)
|
||||
|
||||
// ConcatWith fixes left operand first
|
||||
subtract10From := concatWith(10) // 10 - x
|
||||
assert.Equal(t, 7, subtract10From(3)) // 10 - 3 = 7
|
||||
|
||||
// AppendTo fixes right operand first
|
||||
subtract3From := appendTo(3) // x - 3
|
||||
assert.Equal(t, 7, subtract3From(10)) // 10 - 3 = 7
|
||||
|
||||
// Same result but different partial application order
|
||||
assert.Equal(t, subtract10From(3), subtract3From(10))
|
||||
})
|
||||
|
||||
t.Run("demonstrates order difference with string concatenation", func(t *testing.T) {
|
||||
concat := MakeSemigroup(func(a, b string) string { return a + b })
|
||||
|
||||
concatWith := ConcatWith(concat)
|
||||
appendTo := AppendTo(concat)
|
||||
|
||||
// ConcatWith: prefix is fixed
|
||||
addPrefix := concatWith("Hello, ")
|
||||
assert.Equal(t, "Hello, World", addPrefix("World"))
|
||||
|
||||
// AppendTo: suffix is fixed
|
||||
addSuffix := appendTo("!")
|
||||
assert.Equal(t, "Hello!", addSuffix("Hello"))
|
||||
|
||||
// Different results due to different order
|
||||
assert.NotEqual(t, addPrefix("test"), addSuffix("test"))
|
||||
})
|
||||
}
|
||||
|
||||
// Test composition with ConcatWith and AppendTo
|
||||
func TestConcatWithAppendToComposition(t *testing.T) {
|
||||
t.Run("composing multiple operations", func(t *testing.T) {
|
||||
add := MakeSemigroup(func(a, b int) int { return a + b })
|
||||
|
||||
// Create a pipeline: add 5, then add 3
|
||||
concatWith := ConcatWith(add)
|
||||
appendTo := AppendTo(add)
|
||||
|
||||
add5 := concatWith(5)
|
||||
add3 := appendTo(3)
|
||||
|
||||
// Apply both operations
|
||||
result := add3(add5(2)) // (2 + 5) + 3 = 10
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark ConcatWith
|
||||
func BenchmarkConcatWith(b *testing.B) {
|
||||
add := MakeSemigroup(func(a, b int) int { return a + b })
|
||||
concatWith := ConcatWith(add)
|
||||
add5 := concatWith(5)
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
add5(3)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark AppendTo
|
||||
func BenchmarkAppendTo(b *testing.B) {
|
||||
add := MakeSemigroup(func(a, b int) int { return a + b })
|
||||
appendTo := AppendTo(add)
|
||||
addTo5 := appendTo(5)
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
addTo5(3)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user