1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-15 23:33:46 +02:00

Compare commits

...

1 Commits

Author SHA1 Message Date
Dr. Carsten Leue
7cd575d95a fix: improve Prism and Optional
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 18:22:52 +01:00
6 changed files with 406 additions and 52 deletions

View File

@@ -50,6 +50,7 @@ import (
"github.com/IBM/fp-go/v2/boolean"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
@@ -266,3 +267,197 @@ func RunAll(testcases map[string]Reader) Reader {
return current
}
}
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
// by providing a function that converts R2 to R1. This allows you to focus a test on a
// specific property or subset of a larger data structure.
//
// This is particularly useful when you have an assertion that operates on a specific field
// or property, and you want to apply it to a complete object. Instead of extracting the
// property and then asserting on it, you can transform the assertion to work directly
// on the whole object.
//
// Parameters:
// - f: A function that extracts or transforms R2 into R1
//
// Returns:
// - A function that transforms a Reader[R1, Reader] into a Reader[R2, Reader]
//
// Example:
//
// type User struct {
// Name string
// Age int
// }
//
// // Create an assertion that checks if age is positive
// ageIsPositive := assert.That(func(age int) bool { return age > 0 })
//
// // Focus this assertion on the Age field of User
// userAgeIsPositive := assert.Local(func(u User) int { return u.Age })(ageIsPositive)
//
// // Now we can test the whole User object
// user := User{Name: "Alice", Age: 30}
// userAgeIsPositive(user)(t)
//
//go:inline
func Local[R1, R2 any](f func(R2) R1) func(Kleisli[R1]) Kleisli[R2] {
return reader.Local[Reader](f)
}
// LocalL is similar to Local but uses a Lens to focus on a specific property.
// A Lens is a functional programming construct that provides a composable way to
// focus on a part of a data structure.
//
// This function is particularly useful when you want to focus a test on a specific
// field of a struct using a lens, making the code more declarative and composable.
// Lenses are often code-generated or predefined for common data structures.
//
// Parameters:
// - l: A Lens that focuses from type S to type T
//
// Returns:
// - A function that transforms a Reader[T, Reader] into a Reader[S, Reader]
//
// Example:
//
// type Person struct {
// Name string
// Email string
// }
//
// // Assume we have a lens that focuses on the Email field
// var emailLens = lens.Prop[Person, string]("Email")
//
// // Create an assertion for email format
// validEmail := assert.That(func(email string) bool {
// return strings.Contains(email, "@")
// })
//
// // Focus this assertion on the Email property using a lens
// validPersonEmail := assert.LocalL(emailLens)(validEmail)
//
// // Test a Person object
// person := Person{Name: "Bob", Email: "bob@example.com"}
// validPersonEmail(person)(t)
//
//go:inline
func LocalL[S, T any](l Lens[S, T]) func(Kleisli[T]) Kleisli[S] {
return reader.Local[Reader](l.Get)
}
// fromOptionalGetter is an internal helper that creates an assertion Reader from
// an optional getter function. It asserts that the optional value is present (Some).
func fromOptionalGetter[S, T any](getter func(S) option.Option[T], msgAndArgs ...any) Kleisli[S] {
return func(s S) Reader {
return func(t *testing.T) bool {
return assert.True(t, option.IsSome(getter(s)), msgAndArgs...)
}
}
}
// FromOptional creates an assertion that checks if an Optional can successfully extract a value.
// An Optional is an optic that represents an optional reference to a subpart of a data structure.
//
// This function is useful when you have an Optional optic and want to assert that the optional
// value is present (Some) rather than absent (None). The assertion passes if the Optional's
// GetOption returns Some, and fails if it returns None.
//
// This enables property-focused testing where you verify that a particular optional field or
// sub-structure exists and is accessible.
//
// Parameters:
// - opt: An Optional optic that focuses from type S to type T
//
// Returns:
// - A Reader that asserts the optional value is present when applied to a value of type S
//
// Example:
//
// type Config struct {
// Database *DatabaseConfig // Optional field
// }
//
// type DatabaseConfig struct {
// Host string
// Port int
// }
//
// // Create an Optional that focuses on the Database field
// dbOptional := optional.MakeOptional(
// func(c Config) option.Option[*DatabaseConfig] {
// if c.Database != nil {
// return option.Some(c.Database)
// }
// return option.None[*DatabaseConfig]()
// },
// func(c Config, db *DatabaseConfig) Config {
// c.Database = db
// return c
// },
// )
//
// // Assert that the database config is present
// hasDatabaseConfig := assert.FromOptional(dbOptional)
//
// config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
// hasDatabaseConfig(config)(t) // Passes
//
// emptyConfig := Config{Database: nil}
// hasDatabaseConfig(emptyConfig)(t) // Fails
//
//go:inline
func FromOptional[S, T any](opt Optional[S, T]) reader.Reader[S, Reader] {
return fromOptionalGetter(opt.GetOption, "Optional: %s", opt)
}
// FromPrism creates an assertion that checks if a Prism can successfully extract a value.
// A Prism is an optic used to select part of a sum type (tagged union or variant).
//
// This function is useful when you have a Prism optic and want to assert that a value
// matches a specific variant of a sum type. The assertion passes if the Prism's GetOption
// returns Some (meaning the value is of the expected variant), and fails if it returns None
// (meaning the value is a different variant).
//
// This enables variant-focused testing where you verify that a value is of a particular
// type or matches a specific condition within a sum type.
//
// Parameters:
// - p: A Prism optic that focuses from type S to type T
//
// Returns:
// - A Reader that asserts the prism successfully extracts when applied to a value of type S
//
// Example:
//
// type Result interface{ isResult() }
// type Success struct{ Value int }
// type Failure struct{ Error string }
//
// func (Success) isResult() {}
// func (Failure) isResult() {}
//
// // Create a Prism that focuses on Success variant
// successPrism := prism.MakePrism(
// func(r Result) option.Option[int] {
// if s, ok := r.(Success); ok {
// return option.Some(s.Value)
// }
// return option.None[int]()
// },
// func(v int) Result { return Success{Value: v} },
// )
//
// // Assert that the result is a Success
// isSuccess := assert.FromPrism(successPrism)
//
// result1 := Success{Value: 42}
// isSuccess(result1)(t) // Passes
//
// result2 := Failure{Error: "something went wrong"}
// isSuccess(result2)(t) // Fails
//
//go:inline
func FromPrism[S, T any](p Prism[S, T]) reader.Reader[S, Reader] {
return fromOptionalGetter(p.GetOption, "Prism: %s", p)
}

