mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-29 10:36:04 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df07599a9e | ||
|
|
30ad0e4dd8 |
235
v2/samples/builder/README.md
Normal file
235
v2/samples/builder/README.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# 🏗️ Builder Pattern with fp-go
|
||||
|
||||
This package demonstrates a functional builder pattern using fp-go's optics library. It shows how to construct and validate objects using lenses, prisms, and codecs, separating the building phase from validation.
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
The builder pattern here uses two key types:
|
||||
|
||||
- **`PartialPerson`** 🚧: An intermediate type with unvalidated fields (raw `string` and `int`)
|
||||
- **`Person`** ✅: A validated type with refined fields (`NonEmptyString` and `AdultAge`)
|
||||
|
||||
The pattern provides two approaches for validation:
|
||||
|
||||
1. **Prism-based validation** 🔍 (simple, no error messages)
|
||||
2. **Codec-based validation** 📝 (detailed error reporting)
|
||||
|
||||
## 🎯 Core Concepts
|
||||
|
||||
### 1. 🔧 Auto-Generated Lenses
|
||||
|
||||
The `fp-go:Lens` directive in `types.go` generates lens accessors for both types:
|
||||
|
||||
```go
|
||||
// fp-go:Lens
|
||||
type PartialPerson struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
// fp-go:Lens
|
||||
type Person struct {
|
||||
Name NonEmptyString
|
||||
Age AdultAge
|
||||
}
|
||||
```
|
||||
|
||||
This generates:
|
||||
- `partialPersonLenses` with `.name` and `.age` lenses
|
||||
- `personLenses` with `.Name` and `.Age` lenses
|
||||
|
||||
### 2. 🎁 Exporting Setters as `WithXXX` Methods
|
||||
|
||||
The lens setters are exported as builder methods:
|
||||
|
||||
```go
|
||||
// WithName sets the Name field of a PartialPerson
|
||||
WithName = partialPersonLenses.name.Set
|
||||
|
||||
// WithAge sets the Age field of a PartialPerson
|
||||
WithAge = partialPersonLenses.age.Set
|
||||
```
|
||||
|
||||
These return `Endomorphism[*PartialPerson]` functions that can be composed:
|
||||
|
||||
```go
|
||||
builder := F.Pipe1(
|
||||
A.From(
|
||||
WithName("Alice"),
|
||||
WithAge(25),
|
||||
),
|
||||
allOfPartialPerson,
|
||||
)
|
||||
partial := builder(&PartialPerson{})
|
||||
```
|
||||
|
||||
Or use the convenience function:
|
||||
|
||||
```go
|
||||
builder := MakePerson("Alice", 25)
|
||||
```
|
||||
|
||||
## 🔍 Approach 1: Prism-Based Validation (No Error Messages)
|
||||
|
||||
### Creating Validation Prisms
|
||||
|
||||
Define prisms that validate individual fields:
|
||||
|
||||
> 💡 **Tip**: The `optics/prism` package provides many helpful out-of-the-box prisms for common validations, including:
|
||||
> - `NonEmptyString()` - validates non-empty strings
|
||||
> - `ParseInt()`, `ParseInt64()` - parses integers from strings
|
||||
> - `ParseFloat32()`, `ParseFloat64()` - parses floats from strings
|
||||
> - `ParseBool()` - parses booleans from strings
|
||||
> - `ParseDate(layout)` - parses dates with custom layouts
|
||||
> - `ParseURL()` - parses URLs
|
||||
> - `FromZero()`, `FromNonZero()` - validates zero/non-zero values
|
||||
> - `RegexMatcher()`, `RegexNamedMatcher()` - regex-based validation
|
||||
> - `FromOption()`, `FromEither()`, `FromResult()` - extracts from monadic types
|
||||
> - And many more! Check `optics/prism/prisms.go` for the full list.
|
||||
>
|
||||
> For custom validation logic, create your own prisms:
|
||||
|
||||
```go
|
||||
namePrism = prism.MakePrismWithName(
|
||||
func(s string) Option[NonEmptyString] {
|
||||
if S.IsEmpty(s) {
|
||||
return option.None[NonEmptyString]()
|
||||
}
|
||||
return option.Of(NonEmptyString(s))
|
||||
},
|
||||
func(ns NonEmptyString) string {
|
||||
return string(ns)
|
||||
},
|
||||
"NonEmptyString",
|
||||
)
|
||||
|
||||
agePrism = prism.MakePrismWithName(
|
||||
func(a int) Option[AdultAge] {
|
||||
if a < 18 {
|
||||
return option.None[AdultAge]()
|
||||
}
|
||||
return option.Of(AdultAge(a))
|
||||
},
|
||||
func(aa AdultAge) int {
|
||||
return int(aa)
|
||||
},
|
||||
"AdultAge",
|
||||
)
|
||||
```
|
||||
|
||||
### 🎭 Creating the PersonPrism
|
||||
|
||||
The `PersonPrism` converts between a builder and a validated `Person`:
|
||||
|
||||
```go
|
||||
PersonPrism = prism.MakePrismWithName(
|
||||
buildPerson(), // Forward: builder -> Option[*Person]
|
||||
buildEndomorphism(), // Reverse: *Person -> builder
|
||||
"Person",
|
||||
)
|
||||
```
|
||||
|
||||
**Forward direction** ➡️ (`buildPerson`):
|
||||
1. Applies the builder to an empty `PartialPerson`
|
||||
2. Validates each field using field prisms
|
||||
3. Returns `Some(*Person)` if all validations pass, `None` otherwise
|
||||
|
||||
**Reverse direction** ⬅️ (`buildEndomorphism`):
|
||||
1. Extracts validated fields from `Person`
|
||||
2. Converts them back to raw types
|
||||
3. Returns a builder that reconstructs the `PartialPerson`
|
||||
|
||||
### 💡 Usage Example
|
||||
|
||||
```go
|
||||
// Create a builder
|
||||
builder := MakePerson("Alice", 25)
|
||||
|
||||
// Validate and convert to Person
|
||||
maybePerson := PersonPrism.GetOption(builder)
|
||||
|
||||
// maybePerson is Option[*Person]
|
||||
// - Some(*Person) if validation succeeds ✅
|
||||
// - None if validation fails (no error details) ❌
|
||||
```
|
||||
|
||||
## 📝 Approach 2: Codec-Based Validation (With Error Messages)
|
||||
|
||||
### Creating Field Codecs
|
||||
|
||||
Convert prisms to codecs for detailed validation:
|
||||
|
||||
```go
|
||||
nameCodec = codec.FromRefinement(namePrism)
|
||||
ageCodec = codec.FromRefinement(agePrism)
|
||||
```
|
||||
|
||||
### 🎯 Creating the PersonCodec
|
||||
|
||||
The `PersonCodec` provides bidirectional transformation with validation:
|
||||
|
||||
```go
|
||||
func makePersonCodec() PersonCodec {
|
||||
return codec.MakeType(
|
||||
"Person",
|
||||
codec.Is[*Person](),
|
||||
makePersonValidate(), // Validation with error reporting
|
||||
buildEndomorphism(), // Encoding (same as prism)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `makePersonValidate` function:
|
||||
1. Applies the builder to an empty `PartialPerson`
|
||||
2. Validates each field using field codecs
|
||||
3. Accumulates validation errors using applicative composition 📚
|
||||
4. Returns `Validation[*Person]` (either errors or a valid `Person`)
|
||||
|
||||
### 💡 Usage Example
|
||||
|
||||
```go
|
||||
// Create a builder
|
||||
builder := MakePerson("", 15) // Invalid: empty name, age < 18
|
||||
|
||||
// Validate with detailed errors
|
||||
personCodec := makePersonCodec()
|
||||
validation := personCodec.Validate(builder)
|
||||
|
||||
// validation is Validation[*Person]
|
||||
// - Right(*Person) if validation succeeds ✅
|
||||
// - Left(ValidationErrors) with detailed error messages if validation fails ❌
|
||||
```
|
||||
|
||||
## ⚖️ Key Differences
|
||||
|
||||
| Feature | Prism-Based 🔍 | Codec-Based 📝 |
|
||||
|---------|-------------|-------------|
|
||||
| Error Messages | No (returns `None`) ❌ | Yes (returns detailed errors) ✅ |
|
||||
| Complexity | Simpler 🟢 | More complex 🟡 |
|
||||
| Use Case | Simple validation | Production validation with user feedback |
|
||||
| Return Type | `Option[*Person]` | `Validation[*Person]` |
|
||||
|
||||
## 📝 Pattern Summary
|
||||
|
||||
1. **Define types** 📐: Create `PartialPerson` (unvalidated) and `Person` (validated)
|
||||
2. **Generate lenses** 🔧: Use `fp-go:Lens` directive
|
||||
3. **Export setters** 🎁: Create `WithXXX` methods from lens setters
|
||||
4. **Create validation prisms** 🎭: Define validation rules for each field
|
||||
5. **Choose validation approach** ⚖️:
|
||||
- **Simple** 🔍: Create a `Prism` for quick validation without errors
|
||||
- **Detailed** 📝: Create a `Codec` for validation with error reporting
|
||||
|
||||
## ✨ Benefits
|
||||
|
||||
- **Type Safety** 🛡️: Validated types guarantee business rules at compile time
|
||||
- **Composability** 🧩: Builders can be composed using monoid operations
|
||||
- **Bidirectional** ↔️: Both prisms and codecs support encoding and decoding
|
||||
- **Separation of Concerns** 🎯: Building and validation are separate phases
|
||||
- **Functional** 🔄: Pure functions, no mutation, easy to test
|
||||
|
||||
## 📁 Files
|
||||
|
||||
- `types.go`: Type definitions and lens generation directives
|
||||
- `builder.go`: Prism-based builder implementation
|
||||
- `codec.go`: Codec-based validation implementation
|
||||
- `codec_test.go`: Tests demonstrating usage patterns
|
||||
@@ -38,13 +38,37 @@ type (
|
||||
// a value of type A. It is an alias for reader.Reader[R, A].
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// Prism represents an optic that focuses on a subset of values of type S that can be
|
||||
// converted to type A. It provides bidirectional transformation with validation.
|
||||
// It is an alias for prism.Prism[S, A].
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Type[A, O, I any] = codec.Type[A, O, I]
|
||||
// Lens represents an optic that focuses on a field of type A within a structure of type S.
|
||||
// It provides getter and setter operations for immutable updates.
|
||||
// It is an alias for lens.Lens[S, A].
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
// Type represents a codec that handles bidirectional transformation between types.
|
||||
// A: The validated target type
|
||||
// O: The output encoding type
|
||||
// I: The input decoding type
|
||||
// It is an alias for codec.Type[A, O, I].
|
||||
Type[A, O, I any] = codec.Type[A, O, I]
|
||||
|
||||
// Validate represents a validation function that transforms input I into a validated result A.
|
||||
// It returns a Validation that contains either the validated value or validation errors.
|
||||
// It is an alias for validate.Validate[I, A].
|
||||
Validate[I, A any] = validate.Validate[I, A]
|
||||
Validation[A any] = validation.Validation[A]
|
||||
Encode[A, O any] = codec.Encode[A, O]
|
||||
|
||||
// Validation represents the result of a validation operation.
|
||||
// It contains either a validated value of type A (Right) or validation errors (Left).
|
||||
// It is an alias for validation.Validation[A].
|
||||
Validation[A any] = validation.Validation[A]
|
||||
|
||||
// Encode represents an encoding function that transforms a value of type A into type O.
|
||||
// It is used in codecs for the reverse direction of validation.
|
||||
// It is an alias for codec.Encode[A, O].
|
||||
Encode[A, O any] = codec.Encode[A, O]
|
||||
|
||||
// NonEmptyString is a string type that represents a validated non-empty string.
|
||||
// It is used to ensure that string fields contain meaningful data.
|
||||
|
||||
@@ -107,7 +107,7 @@ func TestHeterogeneousHttpRequests(t *testing.T) {
|
||||
// BenchmarkHeterogeneousHttpRequests shows how to execute multiple HTTP requests in parallel when
|
||||
// the response structure of these requests is different. We use [R.TraverseTuple2] to account for the different types
|
||||
func BenchmarkHeterogeneousHttpRequests(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
heterogeneousHTTPRequests()(context.Background())()
|
||||
for b.Loop() {
|
||||
heterogeneousHTTPRequests()(b.Context())()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user