View File

@@ -19,6 +19,7 @@ import (
"errors"
"testing"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
@@ -212,7 +213,7 @@ func TestError(t *testing.T) {
func TestSuccess(t *testing.T) {
t.Run("should pass for successful result", func(t *testing.T) {
res := result.Of[int](42)
res := result.Of(42)
result := Success(res)(t)
if !result {
t.Error("Expected Success to pass for successful result")
@@ -240,7 +241,7 @@ func TestFailure(t *testing.T) {
t.Run("should fail for successful result", func(t *testing.T) {
mockT := &testing.T{}
res := result.Of[int](42)
res := result.Of(42)
result := Failure(res)(mockT)
if result {
t.Error("Expected Failure to fail for successful result")
@@ -445,41 +446,163 @@ func TestEq(t *testing.T) {
})
}
func TestIntegration(t *testing.T) {
t.Run("complex assertion composition", func(t *testing.T) {
type User struct {
Name string
Age int
Email string
func TestLocal(t *testing.T) {
type User struct {
Name string
Age int
}
t.Run("should focus assertion on a property", func(t *testing.T) {
// Create an assertion that checks if age is positive
ageIsPositive := That(func(age int) bool { return age > 0 })
// Focus this assertion on the Age field of User
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
// Test with a user who has a positive age
user := User{Name: "Alice", Age: 30}
result := userAgeIsPositive(user)(t)
if !result {
t.Error("Expected focused assertion to pass for positive age")
}
})
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
t.Run("should fail when focused property doesn't match", func(t *testing.T) {
mockT := &testing.T{}
ageIsPositive := That(func(age int) bool { return age > 0 })
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
// Test with a user who has zero age
user := User{Name: "Bob", Age: 0}
result := userAgeIsPositive(user)(mockT)
if result {
t.Error("Expected focused assertion to fail for zero age")
}
})
t.Run("should compose with other assertions", func(t *testing.T) {
// Create multiple focused assertions
nameNotEmpty := Local(func(u User) string { return u.Name })(
That(func(name string) bool { return len(name) > 0 }),
)
ageInRange := Local(func(u User) int { return u.Age })(
That(func(age int) bool { return age >= 18 && age <= 100 }),
)
user := User{Name: "Charlie", Age: 25}
assertions := AllOf([]Reader{
Equal("Alice")(user.Name),
Equal(30)(user.Age),
That(func(s string) bool { return len(s) > 0 })(user.Email),
nameNotEmpty(user),
ageInRange(user),
})
result := assertions(t)
if !result {
t.Error("Expected complex assertion composition to pass")
t.Error("Expected composed focused assertions to pass")
}
})
t.Run("test suite with RunAll", func(t *testing.T) {
data := []int{1, 2, 3, 4, 5}
t.Run("should work with Equal assertion", func(t *testing.T) {
// Focus Equal assertion on Name field
nameIsAlice := Local(func(u User) string { return u.Name })(Equal("Alice"))
suite := RunAll(map[string]Reader{
"not_empty": ArrayNotEmpty(data),
"correct_size": ArrayLength[int](5)(data),
"contains_one": ArrayContains(1)(data),
"contains_five": ArrayContains(5)(data),
})
result := suite(t)
user := User{Name: "Alice", Age: 30}
result := nameIsAlice(user)(t)
if !result {
t.Error("Expected test suite to pass")
t.Error("Expected focused Equal assertion to pass")
}
})
}
func TestLocalL(t *testing.T) {
// Note: LocalL requires lens package which provides lens operations.
// This test demonstrates the concept, but actual usage would require
// proper lens definitions.
t.Run("conceptual test for LocalL", func(t *testing.T) {
// LocalL is similar to Local but uses lenses for focusing.
// It would be used like:
// validEmail := That(func(email string) bool { return strings.Contains(email, "@") })
// validPersonEmail := LocalL(emailLens)(validEmail)
//
// The actual implementation would require lens definitions from the lens package.
// This test serves as documentation for the intended usage.
})
}
func TestFromOptional(t *testing.T) {
type DatabaseConfig struct {
Host string
Port int
}
type Config struct {
Database *DatabaseConfig
}
// Create an Optional that focuses on the Database field
dbOptional := Optional[Config, *DatabaseConfig]{
GetOption: func(c Config) option.Option[*DatabaseConfig] {
if c.Database != nil {
return option.Of(c.Database)
}
return option.None[*DatabaseConfig]()
},
Set: func(db *DatabaseConfig) func(Config) Config {
return func(c Config) Config {
c.Database = db
return c
}
},
}
t.Run("should pass when optional value is present", func(t *testing.T) {
config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
hasDatabaseConfig := FromOptional(dbOptional)
result := hasDatabaseConfig(config)(t)
if !result {
t.Error("Expected FromOptional to pass when optional value is present")
}
})
t.Run("should fail when optional value is absent", func(t *testing.T) {
mockT := &testing.T{}
emptyConfig := Config{Database: nil}
hasDatabaseConfig := FromOptional(dbOptional)
result := hasDatabaseConfig(emptyConfig)(mockT)
if result {
t.Error("Expected FromOptional to fail when optional value is absent")
}
})
t.Run("should work with nested optionals", func(t *testing.T) {
type AdvancedSettings struct {
Cache bool
}
type Settings struct {
Advanced *AdvancedSettings
}
advancedOptional := Optional[Settings, *AdvancedSettings]{
GetOption: func(s Settings) option.Option[*AdvancedSettings] {
if s.Advanced != nil {
return option.Of(s.Advanced)
}
return option.None[*AdvancedSettings]()
},
Set: func(adv *AdvancedSettings) func(Settings) Settings {
return func(s Settings) Settings {
s.Advanced = adv
return s
}
},
}
settings := Settings{Advanced: &AdvancedSettings{Cache: true}}
hasAdvanced := FromOptional(advancedOptional)
result := hasAdvanced(settings)(t)
if !result {
t.Error("Expected FromOptional to pass for nested optional")
}
})
}

View File

@@ -3,14 +3,20 @@ package assert
import (
"testing"
"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/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
type (
Result[T any] = result.Result[T]
Reader = reader.Reader[*testing.T, bool]
Kleisli[T any] = reader.Reader[T, Reader]
Predicate[T any] = predicate.Predicate[T]
Result[T any] = result.Result[T]
Reader = reader.Reader[*testing.T, bool]
Kleisli[T any] = reader.Reader[T, Reader]
Predicate[T any] = predicate.Predicate[T]
Lens[S, T any] = lens.Lens[S, T]
Optional[S, T any] = optional.Optional[S, T]
Prism[S, T any] = prism.Prism[S, T]
)

View File

@@ -17,6 +17,8 @@
package lens
import (
"fmt"
"github.com/IBM/fp-go/v2/endomorphism"
EQ "github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
@@ -597,3 +599,11 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[
return MakeLensCurried(F.Flow2(ea.Get, ab), F.Flow2(ba, ea.Set))
}
}
func (l Lens[S, T]) String() string {
return "Lens"
}
func (l Lens[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

View File

@@ -18,6 +18,8 @@
package optional
import (
"fmt"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
@@ -27,6 +29,7 @@ import (
type Optional[S, A any] struct {
GetOption func(s S) O.Option[A]
Set func(a A) EM.Endomorphism[S]
name string
}
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
@@ -41,29 +44,42 @@ func setCopy[SET ~func(*S, A) *S, S, A any](setter SET) func(s *S, a A) *S {
// MakeOptional creates an Optional based on a getter and a setter function. Make sure that the setter creates a (shallow) copy of the
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeOptionalRef`
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
//
//go:inline
func MakeOptional[S, A any](get func(S) O.Option[A], set func(S, A) S) Optional[S, A] {
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(set)}
return MakeOptionalWithName(get, set, "GenericOptional")
}
func MakeOptionalWithName[S, A any](get func(S) O.Option[A], set func(S, A) S, name string) Optional[S, A] {
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(set), name: name}
}
// MakeOptionalRef creates an Optional based on a getter and a setter function. The setter passed in does not have to create a shallow
// copy, the implementation wraps the setter into one that copies the pointer before modifying it
//
//go:inline
func MakeOptionalRef[S, A any](get func(*S) O.Option[A], set func(*S, A) *S) Optional[*S, A] {
return MakeOptional(get, setCopy(set))
}
//go:inline
func MakeOptionalRefWithName[S, A any](get func(*S) O.Option[A], set func(*S, A) *S, name string) Optional[*S, A] {
return MakeOptionalWithName(get, setCopy(set), name)
}
// Id returns am optional implementing the identity operation
func id[S any](creator func(get func(S) O.Option[S], set func(S, S) S) Optional[S, S]) Optional[S, S] {
return creator(O.Some[S], F.Second[S, S])
func idWithName[S any](creator func(get func(S) O.Option[S], set func(S, S) S, name string) Optional[S, S], name string) Optional[S, S] {
return creator(O.Some[S], F.Second[S, S], name)
}
// Id returns am optional implementing the identity operation
func Id[S any]() Optional[S, S] {
return id(MakeOptional[S, S])
return idWithName(MakeOptionalWithName[S, S], "Identity")
}
// Id returns am optional implementing the identity operation
func IdRef[S any]() Optional[*S, *S] {
return id(MakeOptionalRef[S, *S])
return idWithName(MakeOptionalRefWithName[S, *S], "Identity")
}
func optionalModifyOption[S, A any](f func(A) A, optional Optional[S, A], s S) O.Option[S] {
@@ -189,3 +205,11 @@ func IChainAny[S, A any]() func(Optional[S, any]) Optional[S, A] {
return ichain(sa, fromAny, toAny)
}
}
func (l Optional[S, T]) String() string {
return l.name
}
func (l Optional[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

View File

@@ -16,6 +16,8 @@
package prism
import (
"fmt"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
@@ -48,33 +50,19 @@ type (
// },
// func(v int) Result { return Success{Value: v} },
// )
Prism[S, A any] interface {
Prism[S, A any] struct {
// GetOption attempts to extract a value of type A from S.
// Returns Some(a) if the extraction succeeds, None otherwise.
GetOption(s S) Option[A]
GetOption O.Kleisli[S, A]
// ReverseGet constructs an S from an A.
// This operation always succeeds.
ReverseGet(a A) S
}
ReverseGet func(A) S
// prismImpl is the internal implementation of the Prism interface.
prismImpl[S, A any] struct {
get func(S) Option[A]
rev func(A) S
name string
}
)
// GetOption implements the Prism interface for prismImpl.
func (prism prismImpl[S, A]) GetOption(s S) Option[A] {
return prism.get(s)
}
// ReverseGet implements the Prism interface for prismImpl.
func (prism prismImpl[S, A]) ReverseGet(a A) S {
return prism.rev(a)
}
// MakePrism constructs a Prism from GetOption and ReverseGet functions.
//
// Parameters:
@@ -91,7 +79,7 @@ func (prism prismImpl[S, A]) ReverseGet(a A) S {
// func(n int) Option[int] { return Some(n) },
// )
func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
return prismImpl[S, A]{get, rev}
return Prism[S, A]{get, rev, "GenericPrism"}
}
// Id returns an identity prism that focuses on the entire value.
@@ -278,3 +266,11 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Pris
return imap(sa, ab, ba)
}
}
func (l Prism[S, T]) String() string {
return "Prism"
}
func (l Prism[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}