mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-28 13:12:03 +02:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0132e2e92 | ||
|
|
c6b342908d | ||
|
|
962237492f | ||
|
|
168a6e1072 | ||
|
|
4d67b1d254 | ||
|
|
77a8cc6b09 | ||
|
|
bc8743fdfc | ||
|
|
1837d3f86d | ||
|
|
b2d111e8ec | ||
|
|
ae141c85c6 | ||
|
|
1230b4581b | ||
|
|
70c831c8f9 | ||
|
|
cc0c14c7cf | ||
|
|
19159ad49e | ||
|
|
b9c8fb4ff1 | ||
|
|
4529e720bc | ||
|
|
ef8b2ea65d | ||
|
|
5bd7caafdd | ||
|
|
47ebcd79b1 | ||
|
|
dbad94806e | ||
|
|
c4cac1cb3e | ||
|
|
a3fdb03df4 | ||
|
|
47727fd514 | ||
|
|
ece7d088ea | ||
|
|
13d25eca32 | ||
|
|
a68e32308d | ||
|
|
61b948425b | ||
|
|
a276f3acff | ||
|
|
8c656a4297 | ||
|
|
bd9a642e93 | ||
|
|
3b55cae265 | ||
|
|
1472fa5a50 | ||
|
|
49deb57d24 | ||
|
|
abb55ddbd0 |
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x']
|
||||
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x']
|
||||
fail-fast: false # Continue with other versions if one fails
|
||||
steps:
|
||||
# full checkout for semantic-release
|
||||
@@ -42,6 +42,7 @@ jobs:
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
continue-on-error: true
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -64,7 +65,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ['1.24.x', '1.25.x']
|
||||
go-version: ['1.24.x', '1.25.x', '1.26.x']
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -81,6 +82,7 @@ jobs:
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
continue-on-error: true
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -106,6 +108,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
continue-on-error: true
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
231
v2/AGENTS.md
Normal file
231
v2/AGENTS.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Agent Guidelines for fp-go/v2
|
||||
|
||||
This document provides guidelines for AI agents working on the fp-go/v2 project.
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Go Doc Comments
|
||||
|
||||
1. **Use Standard Go Doc Format**
|
||||
- Do NOT use markdown-style links like `[text](url)`
|
||||
- Use simple type references: `ReaderResult`, `Validate[I, A]`, `validation.Success`
|
||||
- Go's documentation system will automatically create links
|
||||
|
||||
2. **Structure**
|
||||
```go
|
||||
// FunctionName does something useful.
|
||||
//
|
||||
// Longer description explaining the purpose and behavior.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: Description of type parameter
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - param: Description of parameter
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - ReturnType: Description of return value
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// code example here
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - RelatedFunction: Brief description
|
||||
func FunctionName[T any](param T) ReturnType {
|
||||
```
|
||||
|
||||
3. **Code Examples**
|
||||
- Use idiomatic Go patterns
|
||||
- Prefer `result.Eitherize1(strconv.Atoi)` over manual error handling
|
||||
- Show realistic, runnable examples
|
||||
|
||||
### File Headers
|
||||
|
||||
Always include the Apache 2.0 license header:
|
||||
|
||||
```go
|
||||
// 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.
|
||||
```
|
||||
|
||||
## Testing Standards
|
||||
|
||||
### Test Structure
|
||||
|
||||
1. **Organize Tests by Category**
|
||||
```go
|
||||
func TestFunctionName_Success(t *testing.T) {
|
||||
t.Run("specific success case", func(t *testing.T) {
|
||||
// test code
|
||||
})
|
||||
}
|
||||
|
||||
func TestFunctionName_Failure(t *testing.T) {
|
||||
t.Run("specific failure case", func(t *testing.T) {
|
||||
// test code
|
||||
})
|
||||
}
|
||||
|
||||
func TestFunctionName_EdgeCases(t *testing.T) {
|
||||
// edge case tests
|
||||
}
|
||||
|
||||
func TestFunctionName_Integration(t *testing.T) {
|
||||
// integration tests
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use Direct Assertions**
|
||||
- Prefer: `assert.Equal(t, validation.Success(expected), actual)`
|
||||
- Avoid: Verbose `either.MonadFold` patterns unless necessary
|
||||
- Exception: When you need to verify pointer is not nil or extract specific fields
|
||||
|
||||
3. **Use Idiomatic Patterns**
|
||||
- Use `result.Eitherize1` for converting `(T, error)` functions
|
||||
- Use `result.Of` for success values
|
||||
- Use `result.Left` for error values
|
||||
|
||||
### Test Coverage
|
||||
|
||||
Include tests for:
|
||||
- **Success cases**: Normal operation with various input types
|
||||
- **Failure cases**: Error handling and error preservation
|
||||
- **Edge cases**: Nil, empty, zero values, boundary conditions
|
||||
- **Integration**: Composition with other functions
|
||||
- **Type safety**: Verify type parameters work correctly
|
||||
- **Benchmarks**: Performance-critical paths
|
||||
|
||||
### Example Test Pattern
|
||||
|
||||
```go
|
||||
func TestFromReaderResult_Success(t *testing.T) {
|
||||
t.Run("converts successful ReaderResult", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
validator := FromReaderResult[string, int](parseIntRR)
|
||||
|
||||
// Act
|
||||
result := validator("42")(nil)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### Functional Patterns
|
||||
|
||||
1. **Prefer Composition**
|
||||
```go
|
||||
validator := F.Pipe1(
|
||||
FromReaderResult[string, int](parseIntRR),
|
||||
Chain(validatePositive),
|
||||
)
|
||||
```
|
||||
|
||||
2. **Use Type-Safe Helpers**
|
||||
- `result.Eitherize1` for `func(T) (R, error)`
|
||||
- `result.Of` for wrapping success values
|
||||
- `result.Left` for wrapping errors
|
||||
|
||||
3. **Avoid Verbose Patterns**
|
||||
- Don't manually handle `(value, error)` tuples when helpers exist
|
||||
- Don't use `either.MonadFold` in tests unless necessary
|
||||
|
||||
4. **Use Void Type for Unit Values**
|
||||
- Use `function.Void` (or `F.Void`) instead of `struct{}`
|
||||
- Use `function.VOID` (or `F.VOID`) instead of `struct{}{}`
|
||||
- Example: `Empty[F.Void, F.Void, any](lazy.Of(pair.MakePair(F.VOID, F.VOID)))`
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **In Production Code**
|
||||
- Use `validation.Success` for successful validations
|
||||
- Use `validation.FailureWithMessage` for simple failures
|
||||
- Use `validation.FailureWithError` to preserve error causes
|
||||
|
||||
2. **In Tests**
|
||||
- Verify error messages and causes
|
||||
- Check error context is preserved
|
||||
- Test error accumulation when applicable
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Converting Error-Based Functions
|
||||
|
||||
```go
|
||||
// Good: Use Eitherize1
|
||||
parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
|
||||
// Avoid: Manual error handling
|
||||
parseIntRR := func(input string) result.Result[int] {
|
||||
val, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return result.Left[int](err)
|
||||
}
|
||||
return result.Of(val)
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Validation Results
|
||||
|
||||
```go
|
||||
// Good: Direct comparison
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Avoid: Verbose extraction (unless you need to verify specific fields)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
```
|
||||
|
||||
### Documentation Examples
|
||||
|
||||
```go
|
||||
// Good: Concise and idiomatic
|
||||
// parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
// validator := FromReaderResult[string, int](parseIntRR)
|
||||
|
||||
// Avoid: Verbose manual patterns
|
||||
// parseIntRR := func(input string) result.Result[int] {
|
||||
// val, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return result.Left[int](err)
|
||||
// }
|
||||
// return result.Of(val)
|
||||
// }
|
||||
```
|
||||
|
||||
## Checklist for New Code
|
||||
|
||||
- [ ] Apache 2.0 license header included
|
||||
- [ ] Go doc comments use standard format (no markdown links)
|
||||
- [ ] Code examples are idiomatic and concise
|
||||
- [ ] Tests cover success, failure, edge cases, and integration
|
||||
- [ ] Tests use direct assertions where possible
|
||||
- [ ] Benchmarks included for performance-critical code
|
||||
- [ ] All tests pass
|
||||
- [ ] Code uses functional composition patterns
|
||||
- [ ] Error handling preserves context and causes
|
||||
@@ -1,6 +1,6 @@
|
||||
# Functional I/O in Go: Context, Errors, and the Reader Pattern
|
||||
|
||||
This document explores how functional programming principles apply to I/O operations in Go, comparing traditional imperative approaches with functional patterns using the `context/readerioresult` and `idiomatic/context/readerresult` packages.
|
||||
This document explores how functional programming principles apply to I/O operations in Go, comparing traditional imperative approaches with functional patterns using the `context/readerioresult`, `idiomatic/context/readerresult`, and `effect` packages.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -10,6 +10,7 @@ This document explores how functional programming principles apply to I/O operat
|
||||
- [Benefits of the Functional Approach](#benefits-of-the-functional-approach)
|
||||
- [Side-by-Side Comparison](#side-by-side-comparison)
|
||||
- [Advanced Patterns](#advanced-patterns)
|
||||
- [The Effect Package: Higher-Level Abstraction](#the-effect-package-higher-level-abstraction)
|
||||
- [When to Use Each Approach](#when-to-use-each-approach)
|
||||
|
||||
## Why Context in I/O Operations
|
||||
@@ -775,6 +776,191 @@ func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
|
||||
}
|
||||
```
|
||||
|
||||
## The Effect Package: Higher-Level Abstraction
|
||||
|
||||
### What is Effect?
|
||||
|
||||
The `effect` package provides a higher-level abstraction over `ReaderReaderIOResult`, offering a complete effect system for managing dependencies, errors, and side effects in a composable way. It's inspired by [effect-ts](https://effect.website/) and provides a cleaner API for complex workflows.
|
||||
|
||||
### Core Type
|
||||
|
||||
```go
|
||||
// Effect represents an effectful computation that:
|
||||
// - Requires a context of type C (dependency injection)
|
||||
// - Can perform I/O operations
|
||||
// - Can fail with an error
|
||||
// - Produces a value of type A on success
|
||||
type Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
|
||||
```
|
||||
|
||||
**Key difference from ReaderIOResult**: Effect adds an additional layer of dependency injection (the `C` type parameter) on top of the runtime `context.Context`, enabling type-safe dependency management.
|
||||
|
||||
### When to Use Effect
|
||||
|
||||
Use the Effect package when you need:
|
||||
|
||||
1. **Type-Safe Dependency Injection**: Your application has typed dependencies (config, services, repositories) that need to be threaded through operations
|
||||
2. **Complex Workflows**: Multiple services and dependencies need to be composed
|
||||
3. **Testability**: You want to easily mock dependencies by providing different contexts
|
||||
4. **Separation of Concerns**: Clear separation between business logic, dependencies, and I/O
|
||||
|
||||
### Effect vs ReaderIOResult
|
||||
|
||||
```go
|
||||
// ReaderIOResult - depends only on runtime context
|
||||
type ReaderIOResult[A any] = func(context.Context) (A, error)
|
||||
|
||||
// Effect - depends on typed context C AND runtime context
|
||||
type Effect[C, A any] = func(C) func(context.Context) (A, error)
|
||||
```
|
||||
|
||||
**ReaderIOResult** is simpler and suitable when you only need runtime context (cancellation, deadlines, request-scoped values).
|
||||
|
||||
**Effect** adds typed dependency injection, making it ideal for applications with complex service dependencies.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
#### Creating Effects
|
||||
|
||||
```go
|
||||
type AppConfig struct {
|
||||
DatabaseURL string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
// Create a successful effect
|
||||
successEffect := effect.Succeed[AppConfig, string]("hello")
|
||||
|
||||
// Create a failed effect
|
||||
failEffect := effect.Fail[AppConfig, string](errors.New("failed"))
|
||||
|
||||
// Lift a pure value
|
||||
pureEffect := effect.Of[AppConfig, int](42)
|
||||
```
|
||||
|
||||
#### Integrating Standard Go Functions
|
||||
|
||||
The `Eitherize` function makes it easy to integrate standard Go functions that return `(value, error)`:
|
||||
|
||||
```go
|
||||
type Database struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
// Convert a standard Go function to an Effect using Eitherize
|
||||
func fetchUser(id int) effect.Effect[Database, User] {
|
||||
return effect.Eitherize(func(db Database, ctx context.Context) (User, error) {
|
||||
var user User
|
||||
err := db.conn.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id).Scan(&user)
|
||||
return user, err
|
||||
})
|
||||
}
|
||||
|
||||
// Use Eitherize1 for Kleisli arrows (functions with an additional parameter)
|
||||
fetchUserKleisli := effect.Eitherize1(func(db Database, ctx context.Context, id int) (User, error) {
|
||||
var user User
|
||||
err := db.conn.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id).Scan(&user)
|
||||
return user, err
|
||||
})
|
||||
// fetchUserKleisli has type: func(int) Effect[Database, User]
|
||||
```
|
||||
|
||||
#### Composing Effects
|
||||
|
||||
```go
|
||||
type Services struct {
|
||||
UserRepo UserRepository
|
||||
EmailSvc EmailService
|
||||
}
|
||||
|
||||
// Compose multiple effects with typed dependencies
|
||||
func processUser(id int, newEmail string) effect.Effect[Services, User] {
|
||||
return F.Pipe3(
|
||||
// Fetch user from repository
|
||||
effect.Eitherize(func(svc Services, ctx context.Context) (User, error) {
|
||||
return svc.UserRepo.GetUser(ctx, id)
|
||||
}),
|
||||
// Validate user (pure function lifted into Effect)
|
||||
effect.ChainEitherK[Services](validateUser),
|
||||
// Update email
|
||||
effect.Chain[Services](func(user User) effect.Effect[Services, User] {
|
||||
return effect.Eitherize(func(svc Services, ctx context.Context) (User, error) {
|
||||
if err := svc.EmailSvc.SendVerification(ctx, newEmail); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
return svc.UserRepo.UpdateEmail(ctx, user.ID, newEmail)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Running Effects
|
||||
|
||||
```go
|
||||
func main() {
|
||||
// Set up typed dependencies once
|
||||
services := Services{
|
||||
UserRepo: &PostgresUserRepo{db: db},
|
||||
EmailSvc: &SMTPEmailService{host: "smtp.example.com"},
|
||||
}
|
||||
|
||||
// Build the effect pipeline (no execution yet)
|
||||
userEffect := processUser(42, "new@email.com")
|
||||
|
||||
// Provide dependencies - returns a Thunk (ReaderIOResult)
|
||||
thunk := effect.Provide(services)(userEffect)
|
||||
|
||||
// Run synchronously - returns a func(context.Context) (User, error)
|
||||
readerResult := effect.RunSync(thunk)
|
||||
|
||||
// Execute with runtime context
|
||||
user, err := readerResult(context.Background())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Updated user: %+v\n", user)
|
||||
}
|
||||
```
|
||||
|
||||
### Comparison: Traditional vs ReaderIOResult vs Effect
|
||||
|
||||
| Aspect | Traditional | ReaderIOResult | Effect |
|
||||
|---|---|---|---|
|
||||
| Error propagation | Manual | Automatic | Automatic |
|
||||
| Dependency injection | Function parameters | Closure / `context.Context` | Typed `C` parameter |
|
||||
| Testability | Requires mocking | Mock `ReaderIOResult` | Provide mock `C` |
|
||||
| Composability | Low | High | High |
|
||||
| Type-safe dependencies | No | No | Yes |
|
||||
| Complexity | Low | Medium | Medium-High |
|
||||
|
||||
### Testing with Effect
|
||||
|
||||
One of the key benefits of Effect is easy testing through dependency substitution:
|
||||
|
||||
```go
|
||||
func TestProcessUser(t *testing.T) {
|
||||
// Create mock services
|
||||
mockServices := Services{
|
||||
UserRepo: &MockUserRepo{
|
||||
users: map[int]User{42: {ID: 42, Age: 25, Email: "old@email.com"}},
|
||||
},
|
||||
EmailSvc: &MockEmailService{},
|
||||
}
|
||||
|
||||
// Run the effect with mock dependencies
|
||||
user, err := effect.RunSync(
|
||||
effect.Provide(mockServices)(processUser(42, "new@email.com")),
|
||||
)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "new@email.com", user.Email)
|
||||
}
|
||||
```
|
||||
|
||||
No database or SMTP server needed — just provide mock implementations of the `Services` struct.
|
||||
|
||||
## When to Use Each Approach
|
||||
|
||||
### Use Traditional Go Style When:
|
||||
@@ -794,6 +980,17 @@ func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
|
||||
5. **Resource management**: Need guaranteed cleanup (Bracket)
|
||||
6. **Parallel execution**: Need to parallelize operations easily
|
||||
7. **Type safety**: Want the type system to track I/O effects
|
||||
8. **Simple dependencies**: Only need runtime `context.Context`, no typed dependencies
|
||||
|
||||
### Use Effect When:
|
||||
|
||||
1. **Type-safe dependency injection**: Application has typed dependencies (config, services, repositories)
|
||||
2. **Complex service architectures**: Multiple services need to be composed with clear dependency management
|
||||
3. **Testability with mocks**: Want to easily substitute dependencies for testing
|
||||
4. **Separation of concerns**: Need clear separation between business logic, dependencies, and I/O
|
||||
5. **Large applications**: Building applications where dependency management is critical
|
||||
6. **Team experience**: Team is comfortable with functional programming and effect systems
|
||||
7. **Integration with standard Go**: Need to integrate many standard `(value, error)` functions using `Eitherize`
|
||||
|
||||
### Use Idiomatic Functional Style (idiomatic/context/readerresult) When:
|
||||
|
||||
@@ -803,6 +1000,7 @@ func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
|
||||
4. **Go integration**: Want seamless integration with existing Go code
|
||||
5. **Production services**: Building high-throughput services
|
||||
6. **Best of both worlds**: Want functional composition with Go's native patterns
|
||||
7. **Simple dependencies**: Only need runtime `context.Context`, no typed dependencies
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -814,11 +1012,18 @@ The functional approach to I/O in Go offers significant advantages:
|
||||
4. **Type Safety**: I/O effects visible in the type system
|
||||
5. **Lazy Evaluation**: Build pipelines, execute when ready
|
||||
6. **Context Propagation**: Automatic threading of context
|
||||
7. **Performance**: Idiomatic version offers 2-10x speedup
|
||||
7. **Dependency Injection**: Type-safe dependency management with Effect
|
||||
8. **Performance**: Idiomatic version offers 2-10x speedup
|
||||
|
||||
The key insight is that **I/O operations return descriptions of effects** (ReaderIOResult) rather than performing effects immediately. This enables powerful composition patterns while maintaining Go's idiomatic error handling through the `(value, error)` tuple pattern.
|
||||
The key insight is that **I/O operations return descriptions of effects** rather than performing effects immediately. This enables powerful composition patterns while maintaining Go's idiomatic error handling through the `(value, error)` tuple pattern.
|
||||
|
||||
For production Go services, the **idiomatic/context/readerresult** package provides the best balance: full functional programming capabilities with native Go performance and familiar error handling patterns.
|
||||
### Choosing the Right Abstraction
|
||||
|
||||
- **ReaderIOResult**: Best for simple I/O pipelines that only need runtime `context.Context`
|
||||
- **Effect**: Best for complex applications with typed dependencies and service architectures
|
||||
- **idiomatic/context/readerresult**: Best for production services needing high performance with functional patterns
|
||||
|
||||
For production Go services, the **idiomatic/context/readerresult** package provides the best balance of performance and functional capabilities. For applications with complex dependency management, the **effect** package provides type-safe dependency injection with a clean, composable API.
|
||||
|
||||
## Further Reading
|
||||
|
||||
@@ -826,4 +1031,6 @@ For production Go services, the **idiomatic/context/readerresult** package provi
|
||||
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison
|
||||
- [idiomatic/doc.go](./idiomatic/doc.go) - Idiomatic package overview
|
||||
- [context/readerioresult](./context/readerioresult/) - ReaderIOResult package
|
||||
- [idiomatic/context/readerresult](./idiomatic/context/readerresult/) - Idiomatic ReaderResult package
|
||||
- [idiomatic/context/readerresult](./idiomatic/context/readerresult/) - Idiomatic ReaderResult package
|
||||
- [effect](./effect/) - Effect package for type-safe dependency injection
|
||||
- [effect-ts](https://effect.website/) - TypeScript effect system that inspired this package
|
||||
@@ -461,7 +461,8 @@ func process() IOResult[string] {
|
||||
- **Result** - Simplified Either with error as left type (recommended for error handling)
|
||||
- **IO** - Lazy evaluation and side effect management
|
||||
- **IOOption** - Combine IO with Option for optional values with side effects
|
||||
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
|
||||
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither when using standard `error` type)
|
||||
- **Effect** - Composable effects with dependency injection and error handling
|
||||
- **Reader** - Dependency injection pattern
|
||||
- **ReaderOption** - Combine Reader with Option for optional values with dependency injection
|
||||
- **ReaderIOOption** - Combine Reader, IO, and Option for optional values with dependency injection and side effects
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// From constructs an array from a set of variadic arguments
|
||||
@@ -163,11 +163,11 @@ func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
|
||||
return G.FilterMapWithIndex[[]A, []B](f)
|
||||
}
|
||||
|
||||
// FilterChain maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
|
||||
// ChainOptionK maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
|
||||
//
|
||||
//go:inline
|
||||
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
|
||||
return G.FilterChain[[]A](f)
|
||||
func ChainOptionK[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
|
||||
return G.ChainOptionK[[]A](f)
|
||||
}
|
||||
|
||||
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
|
||||
@@ -453,7 +453,7 @@ func Size[A any](as []A) int {
|
||||
// the second contains elements for which it returns true.
|
||||
//
|
||||
//go:inline
|
||||
func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
|
||||
func MonadPartition[A any](as []A, pred func(A) bool) pair.Pair[[]A, []A] {
|
||||
return G.MonadPartition(as, pred)
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
|
||||
// for which the predicate returns false, the right one those for which the predicate returns true
|
||||
//
|
||||
//go:inline
|
||||
func Partition[A any](pred func(A) bool) func([]A) tuple.Tuple2[[]A, []A] {
|
||||
func Partition[A any](pred func(A) bool) func([]A) pair.Pair[[]A, []A] {
|
||||
return G.Partition[[]A](pred)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -163,11 +163,11 @@ func TestPartition(t *testing.T) {
|
||||
return n > 2
|
||||
}
|
||||
|
||||
assert.Equal(t, T.MakeTuple2(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
|
||||
assert.Equal(t, T.MakeTuple2(From(1), From(3)), Partition(pred)(From(1, 3)))
|
||||
assert.Equal(t, pair.MakePair(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
|
||||
assert.Equal(t, pair.MakePair(From(1), From(3)), Partition(pred)(From(1, 3)))
|
||||
}
|
||||
|
||||
func TestFilterChain(t *testing.T) {
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
src := From(1, 2, 3)
|
||||
|
||||
f := func(i int) O.Option[[]string] {
|
||||
@@ -177,7 +177,7 @@ func TestFilterChain(t *testing.T) {
|
||||
return O.None[[]string]()
|
||||
}
|
||||
|
||||
res := FilterChain(f)(src)
|
||||
res := ChainOptionK(f)(src)
|
||||
|
||||
assert.Equal(t, From("a1", "b1", "a3", "b3"), res)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
FC "github.com/IBM/fp-go/v2/internal/functor"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Of constructs a single element array
|
||||
@@ -215,7 +215,7 @@ func Filter[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) AS {
|
||||
return FilterWithIndex[AS](F.Ignore1of2[int](pred))
|
||||
}
|
||||
|
||||
func FilterChain[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
|
||||
func ChainOptionK[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
|
||||
return F.Flow2(
|
||||
FilterMap[GA, []GB](f),
|
||||
Flatten[[]GB],
|
||||
@@ -234,7 +234,7 @@ func FilterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) O.Option[B])
|
||||
return F.Bind2nd(MonadFilterMapWithIndex[GA, GB, A, B], f)
|
||||
}
|
||||
|
||||
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, GA] {
|
||||
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) pair.Pair[GA, GA] {
|
||||
left := Empty[GA]()
|
||||
right := Empty[GA]()
|
||||
array.Reduce(as, func(c bool, a A) bool {
|
||||
@@ -246,10 +246,10 @@ func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, G
|
||||
return c
|
||||
}, true)
|
||||
// returns the partition
|
||||
return tuple.MakeTuple2(left, right)
|
||||
return pair.MakePair(left, right)
|
||||
}
|
||||
|
||||
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) tuple.Tuple2[GA, GA] {
|
||||
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) pair.Pair[GA, GA] {
|
||||
return F.Bind2nd(MonadPartition[GA, A], pred)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ package generic
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// ZipWith applies a function to pairs of elements at the same index in two arrays, collecting the results in a new array. If one
|
||||
@@ -34,19 +34,19 @@ func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, f
|
||||
|
||||
// Zip takes two arrays and returns an array of corresponding pairs. If one input array is short, excess elements of the
|
||||
// longer array are discarded
|
||||
func Zip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](fb BS) func(AS) CS {
|
||||
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) T.Tuple2[A, B]])(fb, T.MakeTuple2[A, B])
|
||||
func Zip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](fb BS) func(AS) CS {
|
||||
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) pair.Pair[A, B]])(fb, pair.MakePair[A, B])
|
||||
}
|
||||
|
||||
// Unzip is the function is reverse of [Zip]. Takes an array of pairs and return two corresponding arrays
|
||||
func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS, BS] {
|
||||
func Unzip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](cs CS) pair.Pair[AS, BS] {
|
||||
l := len(cs)
|
||||
as := make(AS, l)
|
||||
bs := make(BS, l)
|
||||
for i := range l {
|
||||
t := cs[i]
|
||||
as[i] = t.F1
|
||||
bs[i] = t.F2
|
||||
as[i] = pair.Head(t)
|
||||
bs[i] = pair.Tail(t)
|
||||
}
|
||||
return T.MakeTuple2(as, bs)
|
||||
return pair.MakePair(as, bs)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package array
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/array/generic"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// ZipWith applies a function to pairs of elements at the same index in two arrays,
|
||||
@@ -55,8 +55,8 @@ func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
|
||||
// // Result: [(a, 1), (b, 2)]
|
||||
//
|
||||
//go:inline
|
||||
func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
|
||||
return G.Zip[[]A, []B, []T.Tuple2[A, B]](fb)
|
||||
func Zip[A, B any](fb []B) func([]A) []pair.Pair[A, B] {
|
||||
return G.Zip[[]A, []B, []pair.Pair[A, B]](fb)
|
||||
}
|
||||
|
||||
// Unzip is the reverse of Zip. It takes an array of pairs (tuples) and returns
|
||||
@@ -78,6 +78,6 @@ func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
|
||||
// ages := result.Tail // [30, 25, 35]
|
||||
//
|
||||
//go:inline
|
||||
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
|
||||
func Unzip[A, B any](cs []pair.Pair[A, B]) pair.Pair[[]A, []B] {
|
||||
return G.Unzip[[]A, []B](cs)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestZip(t *testing.T) {
|
||||
|
||||
res := Zip[string](left)(right)
|
||||
|
||||
assert.Equal(t, From(T.MakeTuple2("a", 1), T.MakeTuple2("b", 2), T.MakeTuple2("c", 3)), res)
|
||||
assert.Equal(t, From(pair.MakePair("a", 1), pair.MakePair("b", 2), pair.MakePair("c", 3)), res)
|
||||
}
|
||||
|
||||
func TestUnzip(t *testing.T) {
|
||||
@@ -51,6 +51,6 @@ func TestUnzip(t *testing.T) {
|
||||
|
||||
unzipped := Unzip(zipped)
|
||||
|
||||
assert.Equal(t, right, unzipped.F1)
|
||||
assert.Equal(t, left, unzipped.F2)
|
||||
assert.Equal(t, right, pair.Head(unzipped))
|
||||
assert.Equal(t, left, pair.Tail(unzipped))
|
||||
}
|
||||
|
||||
@@ -194,6 +194,25 @@ func ArrayNotEmpty[T any](arr []T) Reader {
|
||||
}
|
||||
}
|
||||
|
||||
// ArrayEmpty checks if an array is empty.
|
||||
//
|
||||
// This is the complement of ArrayNotEmpty, asserting that a slice has no elements.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayEmpty(t *testing.T) {
|
||||
// empty := []int{}
|
||||
// assert.ArrayEmpty(empty)(t) // Passes
|
||||
//
|
||||
// numbers := []int{1, 2, 3}
|
||||
// assert.ArrayEmpty(numbers)(t) // Fails
|
||||
// }
|
||||
func ArrayEmpty[T any](arr []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Empty(t, arr)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordNotEmpty checks if a map is not empty.
|
||||
//
|
||||
// Example:
|
||||
@@ -211,6 +230,25 @@ func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
}
|
||||
}
|
||||
|
||||
// RecordEmpty checks if a map is empty.
|
||||
//
|
||||
// This is the complement of RecordNotEmpty, asserting that a map has no key-value pairs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestRecordEmpty(t *testing.T) {
|
||||
// empty := map[string]int{}
|
||||
// assert.RecordEmpty(empty)(t) // Passes
|
||||
//
|
||||
// config := map[string]int{"timeout": 30}
|
||||
// assert.RecordEmpty(config)(t) // Fails
|
||||
// }
|
||||
func RecordEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Empty(t, mp)
|
||||
}
|
||||
}
|
||||
|
||||
// StringNotEmpty checks if a string is not empty.
|
||||
//
|
||||
// Example:
|
||||
@@ -504,15 +542,7 @@ func AllOf(readers []Reader) Reader {
|
||||
//
|
||||
//go:inline
|
||||
func RunAll(testcases map[string]Reader) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
current := true
|
||||
for k, r := range testcases {
|
||||
current = current && t.Run(k, func(t1 *testing.T) {
|
||||
r(t1)
|
||||
})
|
||||
}
|
||||
return current
|
||||
}
|
||||
return SequenceRecord(testcases)
|
||||
}
|
||||
|
||||
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
|
||||
|
||||
@@ -85,6 +85,33 @@ func TestArrayNotEmpty(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayEmpty(t *testing.T) {
|
||||
t.Run("should pass for empty array", func(t *testing.T) {
|
||||
arr := []int{}
|
||||
result := ArrayEmpty(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayEmpty to pass for empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for non-empty array", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayEmpty(arr)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ArrayEmpty to fail for non-empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with different types", func(t *testing.T) {
|
||||
strArr := []string{}
|
||||
result := ArrayEmpty(strArr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayEmpty to pass for empty string array")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty map", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
@@ -131,6 +158,33 @@ func TestArrayLength(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordEmpty(t *testing.T) {
|
||||
t.Run("should pass for empty map", func(t *testing.T) {
|
||||
mp := map[string]int{}
|
||||
result := RecordEmpty(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordEmpty to pass for empty map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for non-empty map", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := RecordEmpty(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected RecordEmpty to fail for non-empty map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with different key-value types", func(t *testing.T) {
|
||||
mp := map[int]string{}
|
||||
result := RecordEmpty(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordEmpty to pass for empty map with int keys")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordLength(t *testing.T) {
|
||||
t.Run("should pass when map length matches", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
@@ -150,6 +204,33 @@ func TestRecordLength(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty string", func(t *testing.T) {
|
||||
str := "Hello, World!"
|
||||
result := StringNotEmpty(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringNotEmpty to pass for non-empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for empty string", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
str := ""
|
||||
result := StringNotEmpty(str)(mockT)
|
||||
if result {
|
||||
t.Error("Expected StringNotEmpty to fail for empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass for string with whitespace", func(t *testing.T) {
|
||||
str := " "
|
||||
result := StringNotEmpty(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringNotEmpty to pass for string with whitespace")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringLength(t *testing.T) {
|
||||
t.Run("should pass when string length matches", func(t *testing.T) {
|
||||
str := "hello"
|
||||
|
||||
122
v2/assert/from.go
Normal file
122
v2/assert/from.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// FromReaderIOResult converts a ReaderIOResult[Reader] into a Reader.
|
||||
//
|
||||
// This function bridges the gap between context-aware, IO-based computations that may fail
|
||||
// (ReaderIOResult) and the simpler Reader type used for test assertions. It executes the
|
||||
// ReaderIOResult computation using the test's context, handles any potential errors by
|
||||
// converting them to test failures via NoError, and returns the resulting Reader.
|
||||
//
|
||||
// The conversion process:
|
||||
// 1. Executes the ReaderIOResult with the test context (t.Context())
|
||||
// 2. Runs the resulting IO operation ()
|
||||
// 3. Extracts the Result, converting errors to test failures using NoError
|
||||
// 4. Returns a Reader that can be applied to *testing.T
|
||||
//
|
||||
// This is particularly useful when you have test assertions that need to:
|
||||
// - Access context for cancellation or deadlines
|
||||
// - Perform IO operations (file access, network calls, etc.)
|
||||
// - Handle potential errors gracefully in tests
|
||||
//
|
||||
// Parameters:
|
||||
// - ri: A ReaderIOResult that produces a Reader when given a context and executed
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that can be directly applied to *testing.T for assertion
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithContext(t *testing.T) {
|
||||
// // Create a ReaderIOResult that performs an IO operation
|
||||
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
|
||||
// return func() result.Result[assert.Reader] {
|
||||
// // Simulate database check
|
||||
// if err := db.PingContext(ctx); err != nil {
|
||||
// return result.Error[assert.Reader](err)
|
||||
// }
|
||||
// return result.Of[assert.Reader](assert.NoError(nil))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIOResult(checkDatabase)
|
||||
// assertion(t)
|
||||
// }
|
||||
func FromReaderIOResult(ri ReaderIOResult[Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return F.Pipe1(
|
||||
ri(t.Context())(),
|
||||
result.GetOrElse(NoError),
|
||||
)(t)
|
||||
}
|
||||
}
|
||||
|
||||
// FromReaderIO converts a ReaderIO[Reader] into a Reader.
|
||||
//
|
||||
// This function bridges the gap between context-aware, IO-based computations (ReaderIO)
|
||||
// and the simpler Reader type used for test assertions. It executes the ReaderIO
|
||||
// computation using the test's context and returns the resulting Reader.
|
||||
//
|
||||
// Unlike FromReaderIOResult, this function does not handle errors explicitly - it assumes
|
||||
// the IO operation will succeed or that any errors are handled within the ReaderIO itself.
|
||||
//
|
||||
// The conversion process:
|
||||
// 1. Executes the ReaderIO with the test context (t.Context())
|
||||
// 2. Runs the resulting IO operation ()
|
||||
// 3. Returns a Reader that can be applied to *testing.T
|
||||
//
|
||||
// This is particularly useful when you have test assertions that need to:
|
||||
// - Access context for cancellation or deadlines
|
||||
// - Perform IO operations that don't fail (or handle failures internally)
|
||||
// - Integrate with context-aware testing utilities
|
||||
//
|
||||
// Parameters:
|
||||
// - ri: A ReaderIO that produces a Reader when given a context and executed
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that can be directly applied to *testing.T for assertion
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithIO(t *testing.T) {
|
||||
// // Create a ReaderIO that performs an IO operation
|
||||
// logAndCheck := func(ctx context.Context) func() assert.Reader {
|
||||
// return func() assert.Reader {
|
||||
// // Log something using context
|
||||
// logger.InfoContext(ctx, "Running test")
|
||||
// // Return an assertion
|
||||
// return assert.Equal(42)(computeValue())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIO(logAndCheck)
|
||||
// assertion(t)
|
||||
// }
|
||||
func FromReaderIO(ri ReaderIO[Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return ri(t.Context())()(t)
|
||||
}
|
||||
}
|
||||
383
v2/assert/from_test.go
Normal file
383
v2/assert/from_test.go
Normal file
@@ -0,0 +1,383 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func TestFromReaderIOResult(t *testing.T) {
|
||||
t.Run("should pass when ReaderIOResult returns success with passing assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns a successful Reader
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
// Return a Reader that always passes
|
||||
return result.Of(func(t *testing.T) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass when ReaderIOResult returns success")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass when ReaderIOResult returns success with Equal assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns a successful Equal assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of(Equal(42)(42))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with Equal assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIOResult returns error", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIOResult that returns an error
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Left[Reader](errors.New("test error"))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIOResult to fail when ReaderIOResult returns error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIOResult returns success but assertion fails", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIOResult that returns a failing assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of(Equal(42)(43))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIOResult to fail when assertion fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should use test context", func(t *testing.T) {
|
||||
contextUsed := false
|
||||
|
||||
// Create a ReaderIOResult that checks if context is provided
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
if ctx != nil {
|
||||
contextUsed = true
|
||||
}
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of(func(t *testing.T) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
reader(t)
|
||||
|
||||
if !contextUsed {
|
||||
t.Error("Expected FromReaderIOResult to use test context")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with NoError assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns NoError assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of(NoError(nil))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with NoError assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with complex assertions", func(t *testing.T) {
|
||||
// Create a ReaderIOResult with multiple composed assertions
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
arr := []int{1, 2, 3}
|
||||
assertions := AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](3)(arr),
|
||||
ArrayContains(2)(arr),
|
||||
})
|
||||
return result.Of(assertions)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with complex assertions")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromReaderIO(t *testing.T) {
|
||||
t.Run("should pass when ReaderIO returns passing assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns a Reader that always passes
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass when ReaderIO returns passing assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass when ReaderIO returns Equal assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns an Equal assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Equal(42)(42)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Equal assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIO returns failing assertion", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIO that returns a failing assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Equal(42)(43)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIO to fail when assertion fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should use test context", func(t *testing.T) {
|
||||
contextUsed := false
|
||||
|
||||
// Create a ReaderIO that checks if context is provided
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
if ctx != nil {
|
||||
contextUsed = true
|
||||
}
|
||||
return func() Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
reader(t)
|
||||
|
||||
if !contextUsed {
|
||||
t.Error("Expected FromReaderIO to use test context")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with NoError assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns NoError assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return NoError(nil)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with NoError assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Error assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns Error assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Error(errors.New("expected error"))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Error assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with complex assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with multiple composed assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
return AllOf([]Reader{
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
ContainsKey[int]("a")(mp),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with complex assertions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with string assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with string assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
str := "hello world"
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(str),
|
||||
StringLength[any, any](11)(str),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with string assertions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Result assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with Result assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
successResult := result.Of(42)
|
||||
return Success(successResult)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Success assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Failure assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO with Failure assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
failureResult := result.Left[int](errors.New("test error"))
|
||||
return Failure(failureResult)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Failure assertion")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromReaderIOResultIntegration tests integration scenarios
|
||||
func TestFromReaderIOResultIntegration(t *testing.T) {
|
||||
t.Run("should work in a realistic scenario with context cancellation", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that uses the context
|
||||
ri := func(testCtx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
// Check if context is valid
|
||||
if testCtx == nil {
|
||||
return result.Left[Reader](errors.New("context is nil"))
|
||||
}
|
||||
|
||||
// Return a successful assertion
|
||||
return result.Of(Equal("test")("test"))
|
||||
}
|
||||
}
|
||||
|
||||
// Use the actual testing.T from the subtest
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected integration test to pass")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromReaderIOIntegration tests integration scenarios
|
||||
func TestFromReaderIOIntegration(t *testing.T) {
|
||||
t.Run("should work in a realistic scenario with logging", func(t *testing.T) {
|
||||
logCalled := false
|
||||
|
||||
// Create a ReaderIO that simulates logging
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
// Simulate logging with context
|
||||
if ctx != nil {
|
||||
logCalled = true
|
||||
}
|
||||
|
||||
// Return an assertion
|
||||
return Equal(100)(100)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
|
||||
if !res {
|
||||
t.Error("Expected integration test to pass")
|
||||
}
|
||||
|
||||
if !logCalled {
|
||||
t.Error("Expected logging to be called")
|
||||
}
|
||||
})
|
||||
}
|
||||
207
v2/assert/logger.go
Normal file
207
v2/assert/logger.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// Logf creates a logging function that outputs formatted test messages using Go's testing.T.Logf.
|
||||
//
|
||||
// This function provides a functional programming approach to test logging, returning a
|
||||
// [ReaderIO] that can be composed with other test operations. It's particularly useful
|
||||
// for debugging tests, tracing execution flow, or documenting test behavior without
|
||||
// affecting test outcomes.
|
||||
//
|
||||
// The function uses a curried design pattern:
|
||||
// 1. First, you provide a format string (prefix) with format verbs (like %v, %d, %s)
|
||||
// 2. This returns a function that takes a value of type T
|
||||
// 3. That function returns a ReaderIO that performs the logging when executed
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - prefix: A format string compatible with fmt.Printf (e.g., "Value: %v", "Count: %d")
|
||||
// The format string should contain exactly one format verb that matches type T
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A function that takes a value of type T and returns a [ReaderIO][*testing.T, Void]
|
||||
// When executed, this ReaderIO logs the formatted message to the test output
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type of value to be logged. Can be any type that can be formatted by fmt
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Debugging test execution by logging intermediate values
|
||||
// - Tracing the flow of complex test scenarios
|
||||
// - Documenting test behavior in the test output
|
||||
// - Logging values in functional pipelines without breaking the chain
|
||||
// - Creating reusable logging operations for specific types
|
||||
//
|
||||
// # Example - Basic Logging
|
||||
//
|
||||
// func TestBasicLogging(t *testing.T) {
|
||||
// // Create a logger for integers
|
||||
// logInt := assert.Logf[int]("Processing value: %d")
|
||||
//
|
||||
// // Use it to log a value
|
||||
// value := 42
|
||||
// logInt(value)(t)() // Outputs: "Processing value: 42"
|
||||
// }
|
||||
//
|
||||
// # Example - Logging in Test Pipeline
|
||||
//
|
||||
// func TestPipelineWithLogging(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Alice", Age: 30}
|
||||
//
|
||||
// // Create a logger for User
|
||||
// logUser := assert.Logf[User]("Testing user: %+v")
|
||||
//
|
||||
// // Log the user being tested
|
||||
// logUser(user)(t)()
|
||||
//
|
||||
// // Continue with assertions
|
||||
// assert.StringNotEmpty(user.Name)(t)
|
||||
// assert.That(func(age int) bool { return age > 0 })(user.Age)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Multiple Loggers for Different Types
|
||||
//
|
||||
// func TestMultipleLoggers(t *testing.T) {
|
||||
// // Create type-specific loggers
|
||||
// logString := assert.Logf[string]("String value: %s")
|
||||
// logInt := assert.Logf[int]("Integer value: %d")
|
||||
// logFloat := assert.Logf[float64]("Float value: %.2f")
|
||||
//
|
||||
// // Use them throughout the test
|
||||
// logString("hello")(t)() // Outputs: "String value: hello"
|
||||
// logInt(42)(t)() // Outputs: "Integer value: 42"
|
||||
// logFloat(3.14159)(t)() // Outputs: "Float value: 3.14"
|
||||
// }
|
||||
//
|
||||
// # Example - Logging Complex Structures
|
||||
//
|
||||
// func TestComplexStructureLogging(t *testing.T) {
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// config := Config{Host: "localhost", Port: 8080, Timeout: 30}
|
||||
//
|
||||
// // Use %+v to include field names
|
||||
// logConfig := assert.Logf[Config]("Configuration: %+v")
|
||||
// logConfig(config)(t)()
|
||||
// // Outputs: "Configuration: {Host:localhost Port:8080 Timeout:30}"
|
||||
//
|
||||
// // Or use %#v for Go-syntax representation
|
||||
// logConfigGo := assert.Logf[Config]("Config (Go syntax): %#v")
|
||||
// logConfigGo(config)(t)()
|
||||
// // Outputs: "Config (Go syntax): assert.Config{Host:"localhost", Port:8080, Timeout:30}"
|
||||
// }
|
||||
//
|
||||
// # Example - Debugging Test Failures
|
||||
//
|
||||
// func TestWithDebugLogging(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// logSlice := assert.Logf[[]int]("Testing slice: %v")
|
||||
//
|
||||
// // Log the input data
|
||||
// logSlice(numbers)(t)()
|
||||
//
|
||||
// // Perform assertions
|
||||
// assert.ArrayNotEmpty(numbers)(t)
|
||||
// assert.ArrayLength[int](5)(numbers)(t)
|
||||
//
|
||||
// // Log intermediate results
|
||||
// sum := 0
|
||||
// for _, n := range numbers {
|
||||
// sum += n
|
||||
// }
|
||||
// logInt := assert.Logf[int]("Sum: %d")
|
||||
// logInt(sum)(t)()
|
||||
//
|
||||
// assert.Equal(15)(sum)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Conditional Logging
|
||||
//
|
||||
// func TestConditionalLogging(t *testing.T) {
|
||||
// logDebug := assert.Logf[string]("DEBUG: %s")
|
||||
//
|
||||
// values := []int{1, 2, 3, 4, 5}
|
||||
// for _, v := range values {
|
||||
// if v%2 == 0 {
|
||||
// logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
|
||||
// }
|
||||
// }
|
||||
// // Outputs:
|
||||
// // DEBUG: Found even number: 2
|
||||
// // DEBUG: Found even number: 4
|
||||
// }
|
||||
//
|
||||
// # Format Verbs
|
||||
//
|
||||
// Common format verbs you can use in the prefix string:
|
||||
// - %v: Default format
|
||||
// - %+v: Default format with field names for structs
|
||||
// - %#v: Go-syntax representation
|
||||
// - %T: Type of the value
|
||||
// - %d: Integer in base 10
|
||||
// - %s: String
|
||||
// - %f: Floating point number
|
||||
// - %t: Boolean (true/false)
|
||||
// - %p: Pointer address
|
||||
//
|
||||
// See the fmt package documentation for a complete list of format verbs.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Logging does not affect test pass/fail status
|
||||
// - Log output appears in test results when running with -v flag or when tests fail
|
||||
// - The function returns Void, indicating it's used for side effects only
|
||||
// - The ReaderIO pattern allows logging to be composed with other operations
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [FromReaderIO]: Converts ReaderIO operations into test assertions
|
||||
// - testing.T.Logf: The underlying Go testing log function
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go testing package: https://pkg.go.dev/testing
|
||||
// - fmt package format verbs: https://pkg.go.dev/fmt
|
||||
// - ReaderIO pattern: Combines Reader (context dependency) with IO (side effects)
|
||||
func Logf[T any](prefix string) func(T) readerio.ReaderIO[*testing.T, Void] {
|
||||
return func(a T) readerio.ReaderIO[*testing.T, Void] {
|
||||
return func(t *testing.T) IO[Void] {
|
||||
return io.FromImpure(func() {
|
||||
t.Logf(prefix, a)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
406
v2/assert/logger_test.go
Normal file
406
v2/assert/logger_test.go
Normal file
@@ -0,0 +1,406 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLogf_BasicInteger tests basic integer logging
|
||||
func TestLogf_BasicInteger(t *testing.T) {
|
||||
logInt := Logf[int]("Processing value: %d")
|
||||
|
||||
// This should not panic and should log the value
|
||||
logInt(42)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicString tests basic string logging
|
||||
func TestLogf_BasicString(t *testing.T) {
|
||||
logString := Logf[string]("String value: %s")
|
||||
|
||||
logString("hello world")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicFloat tests basic float logging
|
||||
func TestLogf_BasicFloat(t *testing.T) {
|
||||
logFloat := Logf[float64]("Float value: %.2f")
|
||||
|
||||
logFloat(3.14159)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicBoolean tests basic boolean logging
|
||||
func TestLogf_BasicBoolean(t *testing.T) {
|
||||
logBool := Logf[bool]("Boolean value: %t")
|
||||
|
||||
logBool(true)(t)()
|
||||
logBool(false)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ComplexStruct tests logging of complex structures
|
||||
func TestLogf_ComplexStruct(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
logUser := Logf[User]("User: %+v")
|
||||
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
logUser(user)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Slice tests logging of slices
|
||||
func TestLogf_Slice(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Slice: %v")
|
||||
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
logSlice(numbers)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Map tests logging of maps
|
||||
func TestLogf_Map(t *testing.T) {
|
||||
logMap := Logf[map[string]int]("Map: %v")
|
||||
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
logMap(data)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Pointer tests logging of pointers
|
||||
func TestLogf_Pointer(t *testing.T) {
|
||||
logPtr := Logf[*int]("Pointer: %p")
|
||||
|
||||
value := 42
|
||||
logPtr(&value)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_NilPointer tests logging of nil pointers
|
||||
func TestLogf_NilPointer(t *testing.T) {
|
||||
logPtr := Logf[*int]("Pointer: %v")
|
||||
|
||||
var nilPtr *int
|
||||
logPtr(nilPtr)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptyString tests logging of empty strings
|
||||
func TestLogf_EmptyString(t *testing.T) {
|
||||
logString := Logf[string]("String: '%s'")
|
||||
|
||||
logString("")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptySlice tests logging of empty slices
|
||||
func TestLogf_EmptySlice(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Slice: %v")
|
||||
|
||||
logSlice([]int{})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptyMap tests logging of empty maps
|
||||
func TestLogf_EmptyMap(t *testing.T) {
|
||||
logMap := Logf[map[string]int]("Map: %v")
|
||||
|
||||
logMap(map[string]int{})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_MultipleTypes tests using multiple loggers for different types
|
||||
func TestLogf_MultipleTypes(t *testing.T) {
|
||||
logString := Logf[string]("String: %s")
|
||||
logInt := Logf[int]("Integer: %d")
|
||||
logFloat := Logf[float64]("Float: %.2f")
|
||||
|
||||
logString("test")(t)()
|
||||
logInt(42)(t)()
|
||||
logFloat(3.14)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_WithinTestPipeline tests logging within a test pipeline
|
||||
func TestLogf_WithinTestPipeline(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
config := Config{Host: "localhost", Port: 8080}
|
||||
|
||||
logConfig := Logf[Config]("Testing config: %+v")
|
||||
logConfig(config)(t)()
|
||||
|
||||
// Continue with assertions
|
||||
StringNotEmpty(config.Host)(t)
|
||||
That(func(port int) bool { return port > 0 })(config.Port)(t)
|
||||
|
||||
// Test passes if no panic occurs and assertions pass
|
||||
}
|
||||
|
||||
// TestLogf_NestedStructures tests logging of nested structures
|
||||
func TestLogf_NestedStructures(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
logPerson := Logf[Person]("Person: %+v")
|
||||
|
||||
person := Person{
|
||||
Name: "Bob",
|
||||
Address: Address{
|
||||
Street: "123 Main St",
|
||||
City: "Springfield",
|
||||
},
|
||||
}
|
||||
|
||||
logPerson(person)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Interface tests logging of interface values
|
||||
func TestLogf_Interface(t *testing.T) {
|
||||
logAny := Logf[any]("Value: %v")
|
||||
|
||||
logAny(42)(t)()
|
||||
logAny("string")(t)()
|
||||
logAny([]int{1, 2, 3})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_GoSyntaxFormat tests logging with Go-syntax format
|
||||
func TestLogf_GoSyntaxFormat(t *testing.T) {
|
||||
type Point struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
logPoint := Logf[Point]("Point: %#v")
|
||||
|
||||
point := Point{X: 10, Y: 20}
|
||||
logPoint(point)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_TypeFormat tests logging with type format
|
||||
func TestLogf_TypeFormat(t *testing.T) {
|
||||
logType := Logf[any]("Type: %T, Value: %v")
|
||||
|
||||
logType(42)(t)()
|
||||
logType("string")(t)()
|
||||
logType(3.14)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_LargeNumbers tests logging of large numbers
|
||||
func TestLogf_LargeNumbers(t *testing.T) {
|
||||
logInt := Logf[int64]("Large number: %d")
|
||||
|
||||
logInt(9223372036854775807)(t)() // Max int64
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_NegativeNumbers tests logging of negative numbers
|
||||
func TestLogf_NegativeNumbers(t *testing.T) {
|
||||
logInt := Logf[int]("Number: %d")
|
||||
|
||||
logInt(-42)(t)()
|
||||
logInt(-100)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_SpecialFloats tests logging of special float values
|
||||
func TestLogf_SpecialFloats(t *testing.T) {
|
||||
logFloat := Logf[float64]("Float: %v")
|
||||
|
||||
logFloat(0.0)(t)()
|
||||
logFloat(-0.0)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_UnicodeStrings tests logging of unicode strings
|
||||
func TestLogf_UnicodeStrings(t *testing.T) {
|
||||
logString := Logf[string]("Unicode: %s")
|
||||
|
||||
logString("Hello, 世界")(t)()
|
||||
logString("Emoji: 🎉🎊")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_MultilineStrings tests logging of multiline strings
|
||||
func TestLogf_MultilineStrings(t *testing.T) {
|
||||
logString := Logf[string]("Multiline:\n%s")
|
||||
|
||||
multiline := `Line 1
|
||||
Line 2
|
||||
Line 3`
|
||||
|
||||
logString(multiline)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ReuseLogger tests reusing the same logger multiple times
|
||||
func TestLogf_ReuseLogger(t *testing.T) {
|
||||
logInt := Logf[int]("Value: %d")
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
logInt(i)(t)()
|
||||
}
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ConditionalLogging tests conditional logging based on values
|
||||
func TestLogf_ConditionalLogging(t *testing.T) {
|
||||
logDebug := Logf[string]("DEBUG: %s")
|
||||
|
||||
values := []int{1, 2, 3, 4, 5}
|
||||
for _, v := range values {
|
||||
if v%2 == 0 {
|
||||
logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
|
||||
}
|
||||
}
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_WithAssertions tests combining logging with assertions
|
||||
func TestLogf_WithAssertions(t *testing.T) {
|
||||
logInt := Logf[int]("Testing value: %d")
|
||||
|
||||
value := 42
|
||||
logInt(value)(t)()
|
||||
|
||||
// Perform assertion after logging
|
||||
Equal(42)(value)(t)
|
||||
|
||||
// Test passes if assertion passes
|
||||
}
|
||||
|
||||
// TestLogf_DebuggingFailures demonstrates using logging to debug test failures
|
||||
func TestLogf_DebuggingFailures(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Input slice: %v")
|
||||
logInt := Logf[int]("Computed sum: %d")
|
||||
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
logSlice(numbers)(t)()
|
||||
|
||||
sum := 0
|
||||
for _, n := range numbers {
|
||||
sum += n
|
||||
}
|
||||
logInt(sum)(t)()
|
||||
|
||||
Equal(15)(sum)(t)
|
||||
|
||||
// Test passes if assertion passes
|
||||
}
|
||||
|
||||
// TestLogf_ComplexDataStructures tests logging of complex nested data
|
||||
func TestLogf_ComplexDataStructures(t *testing.T) {
|
||||
type Metadata struct {
|
||||
Version string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
ID int
|
||||
Title string
|
||||
Metadata Metadata
|
||||
}
|
||||
|
||||
logDoc := Logf[Document]("Document: %+v")
|
||||
|
||||
doc := Document{
|
||||
ID: 1,
|
||||
Title: "Test Document",
|
||||
Metadata: Metadata{
|
||||
Version: "1.0",
|
||||
Tags: []string{"test", "example"},
|
||||
},
|
||||
}
|
||||
|
||||
logDoc(doc)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ArrayTypes tests logging of array types
|
||||
func TestLogf_ArrayTypes(t *testing.T) {
|
||||
logArray := Logf[[5]int]("Array: %v")
|
||||
|
||||
arr := [5]int{1, 2, 3, 4, 5}
|
||||
logArray(arr)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ChannelTypes tests logging of channel types
|
||||
func TestLogf_ChannelTypes(t *testing.T) {
|
||||
logChan := Logf[chan int]("Channel: %v")
|
||||
|
||||
ch := make(chan int, 1)
|
||||
logChan(ch)(t)()
|
||||
close(ch)
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_FunctionTypes tests logging of function types
|
||||
func TestLogf_FunctionTypes(t *testing.T) {
|
||||
logFunc := Logf[func() int]("Function: %v")
|
||||
|
||||
fn := func() int { return 42 }
|
||||
logFunc(fn)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
152
v2/assert/monoid.go
Normal file
152
v2/assert/monoid.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/boolean"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// ApplicativeMonoid returns a [monoid.Monoid] for combining test assertion [Reader]s.
|
||||
//
|
||||
// This monoid combines multiple test assertions using logical AND (conjunction) semantics,
|
||||
// meaning all assertions must pass for the combined assertion to pass. It leverages the
|
||||
// applicative structure of Reader to execute multiple assertions with the same testing.T
|
||||
// context and combines their boolean results using boolean.MonoidAll (logical AND).
|
||||
//
|
||||
// The monoid provides:
|
||||
// - Concat: Combines two assertions such that both must pass (logical AND)
|
||||
// - Empty: Returns an assertion that always passes (identity element)
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Composing multiple test assertions into a single assertion
|
||||
// - Building complex test conditions from simpler ones
|
||||
// - Creating reusable assertion combinators
|
||||
// - Implementing test assertion DSLs
|
||||
//
|
||||
// # Monoid Laws
|
||||
//
|
||||
// The returned monoid satisfies the standard monoid laws:
|
||||
//
|
||||
// 1. Associativity:
|
||||
// Concat(Concat(a1, a2), a3) ≡ Concat(a1, Concat(a2, a3))
|
||||
//
|
||||
// 2. Left Identity:
|
||||
// Concat(Empty(), a) ≡ a
|
||||
//
|
||||
// 3. Right Identity:
|
||||
// Concat(a, Empty()) ≡ a
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [monoid.Monoid][Reader] that combines assertions using logical AND
|
||||
//
|
||||
// # Example - Basic Usage
|
||||
//
|
||||
// func TestUserValidation(t *testing.T) {
|
||||
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
// m := assert.ApplicativeMonoid()
|
||||
//
|
||||
// // Combine multiple assertions
|
||||
// assertion := m.Concat(
|
||||
// assert.Equal("Alice")(user.Name),
|
||||
// m.Concat(
|
||||
// assert.Equal(30)(user.Age),
|
||||
// assert.StringNotEmpty(user.Email),
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// // Execute combined assertion
|
||||
// assertion(t) // All three assertions must pass
|
||||
// }
|
||||
//
|
||||
// # Example - Building Reusable Validators
|
||||
//
|
||||
// func TestWithReusableValidators(t *testing.T) {
|
||||
// m := assert.ApplicativeMonoid()
|
||||
//
|
||||
// // Create a reusable validator
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return m.Concat(
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// m.Concat(
|
||||
// assert.True(u.Age > 0),
|
||||
// assert.StringContains("@")(u.Email),
|
||||
// ),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Bob", Age: 25, Email: "bob@test.com"}
|
||||
// validateUser(user)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Using Empty for Identity
|
||||
//
|
||||
// func TestEmptyIdentity(t *testing.T) {
|
||||
// m := assert.ApplicativeMonoid()
|
||||
// assertion := assert.Equal(42)(42)
|
||||
//
|
||||
// // Empty is the identity - these are equivalent
|
||||
// result1 := m.Concat(m.Empty(), assertion)(t)
|
||||
// result2 := m.Concat(assertion, m.Empty())(t)
|
||||
// result3 := assertion(t)
|
||||
// // All three produce the same result
|
||||
// }
|
||||
//
|
||||
// # Example - Combining with AllOf
|
||||
//
|
||||
// func TestCombiningWithAllOf(t *testing.T) {
|
||||
// // ApplicativeMonoid provides the underlying mechanism for AllOf
|
||||
// arr := []int{1, 2, 3, 4, 5}
|
||||
//
|
||||
// // These are conceptually equivalent:
|
||||
// m := assert.ApplicativeMonoid()
|
||||
// manual := m.Concat(
|
||||
// assert.ArrayNotEmpty(arr),
|
||||
// m.Concat(
|
||||
// assert.ArrayLength[int](5)(arr),
|
||||
// assert.ArrayContains(3)(arr),
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// // AllOf uses ApplicativeMonoid internally
|
||||
// convenient := assert.AllOf([]assert.Reader{
|
||||
// assert.ArrayNotEmpty(arr),
|
||||
// assert.ArrayLength[int](5)(arr),
|
||||
// assert.ArrayContains(3)(arr),
|
||||
// })
|
||||
//
|
||||
// manual(t)
|
||||
// convenient(t)
|
||||
// }
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [AllOf]: Convenient wrapper for combining multiple assertions using this monoid
|
||||
// - [boolean.MonoidAll]: The underlying boolean monoid (logical AND with true as identity)
|
||||
// - [reader.ApplicativeMonoid]: Generic applicative monoid for Reader types
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
|
||||
// - Applicative Functors: https://hackage.haskell.org/package/base/docs/Control-Applicative.html
|
||||
// - Boolean Monoid (All): https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:All
|
||||
func ApplicativeMonoid() monoid.Monoid[Reader] {
|
||||
return reader.ApplicativeMonoid[*testing.T](boolean.MonoidAll)
|
||||
}
|
||||
454
v2/assert/monoid_test.go
Normal file
454
v2/assert/monoid_test.go
Normal file
@@ -0,0 +1,454 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestApplicativeMonoid_Empty tests that Empty returns an assertion that always passes
|
||||
func TestApplicativeMonoid_Empty(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
empty := m.Empty()
|
||||
|
||||
result := empty(t)
|
||||
if !result {
|
||||
t.Error("Expected Empty() to return an assertion that always passes")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_BothPass tests that Concat returns true when both assertions pass
|
||||
func TestApplicativeMonoid_Concat_BothPass(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(42)
|
||||
assertion2 := Equal("hello")("hello")
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected Concat to pass when both assertions pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_FirstFails tests that Concat returns false when first assertion fails
|
||||
func TestApplicativeMonoid_Concat_FirstFails(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(43) // This will fail
|
||||
assertion2 := Equal("hello")("hello")
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when first assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_SecondFails tests that Concat returns false when second assertion fails
|
||||
func TestApplicativeMonoid_Concat_SecondFails(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(42)
|
||||
assertion2 := Equal("hello")("world") // This will fail
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when second assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_BothFail tests that Concat returns false when both assertions fail
|
||||
func TestApplicativeMonoid_Concat_BothFail(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(43) // This will fail
|
||||
assertion2 := Equal("hello")("world") // This will fail
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when both assertions fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_LeftIdentity tests the left identity law: Concat(Empty(), a) = a
|
||||
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Concat(Empty(), assertion) should behave the same as assertion
|
||||
combined := m.Concat(m.Empty(), assertion)
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Left identity law violated: Concat(Empty(), a) should equal a")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RightIdentity tests the right identity law: Concat(a, Empty()) = a
|
||||
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Concat(assertion, Empty()) should behave the same as assertion
|
||||
combined := m.Concat(assertion, m.Empty())
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Right identity law violated: Concat(a, Empty()) should equal a")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Associativity tests the associativity law: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
|
||||
func TestApplicativeMonoid_Associativity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(2)
|
||||
a3 := Equal(3)(3)
|
||||
|
||||
// Concat(Concat(a1, a2), a3)
|
||||
left := m.Concat(m.Concat(a1, a2), a3)
|
||||
|
||||
// Concat(a1, Concat(a2, a3))
|
||||
right := m.Concat(a1, m.Concat(a2, a3))
|
||||
|
||||
result1 := left(t)
|
||||
result2 := right(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Associativity law violated: Concat(Concat(a, b), c) should equal Concat(a, Concat(b, c))")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_AssociativityWithFailure tests associativity when assertions fail
|
||||
func TestApplicativeMonoid_AssociativityWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(3) // This will fail
|
||||
a3 := Equal(3)(3)
|
||||
|
||||
// Concat(Concat(a1, a2), a3)
|
||||
left := m.Concat(m.Concat(a1, a2), a3)
|
||||
|
||||
// Concat(a1, Concat(a2, a3))
|
||||
right := m.Concat(a1, m.Concat(a2, a3))
|
||||
|
||||
result1 := left(mockT)
|
||||
result2 := right(mockT)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Associativity law violated even with failures")
|
||||
}
|
||||
|
||||
if result1 || result2 {
|
||||
t.Error("Expected both to fail when one assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ComplexAssertions tests combining complex assertions
|
||||
func TestApplicativeMonoid_ComplexAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
|
||||
arrayAssertions := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
mapAssertions := m.Concat(
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
)
|
||||
|
||||
combined := m.Concat(arrayAssertions, mapAssertions)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected complex combined assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ComplexAssertionsWithFailure tests complex assertions when one fails
|
||||
func TestApplicativeMonoid_ComplexAssertionsWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
arr := []int{1, 2, 3}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
|
||||
arrayAssertions := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr), // This will fail - array has 3 elements, not 5
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
mapAssertions := m.Concat(
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
)
|
||||
|
||||
combined := m.Concat(arrayAssertions, mapAssertions)
|
||||
|
||||
result := combined(mockT)
|
||||
if result {
|
||||
t.Error("Expected complex combined assertions to fail when one assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_MultipleConcat tests chaining multiple Concat operations
|
||||
func TestApplicativeMonoid_MultipleConcat(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(2)
|
||||
a3 := Equal(3)(3)
|
||||
a4 := Equal(4)(4)
|
||||
|
||||
combined := m.Concat(
|
||||
m.Concat(a1, a2),
|
||||
m.Concat(a3, a4),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected multiple Concat operations to pass when all assertions pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithStringAssertions tests combining string assertions
|
||||
func TestApplicativeMonoid_WithStringAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
str := "hello world"
|
||||
|
||||
combined := m.Concat(
|
||||
StringNotEmpty(str),
|
||||
StringLength[any, any](11)(str),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected string assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithBooleanAssertions tests combining boolean assertions
|
||||
func TestApplicativeMonoid_WithBooleanAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
combined := m.Concat(
|
||||
Equal(true)(true),
|
||||
m.Concat(
|
||||
Equal(false)(false),
|
||||
Equal(true)(true),
|
||||
),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected boolean assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithErrorAssertions tests combining error assertions
|
||||
func TestApplicativeMonoid_WithErrorAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
combined := m.Concat(
|
||||
NoError(nil),
|
||||
Equal("test")("test"),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected error assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_EmptyWithMultipleConcat tests Empty with multiple Concat operations
|
||||
func TestApplicativeMonoid_EmptyWithMultipleConcat(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Multiple Empty values should still act as identity
|
||||
combined := m.Concat(
|
||||
m.Empty(),
|
||||
m.Concat(
|
||||
assertion,
|
||||
m.Empty(),
|
||||
),
|
||||
)
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Multiple Empty values should still act as identity")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_OnlyEmpty tests using only Empty values
|
||||
func TestApplicativeMonoid_OnlyEmpty(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
// Concat of Empty values should still be Empty (identity)
|
||||
combined := m.Concat(m.Empty(), m.Empty())
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected Concat of Empty values to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RealWorldExample tests a realistic use case
|
||||
func TestApplicativeMonoid_RealWorldExample(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
validateUser := func(u User) Reader {
|
||||
return m.Concat(
|
||||
StringNotEmpty(u.Name),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age < 150 })(u.Age),
|
||||
That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(u.Email),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
validUser := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
result := validateUser(validUser)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected valid user to pass all validations")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RealWorldExampleWithFailure tests a realistic use case with failure
|
||||
func TestApplicativeMonoid_RealWorldExampleWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
validateUser := func(u User) Reader {
|
||||
return m.Concat(
|
||||
StringNotEmpty(u.Name),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age < 150 })(u.Age),
|
||||
That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(u.Email),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
invalidUser := User{Name: "Bob", Age: 200, Email: "bob@test.com"} // Age > 150
|
||||
result := validateUser(invalidUser)(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected invalid user to fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_IntegrationWithAllOf demonstrates relationship with AllOf
|
||||
func TestApplicativeMonoid_IntegrationWithAllOf(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
// Using ApplicativeMonoid directly
|
||||
manualCombination := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
// Using AllOf (which uses ApplicativeMonoid internally)
|
||||
allOfCombination := AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
})
|
||||
|
||||
result1 := manualCombination(t)
|
||||
result2 := allOfCombination(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Expected manual combination and AllOf to produce same result")
|
||||
}
|
||||
|
||||
if !result1 || !result2 {
|
||||
t.Error("Expected both combinations to pass")
|
||||
}
|
||||
}
|
||||
650
v2/assert/traverse.go
Normal file
650
v2/assert/traverse.go
Normal file
@@ -0,0 +1,650 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// TraverseArray transforms an array of values into a test suite by applying a function
|
||||
// that generates named test cases for each element.
|
||||
//
|
||||
// This function enables data-driven testing where you have a collection of test inputs
|
||||
// and want to run a named subtest for each one. It follows the functional programming
|
||||
// pattern of "traverse" - transforming a collection while preserving structure and
|
||||
// accumulating effects (in this case, test execution).
|
||||
//
|
||||
// The function takes each element of the array, applies the provided function to generate
|
||||
// a [Pair] of (test name, test assertion), and runs each as a separate subtest using
|
||||
// Go's t.Run. All subtests must pass for the overall test to pass.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes a value of type T and returns a [Pair] containing:
|
||||
// - Head: The test name (string) for the subtest
|
||||
// - Tail: The test assertion ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Kleisli] function that takes an array of T and returns a [Reader] that:
|
||||
// - Executes each element as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation and reporting via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Data-driven testing with multiple test cases
|
||||
// - Parameterized tests where each parameter gets its own subtest
|
||||
// - Testing collections where each element needs validation
|
||||
// - Property-based testing with generated test data
|
||||
//
|
||||
// # Example - Basic Data-Driven Testing
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Input int
|
||||
// Expected int
|
||||
// }
|
||||
//
|
||||
// testCases := []TestCase{
|
||||
// {Input: 2, Expected: 4},
|
||||
// {Input: 3, Expected: 9},
|
||||
// {Input: 4, Expected: 16},
|
||||
// }
|
||||
//
|
||||
// square := func(n int) int { return n * n }
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
|
||||
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
|
||||
// assertion := assert.Equal(tc.Expected)(square(tc.Input))
|
||||
// return pair.MakePair(name, assertion)
|
||||
// })
|
||||
//
|
||||
// traverse(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - String Validation
|
||||
//
|
||||
// func TestStringValidation(t *testing.T) {
|
||||
// inputs := []string{"hello", "world", "test"}
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(s string) assert.Pair[string, assert.Reader] {
|
||||
// return pair.MakePair(
|
||||
// fmt.Sprintf("validate_%s", s),
|
||||
// assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(s),
|
||||
// assert.That(func(str string) bool { return len(str) > 0 })(s),
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// traverse(inputs)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Complex Object Testing
|
||||
//
|
||||
// func TestUsers(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// users := []User{
|
||||
// {Name: "Alice", Age: 30, Email: "alice@example.com"},
|
||||
// {Name: "Bob", Age: 25, Email: "bob@example.com"},
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(u User) assert.Pair[string, assert.Reader] {
|
||||
// return pair.MakePair(
|
||||
// fmt.Sprintf("user_%s", u.Name),
|
||||
// assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// assert.That(func(age int) bool { return age > 0 })(u.Age),
|
||||
// assert.That(func(email string) bool {
|
||||
// return len(email) > 0 && strings.Contains(email, "@")
|
||||
// })(u.Email),
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// traverse(users)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with RunAll
|
||||
//
|
||||
// TraverseArray and [RunAll] serve similar purposes but differ in their approach:
|
||||
//
|
||||
// - TraverseArray: Generates test cases from an array of data
|
||||
//
|
||||
// - Input: Array of values + function to generate test cases
|
||||
//
|
||||
// - Use when: You have test data and need to generate test cases from it
|
||||
//
|
||||
// - RunAll: Executes pre-defined named test cases
|
||||
//
|
||||
// - Input: Map of test names to assertions
|
||||
//
|
||||
// - Use when: You have already defined test cases with names
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [SequenceSeq2]: Similar but works with Go iterators (Seq2) instead of arrays
|
||||
// - [RunAll]: Executes a map of named test cases
|
||||
// - [AllOf]: Combines multiple assertions without subtests
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
func TraverseArray[T any](f func(T) Pair[string, Reader]) Kleisli[[]T] {
|
||||
return func(ts []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for _, src := range ts {
|
||||
test := f(src)
|
||||
res := t.Run(pair.Head(test), func(t *testing.T) {
|
||||
pair.Tail(test)(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceSeq2 executes a sequence of named test cases provided as a Go iterator.
|
||||
//
|
||||
// This function takes a [Seq2] iterator that yields (name, assertion) pairs and
|
||||
// executes each as a separate subtest using Go's t.Run. It's similar to [TraverseArray]
|
||||
// but works directly with Go's iterator protocol (introduced in Go 1.23) rather than
|
||||
// requiring an array.
|
||||
//
|
||||
// The function iterates through all test cases, running each as a named subtest.
|
||||
// All subtests must pass for the overall test to pass. This provides proper test
|
||||
// isolation and clear reporting of which specific test cases fail.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - s: A [Seq2] iterator that yields pairs of:
|
||||
// - Key: Test name (string) for the subtest
|
||||
// - Value: Test assertion ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Reader] that:
|
||||
// - Executes each test case as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Working with iterator-based test data
|
||||
// - Lazy evaluation of test cases
|
||||
// - Integration with Go 1.23+ iterator patterns
|
||||
// - Memory-efficient testing of large test suites
|
||||
//
|
||||
// # Example - Basic Usage with Iterator
|
||||
//
|
||||
// func TestWithIterator(t *testing.T) {
|
||||
// // Create an iterator of test cases
|
||||
// testCases := func(yield func(string, assert.Reader) bool) {
|
||||
// if !yield("test_addition", assert.Equal(4)(2+2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_subtraction", assert.Equal(1)(3-2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Generated Test Cases
|
||||
//
|
||||
// func TestGeneratedCases(t *testing.T) {
|
||||
// // Generate test cases on the fly
|
||||
// generateTests := func(yield func(string, assert.Reader) bool) {
|
||||
// for i := 1; i <= 5; i++ {
|
||||
// name := fmt.Sprintf("test_%d", i)
|
||||
// assertion := assert.Equal(i*i)(i * i)
|
||||
// if !yield(name, assertion) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(generateTests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Filtering Test Cases
|
||||
//
|
||||
// func TestFilteredCases(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Name string
|
||||
// Input int
|
||||
// Expected int
|
||||
// Skip bool
|
||||
// }
|
||||
//
|
||||
// allCases := []TestCase{
|
||||
// {Name: "test1", Input: 2, Expected: 4, Skip: false},
|
||||
// {Name: "test2", Input: 3, Expected: 9, Skip: true},
|
||||
// {Name: "test3", Input: 4, Expected: 16, Skip: false},
|
||||
// }
|
||||
//
|
||||
// // Create iterator that filters out skipped tests
|
||||
// activeTests := func(yield func(string, assert.Reader) bool) {
|
||||
// for _, tc := range allCases {
|
||||
// if !tc.Skip {
|
||||
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
// if !yield(tc.Name, assertion) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(activeTests)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseArray
|
||||
//
|
||||
// SequenceSeq2 and [TraverseArray] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - SequenceSeq2: Works with iterators (Seq2)
|
||||
//
|
||||
// - Input: Iterator yielding (name, assertion) pairs
|
||||
//
|
||||
// - Use when: Working with Go 1.23+ iterators or lazy evaluation
|
||||
//
|
||||
// - Memory: More efficient for large test suites (lazy evaluation)
|
||||
//
|
||||
// - TraverseArray: Works with arrays
|
||||
//
|
||||
// - Input: Array of values + transformation function
|
||||
//
|
||||
// - Use when: You have an array of test data
|
||||
//
|
||||
// - Memory: All test data must be in memory
|
||||
//
|
||||
// # Comparison with RunAll
|
||||
//
|
||||
// SequenceSeq2 and [RunAll] are very similar:
|
||||
//
|
||||
// - SequenceSeq2: Takes an iterator (Seq2)
|
||||
// - RunAll: Takes a map[string]Reader
|
||||
//
|
||||
// Both execute named test cases as subtests. Choose based on your data structure:
|
||||
// use SequenceSeq2 for iterators, RunAll for maps.
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [TraverseArray]: Similar but works with arrays instead of iterators
|
||||
// - [RunAll]: Executes a map of named test cases
|
||||
// - [AllOf]: Combines multiple assertions without subtests
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go iterators: https://go.dev/blog/range-functions
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
|
||||
func SequenceSeq2[T any](s Seq2[string, Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for name, test := range s {
|
||||
res := t.Run(name, func(t *testing.T) {
|
||||
test(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseRecord transforms a map of values into a test suite by applying a function
|
||||
// that generates test assertions for each map entry.
|
||||
//
|
||||
// This function enables data-driven testing where you have a map of test data and want
|
||||
// to run a named subtest for each entry. The map keys become test names, and the function
|
||||
// transforms each value into a test assertion. It follows the functional programming
|
||||
// pattern of "traverse" - transforming a collection while preserving structure and
|
||||
// accumulating effects (in this case, test execution).
|
||||
//
|
||||
// The function takes each key-value pair from the map, applies the provided function to
|
||||
// generate a [Reader] assertion, and runs each as a separate subtest using Go's t.Run.
|
||||
// All subtests must pass for the overall test to pass.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A [Kleisli] function that takes a value of type T and returns a [Reader] assertion
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Kleisli] function that takes a map[string]T and returns a [Reader] that:
|
||||
// - Executes each map entry as a named subtest (using the key as the test name)
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation and reporting via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Data-driven testing with named test cases in a map
|
||||
// - Testing configuration maps where keys are meaningful names
|
||||
// - Validating collections where natural keys exist
|
||||
// - Property-based testing with named scenarios
|
||||
//
|
||||
// # Example - Basic Configuration Testing
|
||||
//
|
||||
// func TestConfigurations(t *testing.T) {
|
||||
// configs := map[string]int{
|
||||
// "timeout": 30,
|
||||
// "maxRetries": 3,
|
||||
// "bufferSize": 1024,
|
||||
// }
|
||||
//
|
||||
// validatePositive := assert.That(func(n int) bool { return n > 0 })
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validatePositive)
|
||||
// traverse(configs)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - User Validation
|
||||
//
|
||||
// func TestUserMap(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// users := map[string]User{
|
||||
// "alice": {Name: "Alice", Age: 30},
|
||||
// "bob": {Name: "Bob", Age: 25},
|
||||
// "carol": {Name: "Carol", Age: 35},
|
||||
// }
|
||||
//
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// assert.That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validateUser)
|
||||
// traverse(users)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - API Endpoint Testing
|
||||
//
|
||||
// func TestEndpoints(t *testing.T) {
|
||||
// type Endpoint struct {
|
||||
// Path string
|
||||
// Method string
|
||||
// }
|
||||
//
|
||||
// endpoints := map[string]Endpoint{
|
||||
// "get_users": {Path: "/api/users", Method: "GET"},
|
||||
// "create_user": {Path: "/api/users", Method: "POST"},
|
||||
// "delete_user": {Path: "/api/users/:id", Method: "DELETE"},
|
||||
// }
|
||||
//
|
||||
// validateEndpoint := func(e Endpoint) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(e.Path),
|
||||
// assert.That(func(path string) bool {
|
||||
// return strings.HasPrefix(path, "/api/")
|
||||
// })(e.Path),
|
||||
// assert.That(func(method string) bool {
|
||||
// return method == "GET" || method == "POST" ||
|
||||
// method == "PUT" || method == "DELETE"
|
||||
// })(e.Method),
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validateEndpoint)
|
||||
// traverse(endpoints)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseArray
|
||||
//
|
||||
// TraverseRecord and [TraverseArray] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - TraverseRecord: Works with maps (records)
|
||||
//
|
||||
// - Input: Map with string keys + transformation function
|
||||
//
|
||||
// - Use when: You have named test data in a map
|
||||
//
|
||||
// - Test names: Derived from map keys
|
||||
//
|
||||
// - TraverseArray: Works with arrays
|
||||
//
|
||||
// - Input: Array of values + function that generates names and assertions
|
||||
//
|
||||
// - Use when: You have sequential test data
|
||||
//
|
||||
// - Test names: Generated by the transformation function
|
||||
//
|
||||
// # Comparison with SequenceRecord
|
||||
//
|
||||
// TraverseRecord and [SequenceRecord] are closely related:
|
||||
//
|
||||
// - TraverseRecord: Transforms values into assertions
|
||||
//
|
||||
// - Input: map[string]T + function T -> Reader
|
||||
//
|
||||
// - Use when: You need to transform data before asserting
|
||||
//
|
||||
// - SequenceRecord: Executes pre-defined assertions
|
||||
//
|
||||
// - Input: map[string]Reader
|
||||
//
|
||||
// - Use when: Assertions are already defined
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [SequenceRecord]: Similar but takes pre-defined assertions
|
||||
// - [TraverseArray]: Similar but works with arrays
|
||||
// - [RunAll]: Alias for SequenceRecord
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
func TraverseRecord[T any](f Kleisli[T]) Kleisli[map[string]T] {
|
||||
return func(m map[string]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for name, src := range m {
|
||||
res := t.Run(name, func(t *testing.T) {
|
||||
f(src)(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceRecord executes a map of named test cases as subtests.
|
||||
//
|
||||
// This function takes a map where keys are test names and values are test assertions
|
||||
// ([Reader]), and executes each as a separate subtest using Go's t.Run. It's the
|
||||
// record (map) equivalent of [SequenceSeq2] and is actually aliased as [RunAll] for
|
||||
// convenience.
|
||||
//
|
||||
// The function iterates through all map entries, running each as a named subtest.
|
||||
// All subtests must pass for the overall test to pass. This provides proper test
|
||||
// isolation and clear reporting of which specific test cases fail.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: A map[string]Reader where:
|
||||
// - Keys: Test names (strings) for the subtests
|
||||
// - Values: Test assertions ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Reader] that:
|
||||
// - Executes each map entry as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Executing a collection of pre-defined named test cases
|
||||
// - Organizing related tests in a map structure
|
||||
// - Running multiple assertions with descriptive names
|
||||
// - Building test suites programmatically
|
||||
//
|
||||
// # Example - Basic Named Tests
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// tests := map[string]assert.Reader{
|
||||
// "addition": assert.Equal(4)(2 + 2),
|
||||
// "subtraction": assert.Equal(1)(3 - 2),
|
||||
// "multiplication": assert.Equal(6)(2 * 3),
|
||||
// "division": assert.Equal(2)(6 / 3),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - String Validation Suite
|
||||
//
|
||||
// func TestStringValidations(t *testing.T) {
|
||||
// testString := "hello world"
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "not_empty": assert.StringNotEmpty(testString),
|
||||
// "correct_length": assert.StringLength[any, any](11)(testString),
|
||||
// "has_space": assert.That(func(s string) bool {
|
||||
// return strings.Contains(s, " ")
|
||||
// })(testString),
|
||||
// "lowercase": assert.That(func(s string) bool {
|
||||
// return s == strings.ToLower(s)
|
||||
// })(testString),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Complex Object Validation
|
||||
//
|
||||
// func TestUserValidation(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "name_not_empty": assert.StringNotEmpty(user.Name),
|
||||
// "age_positive": assert.That(func(age int) bool { return age > 0 })(user.Age),
|
||||
// "age_reasonable": assert.That(func(age int) bool { return age < 150 })(user.Age),
|
||||
// "email_valid": assert.That(func(email string) bool {
|
||||
// return strings.Contains(email, "@") && strings.Contains(email, ".")
|
||||
// })(user.Email),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Array Validation Suite
|
||||
//
|
||||
// func TestArrayValidations(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "not_empty": assert.ArrayNotEmpty(numbers),
|
||||
// "correct_length": assert.ArrayLength[int](5)(numbers),
|
||||
// "contains_three": assert.ArrayContains(3)(numbers),
|
||||
// "all_positive": assert.That(func(arr []int) bool {
|
||||
// for _, n := range arr {
|
||||
// if n <= 0 {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// })(numbers),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseRecord
|
||||
//
|
||||
// SequenceRecord and [TraverseRecord] are closely related:
|
||||
//
|
||||
// - SequenceRecord: Executes pre-defined assertions
|
||||
//
|
||||
// - Input: map[string]Reader (assertions already created)
|
||||
//
|
||||
// - Use when: You have already defined test cases with assertions
|
||||
//
|
||||
// - TraverseRecord: Transforms values into assertions
|
||||
//
|
||||
// - Input: map[string]T + function T -> Reader
|
||||
//
|
||||
// - Use when: You need to transform data before asserting
|
||||
//
|
||||
// # Comparison with SequenceSeq2
|
||||
//
|
||||
// SequenceRecord and [SequenceSeq2] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - SequenceRecord: Works with maps
|
||||
//
|
||||
// - Input: map[string]Reader
|
||||
//
|
||||
// - Use when: You have named test cases in a map
|
||||
//
|
||||
// - Iteration order: Non-deterministic (map iteration)
|
||||
//
|
||||
// - SequenceSeq2: Works with iterators
|
||||
//
|
||||
// - Input: Seq2[string, Reader]
|
||||
//
|
||||
// - Use when: You have test cases in an iterator
|
||||
//
|
||||
// - Iteration order: Deterministic (iterator order)
|
||||
//
|
||||
// # Note on Map Iteration Order
|
||||
//
|
||||
// Go maps have non-deterministic iteration order. If test execution order matters,
|
||||
// consider using [SequenceSeq2] with an iterator that provides deterministic ordering,
|
||||
// or use [TraverseArray] with a slice of test cases.
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [RunAll]: Alias for SequenceRecord
|
||||
// - [TraverseRecord]: Similar but transforms values into assertions
|
||||
// - [SequenceSeq2]: Similar but works with iterators
|
||||
// - [TraverseArray]: Similar but works with arrays
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
|
||||
func SequenceRecord(m map[string]Reader) Reader {
|
||||
return TraverseRecord(reader.Ask[Reader]())(m)
|
||||
}
|
||||
960
v2/assert/traverse_test.go
Normal file
960
v2/assert/traverse_test.go
Normal file
@@ -0,0 +1,960 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// TestTraverseArray_EmptyArray tests that TraverseArray handles empty arrays correctly
|
||||
func TestTraverseArray_EmptyArray(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(n)(n),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with empty array")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_SingleElement tests TraverseArray with a single element
|
||||
func TestTraverseArray_SingleElement(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(n*2)(n*2),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with single element")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_MultipleElements tests TraverseArray with multiple passing elements
|
||||
func TestTraverseArray_MultipleElements(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("square_%d", n),
|
||||
Equal(n*n)(n*n),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{1, 2, 3, 4, 5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with all passing elements")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_WithFailure tests that TraverseArray fails when one element fails
|
||||
func TestTraverseArray_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(10)(n), // Will fail for all except 10
|
||||
)
|
||||
})
|
||||
|
||||
// Run in a subtest - we expect the subtests to fail, so t.Run returns false
|
||||
result := traverse([]int{1, 2, 3})(t)
|
||||
|
||||
// The traverse should return false because assertions fail
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when elements don't match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_MixedResults tests TraverseArray with some passing and some failing
|
||||
func TestTraverseArray_MixedResults(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("is_even_%d", n),
|
||||
Equal(0)(n%2), // Only passes for even numbers
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{2, 3, 4})(t) // 3 is odd, should fail
|
||||
|
||||
// The traverse should return false because one assertion fails
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when some elements fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_StringData tests TraverseArray with string data
|
||||
func TestTraverseArray_StringData(t *testing.T) {
|
||||
words := []string{"hello", "world", "test"}
|
||||
|
||||
traverse := TraverseArray(func(s string) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("validate_%s", s),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(s),
|
||||
That(func(str string) bool { return len(str) > 0 })(s),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(words)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with valid strings")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_ComplexObjects tests TraverseArray with complex objects
|
||||
func TestTraverseArray_ComplexObjects(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "Bob", Age: 25},
|
||||
{Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseArray(func(u User) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("user_%s", u.Name),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with valid users")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_ComplexObjectsWithFailure tests TraverseArray with invalid complex objects
|
||||
func TestTraverseArray_ComplexObjectsWithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "", Age: 25}, // Invalid: empty name
|
||||
{Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseArray(func(u User) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("user_%s", u.Name),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
|
||||
// The traverse should return false because one user is invalid
|
||||
if result {
|
||||
t.Error("Expected traverse to return false with invalid user")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_DataDrivenTesting demonstrates data-driven testing pattern
|
||||
func TestTraverseArray_DataDrivenTesting(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testCases := []TestCase{
|
||||
{Input: 2, Expected: 4},
|
||||
{Input: 3, Expected: 9},
|
||||
{Input: 4, Expected: 16},
|
||||
{Input: 5, Expected: 25},
|
||||
}
|
||||
|
||||
square := func(n int) int { return n * n }
|
||||
|
||||
traverse := TraverseArray(func(tc TestCase) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected),
|
||||
Equal(tc.Expected)(square(tc.Input)),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(testCases)(t)
|
||||
if !result {
|
||||
t.Error("Expected all test cases to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_EmptySequence tests that SequenceSeq2 handles empty sequences correctly
|
||||
func TestSequenceSeq2_EmptySequence(t *testing.T) {
|
||||
emptySeq := func(yield func(string, Reader) bool) {
|
||||
// Empty - yields nothing
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](emptySeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with empty sequence")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_SingleTest tests SequenceSeq2 with a single test
|
||||
func TestSequenceSeq2_SingleTest(t *testing.T) {
|
||||
singleSeq := func(yield func(string, Reader) bool) {
|
||||
yield("test_one", Equal(42)(42))
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](singleSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with single test")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_MultipleTests tests SequenceSeq2 with multiple passing tests
|
||||
func TestSequenceSeq2_MultipleTests(t *testing.T) {
|
||||
multiSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_addition", Equal(4)(2+2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_subtraction", Equal(1)(3-2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_multiplication", Equal(6)(2*3)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](multiSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with all passing tests")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_WithFailure tests that SequenceSeq2 fails when one test fails
|
||||
func TestSequenceSeq2_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
failSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_pass", Equal(4)(2+2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_fail", Equal(5)(2+2)) { // This will fail
|
||||
return
|
||||
}
|
||||
if !yield("test_pass2", Equal(6)(2*3)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](failSeq)(t)
|
||||
|
||||
// The sequence should return false because one test fails
|
||||
if result {
|
||||
t.Error("Expected sequence to return false when one test fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_GeneratedTests tests SequenceSeq2 with generated test cases
|
||||
func TestSequenceSeq2_GeneratedTests(t *testing.T) {
|
||||
generateTests := func(yield func(string, Reader) bool) {
|
||||
for i := 1; i <= 5; i++ {
|
||||
name := fmt.Sprintf("test_%d", i)
|
||||
assertion := Equal(i * i)(i * i)
|
||||
if !yield(name, assertion) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](generateTests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all generated tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_StringTests tests SequenceSeq2 with string assertions
|
||||
func TestSequenceSeq2_StringTests(t *testing.T) {
|
||||
stringSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_hello", StringNotEmpty("hello")) {
|
||||
return
|
||||
}
|
||||
if !yield("test_world", StringNotEmpty("world")) {
|
||||
return
|
||||
}
|
||||
if !yield("test_length", StringLength[any, any](5)("hello")) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](stringSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all string tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_ArrayTests tests SequenceSeq2 with array assertions
|
||||
func TestSequenceSeq2_ArrayTests(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
arraySeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_not_empty", ArrayNotEmpty(arr)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_length", ArrayLength[int](5)(arr)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_contains", ArrayContains(3)(arr)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](arraySeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all array tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_ComplexAssertions tests SequenceSeq2 with complex combined assertions
|
||||
func TestSequenceSeq2_ComplexAssertions(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
|
||||
userSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_name", StringNotEmpty(user.Name)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_age", That(func(age int) bool { return age > 0 && age < 150 })(user.Age)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_email", That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(user.Email)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](userSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all user validation tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_EarlyTermination tests that SequenceSeq2 respects early termination
|
||||
func TestSequenceSeq2_EarlyTermination(t *testing.T) {
|
||||
executionCount := 0
|
||||
|
||||
earlyTermSeq := func(yield func(string, Reader) bool) {
|
||||
executionCount++
|
||||
if !yield("test_1", Equal(1)(1)) {
|
||||
return
|
||||
}
|
||||
executionCount++
|
||||
if !yield("test_2", Equal(2)(2)) {
|
||||
return
|
||||
}
|
||||
executionCount++
|
||||
// This should execute even though we don't check the return
|
||||
yield("test_3", Equal(3)(3))
|
||||
executionCount++
|
||||
}
|
||||
|
||||
SequenceSeq2[Reader](earlyTermSeq)(t)
|
||||
|
||||
// All iterations should execute since we're not terminating early
|
||||
if executionCount != 4 {
|
||||
t.Errorf("Expected 4 executions, got %d", executionCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_WithMapConversion demonstrates converting a map to Seq2
|
||||
func TestSequenceSeq2_WithMapConversion(t *testing.T) {
|
||||
testMap := map[string]Reader{
|
||||
"test_addition": Equal(4)(2 + 2),
|
||||
"test_multiplication": Equal(6)(2 * 3),
|
||||
"test_subtraction": Equal(1)(3 - 2),
|
||||
}
|
||||
|
||||
// Convert map to Seq2
|
||||
mapSeq := func(yield func(string, Reader) bool) {
|
||||
for name, assertion := range testMap {
|
||||
if !yield(name, assertion) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](mapSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all map-based tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_vs_SequenceSeq2 demonstrates the relationship between the two functions
|
||||
func TestTraverseArray_vs_SequenceSeq2(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Name string
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testCases := []TestCase{
|
||||
{Name: "test_1", Input: 2, Expected: 4},
|
||||
{Name: "test_2", Input: 3, Expected: 9},
|
||||
{Name: "test_3", Input: 4, Expected: 16},
|
||||
}
|
||||
|
||||
// Using TraverseArray
|
||||
traverseResult := TraverseArray(func(tc TestCase) Pair[string, Reader] {
|
||||
return pair.MakePair(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input))
|
||||
})(testCases)(t)
|
||||
|
||||
// Using SequenceSeq2
|
||||
seqResult := SequenceSeq2[Reader](func(yield func(string, Reader) bool) {
|
||||
for _, tc := range testCases {
|
||||
if !yield(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})(t)
|
||||
|
||||
if traverseResult != seqResult {
|
||||
t.Error("Expected TraverseArray and SequenceSeq2 to produce same result")
|
||||
}
|
||||
|
||||
if !traverseResult || !seqResult {
|
||||
t.Error("Expected both approaches to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_EmptyMap tests that TraverseRecord handles empty maps correctly
|
||||
func TestTraverseRecord_EmptyMap(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n)(n)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with empty map")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_SingleEntry tests TraverseRecord with a single map entry
|
||||
func TestTraverseRecord_SingleEntry(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n * 2)(n * 2)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{"test_5": 5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with single entry")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_MultipleEntries tests TraverseRecord with multiple passing entries
|
||||
func TestTraverseRecord_MultipleEntries(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n * n)(n * n)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"square_1": 1,
|
||||
"square_2": 2,
|
||||
"square_3": 3,
|
||||
"square_4": 4,
|
||||
"square_5": 5,
|
||||
})(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with all passing entries")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_WithFailure tests that TraverseRecord fails when one entry fails
|
||||
func TestTraverseRecord_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(10)(n) // Will fail for all except 10
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"test_1": 1,
|
||||
"test_2": 2,
|
||||
"test_3": 3,
|
||||
})(t)
|
||||
|
||||
// The traverse should return false because entries don't match
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when entries don't match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_MixedResults tests TraverseRecord with some passing and some failing
|
||||
func TestTraverseRecord_MixedResults(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(0)(n % 2) // Only passes for even numbers
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"even_2": 2,
|
||||
"odd_3": 3,
|
||||
"even_4": 4,
|
||||
})(t)
|
||||
|
||||
// The traverse should return false because some entries fail
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when some entries fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_StringData tests TraverseRecord with string data
|
||||
func TestTraverseRecord_StringData(t *testing.T) {
|
||||
words := map[string]string{
|
||||
"greeting": "hello",
|
||||
"world": "world",
|
||||
"test": "test",
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(s string) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(s),
|
||||
That(func(str string) bool { return len(str) > 0 })(s),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(words)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with valid strings")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ComplexObjects tests TraverseRecord with complex objects
|
||||
func TestTraverseRecord_ComplexObjects(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := map[string]User{
|
||||
"alice": {Name: "Alice", Age: 30},
|
||||
"bob": {Name: "Bob", Age: 25},
|
||||
"charlie": {Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(u User) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with valid users")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ComplexObjectsWithFailure tests TraverseRecord with invalid complex objects
|
||||
func TestTraverseRecord_ComplexObjectsWithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := map[string]User{
|
||||
"alice": {Name: "Alice", Age: 30},
|
||||
"invalid": {Name: "", Age: 25}, // Invalid: empty name
|
||||
"charlie": {Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(u User) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
|
||||
// The traverse should return false because one user is invalid
|
||||
if result {
|
||||
t.Error("Expected traverse to return false with invalid user")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ConfigurationTesting demonstrates configuration testing pattern
|
||||
func TestTraverseRecord_ConfigurationTesting(t *testing.T) {
|
||||
configs := map[string]int{
|
||||
"timeout": 30,
|
||||
"maxRetries": 3,
|
||||
"bufferSize": 1024,
|
||||
}
|
||||
|
||||
validatePositive := That(func(n int) bool { return n > 0 })
|
||||
|
||||
traverse := TraverseRecord(validatePositive)
|
||||
result := traverse(configs)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all configuration values to be positive")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_APIEndpointTesting demonstrates API endpoint testing pattern
|
||||
func TestTraverseRecord_APIEndpointTesting(t *testing.T) {
|
||||
type Endpoint struct {
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
|
||||
endpoints := map[string]Endpoint{
|
||||
"get_users": {Path: "/api/users", Method: "GET"},
|
||||
"create_user": {Path: "/api/users", Method: "POST"},
|
||||
"delete_user": {Path: "/api/users/:id", Method: "DELETE"},
|
||||
}
|
||||
|
||||
validateEndpoint := func(e Endpoint) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(e.Path),
|
||||
That(func(path string) bool {
|
||||
return len(path) > 0 && path[0] == '/'
|
||||
})(e.Path),
|
||||
That(func(method string) bool {
|
||||
return method == "GET" || method == "POST" ||
|
||||
method == "PUT" || method == "DELETE"
|
||||
})(e.Method),
|
||||
})
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(validateEndpoint)
|
||||
result := traverse(endpoints)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all endpoints to be valid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_EmptyMap tests that SequenceRecord handles empty maps correctly
|
||||
func TestSequenceRecord_EmptyMap(t *testing.T) {
|
||||
result := SequenceRecord(map[string]Reader{})(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with empty map")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_SingleTest tests SequenceRecord with a single test
|
||||
func TestSequenceRecord_SingleTest(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"test_one": Equal(42)(42),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with single test")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_MultipleTests tests SequenceRecord with multiple passing tests
|
||||
func TestSequenceRecord_MultipleTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"test_addition": Equal(4)(2 + 2),
|
||||
"test_subtraction": Equal(1)(3 - 2),
|
||||
"test_multiplication": Equal(6)(2 * 3),
|
||||
"test_division": Equal(2)(6 / 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with all passing tests")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_WithFailure tests that SequenceRecord fails when one test fails
|
||||
func TestSequenceRecord_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
tests := map[string]Reader{
|
||||
"test_pass": Equal(4)(2 + 2),
|
||||
"test_fail": Equal(5)(2 + 2), // This will fail
|
||||
"test_pass2": Equal(6)(2 * 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
|
||||
// The sequence should return false because one test fails
|
||||
if result {
|
||||
t.Error("Expected sequence to return false when one test fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_StringTests tests SequenceRecord with string assertions
|
||||
func TestSequenceRecord_StringTests(t *testing.T) {
|
||||
testString := "hello world"
|
||||
|
||||
tests := map[string]Reader{
|
||||
"not_empty": StringNotEmpty(testString),
|
||||
"correct_length": StringLength[any, any](11)(testString),
|
||||
"has_space": That(func(s string) bool {
|
||||
for _, ch := range s {
|
||||
if ch == ' ' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(testString),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all string tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ArrayTests tests SequenceRecord with array assertions
|
||||
func TestSequenceRecord_ArrayTests(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"not_empty": ArrayNotEmpty(arr),
|
||||
"correct_length": ArrayLength[int](5)(arr),
|
||||
"contains_three": ArrayContains(3)(arr),
|
||||
"all_positive": That(func(arr []int) bool {
|
||||
for _, n := range arr {
|
||||
if n <= 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})(arr),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all array tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ComplexAssertions tests SequenceRecord with complex combined assertions
|
||||
func TestSequenceRecord_ComplexAssertions(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"name_not_empty": StringNotEmpty(user.Name),
|
||||
"age_positive": That(func(age int) bool { return age > 0 })(user.Age),
|
||||
"age_reasonable": That(func(age int) bool { return age < 150 })(user.Age),
|
||||
"email_valid": That(func(email string) bool {
|
||||
hasAt := false
|
||||
hasDot := false
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
hasAt = true
|
||||
}
|
||||
if ch == '.' {
|
||||
hasDot = true
|
||||
}
|
||||
}
|
||||
return hasAt && hasDot
|
||||
})(user.Email),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all user validation tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_MathOperations demonstrates basic math operations testing
|
||||
func TestSequenceRecord_MathOperations(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"addition": Equal(4)(2 + 2),
|
||||
"subtraction": Equal(1)(3 - 2),
|
||||
"multiplication": Equal(6)(2 * 3),
|
||||
"division": Equal(2)(6 / 3),
|
||||
"modulo": Equal(1)(7 % 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all math operations to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_BooleanTests tests SequenceRecord with boolean assertions
|
||||
func TestSequenceRecord_BooleanTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"true_is_true": Equal(true)(true),
|
||||
"false_is_false": Equal(false)(false),
|
||||
"not_true": Equal(false)(!true),
|
||||
"not_false": Equal(true)(!false),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all boolean tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ErrorTests tests SequenceRecord with error assertions
|
||||
func TestSequenceRecord_ErrorTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"no_error": NoError(nil),
|
||||
"equal_value": Equal("test")("test"),
|
||||
"not_empty": StringNotEmpty("hello"),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all error tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_vs_SequenceRecord demonstrates the relationship between the two functions
|
||||
func TestTraverseRecord_vs_SequenceRecord(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testData := map[string]TestCase{
|
||||
"test_1": {Input: 2, Expected: 4},
|
||||
"test_2": {Input: 3, Expected: 9},
|
||||
"test_3": {Input: 4, Expected: 16},
|
||||
}
|
||||
|
||||
// Using TraverseRecord
|
||||
traverseResult := TraverseRecord(func(tc TestCase) Reader {
|
||||
return Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
})(testData)(t)
|
||||
|
||||
// Using SequenceRecord (manually creating the map)
|
||||
tests := make(map[string]Reader)
|
||||
for name, tc := range testData {
|
||||
tests[name] = Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
}
|
||||
seqResult := SequenceRecord(tests)(t)
|
||||
|
||||
if traverseResult != seqResult {
|
||||
t.Error("Expected TraverseRecord and SequenceRecord to produce same result")
|
||||
}
|
||||
|
||||
if !traverseResult || !seqResult {
|
||||
t.Error("Expected both approaches to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_WithAllOf demonstrates combining SequenceRecord with AllOf
|
||||
func TestSequenceRecord_WithAllOf(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"array_validations": AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
}),
|
||||
"element_checks": AllOf([]Reader{
|
||||
That(func(a []int) bool { return a[0] == 1 })(arr),
|
||||
That(func(a []int) bool { return a[4] == 5 })(arr),
|
||||
}),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected combined assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ConfigValidation demonstrates real-world configuration validation
|
||||
func TestTraverseRecord_ConfigValidation(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
Min int
|
||||
Max int
|
||||
}
|
||||
|
||||
configs := map[string]Config{
|
||||
"timeout": {Value: 30, Min: 1, Max: 60},
|
||||
"maxRetries": {Value: 3, Min: 1, Max: 10},
|
||||
"bufferSize": {Value: 1024, Min: 512, Max: 4096},
|
||||
}
|
||||
|
||||
validateConfig := func(c Config) Reader {
|
||||
return AllOf([]Reader{
|
||||
That(func(val int) bool { return val >= c.Min })(c.Value),
|
||||
That(func(val int) bool { return val <= c.Max })(c.Value),
|
||||
})
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(validateConfig)
|
||||
result := traverse(configs)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all configurations to be within valid ranges")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_RealWorldExample demonstrates a realistic use case
|
||||
func TestSequenceRecord_RealWorldExample(t *testing.T) {
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
response := Response{StatusCode: 200, Body: `{"status":"ok"}`}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"status_ok": Equal(200)(response.StatusCode),
|
||||
"body_not_empty": StringNotEmpty(response.Body),
|
||||
"body_is_json": That(func(s string) bool {
|
||||
return len(s) > 0 && s[0] == '{' && s[len(s)-1] == '}'
|
||||
})(response.Body),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected response validation to pass")
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,32 @@
|
||||
// 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 assert
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerio"
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"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/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -13,23 +34,506 @@ import (
|
||||
|
||||
type (
|
||||
// Result represents a computation that may fail with an error.
|
||||
//
|
||||
// This is an alias for [result.Result][T], which encapsulates either a successful
|
||||
// value of type T or an error. It's commonly used in test assertions to represent
|
||||
// operations that might fail, allowing for functional error handling without exceptions.
|
||||
//
|
||||
// A Result can be in one of two states:
|
||||
// - Success: Contains a value of type T
|
||||
// - Failure: Contains an error
|
||||
//
|
||||
// This type is particularly useful in testing scenarios where you need to:
|
||||
// - Test functions that return results
|
||||
// - Chain operations that might fail
|
||||
// - Handle errors functionally
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestResultHandling(t *testing.T) {
|
||||
// successResult := result.Of[int](42)
|
||||
// assert.Success(successResult)(t) // Passes
|
||||
//
|
||||
// failureResult := result.Error[int](errors.New("failed"))
|
||||
// assert.Failure(failureResult)(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Success]: Asserts a Result is successful
|
||||
// - [Failure]: Asserts a Result contains an error
|
||||
// - [result.Result]: The underlying Result type
|
||||
Result[T any] = result.Result[T]
|
||||
|
||||
// Reader represents a test assertion that depends on a testing.T context and returns a boolean.
|
||||
// Reader represents a test assertion that depends on a [testing.T] context and returns a boolean.
|
||||
//
|
||||
// This is the core type for all assertions in this package. It's an alias for
|
||||
// [reader.Reader][*testing.T, bool], which is a function that takes a testing context
|
||||
// and produces a boolean result indicating whether the assertion passed.
|
||||
//
|
||||
// The Reader pattern enables:
|
||||
// - Composable assertions that can be combined using functional operators
|
||||
// - Deferred execution - assertions are defined but not executed until applied to a test
|
||||
// - Reusable assertion logic that can be applied to multiple tests
|
||||
// - Functional composition of complex test conditions
|
||||
//
|
||||
// All assertion functions in this package return a Reader, which must be applied
|
||||
// to a *testing.T to execute the assertion:
|
||||
//
|
||||
// assertion := assert.Equal(42)(result) // Creates a Reader
|
||||
// assertion(t) // Executes the assertion
|
||||
//
|
||||
// Readers can be composed using functions like [AllOf], [ApplicativeMonoid], or
|
||||
// functional operators from the reader package.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderComposition(t *testing.T) {
|
||||
// // Create individual assertions
|
||||
// assertion1 := assert.Equal(42)(42)
|
||||
// assertion2 := assert.StringNotEmpty("hello")
|
||||
//
|
||||
// // Combine them
|
||||
// combined := assert.AllOf([]assert.Reader{assertion1, assertion2})
|
||||
//
|
||||
// // Execute the combined assertion
|
||||
// combined(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Kleisli]: Function that produces a Reader from a value
|
||||
// - [AllOf]: Combines multiple Readers
|
||||
// - [ApplicativeMonoid]: Monoid for combining Readers
|
||||
Reader = reader.Reader[*testing.T, bool]
|
||||
|
||||
// Kleisli represents a function that produces a test assertion Reader from a value of type T.
|
||||
// Kleisli represents a function that produces a test assertion [Reader] from a value of type T.
|
||||
//
|
||||
// This is an alias for [reader.Reader][T, Reader], which is a function that takes a value
|
||||
// of type T and returns a Reader (test assertion). This pattern is fundamental to the
|
||||
// "data last" principle used throughout this package.
|
||||
//
|
||||
// Kleisli functions enable:
|
||||
// - Partial application of assertions - configure the expected value first, apply actual value later
|
||||
// - Reusable assertion builders that can be applied to different values
|
||||
// - Functional composition of assertion pipelines
|
||||
// - Point-free style programming with assertions
|
||||
//
|
||||
// Most assertion functions in this package return a Kleisli, which must be applied
|
||||
// to the actual value being tested, and then to a *testing.T:
|
||||
//
|
||||
// kleisli := assert.Equal(42) // Kleisli[int] - expects an int
|
||||
// reader := kleisli(result) // Reader - assertion ready to execute
|
||||
// reader(t) // Execute the assertion
|
||||
//
|
||||
// Or more concisely:
|
||||
//
|
||||
// assert.Equal(42)(result)(t)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestKleisliPattern(t *testing.T) {
|
||||
// // Create a reusable assertion for positive numbers
|
||||
// isPositive := assert.That(func(n int) bool { return n > 0 })
|
||||
//
|
||||
// // Apply it to different values
|
||||
// isPositive(42)(t) // Passes
|
||||
// isPositive(100)(t) // Passes
|
||||
// // isPositive(-5)(t) would fail
|
||||
//
|
||||
// // Can be used with Local for property testing
|
||||
// type User struct { Age int }
|
||||
// checkAge := assert.Local(func(u User) int { return u.Age })(isPositive)
|
||||
// checkAge(User{Age: 25})(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Reader]: The assertion type produced by Kleisli
|
||||
// - [Local]: Focuses a Kleisli on a property of a larger structure
|
||||
Kleisli[T any] = reader.Reader[T, Reader]
|
||||
|
||||
// Predicate represents a function that tests a value of type T and returns a boolean.
|
||||
//
|
||||
// This is an alias for [predicate.Predicate][T], which is a simple function that
|
||||
// takes a value and returns true or false based on some condition. Predicates are
|
||||
// used with the [That] function to create custom assertions.
|
||||
//
|
||||
// Predicates enable:
|
||||
// - Custom validation logic for any type
|
||||
// - Reusable test conditions
|
||||
// - Composition of complex validation rules
|
||||
// - Integration with functional programming patterns
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPredicates(t *testing.T) {
|
||||
// // Simple predicate
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// assert.That(isEven)(42)(t) // Passes
|
||||
//
|
||||
// // String predicate
|
||||
// hasPrefix := func(s string) bool { return strings.HasPrefix(s, "test") }
|
||||
// assert.That(hasPrefix)("test_file.go")(t) // Passes
|
||||
//
|
||||
// // Complex predicate
|
||||
// isValidEmail := func(s string) bool {
|
||||
// return strings.Contains(s, "@") && strings.Contains(s, ".")
|
||||
// }
|
||||
// assert.That(isValidEmail)("user@example.com")(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [That]: Creates an assertion from a Predicate
|
||||
// - [predicate.Predicate]: The underlying predicate type
|
||||
Predicate[T any] = predicate.Predicate[T]
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
//
|
||||
// This is an alias for [lens.Lens][S, T], which provides a composable way to focus
|
||||
// on a specific field within a larger structure. Lenses enable getting and setting
|
||||
// values in nested data structures in a functional, immutable way.
|
||||
//
|
||||
// In the context of testing, lenses are used with [LocalL] to focus assertions
|
||||
// on specific properties of complex objects without manually extracting those properties.
|
||||
//
|
||||
// A Lens[S, T] focuses on a value of type T within a structure of type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestLensUsage(t *testing.T) {
|
||||
// type Address struct { City string }
|
||||
// type User struct { Name string; Address Address }
|
||||
//
|
||||
// // Define lenses (typically generated)
|
||||
// addressLens := lens.Lens[User, Address]{...}
|
||||
// cityLens := lens.Lens[Address, string]{...}
|
||||
//
|
||||
// // Compose lenses to focus on nested field
|
||||
// userCityLens := lens.Compose(addressLens, cityLens)
|
||||
//
|
||||
// // Use with LocalL to assert on nested property
|
||||
// user := User{Name: "Alice", Address: Address{City: "NYC"}}
|
||||
// assert.LocalL(userCityLens)(assert.Equal("NYC"))(user)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [LocalL]: Uses a Lens to focus assertions on a property
|
||||
// - [lens.Lens]: The underlying lens type
|
||||
// - [Optional]: Similar but for values that may not exist
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Optional is an optic that focuses on a value that may or may not be present.
|
||||
//
|
||||
// This is an alias for [optional.Optional][S, T], which is similar to a [Lens] but
|
||||
// handles cases where the focused value might not exist. Optionals are useful for
|
||||
// working with nullable fields, optional properties, or values that might be absent.
|
||||
//
|
||||
// In testing, Optionals are used with [FromOptional] to create assertions that
|
||||
// verify whether an optional value is present and, if so, whether it satisfies
|
||||
// certain conditions.
|
||||
//
|
||||
// An Optional[S, T] focuses on an optional value of type T within a structure of type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestOptionalUsage(t *testing.T) {
|
||||
// type Config struct { Timeout *int }
|
||||
//
|
||||
// // Define optional (typically generated)
|
||||
// timeoutOptional := optional.Optional[Config, int]{...}
|
||||
//
|
||||
// // Test when value is present
|
||||
// config1 := Config{Timeout: ptr(30)}
|
||||
// assert.FromOptional(timeoutOptional)(
|
||||
// assert.Equal(30),
|
||||
// )(config1)(t) // Passes
|
||||
//
|
||||
// // Test when value is absent
|
||||
// config2 := Config{Timeout: nil}
|
||||
// // FromOptional would fail because value is not present
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromOptional]: Creates assertions for optional values
|
||||
// - [optional.Optional]: The underlying optional type
|
||||
// - [Lens]: Similar but for values that always exist
|
||||
Optional[S, T any] = optional.Optional[S, T]
|
||||
|
||||
// Prism is an optic that focuses on a case of a sum type.
|
||||
//
|
||||
// This is an alias for [prism.Prism][S, T], which provides a way to focus on one
|
||||
// variant of a sum type (like Result, Option, Either, etc.). Prisms enable pattern
|
||||
// matching and extraction of values from sum types in a functional way.
|
||||
//
|
||||
// In testing, Prisms are used with [FromPrism] to create assertions that verify
|
||||
// whether a value matches a specific case and, if so, whether the contained value
|
||||
// satisfies certain conditions.
|
||||
//
|
||||
// A Prism[S, T] focuses on a value of type T that may be contained within a sum type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPrismUsage(t *testing.T) {
|
||||
// // Prism for extracting success value from Result
|
||||
// successPrism := prism.Success[int]()
|
||||
//
|
||||
// // Test successful result
|
||||
// successResult := result.Of[int](42)
|
||||
// assert.FromPrism(successPrism)(
|
||||
// assert.Equal(42),
|
||||
// )(successResult)(t) // Passes
|
||||
//
|
||||
// // Prism for extracting error from Result
|
||||
// failurePrism := prism.Failure[int]()
|
||||
//
|
||||
// // Test failed result
|
||||
// failureResult := result.Error[int](errors.New("failed"))
|
||||
// assert.FromPrism(failurePrism)(
|
||||
// assert.Error,
|
||||
// )(failureResult)(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromPrism]: Creates assertions for prism-focused values
|
||||
// - [prism.Prism]: The underlying prism type
|
||||
// - [Optional]: Similar but for optional values
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
|
||||
// ReaderIOResult represents a context-aware, IO-based computation that may fail.
|
||||
//
|
||||
// This is an alias for [readerioresult.ReaderIOResult][A], which combines three
|
||||
// computational effects:
|
||||
// - Reader: Depends on a context (like context.Context)
|
||||
// - IO: Performs side effects (like file I/O, network calls)
|
||||
// - Result: May fail with an error
|
||||
//
|
||||
// In testing, ReaderIOResult is used with [FromReaderIOResult] to convert
|
||||
// context-aware, effectful computations into test assertions. This is useful
|
||||
// when your test assertions need to:
|
||||
// - Access a context for cancellation or deadlines
|
||||
// - Perform IO operations (database queries, API calls, file access)
|
||||
// - Handle potential errors gracefully
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderIOResult(t *testing.T) {
|
||||
// // Create a ReaderIOResult that performs IO and may fail
|
||||
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
|
||||
// return func() result.Result[assert.Reader] {
|
||||
// // Perform database check with context
|
||||
// if err := db.PingContext(ctx); err != nil {
|
||||
// return result.Error[assert.Reader](err)
|
||||
// }
|
||||
// return result.Of[assert.Reader](assert.NoError(nil))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIOResult(checkDatabase)
|
||||
// assertion(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromReaderIOResult]: Converts ReaderIOResult to Reader
|
||||
// - [ReaderIO]: Similar but without error handling
|
||||
// - [readerioresult.ReaderIOResult]: The underlying type
|
||||
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
|
||||
|
||||
// ReaderIO represents a context-aware, IO-based computation.
|
||||
//
|
||||
// This is an alias for [readerio.ReaderIO][A], which combines two computational effects:
|
||||
// - Reader: Depends on a context (like context.Context)
|
||||
// - IO: Performs side effects (like logging, metrics)
|
||||
//
|
||||
// In testing, ReaderIO is used with [FromReaderIO] to convert context-aware,
|
||||
// effectful computations into test assertions. This is useful when your test
|
||||
// assertions need to:
|
||||
// - Access a context for cancellation or deadlines
|
||||
// - Perform IO operations that don't fail (or handle failures internally)
|
||||
// - Integrate with context-aware utilities
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderIO(t *testing.T) {
|
||||
// // Create a ReaderIO that performs IO
|
||||
// logAndCheck := func(ctx context.Context) func() assert.Reader {
|
||||
// return func() assert.Reader {
|
||||
// // Log with context
|
||||
// logger.InfoContext(ctx, "Running test")
|
||||
// // Return assertion
|
||||
// return assert.Equal(42)(computeValue())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIO(logAndCheck)
|
||||
// assertion(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromReaderIO]: Converts ReaderIO to Reader
|
||||
// - [ReaderIOResult]: Similar but with error handling
|
||||
// - [readerio.ReaderIO]: The underlying type
|
||||
ReaderIO[A any] = readerio.ReaderIO[A]
|
||||
|
||||
// Seq2 represents a Go iterator that yields key-value pairs.
|
||||
//
|
||||
// This is an alias for [iter.Seq2][K, A], which is Go's standard iterator type
|
||||
// introduced in Go 1.23. It represents a sequence of key-value pairs that can be
|
||||
// iterated over using a for-range loop.
|
||||
//
|
||||
// In testing, Seq2 is used with [SequenceSeq2] to execute a sequence of named
|
||||
// test cases provided as an iterator. This enables:
|
||||
// - Lazy evaluation of test cases
|
||||
// - Memory-efficient testing of large test suites
|
||||
// - Integration with Go's iterator patterns
|
||||
// - Dynamic generation of test cases
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestSeq2Usage(t *testing.T) {
|
||||
// // Create an iterator of test cases
|
||||
// testCases := func(yield func(string, assert.Reader) bool) {
|
||||
// if !yield("test_addition", assert.Equal(4)(2+2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute all test cases
|
||||
// assert.SequenceSeq2[assert.Reader](testCases)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [SequenceSeq2]: Executes a Seq2 of test cases
|
||||
// - [TraverseArray]: Similar but for arrays
|
||||
// - [iter.Seq2]: The underlying iterator type
|
||||
Seq2[K, A any] = iter.Seq2[K, A]
|
||||
|
||||
// Pair represents a tuple of two values with potentially different types.
|
||||
//
|
||||
// This is an alias for [pair.Pair][L, R], which holds two values: a "head" (or "left")
|
||||
// of type L and a "tail" (or "right") of type R. Pairs are useful for grouping
|
||||
// related values together without defining a custom struct.
|
||||
//
|
||||
// In testing, Pairs are used with [TraverseArray] to associate test names with
|
||||
// their corresponding assertions. Each element in the array is transformed into
|
||||
// a Pair[string, Reader] where the string is the test name and the Reader is
|
||||
// the assertion to execute.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPairUsage(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Input int
|
||||
// Expected int
|
||||
// }
|
||||
//
|
||||
// testCases := []TestCase{
|
||||
// {Input: 2, Expected: 4},
|
||||
// {Input: 3, Expected: 9},
|
||||
// }
|
||||
//
|
||||
// // Transform each test case into a named assertion
|
||||
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
|
||||
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
|
||||
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
// return pair.MakePair(name, assertion)
|
||||
// })
|
||||
//
|
||||
// traverse(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [TraverseArray]: Uses Pairs to create named test cases
|
||||
// - [pair.Pair]: The underlying pair type
|
||||
// - [pair.MakePair]: Creates a Pair
|
||||
// - [pair.Head]: Extracts the first value
|
||||
// - [pair.Tail]: Extracts the second value
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
|
||||
// Void represents the absence of a meaningful value, similar to unit type in functional programming.
|
||||
//
|
||||
// This is an alias for [function.Void], which is used to represent operations that don't
|
||||
// return a meaningful value but may perform side effects. In the context of testing, Void
|
||||
// is used with IO operations that perform actions without producing a result.
|
||||
//
|
||||
// Void is conceptually similar to:
|
||||
// - Unit type in functional languages (Haskell's (), Scala's Unit)
|
||||
// - void in languages like C/Java (but as a value, not just a type)
|
||||
// - Empty struct{} in Go (but with clearer semantic meaning)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithSideEffect(t *testing.T) {
|
||||
// // An IO operation that logs but returns Void
|
||||
// logOperation := func() function.Void {
|
||||
// log.Println("Test executed")
|
||||
// return function.Void{}
|
||||
// }
|
||||
//
|
||||
// // Execute the operation
|
||||
// logOperation()
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [IO]: Wraps side-effecting operations
|
||||
// - [function.Void]: The underlying void type
|
||||
Void = function.Void
|
||||
|
||||
// IO represents a side-effecting computation that produces a value of type A.
|
||||
//
|
||||
// This is an alias for [io.IO][A], which encapsulates operations that perform side effects
|
||||
// (like I/O operations, logging, or state mutations) and return a value. IO is a lazy
|
||||
// computation - it describes an effect but doesn't execute it until explicitly run.
|
||||
//
|
||||
// In testing, IO is used to:
|
||||
// - Defer execution of side effects until needed
|
||||
// - Compose multiple side-effecting operations
|
||||
// - Maintain referential transparency in test setup
|
||||
// - Separate effect description from effect execution
|
||||
//
|
||||
// An IO[A] is essentially a function `func() A` that:
|
||||
// - Encapsulates a side effect
|
||||
// - Returns a value of type A when executed
|
||||
// - Can be composed with other IO operations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestIOOperation(t *testing.T) {
|
||||
// // Define an IO operation that reads a file
|
||||
// readConfig := func() io.IO[string] {
|
||||
// return func() string {
|
||||
// data, _ := os.ReadFile("config.txt")
|
||||
// return string(data)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // The IO is not executed yet - it's just a description
|
||||
// configIO := readConfig()
|
||||
//
|
||||
// // Execute the IO to get the result
|
||||
// config := configIO()
|
||||
// assert.StringNotEmpty(config)(t)
|
||||
// }
|
||||
//
|
||||
// Example with composition:
|
||||
//
|
||||
// func TestIOComposition(t *testing.T) {
|
||||
// // Chain multiple IO operations
|
||||
// pipeline := io.Map(
|
||||
// func(s string) int { return len(s) },
|
||||
// )(readFileIO)
|
||||
//
|
||||
// // Execute the composed operation
|
||||
// length := pipeline()
|
||||
// assert.That(func(n int) bool { return n > 0 })(length)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [ReaderIO]: Combines Reader and IO effects
|
||||
// - [ReaderIOResult]: Adds error handling to ReaderIO
|
||||
// - [io.IO]: The underlying IO type
|
||||
// - [Void]: Represents operations without meaningful return values
|
||||
IO[A any] = io.IO[A]
|
||||
)
|
||||
|
||||
273
v2/cli/README.md
Normal file
273
v2/cli/README.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# CLI Package - Functional Wrappers for urfave/cli/v3
|
||||
|
||||
This package provides functional programming wrappers for the `github.com/urfave/cli/v3` library, enabling Effect-based command actions and type-safe flag handling through Prisms.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Effect-Based Command Actions
|
||||
|
||||
Transform CLI command actions into composable Effects that follow functional programming principles.
|
||||
|
||||
#### Key Functions
|
||||
|
||||
- **`ToAction(effect CommandEffect) func(context.Context, *C.Command) error`**
|
||||
- Converts a CommandEffect into a standard urfave/cli Action function
|
||||
- Enables Effect-based command handlers to work with cli/v3 framework
|
||||
|
||||
- **`FromAction(action func(context.Context, *C.Command) error) CommandEffect`**
|
||||
- Lifts existing cli/v3 action handlers into the Effect type
|
||||
- Allows gradual migration to functional style
|
||||
|
||||
- **`MakeCommand(name, usage string, flags []C.Flag, effect CommandEffect) *C.Command`**
|
||||
- Creates a new Command with an Effect-based action
|
||||
- Convenience function combining command creation with Effect conversion
|
||||
|
||||
- **`MakeCommandWithSubcommands(...) *C.Command`**
|
||||
- Creates a Command with subcommands and an Effect-based action
|
||||
|
||||
#### Example Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/effect"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/cli"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// Define an Effect-based command action
|
||||
processEffect := func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
return func() R.Result[F.Void] {
|
||||
input := cmd.String("input")
|
||||
// Process input...
|
||||
return R.Of(F.Void{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create command with Effect
|
||||
command := cli.MakeCommand(
|
||||
"process",
|
||||
"Process input files",
|
||||
[]C.Flag{
|
||||
&C.StringFlag{Name: "input", Usage: "Input file path"},
|
||||
},
|
||||
processEffect,
|
||||
)
|
||||
|
||||
// Or convert existing action to Effect
|
||||
existingAction := func(ctx context.Context, cmd *C.Command) error {
|
||||
// Existing logic...
|
||||
return nil
|
||||
}
|
||||
effect := cli.FromAction(existingAction)
|
||||
```
|
||||
|
||||
### 2. Flag Type Prisms
|
||||
|
||||
Type-safe extraction and manipulation of CLI flags using Prisms from the optics package.
|
||||
|
||||
#### Available Prisms
|
||||
|
||||
- `StringFlagPrism()` - Extract `*C.StringFlag` from `C.Flag`
|
||||
- `IntFlagPrism()` - Extract `*C.IntFlag` from `C.Flag`
|
||||
- `BoolFlagPrism()` - Extract `*C.BoolFlag` from `C.Flag`
|
||||
- `Float64FlagPrism()` - Extract `*C.Float64Flag` from `C.Flag`
|
||||
- `DurationFlagPrism()` - Extract `*C.DurationFlag` from `C.Flag`
|
||||
- `TimestampFlagPrism()` - Extract `*C.TimestampFlag` from `C.Flag`
|
||||
- `StringSliceFlagPrism()` - Extract `*C.StringSliceFlag` from `C.Flag`
|
||||
- `IntSliceFlagPrism()` - Extract `*C.IntSliceFlag` from `C.Flag`
|
||||
- `Float64SliceFlagPrism()` - Extract `*C.Float64SliceFlag` from `C.Flag`
|
||||
- `UintFlagPrism()` - Extract `*C.UintFlag` from `C.Flag`
|
||||
- `Uint64FlagPrism()` - Extract `*C.Uint64Flag` from `C.Flag`
|
||||
- `Int64FlagPrism()` - Extract `*C.Int64Flag` from `C.Flag`
|
||||
|
||||
#### Example Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/cli"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// Extract a StringFlag from a Flag interface
|
||||
var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
|
||||
prism := cli.StringFlagPrism()
|
||||
|
||||
// Safe extraction returns Option
|
||||
result := prism.GetOption(flag)
|
||||
if O.IsSome(result) {
|
||||
strFlag := O.MonadFold(result,
|
||||
func() *C.StringFlag { return nil },
|
||||
func(f *C.StringFlag) *C.StringFlag { return f },
|
||||
)
|
||||
// Use strFlag...
|
||||
}
|
||||
|
||||
// Type mismatch returns None
|
||||
var intFlag C.Flag = &C.IntFlag{Name: "count"}
|
||||
result = prism.GetOption(intFlag) // Returns None
|
||||
|
||||
// Convert back to Flag
|
||||
strFlag := &C.StringFlag{Name: "output"}
|
||||
flag = prism.ReverseGet(strFlag)
|
||||
```
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### CommandEffect
|
||||
|
||||
```go
|
||||
type CommandEffect = E.Effect[*C.Command, F.Void]
|
||||
```
|
||||
|
||||
A CommandEffect represents a CLI command action as an Effect. It takes a `*C.Command` as context and produces a result wrapped in the Effect monad.
|
||||
|
||||
The Effect structure is:
|
||||
```
|
||||
func(*C.Command) -> func(context.Context) -> func() -> Result[Void]
|
||||
```
|
||||
|
||||
This allows for:
|
||||
- **Composability**: Effects can be composed using standard functional combinators
|
||||
- **Testability**: Pure functions are easier to test
|
||||
- **Error Handling**: Errors are explicitly represented in the Result type
|
||||
- **Context Management**: Context flows naturally through the Effect
|
||||
|
||||
## Benefits
|
||||
|
||||
### 1. Functional Composition
|
||||
|
||||
Effects can be composed using standard functional programming patterns:
|
||||
|
||||
```go
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RRIOE "github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
)
|
||||
|
||||
// Compose multiple effects
|
||||
validateInput := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
|
||||
processData := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
|
||||
saveResults := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
|
||||
|
||||
// Chain effects together
|
||||
pipeline := F.Pipe3(
|
||||
validateInput,
|
||||
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return processData }),
|
||||
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return saveResults }),
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Type Safety
|
||||
|
||||
Prisms provide compile-time type safety when working with flags:
|
||||
|
||||
```go
|
||||
// Type-safe flag extraction
|
||||
flags := []C.Flag{
|
||||
&C.StringFlag{Name: "input"},
|
||||
&C.IntFlag{Name: "count"},
|
||||
}
|
||||
|
||||
for _, flag := range flags {
|
||||
// Safe extraction with pattern matching
|
||||
O.MonadFold(
|
||||
cli.StringFlagPrism().GetOption(flag),
|
||||
func() { /* Not a string flag */ },
|
||||
func(sf *C.StringFlag) { /* Handle string flag */ },
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
Errors are explicitly represented in the Result type:
|
||||
|
||||
```go
|
||||
effect := func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
return func() R.Result[F.Void] {
|
||||
if err := validateInput(cmd); err != nil {
|
||||
return R.Left[F.Void](err) // Explicit error
|
||||
}
|
||||
return R.Of(F.Void{}) // Success
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Testability
|
||||
|
||||
Pure functions are easier to test:
|
||||
|
||||
```go
|
||||
func TestCommandEffect(t *testing.T) {
|
||||
cmd := &C.Command{Name: "test"}
|
||||
effect := myCommandEffect(cmd)
|
||||
|
||||
// Execute effect
|
||||
result := effect(context.Background())()
|
||||
|
||||
// Assert on result
|
||||
assert.True(t, R.IsRight(result))
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Standard Actions to Effects
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
command := &C.Command{
|
||||
Name: "process",
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
input := cmd.String("input")
|
||||
// Process...
|
||||
return nil
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
effect := func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
return func() R.Result[F.Void] {
|
||||
input := cmd.String("input")
|
||||
// Process...
|
||||
return R.Of(F.Void{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
command := cli.MakeCommand("process", "Process files", flags, effect)
|
||||
```
|
||||
|
||||
### Gradual Migration
|
||||
|
||||
You can mix both styles during migration:
|
||||
|
||||
```go
|
||||
// Wrap existing action
|
||||
existingAction := func(ctx context.Context, cmd *C.Command) error {
|
||||
// Legacy code...
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use as Effect
|
||||
effect := cli.FromAction(existingAction)
|
||||
command := cli.MakeCommand("legacy", "Legacy command", flags, effect)
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Effect Package](../effect/) - Core Effect type definitions
|
||||
- [Optics Package](../optics/) - Prism and other optics
|
||||
- [urfave/cli/v3](https://github.com/urfave/cli) - Underlying CLI framework
|
||||
@@ -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)
|
||||
|
||||
|
||||
199
v2/cli/effect.go
Normal file
199
v2/cli/effect.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/effect"
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CommandEffect represents a CLI command action as an Effect.
|
||||
// The Effect takes a *C.Command as context and produces a result.
|
||||
type CommandEffect = E.Effect[*C.Command, F.Void]
|
||||
|
||||
// ToAction converts a CommandEffect into a standard urfave/cli Action function.
|
||||
// This allows Effect-based command handlers to be used with the cli/v3 framework.
|
||||
//
|
||||
// The conversion process:
|
||||
// 1. Takes the Effect which expects a *C.Command context
|
||||
// 2. Executes it with the provided command
|
||||
// 3. Runs the resulting IO operation
|
||||
// 4. Converts the Result to either nil (success) or error (failure)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - effect: The CommandEffect to convert
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A function compatible with C.Command.Action signature
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// effect := func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
// return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
// return func() R.Result[F.Void] {
|
||||
// // Command logic here
|
||||
// return R.Of(F.Void{})
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// action := ToAction(effect)
|
||||
// command := &C.Command{
|
||||
// Name: "example",
|
||||
// Action: action,
|
||||
// }
|
||||
func ToAction(effect CommandEffect) func(context.Context, *C.Command) error {
|
||||
return func(ctx context.Context, cmd *C.Command) error {
|
||||
// Execute the effect: cmd -> ctx -> IO -> Result
|
||||
return F.Pipe3(
|
||||
ctx,
|
||||
effect(cmd),
|
||||
io.Run,
|
||||
// Convert Result[Void] to error
|
||||
ET.Fold(F.Identity[error], F.Constant1[F.Void, error](nil)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FromAction converts a standard urfave/cli Action function into a CommandEffect.
|
||||
// This allows existing cli/v3 action handlers to be lifted into the Effect type.
|
||||
//
|
||||
// The conversion process:
|
||||
// 1. Takes a standard action function (context.Context, *C.Command) -> error
|
||||
// 2. Wraps it in the Effect structure
|
||||
// 3. Converts the error result to a Result type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - action: The standard cli/v3 action function to convert
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A CommandEffect that wraps the original action
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// standardAction := func(ctx context.Context, cmd *C.Command) error {
|
||||
// // Existing command logic
|
||||
// return nil
|
||||
// }
|
||||
// effect := FromAction(standardAction)
|
||||
// // Now can be composed with other Effects
|
||||
func FromAction(action func(context.Context, *C.Command) error) CommandEffect {
|
||||
return func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
return func() R.Result[F.Void] {
|
||||
err := action(ctx, cmd)
|
||||
if err != nil {
|
||||
return R.Left[F.Void](err)
|
||||
}
|
||||
return R.Of(F.Void{})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MakeCommand creates a new Command with an Effect-based action.
|
||||
// This is a convenience function that combines command creation with Effect conversion.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - name: The command name
|
||||
// - usage: The command usage description
|
||||
// - flags: The command flags
|
||||
// - effect: The CommandEffect to use as the action
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A *C.Command configured with the Effect-based action
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// cmd := MakeCommand(
|
||||
// "process",
|
||||
// "Process data files",
|
||||
// []C.Flag{
|
||||
// &C.StringFlag{Name: "input", Usage: "Input file"},
|
||||
// },
|
||||
// func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
// return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
// return func() R.Result[F.Void] {
|
||||
// input := cmd.String("input")
|
||||
// // Process input...
|
||||
// return R.Of(F.Void{})
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
func MakeCommand(
|
||||
name string,
|
||||
usage string,
|
||||
flags []C.Flag,
|
||||
effect CommandEffect,
|
||||
) *C.Command {
|
||||
return &C.Command{
|
||||
Name: name,
|
||||
Usage: usage,
|
||||
Flags: flags,
|
||||
Action: ToAction(effect),
|
||||
}
|
||||
}
|
||||
|
||||
// MakeCommandWithSubcommands creates a new Command with subcommands and an Effect-based action.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - name: The command name
|
||||
// - usage: The command usage description
|
||||
// - flags: The command flags
|
||||
// - commands: The subcommands
|
||||
// - effect: The CommandEffect to use as the action
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A *C.Command configured with subcommands and the Effect-based action
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// cmd := MakeCommandWithSubcommands(
|
||||
// "app",
|
||||
// "Application commands",
|
||||
// []C.Flag{},
|
||||
// []*C.Command{subCmd1, subCmd2},
|
||||
// defaultEffect,
|
||||
// )
|
||||
func MakeCommandWithSubcommands(
|
||||
name string,
|
||||
usage string,
|
||||
flags []C.Flag,
|
||||
commands []*C.Command,
|
||||
effect CommandEffect,
|
||||
) *C.Command {
|
||||
return &C.Command{
|
||||
Name: name,
|
||||
Usage: usage,
|
||||
Flags: flags,
|
||||
Commands: commands,
|
||||
Action: ToAction(effect),
|
||||
}
|
||||
}
|
||||
204
v2/cli/effect_test.go
Normal file
204
v2/cli/effect_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/effect"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestToAction_Success(t *testing.T) {
|
||||
t.Run("converts successful Effect to action", func(t *testing.T) {
|
||||
// Arrange
|
||||
effect := func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
return func() R.Result[F.Void] {
|
||||
return R.Of(F.Void{})
|
||||
}
|
||||
}
|
||||
}
|
||||
action := ToAction(effect)
|
||||
cmd := &C.Command{Name: "test"}
|
||||
|
||||
// Act
|
||||
err := action(context.Background(), cmd)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestToAction_Failure(t *testing.T) {
|
||||
t.Run("converts failed Effect to error", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := errors.New("test error")
|
||||
effect := func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
return func() R.Result[F.Void] {
|
||||
return R.Left[F.Void](expectedErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
action := ToAction(effect)
|
||||
cmd := &C.Command{Name: "test"}
|
||||
|
||||
// Act
|
||||
err := action(context.Background(), cmd)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromAction_Success(t *testing.T) {
|
||||
t.Run("converts successful action to Effect", func(t *testing.T) {
|
||||
// Arrange
|
||||
action := func(ctx context.Context, cmd *C.Command) error {
|
||||
return nil
|
||||
}
|
||||
effect := FromAction(action)
|
||||
cmd := &C.Command{Name: "test"}
|
||||
|
||||
// Act
|
||||
result := effect(cmd)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, R.IsRight(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromAction_Failure(t *testing.T) {
|
||||
t.Run("converts failed action to Effect", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := errors.New("test error")
|
||||
action := func(ctx context.Context, cmd *C.Command) error {
|
||||
return expectedErr
|
||||
}
|
||||
effect := FromAction(action)
|
||||
cmd := &C.Command{Name: "test"}
|
||||
|
||||
// Act
|
||||
result := effect(cmd)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, R.IsLeft(result))
|
||||
err := R.MonadFold(result, F.Identity[error], func(F.Void) error { return nil })
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMakeCommand(t *testing.T) {
|
||||
t.Run("creates command with Effect-based action", func(t *testing.T) {
|
||||
// Arrange
|
||||
effect := func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
return func() R.Result[F.Void] {
|
||||
return R.Of(F.Void{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
cmd := MakeCommand(
|
||||
"test",
|
||||
"Test command",
|
||||
[]C.Flag{},
|
||||
effect,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, cmd)
|
||||
assert.Equal(t, "test", cmd.Name)
|
||||
assert.Equal(t, "Test command", cmd.Usage)
|
||||
assert.NotNil(t, cmd.Action)
|
||||
|
||||
// Test the action
|
||||
err := cmd.Action(context.Background(), cmd)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMakeCommandWithSubcommands(t *testing.T) {
|
||||
t.Run("creates command with subcommands and Effect-based action", func(t *testing.T) {
|
||||
// Arrange
|
||||
subCmd := &C.Command{Name: "sub"}
|
||||
effect := func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
return func() R.Result[F.Void] {
|
||||
return R.Of(F.Void{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Act
|
||||
cmd := MakeCommandWithSubcommands(
|
||||
"parent",
|
||||
"Parent command",
|
||||
[]C.Flag{},
|
||||
[]*C.Command{subCmd},
|
||||
effect,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, cmd)
|
||||
assert.Equal(t, "parent", cmd.Name)
|
||||
assert.Equal(t, "Parent command", cmd.Usage)
|
||||
assert.Len(t, cmd.Commands, 1)
|
||||
assert.Equal(t, "sub", cmd.Commands[0].Name)
|
||||
assert.NotNil(t, cmd.Action)
|
||||
})
|
||||
}
|
||||
|
||||
func TestToAction_Integration(t *testing.T) {
|
||||
t.Run("Effect can access command flags", func(t *testing.T) {
|
||||
// Arrange
|
||||
var capturedValue string
|
||||
effect := func(cmd *C.Command) E.Thunk[F.Void] {
|
||||
return func(ctx context.Context) E.IOResult[F.Void] {
|
||||
return func() R.Result[F.Void] {
|
||||
capturedValue = cmd.String("input")
|
||||
return R.Of(F.Void{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd := &C.Command{
|
||||
Name: "test",
|
||||
Flags: []C.Flag{
|
||||
&C.StringFlag{
|
||||
Name: "input",
|
||||
Value: "default-value",
|
||||
},
|
||||
},
|
||||
Action: ToAction(effect),
|
||||
}
|
||||
|
||||
// Act
|
||||
err := cmd.Action(context.Background(), cmd)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "default-value", capturedValue)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
359
v2/cli/flags.go
Normal file
359
v2/cli/flags.go
Normal file
@@ -0,0 +1,359 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
P "github.com/IBM/fp-go/v2/optics/prism"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// StringFlagPrism creates a Prism for extracting a StringFlag from a Flag.
|
||||
// This provides a type-safe way to work with string flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to cast a Flag to *C.StringFlag.
|
||||
// If the cast succeeds, it returns Some(*C.StringFlag); if it fails, it returns None.
|
||||
//
|
||||
// The prism's ReverseGet converts a *C.StringFlag back to a Flag.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.StringFlag] for safe StringFlag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := StringFlagPrism()
|
||||
//
|
||||
// // Extract StringFlag from Flag
|
||||
// var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
|
||||
// result := prism.GetOption(flag) // Some(*C.StringFlag{...})
|
||||
//
|
||||
// // Type mismatch returns None
|
||||
// var intFlag C.Flag = &C.IntFlag{Name: "count"}
|
||||
// result = prism.GetOption(intFlag) // None[*C.StringFlag]()
|
||||
//
|
||||
// // Convert back to Flag
|
||||
// strFlag := &C.StringFlag{Name: "output"}
|
||||
// flag = prism.ReverseGet(strFlag)
|
||||
func StringFlagPrism() P.Prism[C.Flag, *C.StringFlag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.StringFlag] {
|
||||
if sf, ok := flag.(*C.StringFlag); ok {
|
||||
return O.Some(sf)
|
||||
}
|
||||
return O.None[*C.StringFlag]()
|
||||
},
|
||||
func(f *C.StringFlag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// IntFlagPrism creates a Prism for extracting an IntFlag from a Flag.
|
||||
// This provides a type-safe way to work with integer flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.IntFlag] for safe IntFlag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := IntFlagPrism()
|
||||
//
|
||||
// // Extract IntFlag from Flag
|
||||
// var flag C.Flag = &C.IntFlag{Name: "count", Value: 10}
|
||||
// result := prism.GetOption(flag) // Some(*C.IntFlag{...})
|
||||
func IntFlagPrism() P.Prism[C.Flag, *C.IntFlag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.IntFlag] {
|
||||
if f, ok := flag.(*C.IntFlag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.IntFlag]()
|
||||
},
|
||||
func(f *C.IntFlag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// BoolFlagPrism creates a Prism for extracting a BoolFlag from a Flag.
|
||||
// This provides a type-safe way to work with boolean flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.BoolFlag] for safe BoolFlag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := BoolFlagPrism()
|
||||
//
|
||||
// // Extract BoolFlag from Flag
|
||||
// var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
|
||||
// result := prism.GetOption(flag) // Some(*C.BoolFlag{...})
|
||||
func BoolFlagPrism() P.Prism[C.Flag, *C.BoolFlag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.BoolFlag] {
|
||||
if f, ok := flag.(*C.BoolFlag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.BoolFlag]()
|
||||
},
|
||||
func(f *C.BoolFlag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// Float64FlagPrism creates a Prism for extracting a Float64Flag from a Flag.
|
||||
// This provides a type-safe way to work with float64 flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.Float64Flag] for safe Float64Flag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := Float64FlagPrism()
|
||||
//
|
||||
// // Extract Float64Flag from Flag
|
||||
// var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
|
||||
// result := prism.GetOption(flag) // Some(*C.Float64Flag{...})
|
||||
func Float64FlagPrism() P.Prism[C.Flag, *C.Float64Flag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.Float64Flag] {
|
||||
if f, ok := flag.(*C.Float64Flag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.Float64Flag]()
|
||||
},
|
||||
func(f *C.Float64Flag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// DurationFlagPrism creates a Prism for extracting a DurationFlag from a Flag.
|
||||
// This provides a type-safe way to work with duration flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.DurationFlag] for safe DurationFlag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := DurationFlagPrism()
|
||||
//
|
||||
// // Extract DurationFlag from Flag
|
||||
// var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: 30 * time.Second}
|
||||
// result := prism.GetOption(flag) // Some(*C.DurationFlag{...})
|
||||
func DurationFlagPrism() P.Prism[C.Flag, *C.DurationFlag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.DurationFlag] {
|
||||
if f, ok := flag.(*C.DurationFlag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.DurationFlag]()
|
||||
},
|
||||
func(f *C.DurationFlag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// TimestampFlagPrism creates a Prism for extracting a TimestampFlag from a Flag.
|
||||
// This provides a type-safe way to work with timestamp flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.TimestampFlag] for safe TimestampFlag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := TimestampFlagPrism()
|
||||
//
|
||||
// // Extract TimestampFlag from Flag
|
||||
// var flag C.Flag = &C.TimestampFlag{Name: "created"}
|
||||
// result := prism.GetOption(flag) // Some(*C.TimestampFlag{...})
|
||||
func TimestampFlagPrism() P.Prism[C.Flag, *C.TimestampFlag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.TimestampFlag] {
|
||||
if f, ok := flag.(*C.TimestampFlag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.TimestampFlag]()
|
||||
},
|
||||
func(f *C.TimestampFlag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// StringSliceFlagPrism creates a Prism for extracting a StringSliceFlag from a Flag.
|
||||
// This provides a type-safe way to work with string slice flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.StringSliceFlag] for safe StringSliceFlag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := StringSliceFlagPrism()
|
||||
//
|
||||
// // Extract StringSliceFlag from Flag
|
||||
// var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
|
||||
// result := prism.GetOption(flag) // Some(*C.StringSliceFlag{...})
|
||||
func StringSliceFlagPrism() P.Prism[C.Flag, *C.StringSliceFlag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.StringSliceFlag] {
|
||||
if f, ok := flag.(*C.StringSliceFlag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.StringSliceFlag]()
|
||||
},
|
||||
func(f *C.StringSliceFlag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// IntSliceFlagPrism creates a Prism for extracting an IntSliceFlag from a Flag.
|
||||
// This provides a type-safe way to work with int slice flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.IntSliceFlag] for safe IntSliceFlag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := IntSliceFlagPrism()
|
||||
//
|
||||
// // Extract IntSliceFlag from Flag
|
||||
// var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
|
||||
// result := prism.GetOption(flag) // Some(*C.IntSliceFlag{...})
|
||||
func IntSliceFlagPrism() P.Prism[C.Flag, *C.IntSliceFlag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.IntSliceFlag] {
|
||||
if f, ok := flag.(*C.IntSliceFlag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.IntSliceFlag]()
|
||||
},
|
||||
func(f *C.IntSliceFlag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// Float64SliceFlagPrism creates a Prism for extracting a Float64SliceFlag from a Flag.
|
||||
// This provides a type-safe way to work with float64 slice flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.Float64SliceFlag] for safe Float64SliceFlag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := Float64SliceFlagPrism()
|
||||
//
|
||||
// // Extract Float64SliceFlag from Flag
|
||||
// var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
|
||||
// result := prism.GetOption(flag) // Some(*C.Float64SliceFlag{...})
|
||||
func Float64SliceFlagPrism() P.Prism[C.Flag, *C.Float64SliceFlag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.Float64SliceFlag] {
|
||||
if f, ok := flag.(*C.Float64SliceFlag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.Float64SliceFlag]()
|
||||
},
|
||||
func(f *C.Float64SliceFlag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// UintFlagPrism creates a Prism for extracting a UintFlag from a Flag.
|
||||
// This provides a type-safe way to work with unsigned integer flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.UintFlag] for safe UintFlag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := UintFlagPrism()
|
||||
//
|
||||
// // Extract UintFlag from Flag
|
||||
// var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
|
||||
// result := prism.GetOption(flag) // Some(*C.UintFlag{...})
|
||||
func UintFlagPrism() P.Prism[C.Flag, *C.UintFlag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.UintFlag] {
|
||||
if f, ok := flag.(*C.UintFlag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.UintFlag]()
|
||||
},
|
||||
func(f *C.UintFlag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// Uint64FlagPrism creates a Prism for extracting a Uint64Flag from a Flag.
|
||||
// This provides a type-safe way to work with uint64 flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.Uint64Flag] for safe Uint64Flag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := Uint64FlagPrism()
|
||||
//
|
||||
// // Extract Uint64Flag from Flag
|
||||
// var flag C.Flag = &C.Uint64Flag{Name: "size"}
|
||||
// result := prism.GetOption(flag) // Some(*C.Uint64Flag{...})
|
||||
func Uint64FlagPrism() P.Prism[C.Flag, *C.Uint64Flag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.Uint64Flag] {
|
||||
if f, ok := flag.(*C.Uint64Flag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.Uint64Flag]()
|
||||
},
|
||||
func(f *C.Uint64Flag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
|
||||
// Int64FlagPrism creates a Prism for extracting an Int64Flag from a Flag.
|
||||
// This provides a type-safe way to work with int64 flags, handling type
|
||||
// mismatches gracefully through the Option type.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Prism[C.Flag, *C.Int64Flag] for safe Int64Flag extraction
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// prism := Int64FlagPrism()
|
||||
//
|
||||
// // Extract Int64Flag from Flag
|
||||
// var flag C.Flag = &C.Int64Flag{Name: "offset"}
|
||||
// result := prism.GetOption(flag) // Some(*C.Int64Flag{...})
|
||||
func Int64FlagPrism() P.Prism[C.Flag, *C.Int64Flag] {
|
||||
return P.MakePrism(
|
||||
func(flag C.Flag) O.Option[*C.Int64Flag] {
|
||||
if f, ok := flag.(*C.Int64Flag); ok {
|
||||
return O.Some(f)
|
||||
}
|
||||
return O.None[*C.Int64Flag]()
|
||||
},
|
||||
func(f *C.Int64Flag) C.Flag { return f },
|
||||
)
|
||||
}
|
||||
287
v2/cli/flags_test.go
Normal file
287
v2/cli/flags_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestStringFlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts StringFlag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := StringFlagPrism()
|
||||
var flag C.Flag = &C.StringFlag{Name: "input", Value: "test"}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.StringFlag { return nil }, func(f *C.StringFlag) *C.StringFlag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "input", extracted.Name)
|
||||
assert.Equal(t, "test", extracted.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringFlagPrism_Failure(t *testing.T) {
|
||||
t.Run("returns None for non-StringFlag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := StringFlagPrism()
|
||||
var flag C.Flag = &C.IntFlag{Name: "count"}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringFlagPrism_ReverseGet(t *testing.T) {
|
||||
t.Run("converts StringFlag back to Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := StringFlagPrism()
|
||||
strFlag := &C.StringFlag{Name: "output", Value: "result"}
|
||||
|
||||
// Act
|
||||
flag := prism.ReverseGet(strFlag)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, flag)
|
||||
assert.IsType(t, &C.StringFlag{}, flag)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntFlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts IntFlag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := IntFlagPrism()
|
||||
var flag C.Flag = &C.IntFlag{Name: "count", Value: 42}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.IntFlag { return nil }, func(f *C.IntFlag) *C.IntFlag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "count", extracted.Name)
|
||||
assert.Equal(t, 42, extracted.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBoolFlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts BoolFlag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := BoolFlagPrism()
|
||||
var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.BoolFlag { return nil }, func(f *C.BoolFlag) *C.BoolFlag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "verbose", extracted.Name)
|
||||
assert.Equal(t, true, extracted.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFloat64FlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts Float64Flag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := Float64FlagPrism()
|
||||
var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.Float64Flag { return nil }, func(f *C.Float64Flag) *C.Float64Flag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "ratio", extracted.Name)
|
||||
assert.Equal(t, 0.5, extracted.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDurationFlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts DurationFlag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := DurationFlagPrism()
|
||||
duration := 30 * time.Second
|
||||
var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: duration}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.DurationFlag { return nil }, func(f *C.DurationFlag) *C.DurationFlag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "timeout", extracted.Name)
|
||||
assert.Equal(t, duration, extracted.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTimestampFlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts TimestampFlag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := TimestampFlagPrism()
|
||||
var flag C.Flag = &C.TimestampFlag{Name: "created"}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.TimestampFlag { return nil }, func(f *C.TimestampFlag) *C.TimestampFlag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "created", extracted.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringSliceFlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts StringSliceFlag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := StringSliceFlagPrism()
|
||||
var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.StringSliceFlag { return nil }, func(f *C.StringSliceFlag) *C.StringSliceFlag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "tags", extracted.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntSliceFlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts IntSliceFlag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := IntSliceFlagPrism()
|
||||
var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.IntSliceFlag { return nil }, func(f *C.IntSliceFlag) *C.IntSliceFlag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "ports", extracted.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFloat64SliceFlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts Float64SliceFlag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := Float64SliceFlagPrism()
|
||||
var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.Float64SliceFlag { return nil }, func(f *C.Float64SliceFlag) *C.Float64SliceFlag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "ratios", extracted.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUintFlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts UintFlag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := UintFlagPrism()
|
||||
var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.UintFlag { return nil }, func(f *C.UintFlag) *C.UintFlag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "workers", extracted.Name)
|
||||
assert.Equal(t, uint(4), extracted.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUint64FlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts Uint64Flag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := Uint64FlagPrism()
|
||||
var flag C.Flag = &C.Uint64Flag{Name: "size", Value: 1024}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.Uint64Flag { return nil }, func(f *C.Uint64Flag) *C.Uint64Flag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "size", extracted.Name)
|
||||
assert.Equal(t, uint64(1024), extracted.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInt64FlagPrism_Success(t *testing.T) {
|
||||
t.Run("extracts Int64Flag from Flag", func(t *testing.T) {
|
||||
// Arrange
|
||||
prism := Int64FlagPrism()
|
||||
var flag C.Flag = &C.Int64Flag{Name: "offset", Value: -100}
|
||||
|
||||
// Act
|
||||
result := prism.GetOption(flag)
|
||||
|
||||
// Assert
|
||||
assert.True(t, O.IsSome(result))
|
||||
extracted := O.MonadFold(result, func() *C.Int64Flag { return nil }, func(f *C.Int64Flag) *C.Int64Flag { return f })
|
||||
assert.NotNil(t, extracted)
|
||||
assert.Equal(t, "offset", extracted.Name)
|
||||
assert.Equal(t, int64(-100), extracted.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrisms_EdgeCases(t *testing.T) {
|
||||
t.Run("all prisms return None for wrong type", func(t *testing.T) {
|
||||
// Arrange
|
||||
var flag C.Flag = &C.StringFlag{Name: "test"}
|
||||
|
||||
// Act & Assert
|
||||
assert.True(t, O.IsNone(IntFlagPrism().GetOption(flag)))
|
||||
assert.True(t, O.IsNone(BoolFlagPrism().GetOption(flag)))
|
||||
assert.True(t, O.IsNone(Float64FlagPrism().GetOption(flag)))
|
||||
assert.True(t, O.IsNone(DurationFlagPrism().GetOption(flag)))
|
||||
assert.True(t, O.IsNone(TimestampFlagPrism().GetOption(flag)))
|
||||
assert.True(t, O.IsNone(StringSliceFlagPrism().GetOption(flag)))
|
||||
assert.True(t, O.IsNone(IntSliceFlagPrism().GetOption(flag)))
|
||||
assert.True(t, O.IsNone(Float64SliceFlagPrism().GetOption(flag)))
|
||||
assert.True(t, O.IsNone(UintFlagPrism().GetOption(flag)))
|
||||
assert.True(t, O.IsNone(Uint64FlagPrism().GetOption(flag)))
|
||||
assert.True(t, O.IsNone(Int64FlagPrism().GetOption(flag)))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -13,6 +13,37 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package constant provides the Const functor, a phantom type that ignores its second type parameter.
|
||||
//
|
||||
// The Const functor is a fundamental building block in functional programming that wraps a value
|
||||
// of type E while having a phantom type parameter A. This makes it useful for:
|
||||
// - Accumulating values during traversals (e.g., collecting metadata)
|
||||
// - Implementing optics (lenses, prisms) where you need to track information
|
||||
// - Building applicative functors that combine values using a semigroup
|
||||
//
|
||||
// # The Const Functor
|
||||
//
|
||||
// Const[E, A] wraps a value of type E and has a phantom type parameter A that doesn't affect
|
||||
// the runtime value. This allows it to participate in functor and applicative operations while
|
||||
// maintaining the wrapped value unchanged.
|
||||
//
|
||||
// # Key Properties
|
||||
//
|
||||
// - Map operations ignore the function and preserve the wrapped value
|
||||
// - Ap operations combine wrapped values using a semigroup
|
||||
// - The phantom type A allows type-safe composition with other functors
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Accumulate string values
|
||||
// c1 := Make[string, int]("hello")
|
||||
// c2 := Make[string, int]("world")
|
||||
//
|
||||
// // Map doesn't change the wrapped value
|
||||
// mapped := Map[string, int, string](strconv.Itoa)(c1) // Still contains "hello"
|
||||
//
|
||||
// // Ap combines values using a semigroup
|
||||
// combined := Ap[string, int, int](S.Monoid)(c1)(c2) // Contains "helloworld"
|
||||
package constant
|
||||
|
||||
import (
|
||||
@@ -21,36 +52,209 @@ import (
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// Const is a functor that wraps a value of type E with a phantom type parameter A.
|
||||
//
|
||||
// The Const functor is useful for accumulating values during traversals or implementing
|
||||
// optics. The type parameter A is phantom - it doesn't affect the runtime value but allows
|
||||
// the type to participate in functor and applicative operations.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The type of the wrapped value (the actual data)
|
||||
// - A: The phantom type parameter (not stored, only used for type-level operations)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a Const that wraps a string
|
||||
// c := Make[string, int]("metadata")
|
||||
//
|
||||
// // The int type parameter is phantom - no int value is stored
|
||||
// value := Unwrap(c) // "metadata"
|
||||
type Const[E, A any] struct {
|
||||
value E
|
||||
}
|
||||
|
||||
// Make creates a Const value wrapping the given value.
|
||||
//
|
||||
// This is the primary constructor for Const values. The second type parameter A
|
||||
// is phantom and must be specified explicitly when needed for type inference.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The type of the value to wrap
|
||||
// - A: The phantom type parameter
|
||||
//
|
||||
// Parameters:
|
||||
// - e: The value to wrap
|
||||
//
|
||||
// Returns:
|
||||
// - A Const[E, A] wrapping the value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// c := Make[string, int]("hello")
|
||||
// value := Unwrap(c) // "hello"
|
||||
func Make[E, A any](e E) Const[E, A] {
|
||||
return Const[E, A]{value: e}
|
||||
}
|
||||
|
||||
// Unwrap extracts the wrapped value from a Const.
|
||||
//
|
||||
// This is the inverse of Make, retrieving the actual value stored in the Const.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The type of the wrapped value
|
||||
// - A: The phantom type parameter
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Const to unwrap
|
||||
//
|
||||
// Returns:
|
||||
// - The wrapped value of type E
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// c := Make[string, int]("world")
|
||||
// value := Unwrap(c) // "world"
|
||||
func Unwrap[E, A any](c Const[E, A]) E {
|
||||
return c.value
|
||||
}
|
||||
|
||||
// Of creates a Const containing the monoid's empty value, ignoring the input.
|
||||
//
|
||||
// This implements the Applicative's "pure" operation for Const. It creates a Const
|
||||
// wrapping the monoid's identity element, regardless of the input value.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The type of the wrapped value (must have a monoid)
|
||||
// - A: The input type (ignored)
|
||||
//
|
||||
// Parameters:
|
||||
// - m: The monoid providing the empty value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that ignores its input and returns Const[E, A] with the empty value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import S "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// of := Of[string, int](S.Monoid)
|
||||
// c := of(42) // Const[string, int] containing ""
|
||||
// value := Unwrap(c) // ""
|
||||
func Of[E, A any](m M.Monoid[E]) func(A) Const[E, A] {
|
||||
return F.Constant1[A](Make[E, A](m.Empty()))
|
||||
}
|
||||
|
||||
// MonadMap applies a function to the phantom type parameter without changing the wrapped value.
|
||||
//
|
||||
// This implements the Functor's map operation for Const. Since the type parameter A is phantom,
|
||||
// the function is never actually called - the wrapped value E remains unchanged.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The type of the wrapped value
|
||||
// - A: The input phantom type
|
||||
// - B: The output phantom type
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The Const to map over
|
||||
// - _: The function to apply (ignored)
|
||||
//
|
||||
// Returns:
|
||||
// - A Const[E, B] with the same wrapped value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// c := Make[string, int]("hello")
|
||||
// mapped := MonadMap(c, func(i int) string { return strconv.Itoa(i) })
|
||||
// // mapped still contains "hello", function was never called
|
||||
func MonadMap[E, A, B any](fa Const[E, A], _ func(A) B) Const[E, B] {
|
||||
return Make[E, B](fa.value)
|
||||
}
|
||||
|
||||
// MonadAp combines two Const values using a semigroup.
|
||||
//
|
||||
// This implements the Applicative's ap operation for Const. It combines the wrapped
|
||||
// values from both Const instances using the provided semigroup, ignoring the function
|
||||
// type in the first argument.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The type of the wrapped values (must have a semigroup)
|
||||
// - A: The input phantom type
|
||||
// - B: The output phantom type
|
||||
//
|
||||
// Parameters:
|
||||
// - s: The semigroup for combining wrapped values
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes two Const values and combines their wrapped values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import S "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// ap := MonadAp[string, int, int](S.Monoid)
|
||||
// c1 := Make[string, func(int) int]("hello")
|
||||
// c2 := Make[string, int]("world")
|
||||
// result := ap(c1, c2) // Const containing "helloworld"
|
||||
func MonadAp[E, A, B any](s S.Semigroup[E]) func(fab Const[E, func(A) B], fa Const[E, A]) Const[E, B] {
|
||||
return func(fab Const[E, func(A) B], fa Const[E, A]) Const[E, B] {
|
||||
return Make[E, B](s.Concat(fab.value, fa.value))
|
||||
}
|
||||
}
|
||||
|
||||
// Map applies a function to the phantom type parameter without changing the wrapped value.
|
||||
//
|
||||
// This is the curried version of MonadMap, providing a more functional programming style.
|
||||
// The function is never actually called since A is a phantom type.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The type of the wrapped value
|
||||
// - A: The input phantom type
|
||||
// - B: The output phantom type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The function to apply (ignored)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms Const[E, A] to Const[E, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// c := Make[string, int]("data")
|
||||
// mapped := F.Pipe1(c, Map[string, int, string](strconv.Itoa))
|
||||
// // mapped still contains "data"
|
||||
func Map[E, A, B any](f func(A) B) func(fa Const[E, A]) Const[E, B] {
|
||||
return F.Bind2nd(MonadMap[E, A, B], f)
|
||||
}
|
||||
|
||||
// Ap combines Const values using a semigroup in a curried style.
|
||||
//
|
||||
// This is the curried version of MonadAp, providing data-last style for better composition.
|
||||
// It combines the wrapped values from both Const instances using the provided semigroup.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The type of the wrapped values (must have a semigroup)
|
||||
// - A: The input phantom type
|
||||
// - B: The output phantom type
|
||||
//
|
||||
// Parameters:
|
||||
// - s: The semigroup for combining wrapped values
|
||||
//
|
||||
// Returns:
|
||||
// - A curried function for combining Const values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// c1 := Make[string, int]("hello")
|
||||
// c2 := Make[string, func(int) int]("world")
|
||||
// result := F.Pipe1(c1, Ap[string, int, int](S.Monoid)(c2))
|
||||
// // result contains "helloworld"
|
||||
func Ap[E, A, B any](s S.Semigroup[E]) func(fa Const[E, A]) func(fab Const[E, func(A) B]) Const[E, B] {
|
||||
monadap := MonadAp[E, A, B](s)
|
||||
return func(fa Const[E, A]) func(fab Const[E, func(A) B]) Const[E, B] {
|
||||
|
||||
@@ -16,25 +16,340 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
fa := Make[string, int]("foo")
|
||||
assert.Equal(t, fa, F.Pipe1(fa, Map[string](utils.Double)))
|
||||
// TestMake tests the Make constructor
|
||||
func TestMake(t *testing.T) {
|
||||
t.Run("creates Const with string value", func(t *testing.T) {
|
||||
c := Make[string, int]("hello")
|
||||
assert.Equal(t, "hello", Unwrap(c))
|
||||
})
|
||||
|
||||
t.Run("creates Const with int value", func(t *testing.T) {
|
||||
c := Make[int, string](42)
|
||||
assert.Equal(t, 42, Unwrap(c))
|
||||
})
|
||||
|
||||
t.Run("creates Const with struct value", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Name string
|
||||
Port int
|
||||
}
|
||||
cfg := Config{Name: "server", Port: 8080}
|
||||
c := Make[Config, bool](cfg)
|
||||
assert.Equal(t, cfg, Unwrap(c))
|
||||
})
|
||||
}
|
||||
|
||||
// TestUnwrap tests extracting values from Const
|
||||
func TestUnwrap(t *testing.T) {
|
||||
t.Run("unwraps string value", func(t *testing.T) {
|
||||
c := Make[string, int]("world")
|
||||
value := Unwrap(c)
|
||||
assert.Equal(t, "world", value)
|
||||
})
|
||||
|
||||
t.Run("unwraps empty string", func(t *testing.T) {
|
||||
c := Make[string, int]("")
|
||||
value := Unwrap(c)
|
||||
assert.Equal(t, "", value)
|
||||
})
|
||||
|
||||
t.Run("unwraps zero value", func(t *testing.T) {
|
||||
c := Make[int, string](0)
|
||||
value := Unwrap(c)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOf tests the Of function
|
||||
func TestOf(t *testing.T) {
|
||||
assert.Equal(t, Make[string, int](""), Of[string, int](S.Monoid)(1))
|
||||
t.Run("creates Const with monoid empty value", func(t *testing.T) {
|
||||
of := Of[string, int](S.Monoid)
|
||||
c := of(42)
|
||||
assert.Equal(t, "", Unwrap(c))
|
||||
})
|
||||
|
||||
t.Run("ignores input value", func(t *testing.T) {
|
||||
of := Of[string, int](S.Monoid)
|
||||
c1 := of(1)
|
||||
c2 := of(100)
|
||||
assert.Equal(t, Unwrap(c1), Unwrap(c2))
|
||||
})
|
||||
|
||||
t.Run("works with int monoid", func(t *testing.T) {
|
||||
of := Of[int, string](N.MonoidSum[int]())
|
||||
c := of("ignored")
|
||||
assert.Equal(t, 0, Unwrap(c))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
fab := Make[string, int]("bar")
|
||||
assert.Equal(t, Make[string, int]("foobar"), Ap[string, int, int](S.Monoid)(fab)(Make[string, func(int) int]("foo")))
|
||||
// TestMap tests the Map function
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("preserves wrapped value", func(t *testing.T) {
|
||||
fa := Make[string, int]("foo")
|
||||
result := F.Pipe1(fa, Map[string](utils.Double))
|
||||
assert.Equal(t, "foo", Unwrap(result))
|
||||
})
|
||||
|
||||
t.Run("changes phantom type", func(t *testing.T) {
|
||||
fa := Make[string, int]("data")
|
||||
fb := Map[string, int, string](strconv.Itoa)(fa)
|
||||
// Value unchanged, but type changed from Const[string, int] to Const[string, string]
|
||||
assert.Equal(t, "data", Unwrap(fb))
|
||||
})
|
||||
|
||||
t.Run("function is never called", func(t *testing.T) {
|
||||
called := false
|
||||
fa := Make[string, int]("test")
|
||||
fb := Map[string, int, string](func(i int) string {
|
||||
called = true
|
||||
return strconv.Itoa(i)
|
||||
})(fa)
|
||||
assert.False(t, called, "Map function should not be called")
|
||||
assert.Equal(t, "test", Unwrap(fb))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadMap tests the MonadMap function
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("preserves wrapped value", func(t *testing.T) {
|
||||
fa := Make[string, int]("original")
|
||||
fb := MonadMap(fa, func(i int) string { return strconv.Itoa(i) })
|
||||
assert.Equal(t, "original", Unwrap(fb))
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
fa := Make[int, string](42)
|
||||
fb := MonadMap(fa, func(s string) bool { return len(s) > 0 })
|
||||
assert.Equal(t, 42, Unwrap(fb))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAp tests the Ap function
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("combines string values", func(t *testing.T) {
|
||||
fab := Make[string, int]("bar")
|
||||
fa := Make[string, func(int) int]("foo")
|
||||
result := Ap[string, int, int](S.Monoid)(fab)(fa)
|
||||
assert.Equal(t, "foobar", Unwrap(result))
|
||||
})
|
||||
|
||||
t.Run("combines int values with sum", func(t *testing.T) {
|
||||
fab := Make[int, string](10)
|
||||
fa := Make[int, func(string) string](5)
|
||||
result := Ap[int, string, string](N.SemigroupSum[int]())(fab)(fa)
|
||||
assert.Equal(t, 15, Unwrap(result))
|
||||
})
|
||||
|
||||
t.Run("combines int values with product", func(t *testing.T) {
|
||||
fab := Make[int, bool](3)
|
||||
fa := Make[int, func(bool) bool](4)
|
||||
result := Ap[int, bool, bool](N.SemigroupProduct[int]())(fab)(fa)
|
||||
assert.Equal(t, 12, Unwrap(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAp tests the MonadAp function
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("combines values using semigroup", func(t *testing.T) {
|
||||
ap := MonadAp[string, int, int](S.Monoid)
|
||||
fab := Make[string, func(int) int]("hello")
|
||||
fa := Make[string, int]("world")
|
||||
result := ap(fab, fa)
|
||||
assert.Equal(t, "helloworld", Unwrap(result))
|
||||
})
|
||||
|
||||
t.Run("works with empty strings", func(t *testing.T) {
|
||||
ap := MonadAp[string, int, int](S.Monoid)
|
||||
fab := Make[string, func(int) int]("")
|
||||
fa := Make[string, int]("test")
|
||||
result := ap(fab, fa)
|
||||
assert.Equal(t, "test", Unwrap(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoid tests the Monoid function
|
||||
func TestMonoid(t *testing.T) {
|
||||
t.Run("always returns constant value", func(t *testing.T) {
|
||||
m := Monoid(42)
|
||||
assert.Equal(t, 42, m.Concat(1, 2))
|
||||
assert.Equal(t, 42, m.Concat(100, 200))
|
||||
assert.Equal(t, 42, m.Empty())
|
||||
})
|
||||
|
||||
t.Run("works with strings", func(t *testing.T) {
|
||||
m := Monoid("constant")
|
||||
assert.Equal(t, "constant", m.Concat("a", "b"))
|
||||
assert.Equal(t, "constant", m.Empty())
|
||||
})
|
||||
|
||||
t.Run("works with structs", func(t *testing.T) {
|
||||
type Point struct{ X, Y int }
|
||||
p := Point{X: 1, Y: 2}
|
||||
m := Monoid(p)
|
||||
assert.Equal(t, p, m.Concat(Point{X: 3, Y: 4}, Point{X: 5, Y: 6}))
|
||||
assert.Equal(t, p, m.Empty())
|
||||
})
|
||||
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := Monoid(10)
|
||||
|
||||
// Left identity: Concat(Empty(), x) = x (both return constant)
|
||||
assert.Equal(t, 10, m.Concat(m.Empty(), 5))
|
||||
|
||||
// Right identity: Concat(x, Empty()) = x (both return constant)
|
||||
assert.Equal(t, 10, m.Concat(5, m.Empty()))
|
||||
|
||||
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
left := m.Concat(m.Concat(1, 2), 3)
|
||||
right := m.Concat(1, m.Concat(2, 3))
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, 10, left)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConstFunctorLaws tests functor laws for Const
|
||||
func TestConstFunctorLaws(t *testing.T) {
|
||||
t.Run("identity law", func(t *testing.T) {
|
||||
// map id = id
|
||||
fa := Make[string, int]("test")
|
||||
mapped := Map[string, int, int](F.Identity[int])(fa)
|
||||
assert.Equal(t, Unwrap(fa), Unwrap(mapped))
|
||||
})
|
||||
|
||||
t.Run("composition law", func(t *testing.T) {
|
||||
// map (g . f) = map g . map f
|
||||
fa := Make[string, int]("data")
|
||||
f := func(i int) string { return strconv.Itoa(i) }
|
||||
g := func(s string) bool { return len(s) > 0 }
|
||||
|
||||
// map (g . f)
|
||||
composed := Map[string, int, bool](func(i int) bool { return g(f(i)) })(fa)
|
||||
|
||||
// map g . map f
|
||||
intermediate := F.Pipe1(fa, Map[string, int, string](f))
|
||||
chained := Map[string, string, bool](g)(intermediate)
|
||||
|
||||
assert.Equal(t, Unwrap(composed), Unwrap(chained))
|
||||
})
|
||||
}
|
||||
|
||||
// TestConstApplicativeLaws tests applicative laws for Const
|
||||
func TestConstApplicativeLaws(t *testing.T) {
|
||||
t.Run("identity law", func(t *testing.T) {
|
||||
// For Const, ap combines the wrapped values using the semigroup
|
||||
// ap (of id) v combines empty (from of) with v's value
|
||||
v := Make[string, int]("value")
|
||||
ofId := Of[string, func(int) int](S.Monoid)(F.Identity[int])
|
||||
result := Ap[string, int, int](S.Monoid)(v)(ofId)
|
||||
// Result combines "" (from Of) with "value" using string monoid
|
||||
assert.Equal(t, "value", Unwrap(result))
|
||||
})
|
||||
|
||||
t.Run("homomorphism law", func(t *testing.T) {
|
||||
// ap (of f) (of x) = of (f x)
|
||||
f := func(i int) string { return strconv.Itoa(i) }
|
||||
x := 42
|
||||
|
||||
ofF := Of[string, func(int) string](S.Monoid)(f)
|
||||
ofX := Of[string, int](S.Monoid)(x)
|
||||
left := Ap[string, int, string](S.Monoid)(ofX)(ofF)
|
||||
|
||||
right := Of[string, string](S.Monoid)(f(x))
|
||||
|
||||
assert.Equal(t, Unwrap(left), Unwrap(right))
|
||||
})
|
||||
}
|
||||
|
||||
// TestConstEdgeCases tests edge cases
|
||||
func TestConstEdgeCases(t *testing.T) {
|
||||
t.Run("empty string values", func(t *testing.T) {
|
||||
c := Make[string, int]("")
|
||||
assert.Equal(t, "", Unwrap(c))
|
||||
|
||||
mapped := Map[string, int, string](strconv.Itoa)(c)
|
||||
assert.Equal(t, "", Unwrap(mapped))
|
||||
})
|
||||
|
||||
t.Run("zero values", func(t *testing.T) {
|
||||
c := Make[int, string](0)
|
||||
assert.Equal(t, 0, Unwrap(c))
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var ptr *int
|
||||
c := Make[*int, string](ptr)
|
||||
assert.Nil(t, Unwrap(c))
|
||||
})
|
||||
|
||||
t.Run("multiple map operations", func(t *testing.T) {
|
||||
c := Make[string, int]("original")
|
||||
// Chain multiple map operations
|
||||
step1 := Map[string, int, string](strconv.Itoa)(c)
|
||||
step2 := Map[string, string, bool](func(s string) bool { return len(s) > 0 })(step1)
|
||||
result := Map[string, bool, int](func(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})(step2)
|
||||
assert.Equal(t, "original", Unwrap(result))
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkMake benchmarks the Make constructor
|
||||
func BenchmarkMake(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = Make[string, int]("test")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkUnwrap benchmarks the Unwrap function
|
||||
func BenchmarkUnwrap(b *testing.B) {
|
||||
c := Make[string, int]("test")
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = Unwrap(c)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMap benchmarks the Map function
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
c := Make[string, int]("test")
|
||||
mapFn := Map[string, int, string](strconv.Itoa)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = mapFn(c)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkAp benchmarks the Ap function
|
||||
func BenchmarkAp(b *testing.B) {
|
||||
fab := Make[string, int]("hello")
|
||||
fa := Make[string, func(int) int]("world")
|
||||
apFn := Ap[string, int, int](S.Monoid)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = apFn(fab)(fa)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMonoid benchmarks the Monoid function
|
||||
func BenchmarkMonoid(b *testing.B) {
|
||||
m := Monoid(42)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = m.Concat(1, 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// 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 constant
|
||||
|
||||
import (
|
||||
@@ -5,7 +20,47 @@ import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// Monoid returns a [M.Monoid] that returns a constant value in all operations
|
||||
// Monoid creates a monoid that always returns a constant value.
|
||||
//
|
||||
// This creates a trivial monoid where both the Concat operation and Empty
|
||||
// always return the same constant value, regardless of inputs. This is useful
|
||||
// for testing, placeholder implementations, or when you need a monoid instance
|
||||
// but the actual combining behavior doesn't matter.
|
||||
//
|
||||
// # Monoid Laws
|
||||
//
|
||||
// The constant monoid satisfies all monoid laws trivially:
|
||||
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always returns 'a'
|
||||
// - Left Identity: Concat(Empty(), x) = x - both return 'a'
|
||||
// - Right Identity: Concat(x, Empty()) = x - both return 'a'
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the constant value
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The constant value to return in all operations
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[A] that always returns the constant value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a monoid that always returns 42
|
||||
// m := Monoid(42)
|
||||
// result := m.Concat(1, 2) // 42
|
||||
// empty := m.Empty() // 42
|
||||
//
|
||||
// // Useful for testing or placeholder implementations
|
||||
// type Config struct {
|
||||
// Timeout int
|
||||
// }
|
||||
// defaultConfig := Monoid(Config{Timeout: 30})
|
||||
// config := defaultConfig.Concat(Config{Timeout: 10}, Config{Timeout: 20})
|
||||
// // config is Config{Timeout: 30}
|
||||
//
|
||||
// See also:
|
||||
// - function.Constant2: The underlying constant function
|
||||
// - M.MakeMonoid: The monoid constructor
|
||||
func Monoid[A any](a A) M.Monoid[A] {
|
||||
return M.MakeMonoid(function.Constant2[A, A](a), a)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// WithContext wraps an existing [ReaderIOResult] and performs a context check for cancellation before delegating.
|
||||
@@ -85,3 +86,7 @@ func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
|
||||
WithContext,
|
||||
)
|
||||
}
|
||||
|
||||
func pairFromContextCancel(newCtx context.Context, cancelFct context.CancelFunc) ContextCancel {
|
||||
return pair.MakePair(cancelFct, newCtx)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,25 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package file provides context-aware file operations that integrate with the ReaderIOResult monad.
|
||||
// It offers safe, composable file I/O operations that respect context cancellation and properly
|
||||
// manage resources using the RAII pattern.
|
||||
//
|
||||
// All operations in this package:
|
||||
// - Respect context.Context for cancellation and timeouts
|
||||
// - Return ReaderIOResult for composable error handling
|
||||
// - Automatically manage resource cleanup
|
||||
// - Are safe to use in concurrent environments
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Read a file with automatic resource management
|
||||
// readOp := ReadFile("data.txt")
|
||||
// result := readOp(ctx)()
|
||||
//
|
||||
// // Open and manually manage a file
|
||||
// fileOp := Open("config.json")
|
||||
// fileResult := fileOp(ctx)()
|
||||
package file
|
||||
|
||||
import (
|
||||
@@ -29,32 +48,181 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// Open opens a file for reading within the given context
|
||||
// Open opens a file for reading within the given context.
|
||||
// The operation respects context cancellation and returns a ReaderIOResult
|
||||
// that produces an os.File handle on success.
|
||||
//
|
||||
// The returned file handle should be closed using the Close function when no longer needed,
|
||||
// or managed automatically using WithResource or ReadFile.
|
||||
//
|
||||
// Parameters:
|
||||
// - path: The path to the file to open
|
||||
//
|
||||
// Returns:
|
||||
// - ReaderIOResult[*os.File]: A context-aware computation that opens the file
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// openFile := Open("data.txt")
|
||||
// result := openFile(ctx)()
|
||||
// either.Fold(
|
||||
// result,
|
||||
// func(err error) { log.Printf("Error: %v", err) },
|
||||
// func(f *os.File) {
|
||||
// defer f.Close()
|
||||
// // Use file...
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// See Also:
|
||||
// - ReadFile: For reading entire file contents with automatic resource management
|
||||
// - Close: For closing file handles
|
||||
Open = F.Flow3(
|
||||
IOEF.Open,
|
||||
RIOE.FromIOEither[*os.File],
|
||||
RIOE.WithContext[*os.File],
|
||||
)
|
||||
|
||||
// Remove removes a file by name
|
||||
// Create creates or truncates a file for writing within the given context.
|
||||
// If the file already exists, it is truncated. If it doesn't exist, it is created
|
||||
// with mode 0666 (before umask).
|
||||
//
|
||||
// The operation respects context cancellation and returns a ReaderIOResult
|
||||
// that produces an os.File handle on success.
|
||||
//
|
||||
// The returned file handle should be closed using the Close function when no longer needed,
|
||||
// or managed automatically using WithResource or WriteFile.
|
||||
//
|
||||
// Parameters:
|
||||
// - path: The path to the file to create or truncate
|
||||
//
|
||||
// Returns:
|
||||
// - ReaderIOResult[*os.File]: A context-aware computation that creates the file
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// createFile := Create("output.txt")
|
||||
// result := createFile(ctx)()
|
||||
// either.Fold(
|
||||
// result,
|
||||
// func(err error) { log.Printf("Error: %v", err) },
|
||||
// func(f *os.File) {
|
||||
// defer f.Close()
|
||||
// f.WriteString("Hello, World!")
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// See Also:
|
||||
// - WriteFile: For writing data to a file with automatic resource management
|
||||
// - Open: For opening files for reading
|
||||
// - Close: For closing file handles
|
||||
Create = F.Flow3(
|
||||
IOEF.Create,
|
||||
RIOE.FromIOEither[*os.File],
|
||||
RIOE.WithContext[*os.File],
|
||||
)
|
||||
|
||||
// Remove removes a file by name.
|
||||
// The operation returns the filename on success, allowing for easy composition
|
||||
// with other file operations.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The path to the file to remove
|
||||
//
|
||||
// Returns:
|
||||
// - ReaderIOResult[string]: A computation that removes the file and returns its name
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// removeOp := Remove("temp.txt")
|
||||
// result := removeOp(ctx)()
|
||||
// either.Fold(
|
||||
// result,
|
||||
// func(err error) { log.Printf("Failed to remove: %v", err) },
|
||||
// func(name string) { log.Printf("Removed: %s", name) },
|
||||
// )
|
||||
//
|
||||
// See Also:
|
||||
// - Open: For opening files
|
||||
// - ReadFile: For reading file contents
|
||||
Remove = F.Flow2(
|
||||
IOEF.Remove,
|
||||
RIOE.FromIOEither[string],
|
||||
)
|
||||
)
|
||||
|
||||
// Close closes an object
|
||||
func Close[C io.Closer](c C) RIOE.ReaderIOResult[struct{}] {
|
||||
// Close closes an io.Closer resource and returns a ReaderIOResult.
|
||||
// This function is generic and works with any type that implements io.Closer,
|
||||
// including os.File, network connections, and other closeable resources.
|
||||
//
|
||||
// The function captures any error that occurs during closing and returns it
|
||||
// as part of the ReaderIOResult. On success, it returns Void (empty struct).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - C: Any type that implements io.Closer
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The resource to close
|
||||
//
|
||||
// Returns:
|
||||
// - ReaderIOResult[Void]: A computation that closes the resource
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// file, _ := os.Open("data.txt")
|
||||
// closeOp := Close(file)
|
||||
// result := closeOp(ctx)()
|
||||
//
|
||||
// Note: This function is typically used with WithResource for automatic resource management
|
||||
// rather than being called directly.
|
||||
//
|
||||
// See Also:
|
||||
// - Open: For opening files
|
||||
// - ReadFile: For reading files with automatic closing
|
||||
func Close[C io.Closer](c C) ReaderIOResult[Void] {
|
||||
return F.Pipe2(
|
||||
c,
|
||||
IOEF.Close[C],
|
||||
RIOE.FromIOEither[struct{}],
|
||||
RIOE.FromIOEither[Void],
|
||||
)
|
||||
}
|
||||
|
||||
// ReadFile reads a file in the scope of a context
|
||||
func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
|
||||
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOResult[[]byte] {
|
||||
// ReadFile reads the entire contents of a file in a context-aware manner.
|
||||
// This function automatically manages the file resource using the RAII pattern,
|
||||
// ensuring the file is properly closed even if an error occurs or the context is canceled.
|
||||
//
|
||||
// The operation:
|
||||
// - Opens the file for reading
|
||||
// - Reads all contents into a byte slice
|
||||
// - Automatically closes the file when done
|
||||
// - Respects context cancellation during the read operation
|
||||
//
|
||||
// Parameters:
|
||||
// - path: The path to the file to read
|
||||
//
|
||||
// Returns:
|
||||
// - ReaderIOResult[[]byte]: A computation that reads the file contents
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readOp := ReadFile("config.json")
|
||||
// result := readOp(ctx)()
|
||||
// either.Fold(
|
||||
// result,
|
||||
// func(err error) { log.Printf("Read error: %v", err) },
|
||||
// func(data []byte) { log.Printf("Read %d bytes", len(data)) },
|
||||
// )
|
||||
//
|
||||
// The function uses WithResource internally to ensure proper cleanup:
|
||||
//
|
||||
// ReadFile(path) = WithResource(Open(path), Close)(readAllBytes)
|
||||
//
|
||||
// See Also:
|
||||
// - Open: For opening files without automatic reading
|
||||
// - Close: For closing file handles
|
||||
// - WithResource: For custom resource management patterns
|
||||
func ReadFile(path string) ReaderIOResult[[]byte] {
|
||||
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) ReaderIOResult[[]byte] {
|
||||
return func(ctx context.Context) IOE.IOEither[error, []byte] {
|
||||
return func() ET.Either[error, []byte] {
|
||||
return file.ReadAll(ctx, r)
|
||||
@@ -62,3 +230,48 @@ func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WriteFile writes data to a file in a context-aware manner.
|
||||
// This function automatically manages the file resource using the RAII pattern,
|
||||
// ensuring the file is properly closed even if an error occurs or the context is canceled.
|
||||
//
|
||||
// If the file doesn't exist, it is created with mode 0666 (before umask).
|
||||
// If the file already exists, it is truncated before writing.
|
||||
//
|
||||
// The operation:
|
||||
// - Creates or truncates the file for writing
|
||||
// - Writes all data to the file
|
||||
// - Automatically closes the file when done
|
||||
// - Respects context cancellation during the write operation
|
||||
//
|
||||
// Parameters:
|
||||
// - data: The byte slice to write to the file
|
||||
//
|
||||
// Returns:
|
||||
// - Kleisli[string, []byte]: A function that takes a file path and returns a computation
|
||||
// that writes the data and returns the written bytes on success
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// writeOp := WriteFile([]byte("Hello, World!"))
|
||||
// result := writeOp("output.txt")(ctx)()
|
||||
// either.Fold(
|
||||
// result,
|
||||
// func(err error) { log.Printf("Write error: %v", err) },
|
||||
// func(data []byte) { log.Printf("Wrote %d bytes", len(data)) },
|
||||
// )
|
||||
//
|
||||
// The function uses WithResource internally to ensure proper cleanup:
|
||||
//
|
||||
// WriteFile(data) = Create >> WriteAll(data) >> Close
|
||||
//
|
||||
// See Also:
|
||||
// - ReadFile: For reading file contents with automatic resource management
|
||||
// - Create: For creating files without automatic writing
|
||||
// - WriteAll: For writing to an already-open file handle
|
||||
func WriteFile(data []byte) Kleisli[string, []byte] {
|
||||
return F.Flow2(
|
||||
Create,
|
||||
WriteAll[*os.File](data),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,11 +18,16 @@ package file
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
J "github.com/IBM/fp-go/v2/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type RecordType struct {
|
||||
@@ -49,3 +54,267 @@ func ExampleReadFile() {
|
||||
// Output:
|
||||
// Right[string](Carsten)
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("Success - creates new file", func(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "test_create.txt")
|
||||
|
||||
createOp := Create(tempFile)
|
||||
result := createOp(ctx)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify file was created
|
||||
_, err := os.Stat(tempFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Clean up file handle
|
||||
E.MonadFold(result,
|
||||
func(error) *os.File { return nil },
|
||||
func(f *os.File) *os.File { f.Close(); return f },
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("Success - truncates existing file", func(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "test_truncate.txt")
|
||||
|
||||
// Create file with initial content
|
||||
err := os.WriteFile(tempFile, []byte("initial content"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create should truncate
|
||||
createOp := Create(tempFile)
|
||||
result := createOp(ctx)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Close the file
|
||||
E.MonadFold(result,
|
||||
func(error) *os.File { return nil },
|
||||
func(f *os.File) *os.File { f.Close(); return f },
|
||||
)
|
||||
|
||||
// Verify file was truncated
|
||||
content, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, content)
|
||||
})
|
||||
|
||||
t.Run("Failure - invalid path", func(t *testing.T) {
|
||||
// Try to create file in non-existent directory
|
||||
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
|
||||
|
||||
createOp := Create(invalidPath)
|
||||
result := createOp(ctx)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("Success - file can be written to", func(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
|
||||
|
||||
createOp := Create(tempFile)
|
||||
result := createOp(ctx)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Write to the file
|
||||
E.MonadFold(result,
|
||||
func(err error) *os.File { t.Fatalf("Unexpected error: %v", err); return nil },
|
||||
func(f *os.File) *os.File {
|
||||
defer f.Close()
|
||||
_, err := f.WriteString("test content")
|
||||
assert.NoError(t, err)
|
||||
return f
|
||||
},
|
||||
)
|
||||
|
||||
// Verify content was written
|
||||
content, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test content", string(content))
|
||||
})
|
||||
|
||||
t.Run("Context cancellation", func(t *testing.T) {
|
||||
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
|
||||
|
||||
createOp := Create(tempFile)
|
||||
result := createOp(cancelCtx)()
|
||||
|
||||
// Note: File creation itself doesn't check context, but this tests the pattern
|
||||
// In practice, context cancellation would affect subsequent operations
|
||||
_ = result
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("Success - writes data to new file", func(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
|
||||
testData := []byte("Hello, World!")
|
||||
|
||||
writeOp := WriteFile(testData)
|
||||
result := writeOp(tempFile)(ctx)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify returned data
|
||||
E.MonadFold(result,
|
||||
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
|
||||
func(data []byte) []byte {
|
||||
assert.Equal(t, testData, data)
|
||||
return data
|
||||
},
|
||||
)
|
||||
|
||||
// Verify file content
|
||||
content, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testData, content)
|
||||
})
|
||||
|
||||
t.Run("Success - overwrites existing file", func(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "test_overwrite.txt")
|
||||
|
||||
// Write initial content
|
||||
err := os.WriteFile(tempFile, []byte("old content"), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Overwrite with new content
|
||||
newData := []byte("new content")
|
||||
writeOp := WriteFile(newData)
|
||||
result := writeOp(tempFile)(ctx)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify file was overwritten
|
||||
content, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newData, content)
|
||||
})
|
||||
|
||||
t.Run("Success - writes empty data", func(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "test_empty.txt")
|
||||
emptyData := []byte{}
|
||||
|
||||
writeOp := WriteFile(emptyData)
|
||||
result := writeOp(tempFile)(ctx)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify file is empty
|
||||
content, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, content)
|
||||
})
|
||||
|
||||
t.Run("Success - writes large data", func(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "test_large.txt")
|
||||
largeData := make([]byte, 1024*1024) // 1MB
|
||||
for i := range largeData {
|
||||
largeData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
writeOp := WriteFile(largeData)
|
||||
result := writeOp(tempFile)(ctx)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify file content
|
||||
content, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, largeData, content)
|
||||
})
|
||||
|
||||
t.Run("Failure - invalid path", func(t *testing.T) {
|
||||
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
|
||||
testData := []byte("test")
|
||||
|
||||
writeOp := WriteFile(testData)
|
||||
result := writeOp(invalidPath)(ctx)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("Success - writes binary data", func(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "test_binary.bin")
|
||||
binaryData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
|
||||
|
||||
writeOp := WriteFile(binaryData)
|
||||
result := writeOp(tempFile)(ctx)()
|
||||
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Verify binary content
|
||||
content, err := os.ReadFile(tempFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, binaryData, content)
|
||||
})
|
||||
|
||||
t.Run("Integration - write then read", func(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "test_roundtrip.txt")
|
||||
testData := []byte("Round trip test data")
|
||||
|
||||
// Write data
|
||||
writeOp := WriteFile(testData)
|
||||
writeResult := writeOp(tempFile)(ctx)()
|
||||
assert.True(t, E.IsRight(writeResult))
|
||||
|
||||
// Read data back
|
||||
readOp := ReadFile(tempFile)
|
||||
readResult := readOp(ctx)()
|
||||
assert.True(t, E.IsRight(readResult))
|
||||
|
||||
// Verify data matches
|
||||
E.MonadFold(readResult,
|
||||
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
|
||||
func(data []byte) []byte {
|
||||
assert.Equal(t, testData, data)
|
||||
return data
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("Composition with Map", func(t *testing.T) {
|
||||
tempFile := filepath.Join(t.TempDir(), "test_compose.txt")
|
||||
testData := []byte("test data")
|
||||
|
||||
// Write and transform result
|
||||
pipeline := F.Pipe1(
|
||||
WriteFile(testData)(tempFile),
|
||||
R.Map(func(data []byte) int { return len(data) }),
|
||||
)
|
||||
|
||||
result := pipeline(ctx)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
E.MonadFold(result,
|
||||
func(err error) int { t.Fatalf("Unexpected error: %v", err); return 0 },
|
||||
func(length int) int {
|
||||
assert.Equal(t, len(testData), length)
|
||||
return length
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("Context cancellation during write", func(t *testing.T) {
|
||||
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
|
||||
testData := []byte("test")
|
||||
|
||||
writeOp := WriteFile(testData)
|
||||
result := writeOp(tempFile)(cancelCtx)()
|
||||
|
||||
// Note: The actual write may complete before cancellation is checked
|
||||
// This test verifies the pattern works with cancelled contexts
|
||||
_ = result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ var (
|
||||
)
|
||||
|
||||
// CreateTemp created a temp file with proper parametrization
|
||||
func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
|
||||
func CreateTemp(dir, pattern string) ReaderIOResult[*os.File] {
|
||||
return F.Pipe2(
|
||||
IOEF.CreateTemp(dir, pattern),
|
||||
RIOE.FromIOEither[*os.File],
|
||||
@@ -47,6 +47,6 @@ func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
|
||||
}
|
||||
|
||||
// WithTempFile creates a temporary file, then invokes a callback to create a resource based on the file, then close and remove the temp file
|
||||
func WithTempFile[A any](f func(*os.File) RIOE.ReaderIOResult[A]) RIOE.ReaderIOResult[A] {
|
||||
func WithTempFile[A any](f Kleisli[*os.File, A]) ReaderIOResult[A] {
|
||||
return RIOE.WithResource[A](onCreateTempFile, onReleaseTempFile)(f)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestWithTempFile(t *testing.T) {
|
||||
|
||||
func TestWithTempFileOnClosedFile(t *testing.T) {
|
||||
|
||||
res := WithTempFile(func(f *os.File) RIOE.ReaderIOResult[[]byte] {
|
||||
res := WithTempFile(func(f *os.File) ReaderIOResult[[]byte] {
|
||||
return F.Pipe2(
|
||||
f,
|
||||
onWriteAll[*os.File]([]byte("Carsten")),
|
||||
|
||||
90
v2/context/readerioresult/file/types.go
Normal file
90
v2/context/readerioresult/file/types.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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 file
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
type (
|
||||
// ReaderIOResult represents a context-aware computation that performs side effects
|
||||
// and can fail with an error. This is the main type used throughout the file package
|
||||
// for all file operations.
|
||||
//
|
||||
// ReaderIOResult[A] is equivalent to:
|
||||
// func(context.Context) func() Either[error, A]
|
||||
//
|
||||
// The computation:
|
||||
// - Takes a context.Context for cancellation and timeouts
|
||||
// - Performs side effects (IO operations)
|
||||
// - Returns Either an error or a value of type A
|
||||
//
|
||||
// See Also:
|
||||
// - readerioresult.ReaderIOResult: The underlying type definition
|
||||
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
|
||||
|
||||
// Void represents the absence of a meaningful value, similar to unit type in other languages.
|
||||
// It is used when a function performs side effects but doesn't return a meaningful result.
|
||||
//
|
||||
// Void is typically used as the success type in operations like Close that perform
|
||||
// an action but don't produce a useful value.
|
||||
//
|
||||
// Example:
|
||||
// Close[*os.File](file) // Returns ReaderIOResult[Void]
|
||||
//
|
||||
// See Also:
|
||||
// - function.Void: The underlying type definition
|
||||
Void = function.Void
|
||||
|
||||
// Kleisli represents a Kleisli arrow for ReaderIOResult.
|
||||
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
|
||||
//
|
||||
// Kleisli arrows are used for monadic composition, allowing you to chain operations
|
||||
// that produce ReaderIOResults. They are particularly useful with Chain and Bind operations.
|
||||
//
|
||||
// Kleisli[A, B] is equivalent to:
|
||||
// func(A) ReaderIOResult[B]
|
||||
//
|
||||
// Example:
|
||||
// // A Kleisli arrow that reads a file given its path
|
||||
// var readFileK Kleisli[string, []byte] = ReadFile
|
||||
//
|
||||
// See Also:
|
||||
// - readerioresult.Kleisli: The underlying type definition
|
||||
// - Operator: For transforming ReaderIOResults
|
||||
Kleisli[A, B any] = readerioresult.Kleisli[A, B]
|
||||
|
||||
// Operator represents a transformation from one ReaderIOResult to another.
|
||||
// This is useful for point-free style composition and building reusable transformations.
|
||||
//
|
||||
// Operator[A, B] is equivalent to:
|
||||
// func(ReaderIOResult[A]) ReaderIOResult[B]
|
||||
//
|
||||
// Operators are used to transform computations without executing them, enabling
|
||||
// powerful composition patterns.
|
||||
//
|
||||
// Example:
|
||||
// // An operator that maps over file contents
|
||||
// var toUpper Operator[[]byte, string] = Map(func(data []byte) string {
|
||||
// return strings.ToUpper(string(data))
|
||||
// })
|
||||
//
|
||||
// See Also:
|
||||
// - readerioresult.Operator: The underlying type definition
|
||||
// - Kleisli: For functions that produce ReaderIOResults
|
||||
Operator[A, B any] = readerioresult.Operator[A, B]
|
||||
)
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte] {
|
||||
return func(w W) RIOE.ReaderIOResult[[]byte] {
|
||||
func onWriteAll[W io.Writer](data []byte) Kleisli[W, []byte] {
|
||||
return func(w W) ReaderIOResult[[]byte] {
|
||||
return F.Pipe1(
|
||||
RIOE.TryCatch(func(_ context.Context) func() ([]byte, error) {
|
||||
return func() ([]byte, error) {
|
||||
@@ -38,9 +38,9 @@ func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte]
|
||||
}
|
||||
|
||||
// WriteAll uses a generator function to create a stream, writes data to it and closes it
|
||||
func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
|
||||
func WriteAll[W io.WriteCloser](data []byte) Operator[W, []byte] {
|
||||
onWrite := onWriteAll[W](data)
|
||||
return func(onCreate RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
|
||||
return func(onCreate ReaderIOResult[W]) ReaderIOResult[[]byte] {
|
||||
return RIOE.WithResource[[]byte](
|
||||
onCreate,
|
||||
Close[W])(
|
||||
@@ -50,7 +50,7 @@ func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]
|
||||
}
|
||||
|
||||
// Write uses a generator function to create a stream, writes data to it and closes it
|
||||
func Write[R any, W io.WriteCloser](acquire RIOE.ReaderIOResult[W]) func(use func(W) RIOE.ReaderIOResult[R]) RIOE.ReaderIOResult[R] {
|
||||
func Write[R any, W io.WriteCloser](acquire ReaderIOResult[W]) Kleisli[Kleisli[W, R], R] {
|
||||
return RIOE.WithResource[R](
|
||||
acquire,
|
||||
Close[W])
|
||||
|
||||
@@ -369,7 +369,7 @@ func onExitAny(
|
||||
// )
|
||||
func LogEntryExitWithCallback[A any](
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
cb Reader[context.Context, *slog.Logger],
|
||||
name string) Operator[A, A] {
|
||||
|
||||
nameAttr := slog.String("name", name)
|
||||
@@ -499,12 +499,12 @@ func LogEntryExit[A any](name string) Operator[A, A] {
|
||||
func curriedLog(
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
message string) func(slog.Attr) func(context.Context) func() struct{} {
|
||||
return F.Curry2(func(a slog.Attr, ctx context.Context) func() struct{} {
|
||||
message string) func(slog.Attr) ReaderIO[Void] {
|
||||
return F.Curry2(func(a slog.Attr, ctx context.Context) IO[Void] {
|
||||
logger := cb(ctx)
|
||||
return func() struct{} {
|
||||
return func() Void {
|
||||
logger.LogAttrs(ctx, logLevel, message, a)
|
||||
return struct{}{}
|
||||
return F.VOID
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -571,7 +571,7 @@ func curriedLog(
|
||||
// - Conditional logging: Enable/disable logging based on logger configuration
|
||||
func SLogWithCallback[A any](
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
cb Reader[context.Context, *slog.Logger],
|
||||
message string) Kleisli[Result[A], A] {
|
||||
|
||||
return F.Pipe1(
|
||||
@@ -582,18 +582,23 @@ func SLogWithCallback[A any](
|
||||
curriedLog(logLevel, cb, message),
|
||||
),
|
||||
// preserve the original context
|
||||
reader.Chain(reader.Sequence(readerio.MapTo[struct{}, Result[A]])),
|
||||
reader.Chain(reader.Sequence(readerio.MapTo[Void, Result[A]])),
|
||||
)
|
||||
}
|
||||
|
||||
// SLog creates a Kleisli arrow that logs a Result value (success or error) with a message.
|
||||
//
|
||||
// This function logs both successful values and errors at Info level using the logger from the context.
|
||||
// This function logs both successful values and errors at Info level. It retrieves the logger
|
||||
// using logging.GetLoggerFromContext, which returns either:
|
||||
// - The logger stored in the context via logging.WithLogger, or
|
||||
// - The global logger (set via logging.SetLogger or slog.Default())
|
||||
//
|
||||
// It's a convenience wrapper around SLogWithCallback with standard settings.
|
||||
//
|
||||
// The logged output includes:
|
||||
// - For success: The message with the value as a structured "value" attribute
|
||||
// - For error: The message with the error as a structured "error" attribute
|
||||
// The message parameter becomes the main log message text, and the Result value or error
|
||||
// is attached as a structured logging attribute:
|
||||
// - For success: Logs message with attribute value=<the actual value>
|
||||
// - For error: Logs message with attribute error=<the error>
|
||||
//
|
||||
// The Result is passed through unchanged after logging, making this function transparent in the
|
||||
// computation pipeline.
|
||||
@@ -647,25 +652,47 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
|
||||
}
|
||||
|
||||
// TapSLog creates an operator that logs only successful values with a message and passes them through unchanged.
|
||||
// TapSLog creates an operator that logs both successful values and errors with a message,
|
||||
// and passes the ReaderIOResult through unchanged.
|
||||
//
|
||||
// This function is useful for debugging and monitoring values as they flow through a ReaderIOResult
|
||||
// computation chain. Unlike SLog which logs both successes and errors, TapSLog only logs when the
|
||||
// computation is successful. If the computation contains an error, no logging occurs and the error
|
||||
// is propagated unchanged.
|
||||
// computation chain. It logs both successful values and errors at Info level. It retrieves the logger
|
||||
// using logging.GetLoggerFromContext, which returns either:
|
||||
// - The logger stored in the context via logging.WithLogger, or
|
||||
// - The global logger (set via logging.SetLogger or slog.Default())
|
||||
//
|
||||
// The logged output includes:
|
||||
// - The provided message
|
||||
// - The value being passed through (as a structured "value" attribute)
|
||||
// The ReaderIOResult is returned unchanged after logging.
|
||||
//
|
||||
// The difference between TapSLog and SLog is their position in the pipeline:
|
||||
// - SLog is a Kleisli[Result[A], A] used with Chain to intercept the raw Result
|
||||
// - TapSLog is an Operator[A, A] used directly in a pipe on a ReaderIOResult[A]
|
||||
//
|
||||
// Both log the same information (success value or error), but TapSLog is more ergonomic
|
||||
// when composing ReaderIOResult pipelines with F.Pipe.
|
||||
//
|
||||
// The message parameter becomes the main log message text, and the Result value or error
|
||||
// is attached as a structured logging attribute:
|
||||
// - For success: Logs message with attribute value=<the actual value>
|
||||
// - For error: Logs message with attribute error=<the error>
|
||||
//
|
||||
// For example, TapSLog[User]("Fetched user") with a successful result produces:
|
||||
//
|
||||
// Log message: "Fetched user"
|
||||
// Structured attribute: value={ID:123 Name:"Alice"}
|
||||
//
|
||||
// With an error result, it produces:
|
||||
//
|
||||
// Log message: "Fetched user"
|
||||
// Structured attribute: error="user not found"
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the value to log and pass through
|
||||
// - A: The success type of the ReaderIOResult to log and pass through
|
||||
//
|
||||
// Parameters:
|
||||
// - message: A descriptive message to include in the log entry
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that logs successful values and returns them unchanged
|
||||
// - An Operator that logs the Result (value or error) and returns the ReaderIOResult unchanged
|
||||
//
|
||||
// Example with simple value logging:
|
||||
//
|
||||
@@ -680,7 +707,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
// )
|
||||
//
|
||||
// result := pipeline(t.Context())()
|
||||
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // Returns: result.Of("Alice")
|
||||
//
|
||||
// Example in a processing pipeline:
|
||||
@@ -695,36 +722,36 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
// )
|
||||
//
|
||||
// result := processOrder(t.Context())()
|
||||
// // Logs each successful step with the intermediate values
|
||||
// // If any step fails, subsequent TapSLog calls don't log
|
||||
// // Logs each step with its value or error
|
||||
//
|
||||
// Example with error handling:
|
||||
//
|
||||
// pipeline := F.Pipe3(
|
||||
// fetchData(id),
|
||||
// TapSLog[Data]("Data fetched"),
|
||||
// Chain(func(d Data) ReaderIOResult[Result] {
|
||||
// Chain(func(d Data) ReaderIOResult[Data] {
|
||||
// if d.IsValid() {
|
||||
// return Of(processData(d))
|
||||
// }
|
||||
// return Left[Result](errors.New("invalid data"))
|
||||
// return Left[Data](errors.New("invalid data"))
|
||||
// }),
|
||||
// TapSLog[Result]("Data processed"),
|
||||
// TapSLog[Data]("Data processed"),
|
||||
// )
|
||||
//
|
||||
// // If fetchData succeeds: logs "Data fetched" with the data
|
||||
// // If processing succeeds: logs "Data processed" with the result
|
||||
// // If processing fails: "Data processed" is NOT logged (error propagates)
|
||||
// // If fetchData succeeds: logs "Data fetched" value={...}
|
||||
// // If fetchData fails: logs "Data fetched" error="..."
|
||||
// // If processing succeeds: logs "Data processed" value={...}
|
||||
// // If processing fails: logs "Data processed" error="invalid data"
|
||||
//
|
||||
// Use Cases:
|
||||
// - Debugging: Inspect intermediate successful values in a computation pipeline
|
||||
// - Monitoring: Track successful data flow through complex operations
|
||||
// - Troubleshooting: Identify where successful computations stop (last logged value before error)
|
||||
// - Auditing: Log important successful values for compliance or security
|
||||
// - Development: Understand data transformations during development
|
||||
// - Debugging: Inspect intermediate values and errors in a computation pipeline
|
||||
// - Monitoring: Track both successful and failed data flow through complex operations
|
||||
// - Troubleshooting: Identify where errors are introduced or propagated
|
||||
// - Auditing: Log important values and failures for compliance or security
|
||||
// - Development: Understand data transformations and error paths during development
|
||||
//
|
||||
// Note: This function only logs successful values. Errors are silently propagated without logging.
|
||||
// For logging both successes and errors, use SLog instead.
|
||||
// Note: This function logs both successful values and errors. It is equivalent to SLog
|
||||
// but expressed as an Operator for direct use in F.Pipe pipelines on ReaderIOResult values.
|
||||
//
|
||||
//go:inline
|
||||
func TapSLog[A any](message string) Operator[A, A] {
|
||||
|
||||
@@ -19,6 +19,10 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIOResult.
|
||||
@@ -45,7 +49,7 @@ import (
|
||||
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
|
||||
func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context], g func(A) B) Operator[A, B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
@@ -70,6 +74,139 @@ func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFu
|
||||
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
func Contramap[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
|
||||
func ContramapIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
|
||||
return LocalIOK[A](f)
|
||||
}
|
||||
|
||||
// LocalIOK transforms the context using an IO-based function before passing it to a ReaderIOResult.
|
||||
// This is similar to Local but the context transformation itself is wrapped in an IO effect.
|
||||
//
|
||||
// The function f takes a context and returns an IO effect that produces a ContextCancel
|
||||
// (a pair of CancelFunc and the new Context). This allows the context transformation to
|
||||
// perform side effects.
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// This function is useful for sharing information via the Context that is computed through
|
||||
// side effects that cannot fail, such as:
|
||||
// - Generating unique request IDs or trace IDs
|
||||
// - Recording timestamps or metrics
|
||||
// - Logging context information
|
||||
// - Computing derived values from existing context data
|
||||
//
|
||||
// The side effect is executed during the context transformation, and the resulting data is
|
||||
// stored in the context for downstream computations to access.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The success type (unchanged through the transformation)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: An IO-based Kleisli function that transforms the context
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - An Operator that applies the context transformation before executing the ReaderIOResult
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Generate a request ID via side effect and add to context
|
||||
// addRequestID := func(ctx context.Context) io.IO[ContextCancel] {
|
||||
// return func() ContextCancel {
|
||||
// // Side effect: generate unique ID
|
||||
// requestID := uuid.New().String()
|
||||
// // Share the ID via context
|
||||
// newCtx := context.WithValue(ctx, "requestID", requestID)
|
||||
// return pair.MakePair(func() {}, newCtx)
|
||||
// }
|
||||
// }
|
||||
// adapted := LocalIOK[int](addRequestID)(computation)
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Local: For pure context transformations
|
||||
// - LocalIOResultK: For context transformations that can fail
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
|
||||
return LocalIOResultK[A](function.Flow2(f, ioresult.FromIO))
|
||||
}
|
||||
|
||||
// LocalIOResultK transforms the context using an IOResult-based function before passing it to a ReaderIOResult.
|
||||
// This is similar to Local but the context transformation can fail with an error.
|
||||
//
|
||||
// The function f takes a context and returns an IOResult that produces either an error or a ContextCancel
|
||||
// (a pair of CancelFunc and the new Context). If the transformation fails, the error is propagated
|
||||
// and the original ReaderIOResult is not executed.
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// This function is particularly useful for sharing information via the Context that is computed
|
||||
// through side effects, such as:
|
||||
// - Loading configuration from a file or database
|
||||
// - Fetching authentication tokens from an external service
|
||||
// - Computing derived values that require I/O operations
|
||||
// - Validating and enriching context with data from external sources
|
||||
//
|
||||
// The side effect is executed during the context transformation, and the resulting data is
|
||||
// stored in the context for downstream computations to access.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The success type (unchanged through the transformation)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: An IOResult-based Kleisli function that transforms the context and may fail
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - An Operator that applies the context transformation before executing the ReaderIOResult
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Load configuration via side effect and add to context
|
||||
// loadConfig := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
|
||||
// return func() result.Result[ContextCancel] {
|
||||
// // Side effect: read from file system
|
||||
// config, err := os.ReadFile("config.json")
|
||||
// if err != nil {
|
||||
// return result.Left[ContextCancel](err)
|
||||
// }
|
||||
// // Share the loaded config via context
|
||||
// newCtx := context.WithValue(ctx, "config", config)
|
||||
// return result.Of(pair.MakePair(func() {}, newCtx))
|
||||
// }
|
||||
// }
|
||||
// adapted := LocalIOResultK[int](loadConfig)(computation)
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Local: For pure context transformations
|
||||
// - LocalIOK: For context transformations with side effects that cannot fail
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOResultK[A any](f ioresult.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
|
||||
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return func(ctx context.Context) IOResult[A] {
|
||||
return func() Result[A] {
|
||||
if ctx.Err() != nil {
|
||||
return result.Left[A](context.Cause(ctx))
|
||||
}
|
||||
p, err := result.Unwrap(f(ctx)())
|
||||
if err != nil {
|
||||
return result.Left[A](err)
|
||||
}
|
||||
// unwrap
|
||||
otherCancel, otherCtx := pair.Unpack(p)
|
||||
defer otherCancel()
|
||||
return rr(otherCtx)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/ioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -36,9 +40,9 @@ func TestPromapBasic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
newCtx := context.WithValue(ctx, "key", 42)
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
@@ -61,9 +65,9 @@ func TestContramapBasic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
newCtx := context.WithValue(ctx, "key", 100)
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
@@ -85,9 +89,9 @@ func TestLocalBasic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addUser := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
newCtx := context.WithValue(ctx, "user", "Alice")
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
|
||||
}
|
||||
|
||||
adapted := Local[string](addUser)(getValue)
|
||||
@@ -96,3 +100,311 @@ func TestLocalBasic(t *testing.T) {
|
||||
assert.Equal(t, R.Of("Alice"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOK_Success tests LocalIOK with successful context transformation
|
||||
func TestLocalIOK_Success(t *testing.T) {
|
||||
t.Run("transforms context with IO effect", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
if v := ctx.Value("user"); v != nil {
|
||||
return R.Of(v.(string))
|
||||
}
|
||||
return R.Of("unknown")
|
||||
}
|
||||
}
|
||||
|
||||
addUser := func(ctx context.Context) io.IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
newCtx := context.WithValue(ctx, "user", "Bob")
|
||||
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string](addUser)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of("Bob"), result)
|
||||
})
|
||||
|
||||
t.Run("preserves original value type", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
if v := ctx.Value("count"); v != nil {
|
||||
return R.Of(v.(int))
|
||||
}
|
||||
return R.Of(0)
|
||||
}
|
||||
}
|
||||
|
||||
addCount := func(ctx context.Context) io.IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
newCtx := context.WithValue(ctx, "count", 42)
|
||||
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[int](addCount)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of(42), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOK_CancelledContext tests LocalIOK with cancelled context
|
||||
func TestLocalIOK_CancelledContext(t *testing.T) {
|
||||
t.Run("returns error when context is cancelled", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
return R.Of("should not reach here")
|
||||
}
|
||||
}
|
||||
|
||||
addUser := func(ctx context.Context) io.IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
newCtx := context.WithValue(ctx, "user", "Charlie")
|
||||
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
adapted := LocalIOK[string](addUser)(getValue)
|
||||
result := adapted(ctx)()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOK_CancelFuncCalled tests that CancelFunc is properly called
|
||||
func TestLocalIOK_CancelFuncCalled(t *testing.T) {
|
||||
t.Run("calls cancel function after execution", func(t *testing.T) {
|
||||
cancelCalled := false
|
||||
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
return R.Of("test")
|
||||
}
|
||||
}
|
||||
|
||||
addUser := func(ctx context.Context) io.IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
newCtx := context.WithValue(ctx, "user", "Dave")
|
||||
cancelFunc := context.CancelFunc(func() {
|
||||
cancelCalled = true
|
||||
})
|
||||
return pair.MakePair(cancelFunc, newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string](addUser)(getValue)
|
||||
_ = adapted(t.Context())()
|
||||
|
||||
assert.True(t, cancelCalled, "cancel function should be called")
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOResultK_Success tests LocalIOResultK with successful context transformation
|
||||
func TestLocalIOResultK_Success(t *testing.T) {
|
||||
t.Run("transforms context with IOResult effect", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
if v := ctx.Value("role"); v != nil {
|
||||
return R.Of(v.(string))
|
||||
}
|
||||
return R.Of("guest")
|
||||
}
|
||||
}
|
||||
|
||||
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
|
||||
return func() R.Result[ContextCancel] {
|
||||
newCtx := context.WithValue(ctx, "role", "admin")
|
||||
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOResultK[string](addRole)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of("admin"), result)
|
||||
})
|
||||
|
||||
t.Run("preserves original value type", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
if v := ctx.Value("score"); v != nil {
|
||||
return R.Of(v.(int))
|
||||
}
|
||||
return R.Of(0)
|
||||
}
|
||||
}
|
||||
|
||||
addScore := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
|
||||
return func() R.Result[ContextCancel] {
|
||||
newCtx := context.WithValue(ctx, "score", 100)
|
||||
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOResultK[int](addScore)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of(100), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOResultK_Failure tests LocalIOResultK with failed context transformation
|
||||
func TestLocalIOResultK_Failure(t *testing.T) {
|
||||
t.Run("propagates transformation error", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
return R.Of("should not reach here")
|
||||
}
|
||||
}
|
||||
|
||||
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
|
||||
return func() R.Result[ContextCancel] {
|
||||
return R.Left[ContextCancel](assert.AnError)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOResultK[string](failTransform)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
_, err := R.UnwrapError(result)
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
t.Run("does not execute original computation on transformation failure", func(t *testing.T) {
|
||||
executed := false
|
||||
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
executed = true
|
||||
return R.Of("should not execute")
|
||||
}
|
||||
}
|
||||
|
||||
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
|
||||
return func() R.Result[ContextCancel] {
|
||||
return R.Left[ContextCancel](assert.AnError)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOResultK[string](failTransform)(getValue)
|
||||
_ = adapted(t.Context())()
|
||||
|
||||
assert.False(t, executed, "original computation should not execute")
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOResultK_CancelledContext tests LocalIOResultK with cancelled context
|
||||
func TestLocalIOResultK_CancelledContext(t *testing.T) {
|
||||
t.Run("returns error when context is cancelled", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
return R.Of("should not reach here")
|
||||
}
|
||||
}
|
||||
|
||||
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
|
||||
return func() R.Result[ContextCancel] {
|
||||
newCtx := context.WithValue(ctx, "role", "user")
|
||||
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
adapted := LocalIOResultK[string](addRole)(getValue)
|
||||
result := adapted(ctx)()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOResultK_CancelFuncCalled tests that CancelFunc is properly called
|
||||
func TestLocalIOResultK_CancelFuncCalled(t *testing.T) {
|
||||
t.Run("calls cancel function after successful execution", func(t *testing.T) {
|
||||
cancelCalled := false
|
||||
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
return R.Of("test")
|
||||
}
|
||||
}
|
||||
|
||||
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
|
||||
return func() R.Result[ContextCancel] {
|
||||
newCtx := context.WithValue(ctx, "role", "user")
|
||||
cancelFunc := context.CancelFunc(func() {
|
||||
cancelCalled = true
|
||||
})
|
||||
return R.Of(pair.MakePair(cancelFunc, newCtx))
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOResultK[string](addRole)(getValue)
|
||||
_ = adapted(t.Context())()
|
||||
|
||||
assert.True(t, cancelCalled, "cancel function should be called")
|
||||
})
|
||||
|
||||
t.Run("does not call cancel function on transformation failure", func(t *testing.T) {
|
||||
cancelCalled := false
|
||||
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
return R.Of("test")
|
||||
}
|
||||
}
|
||||
|
||||
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
|
||||
return func() R.Result[ContextCancel] {
|
||||
cancelFunc := context.CancelFunc(func() {
|
||||
cancelCalled = true
|
||||
})
|
||||
_ = cancelFunc // avoid unused warning
|
||||
return R.Left[ContextCancel](assert.AnError)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOResultK[string](failTransform)(getValue)
|
||||
_ = adapted(t.Context())()
|
||||
|
||||
assert.False(t, cancelCalled, "cancel function should not be called on failure")
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOResultK_Integration tests integration with other operations
|
||||
func TestLocalIOResultK_Integration(t *testing.T) {
|
||||
t.Run("composes with Map", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
if v := ctx.Value("value"); v != nil {
|
||||
return R.Of(v.(int))
|
||||
}
|
||||
return R.Of(0)
|
||||
}
|
||||
}
|
||||
|
||||
addValue := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
|
||||
return func() R.Result[ContextCancel] {
|
||||
newCtx := context.WithValue(ctx, "value", 10)
|
||||
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
|
||||
}
|
||||
}
|
||||
|
||||
double := func(x int) int { return x * 2 }
|
||||
|
||||
adapted := F.Flow2(
|
||||
LocalIOResultK[int](addValue),
|
||||
Map(double),
|
||||
)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of(20), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
@@ -452,7 +453,7 @@ func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
|
||||
// Returns a function that chains Option-returning functions into ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -800,7 +801,7 @@ func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
|
||||
func FromReaderOption[A any](onNone Lazy[error]) Kleisli[ReaderOption[context.Context, A], A] {
|
||||
return RIOR.FromReaderOption[context.Context, A](onNone)
|
||||
}
|
||||
|
||||
@@ -895,17 +896,17 @@ func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
func ChainReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
func ChainFirstReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
func TapReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -1054,14 +1055,14 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
// fetchData,
|
||||
// withTimeout,
|
||||
// )
|
||||
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
func Local[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
|
||||
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return func(ctx context.Context) IOResult[A] {
|
||||
return func() Result[A] {
|
||||
if ctx.Err() != nil {
|
||||
return result.Left[A](context.Cause(ctx))
|
||||
}
|
||||
otherCtx, otherCancel := f(ctx)
|
||||
otherCancel, otherCtx := pair.Unpack(f(ctx))
|
||||
defer otherCancel()
|
||||
return rr(otherCtx)()
|
||||
}
|
||||
@@ -1123,9 +1124,10 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// )
|
||||
// value, err := result(t.Context())() // Returns (Data{Value: "quick"}, nil)
|
||||
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
})
|
||||
return Local[A](
|
||||
func(ctx context.Context) ContextCancel {
|
||||
return pairFromContextCancel(context.WithTimeout(ctx, timeout))
|
||||
})
|
||||
}
|
||||
|
||||
// WithDeadline adds an absolute deadline to the context for a ReaderIOResult computation.
|
||||
@@ -1188,7 +1190,7 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
// )
|
||||
// value, err := result(parentCtx)() // Will use parent's 1-hour deadline
|
||||
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
|
||||
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithDeadline(ctx, deadline)
|
||||
return Local[A](func(ctx context.Context) ContextCancel {
|
||||
return pairFromContextCancel(context.WithDeadline(ctx, deadline))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,6 +55,10 @@ type (
|
||||
// Either[A] is equivalent to Either[error, A] from the either package.
|
||||
Either[A any] = either.Either[error, A]
|
||||
|
||||
// Result represents a computation that can either succeed with a value of type A
|
||||
// or fail with an error. This is an alias for result.Result[A].
|
||||
//
|
||||
// Result[A] is equivalent to Either[error, A]
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Lazy represents a deferred computation that produces a value of type A when executed.
|
||||
@@ -73,6 +77,10 @@ type (
|
||||
// IOEither[A] is equivalent to func() Either[error, A]
|
||||
IOEither[A any] = ioeither.IOEither[error, A]
|
||||
|
||||
// IOResult represents a side-effectful computation that can fail with an error.
|
||||
// This combines IO (side effects) with Result (error handling).
|
||||
//
|
||||
// IOResult[A] is equivalent to func() Result[A]
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
|
||||
// Reader represents a computation that depends on a context of type R.
|
||||
@@ -118,6 +126,13 @@ type (
|
||||
// result := fetchUser("123")(ctx)()
|
||||
ReaderIOResult[A any] = RIOR.ReaderIOResult[context.Context, A]
|
||||
|
||||
// Kleisli represents a Kleisli arrow for ReaderIOResult.
|
||||
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
|
||||
//
|
||||
// Kleisli arrows are used for monadic composition, allowing you to chain operations
|
||||
// that produce ReaderIOResults. They are particularly useful with Chain operations.
|
||||
//
|
||||
// Kleisli[A, B] is equivalent to func(A) ReaderIOResult[B]
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderIOResult[B]]
|
||||
|
||||
// Operator represents a transformation from one ReaderIOResult to another.
|
||||
@@ -133,26 +148,76 @@ type (
|
||||
// result := toUpper(computation)
|
||||
Operator[A, B any] = Kleisli[ReaderIOResult[A], B]
|
||||
|
||||
ReaderResult[A any] = readerresult.ReaderResult[A]
|
||||
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
|
||||
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
|
||||
// ReaderResult represents a context-dependent computation that can fail.
|
||||
// This is specialized to use context.Context as the context type.
|
||||
//
|
||||
// ReaderResult[A] is equivalent to func(context.Context) Result[A]
|
||||
ReaderResult[A any] = readerresult.ReaderResult[A]
|
||||
|
||||
// ReaderEither represents a context-dependent computation that can fail.
|
||||
// It takes a context of type R and produces an Either[E, A].
|
||||
//
|
||||
// ReaderEither[R, E, A] is equivalent to func(R) Either[E, A]
|
||||
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
|
||||
|
||||
// ReaderOption represents a context-dependent computation that may not produce a value.
|
||||
// It takes a context of type R and produces an Option[A].
|
||||
//
|
||||
// ReaderOption[R, A] is equivalent to func(R) Option[A]
|
||||
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
|
||||
|
||||
// Endomorphism represents a function from a type to itself.
|
||||
// It is used for transformations that preserve the type.
|
||||
//
|
||||
// Endomorphism[A] is equivalent to func(A) A
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Consumer represents a function that consumes a value without producing a result.
|
||||
// It is used for side effects like logging or updating state.
|
||||
//
|
||||
// Consumer[A] is equivalent to func(A)
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
|
||||
// Prism represents an optic for working with sum types (tagged unions).
|
||||
// It provides a way to focus on a specific variant of a sum type.
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Lens represents an optic for working with product types (records/structs).
|
||||
// It provides a way to focus on a specific field of a product type.
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Trampoline represents a computation that can be executed in a stack-safe manner.
|
||||
// It is used for tail-recursive computations that would otherwise overflow the stack.
|
||||
Trampoline[B, L any] = tailrec.Trampoline[B, L]
|
||||
|
||||
// Predicate represents a function that tests a value of type A.
|
||||
// It returns true if the value satisfies the predicate, false otherwise.
|
||||
//
|
||||
// Predicate[A] is equivalent to func(A) bool
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Pair represents a tuple of two values of types A and B.
|
||||
// It is used to group two related values together.
|
||||
Pair[A, B any] = pair.Pair[A, B]
|
||||
|
||||
// IORef represents a mutable reference that can be safely accessed in IO computations.
|
||||
// It provides thread-safe read and write operations.
|
||||
IORef[A any] = ioref.IORef[A]
|
||||
|
||||
// State represents a stateful computation that transforms a state of type S
|
||||
// and produces a value of type A.
|
||||
//
|
||||
// State[S, A] is equivalent to func(S) Pair[A, S]
|
||||
State[S, A any] = state.State[S, A]
|
||||
|
||||
// Void represents the absence of a value, similar to unit type in other languages.
|
||||
// It is used when a function performs side effects but doesn't return a meaningful value.
|
||||
Void = function.Void
|
||||
|
||||
// ContextCancel represents a pair of a cancel function and a context.
|
||||
// It is used in operations that create new contexts with cancellation capabilities.
|
||||
//
|
||||
// The first element is the CancelFunc that should be called to release resources.
|
||||
// The second element is the new Context that was created.
|
||||
ContextCancel = Pair[context.CancelFunc, context.Context]
|
||||
)
|
||||
|
||||
213
v2/context/readerreaderioresult/eitherize.go
Normal file
213
v2/context/readerreaderioresult/eitherize.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
)
|
||||
|
||||
// Eitherize converts a function that returns a value and error into a ReaderReaderIOResult.
|
||||
//
|
||||
// This function takes a function that accepts an outer context R and context.Context,
|
||||
// returning a value T and an error, and converts it into a ReaderReaderIOResult[R, T].
|
||||
// The error is automatically converted into the Left case of the Result, while successful
|
||||
// values become the Right case.
|
||||
//
|
||||
// This is particularly useful for integrating standard Go error-handling patterns into
|
||||
// the functional programming style of ReaderReaderIOResult. It is especially helpful
|
||||
// for adapting interface member functions that accept a context. When you have an
|
||||
// interface method with signature (receiver, context.Context) (T, error), you can
|
||||
// use Eitherize to convert it into a ReaderReaderIOResult where the receiver becomes
|
||||
// the outer reader context R.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - R: The outer reader context type (e.g., application configuration)
|
||||
// - T: The success value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes R and context.Context and returns (T, error)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - ReaderReaderIOResult[R, T]: A computation that depends on R and context.Context,
|
||||
// performs IO, and produces a Result[T]
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // A function using standard Go error handling
|
||||
// func fetchUser(cfg AppConfig, ctx context.Context) (*User, error) {
|
||||
// // Implementation that may return an error
|
||||
// return &User{ID: 1, Name: "Alice"}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to ReaderReaderIOResult
|
||||
// fetchUserRR := Eitherize(fetchUser)
|
||||
//
|
||||
// // Use in functional composition
|
||||
// result := F.Pipe1(
|
||||
// fetchUserRR,
|
||||
// Map[AppConfig](func(u *User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// // Execute with config and context
|
||||
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
|
||||
// outcome := result(cfg)(context.Background())()
|
||||
//
|
||||
// # Adapting Interface Methods
|
||||
//
|
||||
// Eitherize is particularly useful for adapting interface member functions:
|
||||
//
|
||||
// type UserRepository interface {
|
||||
// GetUser(ctx context.Context, id int) (*User, error)
|
||||
// }
|
||||
//
|
||||
// type UserRepo struct {
|
||||
// db *sql.DB
|
||||
// }
|
||||
//
|
||||
// func (r *UserRepo) GetUser(ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation
|
||||
// return &User{ID: id}, nil
|
||||
// }
|
||||
//
|
||||
// // Adapt the method by binding the first parameter (receiver)
|
||||
// repo := &UserRepo{db: db}
|
||||
// getUserRR := Eitherize(func(id int, ctx context.Context) (*User, error) {
|
||||
// return repo.GetUser(ctx, id)
|
||||
// })
|
||||
//
|
||||
// // Now getUserRR has type: ReaderReaderIOResult[int, *User]
|
||||
// // The receiver (repo) is captured in the closure
|
||||
// // The id becomes the outer reader context R
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Eitherize1: For functions that take an additional parameter
|
||||
// - ioresult.Eitherize2: The underlying conversion function
|
||||
func Eitherize[R, T any](f func(R, context.Context) (T, error)) ReaderReaderIOResult[R, T] {
|
||||
return F.Pipe1(
|
||||
ioresult.Eitherize2(f),
|
||||
F.Curry2,
|
||||
)
|
||||
}
|
||||
|
||||
// Eitherize1 converts a function that takes an additional parameter and returns a value
|
||||
// and error into a Kleisli arrow.
|
||||
//
|
||||
// This function takes a function that accepts an outer context R, context.Context, and
|
||||
// an additional parameter A, returning a value T and an error, and converts it into a
|
||||
// Kleisli arrow (A -> ReaderReaderIOResult[R, T]). The error is automatically converted
|
||||
// into the Left case of the Result, while successful values become the Right case.
|
||||
//
|
||||
// This is useful for creating composable operations that depend on both contexts and
|
||||
// an input value, following standard Go error-handling patterns. It is especially helpful
|
||||
// for adapting interface member functions that accept a context and additional parameters.
|
||||
// When you have an interface method with signature (receiver, context.Context, A) (T, error),
|
||||
// you can use Eitherize1 to convert it into a Kleisli arrow where the receiver becomes
|
||||
// the outer reader context R and A becomes the input parameter.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - R: The outer reader context type (e.g., application configuration)
|
||||
// - A: The input parameter type
|
||||
// - T: The success value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes R, context.Context, and A, returning (T, error)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Kleisli[R, A, T]: A function from A to ReaderReaderIOResult[R, T]
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // A function using standard Go error handling
|
||||
// func fetchUserByID(cfg AppConfig, ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation that may return an error
|
||||
// return &User{ID: id, Name: "Alice"}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to Kleisli arrow
|
||||
// fetchUserKleisli := Eitherize1(fetchUserByID)
|
||||
//
|
||||
// // Use in functional composition with Chain
|
||||
// pipeline := F.Pipe1(
|
||||
// Of[AppConfig](123),
|
||||
// Chain[AppConfig](fetchUserKleisli),
|
||||
// )
|
||||
//
|
||||
// // Execute with config and context
|
||||
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
|
||||
// outcome := pipeline(cfg)(context.Background())()
|
||||
//
|
||||
// # Adapting Interface Methods
|
||||
//
|
||||
// Eitherize1 is particularly useful for adapting interface member functions with parameters:
|
||||
//
|
||||
// type UserRepository interface {
|
||||
// GetUserByID(ctx context.Context, id int) (*User, error)
|
||||
// UpdateUser(ctx context.Context, user *User) error
|
||||
// }
|
||||
//
|
||||
// type UserRepo struct {
|
||||
// db *sql.DB
|
||||
// }
|
||||
//
|
||||
// func (r *UserRepo) GetUserByID(ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation
|
||||
// return &User{ID: id}, nil
|
||||
// }
|
||||
//
|
||||
// // Adapt the method - receiver becomes R, id becomes A
|
||||
// repo := &UserRepo{db: db}
|
||||
// getUserKleisli := Eitherize1(func(r *UserRepo, ctx context.Context, id int) (*User, error) {
|
||||
// return r.GetUserByID(ctx, id)
|
||||
// })
|
||||
//
|
||||
// // Now getUserKleisli has type: Kleisli[*UserRepo, int, *User]
|
||||
// // Which is: func(int) ReaderReaderIOResult[*UserRepo, *User]
|
||||
// // Use it in composition:
|
||||
// pipeline := F.Pipe1(
|
||||
// Of[*UserRepo](123),
|
||||
// Chain[*UserRepo](getUserKleisli),
|
||||
// )
|
||||
// result := pipeline(repo)(context.Background())()
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Eitherize: For functions without an additional parameter
|
||||
// - Chain: For composing Kleisli arrows
|
||||
// - ioresult.Eitherize3: The underlying conversion function
|
||||
func Eitherize1[R, A, T any](f func(R, context.Context, A) (T, error)) Kleisli[R, A, T] {
|
||||
return F.Flow2(
|
||||
F.Bind3of3(ioresult.Eitherize3(f)),
|
||||
F.Curry2,
|
||||
)
|
||||
}
|
||||
507
v2/context/readerreaderioresult/eitherize_test.go
Normal file
507
v2/context/readerreaderioresult/eitherize_test.go
Normal file
@@ -0,0 +1,507 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestConfig struct {
|
||||
Prefix string
|
||||
MaxLen int
|
||||
}
|
||||
|
||||
var testConfig = TestConfig{
|
||||
Prefix: "test",
|
||||
MaxLen: 100,
|
||||
}
|
||||
|
||||
// TestEitherize_Success tests successful conversion with Eitherize
|
||||
func TestEitherize_Success(t *testing.T) {
|
||||
t.Run("converts successful function to ReaderReaderIOResult", func(t *testing.T) {
|
||||
// Arrange
|
||||
successFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return cfg.Prefix + "-success", nil
|
||||
}
|
||||
rr := Eitherize(successFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("test-success"), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves context values", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ctxKey string
|
||||
key := ctxKey("testKey")
|
||||
expectedValue := "contextValue"
|
||||
|
||||
contextFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
value := ctx.Value(key)
|
||||
if value == nil {
|
||||
return "", errors.New("context value not found")
|
||||
}
|
||||
return value.(string), nil
|
||||
}
|
||||
rr := Eitherize(contextFunc)
|
||||
|
||||
ctx := context.WithValue(context.Background(), key, expectedValue)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(ctx)()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(expectedValue), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// Arrange
|
||||
intFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return cfg.MaxLen, nil
|
||||
}
|
||||
rr := Eitherize(intFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(100), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_Failure tests error handling with Eitherize
|
||||
func TestEitherize_Failure(t *testing.T) {
|
||||
t.Run("converts error to Left", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := errors.New("operation failed")
|
||||
failFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return "", expectedErr
|
||||
}
|
||||
rr := Eitherize(failFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
assert.Equal(t, result.Left[string](expectedErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error message", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := fmt.Errorf("validation error: field is required")
|
||||
failFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return 0, expectedErr
|
||||
}
|
||||
rr := Eitherize(failFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
leftValue := result.MonadFold(outcome,
|
||||
F.Identity[error],
|
||||
func(int) error { return nil },
|
||||
)
|
||||
assert.Equal(t, expectedErr, leftValue)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_EdgeCases tests edge cases for Eitherize
|
||||
func TestEitherize_EdgeCases(t *testing.T) {
|
||||
t.Run("handles nil context", func(t *testing.T) {
|
||||
// Arrange
|
||||
nilCtxFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
if ctx == nil {
|
||||
return "nil-context", nil
|
||||
}
|
||||
return "non-nil-context", nil
|
||||
}
|
||||
rr := Eitherize(nilCtxFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(nil)()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("nil-context"), outcome)
|
||||
})
|
||||
|
||||
t.Run("handles zero value config", func(t *testing.T) {
|
||||
// Arrange
|
||||
zeroFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return cfg.Prefix, nil
|
||||
}
|
||||
rr := Eitherize(zeroFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(TestConfig{})(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(""), outcome)
|
||||
})
|
||||
|
||||
t.Run("handles pointer types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context) (*User, error) {
|
||||
return &User{Name: "Alice"}, nil
|
||||
}
|
||||
rr := Eitherize(ptrFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsRight(outcome))
|
||||
user := result.MonadFold(outcome,
|
||||
func(error) *User { return nil },
|
||||
F.Identity[*User],
|
||||
)
|
||||
assert.NotNil(t, user)
|
||||
assert.Equal(t, "Alice", user.Name)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_Integration tests integration with other operations
|
||||
func TestEitherize_Integration(t *testing.T) {
|
||||
t.Run("composes with Map", func(t *testing.T) {
|
||||
// Arrange
|
||||
baseFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
rr := Eitherize(baseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
rr,
|
||||
Map[TestConfig](func(n int) string { return strconv.Itoa(n) }),
|
||||
)
|
||||
outcome := pipeline(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("42"), outcome)
|
||||
})
|
||||
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
firstFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return 10, nil
|
||||
}
|
||||
secondFunc := func(n int) ReaderReaderIOResult[TestConfig, string] {
|
||||
return Of[TestConfig](fmt.Sprintf("value: %d", n))
|
||||
}
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
Eitherize(firstFunc),
|
||||
Chain[TestConfig](secondFunc),
|
||||
)
|
||||
outcome := pipeline(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("value: 10"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Success tests successful conversion with Eitherize1
|
||||
func TestEitherize1_Success(t *testing.T) {
|
||||
t.Run("converts successful function to Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
addFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
return n + cfg.MaxLen, nil
|
||||
}
|
||||
kleisli := Eitherize1(addFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(10)(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(110), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with string input", func(t *testing.T) {
|
||||
// Arrange
|
||||
concatFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
|
||||
return cfg.Prefix + "-" + s, nil
|
||||
}
|
||||
kleisli := Eitherize1(concatFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli("input")(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("test-input"), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves context in Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ctxKey string
|
||||
key := ctxKey("multiplier")
|
||||
|
||||
multiplyFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
multiplier := ctx.Value(key)
|
||||
if multiplier == nil {
|
||||
return n, nil
|
||||
}
|
||||
return n * multiplier.(int), nil
|
||||
}
|
||||
kleisli := Eitherize1(multiplyFunc)
|
||||
|
||||
ctx := context.WithValue(context.Background(), key, 3)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(5)(testConfig)(ctx)()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(15), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Failure tests error handling with Eitherize1
|
||||
func TestEitherize1_Failure(t *testing.T) {
|
||||
t.Run("converts error to Left in Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := errors.New("division by zero")
|
||||
divideFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
if n == 0 {
|
||||
return 0, expectedErr
|
||||
}
|
||||
return 100 / n, nil
|
||||
}
|
||||
kleisli := Eitherize1(divideFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(0)(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
assert.Equal(t, result.Left[int](expectedErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error context", func(t *testing.T) {
|
||||
// Arrange
|
||||
validateFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
|
||||
if len(s) > cfg.MaxLen {
|
||||
return "", fmt.Errorf("string too long: %d > %d", len(s), cfg.MaxLen)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
kleisli := Eitherize1(validateFunc)
|
||||
|
||||
longString := string(make([]byte, 200))
|
||||
|
||||
// Act
|
||||
outcome := kleisli(longString)(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
leftValue := result.MonadFold(outcome,
|
||||
F.Identity[error],
|
||||
func(string) error { return nil },
|
||||
)
|
||||
assert.Contains(t, leftValue.Error(), "string too long")
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_EdgeCases tests edge cases for Eitherize1
|
||||
func TestEitherize1_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero value input", func(t *testing.T) {
|
||||
// Arrange
|
||||
zeroFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
kleisli := Eitherize1(zeroFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(0)(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
|
||||
t.Run("handles pointer input", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
Value int
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
|
||||
if in == nil {
|
||||
return 0, errors.New("nil input")
|
||||
}
|
||||
return in.Value, nil
|
||||
}
|
||||
kleisli := Eitherize1(ptrFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(&Input{Value: 42})(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("handles nil pointer input", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
Value int
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
|
||||
if in == nil {
|
||||
return 0, errors.New("nil input")
|
||||
}
|
||||
return in.Value, nil
|
||||
}
|
||||
kleisli := Eitherize1(ptrFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli((*Input)(nil))(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Integration tests integration with other operations
|
||||
func TestEitherize1_Integration(t *testing.T) {
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
doubleFunc := func(n int) ReaderReaderIOResult[TestConfig, int] {
|
||||
return Of[TestConfig](n * 2)
|
||||
}
|
||||
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe2(
|
||||
Of[TestConfig]("42"),
|
||||
Chain[TestConfig](parseKleisli),
|
||||
Chain[TestConfig](doubleFunc),
|
||||
)
|
||||
outcome := pipeline(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(84), outcome)
|
||||
})
|
||||
|
||||
t.Run("handles error in chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
Of[TestConfig]("not-a-number"),
|
||||
Chain(parseKleisli),
|
||||
)
|
||||
outcome := pipeline(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
|
||||
t.Run("composes multiple Kleisli arrows", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
formatFunc := func(cfg TestConfig, ctx context.Context, n int) (string, error) {
|
||||
return fmt.Sprintf("%s-%d", cfg.Prefix, n), nil
|
||||
}
|
||||
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
formatKleisli := Eitherize1(formatFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe2(
|
||||
Of[TestConfig]("123"),
|
||||
Chain[TestConfig](parseKleisli),
|
||||
Chain[TestConfig](formatKleisli),
|
||||
)
|
||||
outcome := pipeline(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("test-123"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_TypeSafety tests type safety across different scenarios
|
||||
func TestEitherize_TypeSafety(t *testing.T) {
|
||||
t.Run("Eitherize with complex types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ComplexResult struct {
|
||||
Data map[string]int
|
||||
Count int
|
||||
}
|
||||
|
||||
complexFunc := func(cfg TestConfig, ctx context.Context) (ComplexResult, error) {
|
||||
return ComplexResult{
|
||||
Data: map[string]int{"key": 42},
|
||||
Count: 1,
|
||||
}, nil
|
||||
}
|
||||
rr := Eitherize(complexFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsRight(outcome))
|
||||
value := result.MonadFold(outcome,
|
||||
func(error) ComplexResult { return ComplexResult{} },
|
||||
F.Identity[ComplexResult],
|
||||
)
|
||||
assert.Equal(t, 42, value.Data["key"])
|
||||
assert.Equal(t, 1, value.Count)
|
||||
})
|
||||
|
||||
t.Run("Eitherize1 with different input and output types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
ID int
|
||||
}
|
||||
type Output struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
convertFunc := func(cfg TestConfig, ctx context.Context, in Input) (Output, error) {
|
||||
return Output{Name: fmt.Sprintf("%s-%d", cfg.Prefix, in.ID)}, nil
|
||||
}
|
||||
kleisli := Eitherize1(convertFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(Input{ID: 99})(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(Output{Name: "test-99"}), outcome)
|
||||
})
|
||||
}
|
||||
@@ -87,8 +87,8 @@ var (
|
||||
// assembleProviders constructs the provider map for item and non-item providers
|
||||
assembleProviders = F.Flow3(
|
||||
A.Partition(isItemProvider),
|
||||
T.Map2(collectProviders, collectItemProviders),
|
||||
T.Tupled2(mergeProviders.Concat),
|
||||
pair.BiMap(collectProviders, collectItemProviders),
|
||||
pair.Paired(mergeProviders.Concat),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -25,6 +25,31 @@ import (
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// Do creates an Effect with an initial state value.
|
||||
// This is the starting point for do-notation style effect composition,
|
||||
// allowing you to build up complex state transformations step by step.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - S: The state type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - empty: The initial state value
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, S]: An effect that produces the initial state
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
// eff := effect.Do[MyContext](State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[C, S any](
|
||||
empty S,
|
||||
@@ -32,6 +57,40 @@ func Do[C, S any](
|
||||
return readerreaderioresult.Of[C](empty)
|
||||
}
|
||||
|
||||
// Bind executes an effectful computation and binds its result to the state.
|
||||
// This is the core operation for do-notation, allowing you to sequence effects
|
||||
// while accumulating results in a state structure.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effects
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of value produced by the effect
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - setter: A function that takes the effect result and returns a state updater
|
||||
// - f: An effectful computation that depends on the current state
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, S1, S2]: A function that transforms the state effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// eff := effect.Bind(
|
||||
// func(age int) func(State) State {
|
||||
// return func(s State) State {
|
||||
// s.Age = age
|
||||
// return s
|
||||
// }
|
||||
// },
|
||||
// func(s State) Effect[MyContext, int] {
|
||||
// return effect.Of[MyContext](30)
|
||||
// },
|
||||
// )(effect.Do[MyContext](State{}))
|
||||
//
|
||||
//go:inline
|
||||
func Bind[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
@@ -40,6 +99,39 @@ func Bind[C, S1, S2, T any](
|
||||
return readerreaderioresult.Bind(setter, f)
|
||||
}
|
||||
|
||||
// Let computes a pure value from the current state and binds it to the state.
|
||||
// Unlike Bind, this doesn't perform any effects - it's for pure computations.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of computed value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - setter: A function that takes the computed value and returns a state updater
|
||||
// - f: A pure function that computes a value from the current state
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, S1, S2]: A function that transforms the state effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// eff := effect.Let[MyContext](
|
||||
// func(nameLen int) func(State) State {
|
||||
// return func(s State) State {
|
||||
// s.NameLength = nameLen
|
||||
// return s
|
||||
// }
|
||||
// },
|
||||
// func(s State) int {
|
||||
// return len(s.Name)
|
||||
// },
|
||||
// )(stateEff)
|
||||
//
|
||||
//go:inline
|
||||
func Let[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
@@ -48,6 +140,37 @@ func Let[C, S1, S2, T any](
|
||||
return readerreaderioresult.Let[C](setter, f)
|
||||
}
|
||||
|
||||
// LetTo binds a constant value to the state.
|
||||
// This is useful for setting fixed values in your state structure.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of the constant value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - setter: A function that takes the constant and returns a state updater
|
||||
// - b: The constant value to bind
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, S1, S2]: A function that transforms the state effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// eff := effect.LetTo[MyContext](
|
||||
// func(age int) func(State) State {
|
||||
// return func(s State) State {
|
||||
// s.Age = age
|
||||
// return s
|
||||
// }
|
||||
// },
|
||||
// 42,
|
||||
// )(stateEff)
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
@@ -56,6 +179,30 @@ func LetTo[C, S1, S2, T any](
|
||||
return readerreaderioresult.LetTo[C](setter, b)
|
||||
}
|
||||
|
||||
// BindTo wraps a value in an initial state structure.
|
||||
// This is typically used to start a bind chain by converting a simple value
|
||||
// into a state structure.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - S1: The state type to create
|
||||
// - T: The type of the input value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - setter: A function that creates a state from the value
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, T, S1]: A function that wraps the value in state
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// eff := effect.BindTo[MyContext](func(name string) State {
|
||||
// return State{Name: name}
|
||||
// })(effect.Of[MyContext]("Alice"))
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[C, S1, T any](
|
||||
setter func(T) S1,
|
||||
@@ -63,6 +210,39 @@ func BindTo[C, S1, T any](
|
||||
return readerreaderioresult.BindTo[C](setter)
|
||||
}
|
||||
|
||||
// ApS applies an effect and binds its result to the state using a setter function.
|
||||
// This is similar to Bind but takes a pre-existing effect rather than a function
|
||||
// that creates an effect from the state.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effects
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of value produced by the effect
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - setter: A function that takes the effect result and returns a state updater
|
||||
// - fa: The effect to apply
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, S1, S2]: A function that transforms the state effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// ageEffect := effect.Of[MyContext](30)
|
||||
// eff := effect.ApS(
|
||||
// func(age int) func(State) State {
|
||||
// return func(s State) State {
|
||||
// s.Age = age
|
||||
// return s
|
||||
// }
|
||||
// },
|
||||
// ageEffect,
|
||||
// )(stateEff)
|
||||
//
|
||||
//go:inline
|
||||
func ApS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
@@ -71,6 +251,33 @@ func ApS[C, S1, S2, T any](
|
||||
return readerreaderioresult.ApS(setter, fa)
|
||||
}
|
||||
|
||||
// ApSL applies an effect and updates a field in the state using a lens.
|
||||
// This provides a more ergonomic way to update nested state structures.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effects
|
||||
// - S: The state type
|
||||
// - T: The type of the field being updated
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - lens: A lens focusing on the field to update
|
||||
// - fa: The effect producing the new field value
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, S, S]: A function that updates the state field
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(s State) int { return s.Age },
|
||||
// func(s State, age int) State { s.Age = age; return s },
|
||||
// )
|
||||
// ageEffect := effect.Of[MyContext](30)
|
||||
// eff := effect.ApSL(ageLens, ageEffect)(stateEff)
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
@@ -79,6 +286,37 @@ func ApSL[C, S, T any](
|
||||
return readerreaderioresult.ApSL(lens, fa)
|
||||
}
|
||||
|
||||
// BindL executes an effectful computation on a field and updates it using a lens.
|
||||
// The effect function receives the current field value and produces a new value.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effects
|
||||
// - S: The state type
|
||||
// - T: The type of the field being updated
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - lens: A lens focusing on the field to update
|
||||
// - f: An effectful function that transforms the field value
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, S, S]: A function that updates the state field
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(s State) int { return s.Age },
|
||||
// func(s State, age int) State { s.Age = age; return s },
|
||||
// )
|
||||
// eff := effect.BindL(
|
||||
// ageLens,
|
||||
// func(age int) Effect[MyContext, int] {
|
||||
// return effect.Of[MyContext](age + 1)
|
||||
// },
|
||||
// )(stateEff)
|
||||
//
|
||||
//go:inline
|
||||
func BindL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
@@ -87,6 +325,35 @@ func BindL[C, S, T any](
|
||||
return readerreaderioresult.BindL(lens, f)
|
||||
}
|
||||
|
||||
// LetL computes a new field value from the current value using a lens.
|
||||
// This is a pure transformation of a field within the state.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - S: The state type
|
||||
// - T: The type of the field being updated
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - lens: A lens focusing on the field to update
|
||||
// - f: A pure function that transforms the field value
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, S, S]: A function that updates the state field
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(s State) int { return s.Age },
|
||||
// func(s State, age int) State { s.Age = age; return s },
|
||||
// )
|
||||
// eff := effect.LetL[MyContext](
|
||||
// ageLens,
|
||||
// func(age int) int { return age * 2 },
|
||||
// )(stateEff)
|
||||
//
|
||||
//go:inline
|
||||
func LetL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
@@ -95,6 +362,31 @@ func LetL[C, S, T any](
|
||||
return readerreaderioresult.LetL[C](lens, f)
|
||||
}
|
||||
|
||||
// LetToL sets a field to a constant value using a lens.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - S: The state type
|
||||
// - T: The type of the field being updated
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - lens: A lens focusing on the field to update
|
||||
// - b: The constant value to set
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, S, S]: A function that updates the state field
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(s State) int { return s.Age },
|
||||
// func(s State, age int) State { s.Age = age; return s },
|
||||
// )
|
||||
// eff := effect.LetToL[MyContext](ageLens, 42)(stateEff)
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
|
||||
32
v2/effect/common_test.go
Normal file
32
v2/effect/common_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// TestContext is a common test context type used across effect tests
|
||||
type TestContext struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
// runEffect is a helper function to run an effect with a context and return the result
|
||||
func runEffect[C, A any](eff Effect[C, A], ctx C) (A, error) {
|
||||
ioResult := Provide[A](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
return readerResult(context.Background())
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
@@ -8,31 +23,182 @@ import (
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Local transforms the context required by an effect using a pure function.
|
||||
// This allows you to adapt an effect that requires one context type to work
|
||||
// with a different context type by providing a transformation function.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C1: The outer context type (what you have)
|
||||
// - C2: The inner context type (what the effect needs)
|
||||
// - A: The value type produced by the effect
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - acc: A pure function that transforms C1 to C2
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Kleisli[C1, Effect[C2, A], A]: A function that adapts the effect to use C1
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// type AppConfig struct { DB DatabaseConfig }
|
||||
// type DatabaseConfig struct { Host string }
|
||||
// dbEffect := effect.Of[DatabaseConfig]("connected")
|
||||
// appEffect := effect.Local[AppConfig, DatabaseConfig, string](
|
||||
// func(app AppConfig) DatabaseConfig { return app.DB },
|
||||
// )(dbEffect)
|
||||
//
|
||||
//go:inline
|
||||
func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
|
||||
func Local[A, C1, C2 any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
|
||||
return readerreaderioresult.Local[A](acc)
|
||||
}
|
||||
|
||||
// Contramap is an alias for Local, following the contravariant functor naming convention.
|
||||
// It transforms the context required by an effect using a pure function.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C1: The outer context type (what you have)
|
||||
// - C2: The inner context type (what the effect needs)
|
||||
// - A: The value type produced by the effect
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - acc: A pure function that transforms C1 to C2
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Kleisli[C1, Effect[C2, A], A]: A function that adapts the effect to use C1
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
|
||||
func Contramap[A, C1, C2 any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
|
||||
return readerreaderioresult.Local[A](acc)
|
||||
}
|
||||
|
||||
// LocalIOK transforms the context using an IO-based function.
|
||||
// This allows the context transformation itself to perform I/O operations.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The value type produced by the effect
|
||||
// - C1: The inner context type (what the effect needs)
|
||||
// - C2: The outer context type (what you have)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: An IO function that transforms C2 to C1
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// loadConfig := func(path string) io.IO[Config] {
|
||||
// return func() Config { /* load from file */ }
|
||||
// }
|
||||
// transform := effect.LocalIOK[string](loadConfig)
|
||||
// adapted := transform(configEffect)
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOK[A, C1, C2 any](f io.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalIOK[A](f)
|
||||
}
|
||||
|
||||
// LocalIOResultK transforms the context using an IOResult-based function.
|
||||
// This allows the context transformation to perform I/O and handle errors.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The value type produced by the effect
|
||||
// - C1: The inner context type (what the effect needs)
|
||||
// - C2: The outer context type (what you have)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: An IOResult function that transforms C2 to C1
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// loadConfig := func(path string) ioresult.IOResult[Config] {
|
||||
// return func() result.Result[Config] {
|
||||
// // load from file, may fail
|
||||
// }
|
||||
// }
|
||||
// transform := effect.LocalIOResultK[string](loadConfig)
|
||||
// adapted := transform(configEffect)
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOResultK[A, C1, C2 any](f ioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalIOResultK[A](f)
|
||||
}
|
||||
|
||||
// LocalResultK transforms the context using a Result-based function.
|
||||
// This allows the context transformation to fail with an error.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The value type produced by the effect
|
||||
// - C1: The inner context type (what the effect needs)
|
||||
// - C2: The outer context type (what you have)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Result function that transforms C2 to C1
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// validateConfig := func(raw RawConfig) result.Result[Config] {
|
||||
// if raw.IsValid() {
|
||||
// return result.Of(raw.ToConfig())
|
||||
// }
|
||||
// return result.Left[Config](errors.New("invalid"))
|
||||
// }
|
||||
// transform := effect.LocalResultK[string](validateConfig)
|
||||
// adapted := transform(configEffect)
|
||||
//
|
||||
//go:inline
|
||||
func LocalResultK[A, C1, C2 any](f result.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalResultK[A](f)
|
||||
}
|
||||
|
||||
// LocalThunkK transforms the context using a Thunk (ReaderIOResult) function.
|
||||
// This allows the context transformation to depend on context.Context, perform I/O, and handle errors.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The value type produced by the effect
|
||||
// - C1: The inner context type (what the effect needs)
|
||||
// - C2: The outer context type (what you have)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Thunk function that transforms C2 to C1
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// loadConfig := func(path string) readerioresult.ReaderIOResult[Config] {
|
||||
// return func(ctx context.Context) ioresult.IOResult[Config] {
|
||||
// // load from file with context, may fail
|
||||
// }
|
||||
// }
|
||||
// transform := effect.LocalThunkK[string](loadConfig)
|
||||
// adapted := transform(configEffect)
|
||||
//
|
||||
//go:inline
|
||||
func LocalThunkK[A, C1, C2 any](f thunk.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalReaderIOResultK[A](f)
|
||||
|
||||
@@ -44,11 +44,11 @@ func TestLocal(t *testing.T) {
|
||||
}
|
||||
|
||||
// Apply Local to transform the context
|
||||
kleisli := Local[OuterContext, InnerContext, string](accessor)
|
||||
kleisli := Local[string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
// Run with OuterContext
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
ioResult := Provide[string](OuterContext{
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
@@ -70,11 +70,11 @@ func TestLocal(t *testing.T) {
|
||||
return InnerContext{Value: outer.Value + " transformed"}
|
||||
}
|
||||
|
||||
kleisli := Local[OuterContext, InnerContext, string](accessor)
|
||||
kleisli := Local[string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
// Run with OuterContext
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
ioResult := Provide[string](OuterContext{
|
||||
Value: "original",
|
||||
Number: 100,
|
||||
})(outerEffect)
|
||||
@@ -93,10 +93,10 @@ func TestLocal(t *testing.T) {
|
||||
return InnerContext{Value: outer.Value}
|
||||
}
|
||||
|
||||
kleisli := Local[OuterContext, InnerContext, string](accessor)
|
||||
kleisli := Local[string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
ioResult := Provide[string](OuterContext{
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
@@ -122,12 +122,12 @@ func TestLocal(t *testing.T) {
|
||||
level3Effect := Of[Level3]("deep result")
|
||||
|
||||
// Transform Level2 -> Level3
|
||||
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
|
||||
local23 := Local[string](func(l2 Level2) Level3 {
|
||||
return Level3{C: l2.B + "-c"}
|
||||
})
|
||||
|
||||
// Transform Level1 -> Level2
|
||||
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
|
||||
local12 := Local[string](func(l1 Level1) Level2 {
|
||||
return Level2{B: l1.A + "-b"}
|
||||
})
|
||||
|
||||
@@ -136,7 +136,7 @@ func TestLocal(t *testing.T) {
|
||||
level1Effect := local12(level2Effect)
|
||||
|
||||
// Run with Level1 context
|
||||
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
|
||||
ioResult := Provide[string](Level1{A: "a"})(level1Effect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -165,11 +165,11 @@ func TestLocal(t *testing.T) {
|
||||
return app.DB
|
||||
}
|
||||
|
||||
kleisli := Local[AppConfig, DatabaseConfig, string](accessor)
|
||||
kleisli := Local[string](accessor)
|
||||
appEffect := kleisli(dbEffect)
|
||||
|
||||
// Run with full AppConfig
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
ioResult := Provide[string](AppConfig{
|
||||
DB: DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
@@ -195,21 +195,21 @@ func TestContramap(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test Local
|
||||
localKleisli := Local[OuterContext, InnerContext, int](accessor)
|
||||
localKleisli := Local[int](accessor)
|
||||
localEffect := localKleisli(innerEffect)
|
||||
|
||||
// Test Contramap
|
||||
contramapKleisli := Contramap[OuterContext, InnerContext, int](accessor)
|
||||
contramapKleisli := Contramap[int](accessor)
|
||||
contramapEffect := contramapKleisli(innerEffect)
|
||||
|
||||
outerCtx := OuterContext{Value: "test", Number: 100}
|
||||
|
||||
// Run both
|
||||
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
|
||||
localIO := Provide[int](outerCtx)(localEffect)
|
||||
localReader := RunSync(localIO)
|
||||
localResult, localErr := localReader(context.Background())
|
||||
|
||||
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
|
||||
contramapIO := Provide[int](outerCtx)(contramapEffect)
|
||||
contramapReader := RunSync(contramapIO)
|
||||
contramapResult, contramapErr := contramapReader(context.Background())
|
||||
|
||||
@@ -225,10 +225,10 @@ func TestContramap(t *testing.T) {
|
||||
return InnerContext{Value: outer.Value + " modified"}
|
||||
}
|
||||
|
||||
kleisli := Contramap[OuterContext, InnerContext, string](accessor)
|
||||
kleisli := Contramap[string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
ioResult := Provide[string](OuterContext{
|
||||
Value: "original",
|
||||
Number: 50,
|
||||
})(outerEffect)
|
||||
@@ -247,10 +247,10 @@ func TestContramap(t *testing.T) {
|
||||
return InnerContext{Value: outer.Value}
|
||||
}
|
||||
|
||||
kleisli := Contramap[OuterContext, InnerContext, int](accessor)
|
||||
kleisli := Contramap[int](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterContext, int](OuterContext{
|
||||
ioResult := Provide[int](OuterContext{
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
@@ -278,12 +278,12 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
|
||||
effect3 := Of[Config3]("result")
|
||||
|
||||
// Use Local for first transformation
|
||||
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
|
||||
local23 := Local[string](func(c2 Config2) Config3 {
|
||||
return Config3{Info: c2.Data}
|
||||
})
|
||||
|
||||
// Use Contramap for second transformation
|
||||
contramap12 := Contramap[Config1, Config2, string](func(c1 Config1) Config2 {
|
||||
contramap12 := Contramap[string](func(c1 Config1) Config2 {
|
||||
return Config2{Data: c1.Value}
|
||||
})
|
||||
|
||||
@@ -292,7 +292,7 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
|
||||
effect1 := contramap12(effect2)
|
||||
|
||||
// Run
|
||||
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
|
||||
ioResult := Provide[string](Config1{Value: "test"})(effect1)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -326,7 +326,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
appEffect := transform(dbEffect)
|
||||
|
||||
// Run with AppConfig
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
ioResult := Provide[string](AppConfig{
|
||||
ConfigPath: "/etc/app.conf",
|
||||
})(appEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
@@ -356,7 +356,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
transform := LocalEffectK[string](failingTransform)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
|
||||
ioResult := Provide[string](OuterCtx{Path: "test"})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
@@ -384,7 +384,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
transformK := LocalEffectK[string](transform)
|
||||
outerEffect := transformK(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
|
||||
ioResult := Provide[string](OuterCtx{Path: "test"})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
@@ -417,7 +417,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
transform := LocalEffectK[string](loadConfigEffect)
|
||||
appEffect := transform(configEffect)
|
||||
|
||||
ioResult := Provide[AppContext, string](AppContext{
|
||||
ioResult := Provide[string](AppContext{
|
||||
ConfigFile: "config.json",
|
||||
})(appEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
@@ -456,7 +456,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
level1Effect := transform12(level2Effect)
|
||||
|
||||
// Run with Level1 context
|
||||
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
|
||||
ioResult := Provide[string](Level1{A: "a"})(level1Effect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -497,7 +497,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
transform := LocalEffectK[string](transformWithContext)
|
||||
appEffect := transform(dbEffect)
|
||||
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
ioResult := Provide[string](AppConfig{
|
||||
Environment: "prod",
|
||||
DBHost: "localhost",
|
||||
DBPort: 5432,
|
||||
@@ -534,14 +534,14 @@ func TestLocalEffectK(t *testing.T) {
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
// Test with invalid config
|
||||
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
|
||||
ioResult := Provide[string](RawConfig{APIKey: ""})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test with valid config
|
||||
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
|
||||
ioResult2 := Provide[string](RawConfig{APIKey: "valid-key"})(outerEffect)
|
||||
readerResult2 := RunSync(ioResult2)
|
||||
result, err2 := readerResult2(context.Background())
|
||||
|
||||
@@ -569,7 +569,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
})
|
||||
|
||||
// Use Local for second transformation (pure)
|
||||
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
|
||||
local12 := Local[string](func(l1 Level1) Level2 {
|
||||
return Level2{Data: l1.Value}
|
||||
})
|
||||
|
||||
@@ -578,7 +578,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
effect1 := local12(effect2)
|
||||
|
||||
// Run
|
||||
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
|
||||
ioResult := Provide[string](Level1{Value: "test"})(effect1)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -610,7 +610,7 @@ func TestLocalEffectK(t *testing.T) {
|
||||
transform := LocalEffectK[int](complexTransform)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
|
||||
ioResult := Provide[int](OuterCtx{Multiplier: 3})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
|
||||
@@ -1,51 +1,614 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// FromThunk lifts a Thunk (context-independent IO computation with error handling) into an Effect.
|
||||
// This allows you to integrate computations that don't need the effect's context type C
|
||||
// into effect chains. The Thunk will be executed with the runtime context when the effect runs.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect (not used by the thunk)
|
||||
// - A: The type of the success value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Thunk[A] that performs IO with error handling
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, A]: An effect that ignores its context and executes the thunk
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// thunk := func(ctx context.Context) io.IO[result.Result[int]] {
|
||||
// return func() result.Result[int] {
|
||||
// // Perform IO operation
|
||||
// return result.Of(42)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.FromThunk[MyContext](thunk)
|
||||
// // eff can be used in any context but executes the thunk
|
||||
//
|
||||
//go:inline
|
||||
func FromThunk[C, A any](f Thunk[A]) Effect[C, A] {
|
||||
return reader.Of[C](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromResult[C, A any](r Result[A]) Effect[C, A] {
|
||||
return readerreaderioresult.FromEither[C](r)
|
||||
}
|
||||
|
||||
// Succeed creates a successful Effect that produces the given value.
|
||||
// This is the primary way to lift a pure value into the Effect context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The type of the success value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - a: The value to wrap in a successful effect
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, A]: An effect that always succeeds with the given value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// eff := effect.Succeed[MyContext](42)
|
||||
// result, err := runEffect(eff, myContext)
|
||||
// // result == 42, err == nil
|
||||
func Succeed[C, A any](a A) Effect[C, A] {
|
||||
return readerreaderioresult.Of[C](a)
|
||||
}
|
||||
|
||||
// Fail creates a failed Effect with the given error.
|
||||
// This is used to represent computations that have failed.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The type of the success value (never produced)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - err: The error that caused the failure
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, A]: An effect that always fails with the given error
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// eff := effect.Fail[MyContext, int](errors.New("failed"))
|
||||
// _, err := runEffect(eff, myContext)
|
||||
// // err == errors.New("failed")
|
||||
func Fail[C, A any](err error) Effect[C, A] {
|
||||
return readerreaderioresult.Left[C, A](err)
|
||||
}
|
||||
|
||||
// Of creates a successful Effect that produces the given value.
|
||||
// This is an alias for Succeed and follows the pointed functor convention.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The type of the success value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - a: The value to wrap in a successful effect
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, A]: An effect that always succeeds with the given value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// eff := effect.Of[MyContext]("hello")
|
||||
// result, err := runEffect(eff, myContext)
|
||||
// // result == "hello", err == nil
|
||||
func Of[C, A any](a A) Effect[C, A] {
|
||||
return readerreaderioresult.Of[C](a)
|
||||
}
|
||||
|
||||
// Map transforms the success value of an Effect using the provided function.
|
||||
// If the effect fails, the error is propagated unchanged.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: The transformation function to apply to the success value
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, B]: A function that transforms Effect[C, A] to Effect[C, B]
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// mapped := effect.Map[MyContext](func(x int) string {
|
||||
// return strconv.Itoa(x)
|
||||
// })(eff)
|
||||
// // mapped produces "42"
|
||||
func Map[C, A, B any](f func(A) B) Operator[C, A, B] {
|
||||
return readerreaderioresult.Map[C](f)
|
||||
}
|
||||
|
||||
// Chain sequences two effects, where the second effect depends on the result of the first.
|
||||
// This is the monadic bind operation (flatMap) for effects.
|
||||
// If the first effect fails, the second is not executed.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effects
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes the result of the first effect and returns a new effect
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, B]: A function that transforms Effect[C, A] to Effect[C, B]
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// chained := effect.Chain[MyContext](func(x int) Effect[MyContext, string] {
|
||||
// return effect.Of[MyContext](strconv.Itoa(x * 2))
|
||||
// })(eff)
|
||||
// // chained produces "84"
|
||||
//
|
||||
//go:inline
|
||||
func Chain[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, B] {
|
||||
return readerreaderioresult.Chain(f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirst[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, A] {
|
||||
return readerreaderioresult.ChainFirst(f)
|
||||
}
|
||||
|
||||
// ChainIOK chains an effect with a function that returns an IO action.
|
||||
// This is useful for integrating IO-based computations (synchronous side effects)
|
||||
// into effect chains. The IO action is automatically lifted into the Effect context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns IO[B]
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, B]: A function that chains the IO-returning function with the effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// performIO := func(n int) io.IO[string] {
|
||||
// return func() string {
|
||||
// // Perform synchronous side effect
|
||||
// return fmt.Sprintf("Value: %d", n)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// chained := effect.ChainIOK[MyContext](performIO)(eff)
|
||||
// // chained produces "Value: 42"
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, B] {
|
||||
return readerreaderioresult.ChainIOK[C](f)
|
||||
}
|
||||
|
||||
// ChainFirstIOK chains an effect with a function that returns an IO action,
|
||||
// but discards the result and returns the original value.
|
||||
// This is useful for performing side effects (like logging) without changing the value.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The value type (preserved)
|
||||
// - B: The type produced by the IO action (discarded)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns IO[B] for side effects
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, A]: A function that executes the IO action but preserves the original value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// logValue := func(n int) io.IO[any] {
|
||||
// return func() any {
|
||||
// fmt.Printf("Processing: %d\n", n)
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// logged := effect.ChainFirstIOK[MyContext](logValue)(eff)
|
||||
// // Prints "Processing: 42" but still produces 42
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, A] {
|
||||
return readerreaderioresult.ChainFirstIOK[C](f)
|
||||
}
|
||||
|
||||
// TapIOK is an alias for ChainFirstIOK.
|
||||
// It chains an effect with a function that returns an IO action for side effects,
|
||||
// but preserves the original value. This is useful for logging, debugging, or
|
||||
// performing actions without changing the result.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The value type (preserved)
|
||||
// - B: The type produced by the IO action (discarded)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns IO[B] for side effects
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, A]: A function that executes the IO action but preserves the original value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// logValue := func(n int) io.IO[any] {
|
||||
// return func() any {
|
||||
// fmt.Printf("Value: %d\n", n)
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// tapped := effect.TapIOK[MyContext](logValue)(eff)
|
||||
// // Prints "Value: 42" but still produces 42
|
||||
//
|
||||
//go:inline
|
||||
func TapIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, A] {
|
||||
return readerreaderioresult.ChainFirstIOK[C](f)
|
||||
}
|
||||
|
||||
// Ap applies a function wrapped in an Effect to a value wrapped in an Effect.
|
||||
// This is the applicative apply operation, useful for applying effects in parallel.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - B: The output value type
|
||||
// - C: The context type required by the effects
|
||||
// - A: The input value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fa: The effect containing the value to apply the function to
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, func(A) B, B]: A function that applies the function effect to the value effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// fnEff := effect.Of[MyContext](func(x int) int { return x * 2 })
|
||||
// valEff := effect.Of[MyContext](21)
|
||||
// result := effect.Ap[int](valEff)(fnEff)
|
||||
// // result produces 42
|
||||
func Ap[B, C, A any](fa Effect[C, A]) Operator[C, func(A) B, B] {
|
||||
return readerreaderioresult.Ap[B](fa)
|
||||
}
|
||||
|
||||
// Suspend delays the evaluation of an effect until it is run.
|
||||
// This is useful for recursive effects or when you need lazy evaluation.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The type of the success value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fa: A lazy computation that produces an effect
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, A]: An effect that evaluates the lazy computation when run
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// var recursiveEff func(int) Effect[MyContext, int]
|
||||
// recursiveEff = func(n int) Effect[MyContext, int] {
|
||||
// if n <= 0 {
|
||||
// return effect.Of[MyContext](0)
|
||||
// }
|
||||
// return effect.Suspend(func() Effect[MyContext, int] {
|
||||
// return effect.Map[MyContext](func(x int) int {
|
||||
// return x + n
|
||||
// })(recursiveEff(n - 1))
|
||||
// })
|
||||
// }
|
||||
func Suspend[C, A any](fa Lazy[Effect[C, A]]) Effect[C, A] {
|
||||
return readerreaderioresult.Defer(fa)
|
||||
}
|
||||
|
||||
// Tap executes a side effect for its effect, but returns the original value.
|
||||
// This is useful for logging, debugging, or performing actions without changing the result.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effects
|
||||
// - A: The value type
|
||||
// - ANY: The type produced by the side effect (ignored)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that performs a side effect based on the value
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, A]: A function that executes the side effect but preserves the original value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// tapped := effect.Tap[MyContext](func(x int) Effect[MyContext, any] {
|
||||
// fmt.Println("Value:", x)
|
||||
// return effect.Of[MyContext, any](nil)
|
||||
// })(eff)
|
||||
// // Prints "Value: 42" but still produces 42
|
||||
func Tap[C, A, ANY any](f Kleisli[C, A, ANY]) Operator[C, A, A] {
|
||||
return readerreaderioresult.Tap(f)
|
||||
}
|
||||
|
||||
// Ternary creates a conditional effect based on a predicate.
|
||||
// If the predicate returns true, onTrue is executed; otherwise, onFalse is executed.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effects
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - pred: A predicate function to test the input value
|
||||
// - onTrue: The effect to execute if the predicate is true
|
||||
// - onFalse: The effect to execute if the predicate is false
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Kleisli[C, A, B]: A function that conditionally executes one of two effects
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// kleisli := effect.Ternary(
|
||||
// func(x int) bool { return x > 10 },
|
||||
// func(x int) Effect[MyContext, string] {
|
||||
// return effect.Of[MyContext]("large")
|
||||
// },
|
||||
// func(x int) Effect[MyContext, string] {
|
||||
// return effect.Of[MyContext]("small")
|
||||
// },
|
||||
// )
|
||||
// result := kleisli(15) // produces "large"
|
||||
func Ternary[C, A, B any](pred Predicate[A], onTrue, onFalse Kleisli[C, A, B]) Kleisli[C, A, B] {
|
||||
return function.Ternary(pred, onTrue, onFalse)
|
||||
}
|
||||
|
||||
// ChainResultK chains an effect with a function that returns a Result.
|
||||
// This is useful for integrating Result-based computations into effect chains.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns Result[B]
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, B]: A function that chains the Result-returning function with the effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
// eff := effect.Of[MyContext]("42")
|
||||
// chained := effect.ChainResultK[MyContext](parseIntResult)(eff)
|
||||
// // chained produces 42 as an int
|
||||
//
|
||||
//go:inline
|
||||
func ChainResultK[C, A, B any](f result.Kleisli[A, B]) Operator[C, A, B] {
|
||||
return readerreaderioresult.ChainResultK[C](f)
|
||||
}
|
||||
|
||||
// ChainReaderK chains an effect with a function that returns a Reader.
|
||||
// This is useful for integrating Reader-based computations (pure context-dependent functions)
|
||||
// into effect chains. The Reader is automatically lifted into the Effect context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns Reader[C, B]
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, B]: A function that chains the Reader-returning function with the effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
//
|
||||
// getMultiplied := func(n int) reader.Reader[Config, int] {
|
||||
// return func(cfg Config) int {
|
||||
// return n * cfg.Multiplier
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.Of[Config](5)
|
||||
// chained := effect.ChainReaderK[Config](getMultiplied)(eff)
|
||||
// // With Config{Multiplier: 3}, produces 15
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderK[C, A, B any](f reader.Kleisli[C, A, B]) Operator[C, A, B] {
|
||||
return readerreaderioresult.ChainReaderK(f)
|
||||
}
|
||||
|
||||
// ChainThunkK chains an effect with a function that returns a Thunk.
|
||||
// This is useful for integrating Thunk-based computations (context-independent IO with error handling)
|
||||
// into effect chains. The Thunk is automatically lifted into the Effect context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns Thunk[B] (readerioresult.Kleisli[A, B])
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, B]: A function that chains the Thunk-returning function with the effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// performIO := func(n int) readerioresult.ReaderIOResult[string] {
|
||||
// return func(ctx context.Context) io.IO[result.Result[string]] {
|
||||
// return func() result.Result[string] {
|
||||
// // Perform IO operation that doesn't need effect context
|
||||
// return result.Of(fmt.Sprintf("Processed: %d", n))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// chained := effect.ChainThunkK[MyContext](performIO)(eff)
|
||||
// // chained produces "Processed: 42"
|
||||
//
|
||||
//go:inline
|
||||
func ChainThunkK[C, A, B any](f thunk.Kleisli[A, B]) Operator[C, A, B] {
|
||||
return fromreader.ChainReaderK(
|
||||
Chain[C, A, B],
|
||||
FromThunk[C, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainReaderIOK chains an effect with a function that returns a ReaderIO.
|
||||
// This is useful for integrating ReaderIO-based computations (context-dependent IO operations)
|
||||
// into effect chains. The ReaderIO is automatically lifted into the Effect context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns ReaderIO[C, B]
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, B]: A function that chains the ReaderIO-returning function with the effect
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// type Config struct { LogPrefix string }
|
||||
//
|
||||
// logAndDouble := func(n int) readerio.ReaderIO[Config, int] {
|
||||
// return func(cfg Config) io.IO[int] {
|
||||
// return func() int {
|
||||
// fmt.Printf("%s: %d\n", cfg.LogPrefix, n)
|
||||
// return n * 2
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.Of[Config](21)
|
||||
// chained := effect.ChainReaderIOK[Config](logAndDouble)(eff)
|
||||
// // Logs "prefix: 21" and produces 42
|
||||
//
|
||||
//go:inline
|
||||
func ChainReaderIOK[C, A, B any](f readerio.Kleisli[C, A, B]) Operator[C, A, B] {
|
||||
return readerreaderioresult.ChainReaderIOK(f)
|
||||
}
|
||||
|
||||
// Read provides a context to an effect, partially applying it.
|
||||
// This converts an Effect[C, A] to a Thunk[A] by supplying the required context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the success value
|
||||
// - C: The context type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - c: The context to provide to the effect
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(Effect[C, A]) Thunk[A]: A function that converts an effect to a thunk
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// ctx := MyContext{Value: "test"}
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// thunk := effect.Read[int](ctx)(eff)
|
||||
// // thunk is now a Thunk[int] that can be run without context
|
||||
//
|
||||
//go:inline
|
||||
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
|
||||
return readerreaderioresult.Read[A](c)
|
||||
}
|
||||
|
||||
649
v2/effect/effect_additional_test.go
Normal file
649
v2/effect/effect_additional_test.go
Normal file
@@ -0,0 +1,649 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestSucceed tests the Succeed function
|
||||
func TestSucceed_Success(t *testing.T) {
|
||||
t.Run("creates successful effect with int", func(t *testing.T) {
|
||||
eff := Succeed[TestConfig](42)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("creates successful effect with string", func(t *testing.T) {
|
||||
eff := Succeed[TestConfig]("hello")
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of("hello"), outcome)
|
||||
})
|
||||
|
||||
t.Run("creates successful effect with zero value", func(t *testing.T) {
|
||||
eff := Succeed[TestConfig](0)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFail tests the Fail function
|
||||
func TestFail_Failure(t *testing.T) {
|
||||
t.Run("creates failed effect with error", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
eff := Fail[TestConfig, int](testErr)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error message", func(t *testing.T) {
|
||||
testErr := errors.New("specific error message")
|
||||
eff := Fail[TestConfig, string](testErr)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
extractedErr := result.MonadFold(outcome,
|
||||
F.Identity[error],
|
||||
func(string) error { return nil },
|
||||
)
|
||||
assert.Equal(t, testErr, extractedErr)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOf tests the Of function
|
||||
func TestOf_Success(t *testing.T) {
|
||||
t.Run("creates successful effect with value", func(t *testing.T) {
|
||||
eff := Of[TestConfig](100)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(100), outcome)
|
||||
})
|
||||
|
||||
t.Run("is equivalent to Succeed", func(t *testing.T) {
|
||||
value := "test"
|
||||
eff1 := Of[TestConfig](value)
|
||||
eff2 := Succeed[TestConfig](value)
|
||||
outcome1 := eff1(testConfig)(context.Background())()
|
||||
outcome2 := eff2(testConfig)(context.Background())()
|
||||
assert.Equal(t, outcome1, outcome2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMap tests the Map function
|
||||
func TestMap_Success(t *testing.T) {
|
||||
t.Run("transforms success value", func(t *testing.T) {
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
Map[TestConfig](func(x int) int { return x * 2 }),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(84), outcome)
|
||||
})
|
||||
|
||||
t.Run("transforms type", func(t *testing.T) {
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
Map[TestConfig](func(x int) string { return strconv.Itoa(x) }),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of("42"), outcome)
|
||||
})
|
||||
|
||||
t.Run("chains multiple maps", func(t *testing.T) {
|
||||
eff := F.Pipe2(
|
||||
Of[TestConfig](10),
|
||||
Map[TestConfig](func(x int) int { return x + 5 }),
|
||||
Map[TestConfig](func(x int) int { return x * 2 }),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(30), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMap_Failure(t *testing.T) {
|
||||
t.Run("propagates error unchanged", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
eff := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
Map[TestConfig](func(x int) int { return x * 2 }),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChain tests the Chain function
|
||||
func TestChain_Success(t *testing.T) {
|
||||
t.Run("sequences two effects", func(t *testing.T) {
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
Chain(func(x int) Effect[TestConfig, string] {
|
||||
return Of[TestConfig](strconv.Itoa(x))
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of("42"), outcome)
|
||||
})
|
||||
|
||||
t.Run("chains multiple effects", func(t *testing.T) {
|
||||
eff := F.Pipe2(
|
||||
Of[TestConfig](10),
|
||||
Chain(func(x int) Effect[TestConfig, int] {
|
||||
return Of[TestConfig](x + 5)
|
||||
}),
|
||||
Chain(func(x int) Effect[TestConfig, int] {
|
||||
return Of[TestConfig](x * 2)
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(30), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChain_Failure(t *testing.T) {
|
||||
t.Run("propagates error from first effect", func(t *testing.T) {
|
||||
testErr := errors.New("first error")
|
||||
eff := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
Chain(func(x int) Effect[TestConfig, string] {
|
||||
return Of[TestConfig]("should not execute")
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[string](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("propagates error from second effect", func(t *testing.T) {
|
||||
testErr := errors.New("second error")
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
Chain(func(x int) Effect[TestConfig, string] {
|
||||
return Fail[TestConfig, string](testErr)
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[string](testErr), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainIOK tests the ChainIOK function
|
||||
func TestChainIOK_Success(t *testing.T) {
|
||||
t.Run("chains with IO action", func(t *testing.T) {
|
||||
counter := 0
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
ChainIOK[TestConfig](func(x int) io.IO[string] {
|
||||
return func() string {
|
||||
counter++
|
||||
return fmt.Sprintf("Value: %d", x)
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of("Value: 42"), outcome)
|
||||
assert.Equal(t, 1, counter)
|
||||
})
|
||||
|
||||
t.Run("chains multiple IO actions", func(t *testing.T) {
|
||||
log := []string{}
|
||||
eff := F.Pipe2(
|
||||
Of[TestConfig](10),
|
||||
ChainIOK[TestConfig](func(x int) io.IO[int] {
|
||||
return func() int {
|
||||
log = append(log, "first")
|
||||
return x + 5
|
||||
}
|
||||
}),
|
||||
ChainIOK[TestConfig](func(x int) io.IO[int] {
|
||||
return func() int {
|
||||
log = append(log, "second")
|
||||
return x * 2
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(30), outcome)
|
||||
assert.Equal(t, []string{"first", "second"}, log)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainIOK_Failure(t *testing.T) {
|
||||
t.Run("propagates error from previous effect", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
executed := false
|
||||
eff := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
ChainIOK[TestConfig](func(x int) io.IO[string] {
|
||||
return func() string {
|
||||
executed = true
|
||||
return "should not execute"
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[string](testErr), outcome)
|
||||
assert.False(t, executed)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainFirstIOK tests the ChainFirstIOK function
|
||||
func TestChainFirstIOK_Success(t *testing.T) {
|
||||
t.Run("executes IO but preserves value", func(t *testing.T) {
|
||||
log := []string{}
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
|
||||
return func() any {
|
||||
log = append(log, fmt.Sprintf("logged: %d", x))
|
||||
return nil
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, []string{"logged: 42"}, log)
|
||||
})
|
||||
|
||||
t.Run("chains multiple side effects", func(t *testing.T) {
|
||||
log := []string{}
|
||||
eff := F.Pipe2(
|
||||
Of[TestConfig](10),
|
||||
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
|
||||
return func() any {
|
||||
log = append(log, "first")
|
||||
return nil
|
||||
}
|
||||
}),
|
||||
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
|
||||
return func() any {
|
||||
log = append(log, "second")
|
||||
return nil
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(10), outcome)
|
||||
assert.Equal(t, []string{"first", "second"}, log)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstIOK_Failure(t *testing.T) {
|
||||
t.Run("propagates error without executing IO", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
executed := false
|
||||
eff := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
|
||||
return func() any {
|
||||
executed = true
|
||||
return nil
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
assert.False(t, executed)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTapIOK tests the TapIOK function
|
||||
func TestTapIOK_Success(t *testing.T) {
|
||||
t.Run("executes IO but preserves value", func(t *testing.T) {
|
||||
log := []string{}
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
TapIOK[TestConfig](func(x int) io.IO[any] {
|
||||
return func() any {
|
||||
log = append(log, fmt.Sprintf("tapped: %d", x))
|
||||
return nil
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, []string{"tapped: 42"}, log)
|
||||
})
|
||||
|
||||
t.Run("is equivalent to ChainFirstIOK", func(t *testing.T) {
|
||||
log1 := []string{}
|
||||
log2 := []string{}
|
||||
|
||||
eff1 := F.Pipe1(
|
||||
Of[TestConfig](10),
|
||||
TapIOK[TestConfig](func(x int) io.IO[any] {
|
||||
return func() any {
|
||||
log1 = append(log1, "tap")
|
||||
return nil
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
eff2 := F.Pipe1(
|
||||
Of[TestConfig](10),
|
||||
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
|
||||
return func() any {
|
||||
log2 = append(log2, "tap")
|
||||
return nil
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
outcome1 := eff1(testConfig)(context.Background())()
|
||||
outcome2 := eff2(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, outcome1, outcome2)
|
||||
assert.Equal(t, log1, log2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapIOK_Failure(t *testing.T) {
|
||||
t.Run("propagates error without executing IO", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
executed := false
|
||||
eff := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
TapIOK[TestConfig](func(x int) io.IO[any] {
|
||||
return func() any {
|
||||
executed = true
|
||||
return nil
|
||||
}
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
assert.False(t, executed)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainResultK tests the ChainResultK function
|
||||
func TestChainResultK_Success(t *testing.T) {
|
||||
t.Run("chains with Result-returning function", func(t *testing.T) {
|
||||
parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig]("42"),
|
||||
ChainResultK[TestConfig](parseIntResult),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("chains multiple Result operations", func(t *testing.T) {
|
||||
parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
eff := F.Pipe2(
|
||||
Of[TestConfig]("10"),
|
||||
ChainResultK[TestConfig](parseIntResult),
|
||||
ChainResultK[TestConfig](func(x int) result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Value: %d", x*2))
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of("Value: 20"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainResultK_Failure(t *testing.T) {
|
||||
t.Run("propagates error from previous effect", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
eff := F.Pipe1(
|
||||
Fail[TestConfig, string](testErr),
|
||||
ChainResultK[TestConfig](parseIntResult),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("propagates error from Result function", func(t *testing.T) {
|
||||
parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig]("not a number"),
|
||||
ChainResultK[TestConfig](parseIntResult),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAp tests the Ap function
|
||||
func TestAp_Success(t *testing.T) {
|
||||
t.Run("applies function effect to value effect", func(t *testing.T) {
|
||||
fnEff := Of[TestConfig](func(x int) int { return x * 2 })
|
||||
valEff := Of[TestConfig](21)
|
||||
eff := Ap[int](valEff)(fnEff)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("applies function with different types", func(t *testing.T) {
|
||||
fnEff := Of[TestConfig](func(x int) string { return strconv.Itoa(x) })
|
||||
valEff := Of[TestConfig](42)
|
||||
eff := Ap[string](valEff)(fnEff)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of("42"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp_Failure(t *testing.T) {
|
||||
t.Run("propagates error from function effect", func(t *testing.T) {
|
||||
testErr := errors.New("function error")
|
||||
fnEff := Fail[TestConfig, func(int) int](testErr)
|
||||
valEff := Of[TestConfig](42)
|
||||
eff := Ap[int](valEff)(fnEff)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("propagates error from value effect", func(t *testing.T) {
|
||||
testErr := errors.New("value error")
|
||||
fnEff := Of[TestConfig](func(x int) int { return x * 2 })
|
||||
valEff := Fail[TestConfig, int](testErr)
|
||||
eff := Ap[int](valEff)(fnEff)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSuspend tests the Suspend function
|
||||
func TestSuspend_Success(t *testing.T) {
|
||||
t.Run("delays evaluation of effect", func(t *testing.T) {
|
||||
counter := 0
|
||||
eff := Suspend(func() Effect[TestConfig, int] {
|
||||
counter++
|
||||
return Of[TestConfig](42)
|
||||
})
|
||||
assert.Equal(t, 0, counter, "should not evaluate immediately")
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, 1, counter, "should evaluate when run")
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("enables recursive effects", func(t *testing.T) {
|
||||
var factorial func(int) Effect[TestConfig, int]
|
||||
factorial = func(n int) Effect[TestConfig, int] {
|
||||
if n <= 1 {
|
||||
return Of[TestConfig](1)
|
||||
}
|
||||
return Suspend(func() Effect[TestConfig, int] {
|
||||
return F.Pipe1(
|
||||
factorial(n-1),
|
||||
Map[TestConfig](func(x int) int { return x * n }),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
outcome := factorial(5)(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(120), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTap tests the Tap function
|
||||
func TestTap_Success(t *testing.T) {
|
||||
t.Run("executes side effect but preserves value", func(t *testing.T) {
|
||||
log := []string{}
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
Tap(func(x int) Effect[TestConfig, any] {
|
||||
log = append(log, fmt.Sprintf("tapped: %d", x))
|
||||
return Of[TestConfig, any](nil)
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, []string{"tapped: 42"}, log)
|
||||
})
|
||||
|
||||
t.Run("chains multiple taps", func(t *testing.T) {
|
||||
log := []string{}
|
||||
eff := F.Pipe2(
|
||||
Of[TestConfig](10),
|
||||
Tap(func(x int) Effect[TestConfig, any] {
|
||||
log = append(log, "first")
|
||||
return Of[TestConfig, any](nil)
|
||||
}),
|
||||
Tap(func(x int) Effect[TestConfig, any] {
|
||||
log = append(log, "second")
|
||||
return Of[TestConfig, any](nil)
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of(10), outcome)
|
||||
assert.Equal(t, []string{"first", "second"}, log)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTap_Failure(t *testing.T) {
|
||||
t.Run("propagates error without executing tap", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
executed := false
|
||||
eff := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
Tap(func(x int) Effect[TestConfig, any] {
|
||||
executed = true
|
||||
return Of[TestConfig, any](nil)
|
||||
}),
|
||||
)
|
||||
outcome := eff(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
assert.False(t, executed)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTernary tests the Ternary function
|
||||
func TestTernary_Success(t *testing.T) {
|
||||
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestConfig, string] {
|
||||
return Of[TestConfig]("large")
|
||||
},
|
||||
func(x int) Effect[TestConfig, string] {
|
||||
return Of[TestConfig]("small")
|
||||
},
|
||||
)
|
||||
outcome := kleisli(15)(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of("large"), outcome)
|
||||
})
|
||||
|
||||
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestConfig, string] {
|
||||
return Of[TestConfig]("large")
|
||||
},
|
||||
func(x int) Effect[TestConfig, string] {
|
||||
return Of[TestConfig]("small")
|
||||
},
|
||||
)
|
||||
outcome := kleisli(5)(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of("small"), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with boundary value", func(t *testing.T) {
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x >= 10 },
|
||||
func(x int) Effect[TestConfig, string] {
|
||||
return Of[TestConfig]("gte")
|
||||
},
|
||||
func(x int) Effect[TestConfig, string] {
|
||||
return Of[TestConfig]("lt")
|
||||
},
|
||||
)
|
||||
outcome := kleisli(10)(testConfig)(context.Background())()
|
||||
assert.Equal(t, result.Of("gte"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRead tests the Read function
|
||||
func TestRead_Success(t *testing.T) {
|
||||
t.Run("provides context to effect", func(t *testing.T) {
|
||||
eff := Of[TestConfig](42)
|
||||
thunk := Read[int](testConfig)(eff)
|
||||
outcome := thunk(context.Background())()
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("converts effect to thunk", func(t *testing.T) {
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](10),
|
||||
Map[TestConfig](func(x int) int { return x * testConfig.Multiplier }),
|
||||
)
|
||||
thunk := Read[int](testConfig)(eff)
|
||||
outcome := thunk(context.Background())()
|
||||
assert.Equal(t, result.Of(30), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with different contexts", func(t *testing.T) {
|
||||
cfg1 := TestConfig{Multiplier: 2, Prefix: "A", DatabaseURL: ""}
|
||||
cfg2 := TestConfig{Multiplier: 5, Prefix: "B", DatabaseURL: ""}
|
||||
|
||||
// Create an effect that uses the context's Multiplier
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](10),
|
||||
ChainReaderK(func(x int) reader.Reader[TestConfig, int] {
|
||||
return func(cfg TestConfig) int {
|
||||
return x * cfg.Multiplier
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
thunk1 := Read[int](cfg1)(eff)
|
||||
thunk2 := Read[int](cfg2)(eff)
|
||||
|
||||
outcome1 := thunk1(context.Background())()
|
||||
outcome2 := thunk2(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(20), outcome1)
|
||||
assert.Equal(t, result.Of(50), outcome2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRead_Failure(t *testing.T) {
|
||||
t.Run("propagates error from effect", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
eff := Fail[TestConfig, int](testErr)
|
||||
thunk := Read[int](testConfig)(eff)
|
||||
outcome := thunk(context.Background())()
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
}
|
||||
213
v2/effect/effect_missing_test.go
Normal file
213
v2/effect/effect_missing_test.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
t.Run("provides context to effect", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test-context"}
|
||||
eff := Of[TestContext](42)
|
||||
|
||||
thunk := Read[int](ctx)(eff)
|
||||
ioResult := thunk(context.Background())
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
value, err := result.Unwrap(res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("provides context to failing effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("read error")
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Fail[TestContext, string](expectedErr)
|
||||
|
||||
thunk := Read[string](ctx)(eff)
|
||||
ioResult := thunk(context.Background())
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
_, err := result.Unwrap(res)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("provides context to chained effects", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "base"}
|
||||
eff := Chain(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x * 2))
|
||||
})(Of[TestContext](21))
|
||||
|
||||
thunk := Read[string](ctx)(eff)
|
||||
ioResult := thunk(context.Background())
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
value, err := result.Unwrap(res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "42", value)
|
||||
})
|
||||
|
||||
t.Run("works with different context types", func(t *testing.T) {
|
||||
type CustomContext struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
ctx := CustomContext{ID: 100, Name: "custom"}
|
||||
eff := Of[CustomContext]("result")
|
||||
|
||||
thunk := Read[string](ctx)(eff)
|
||||
ioResult := thunk(context.Background())
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
value, err := result.Unwrap(res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", value)
|
||||
})
|
||||
|
||||
t.Run("can be composed with RunSync", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext](100)
|
||||
|
||||
thunk := Read[int](ctx)(eff)
|
||||
readerResult := RunSync(thunk)
|
||||
value, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainResultK(t *testing.T) {
|
||||
t.Run("chains successful Result function", func(t *testing.T) {
|
||||
parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
eff := Of[TestContext]("42")
|
||||
chained := ChainResultK[TestContext](parseIntResult)(eff)
|
||||
|
||||
result, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("chains failing Result function", func(t *testing.T) {
|
||||
parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
eff := Of[TestContext]("not-a-number")
|
||||
chained := ChainResultK[TestContext](parseIntResult)(eff)
|
||||
|
||||
_, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid syntax")
|
||||
})
|
||||
|
||||
t.Run("propagates error from original effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("original error")
|
||||
parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
eff := Fail[TestContext, string](expectedErr)
|
||||
chained := ChainResultK[TestContext](parseIntResult)(eff)
|
||||
|
||||
_, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("chains multiple Result functions", func(t *testing.T) {
|
||||
parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
formatResult := func(x int) result.Result[string] {
|
||||
return result.Of("value: " + strconv.Itoa(x))
|
||||
}
|
||||
|
||||
eff := Of[TestContext]("42")
|
||||
chained := ChainResultK[TestContext](formatResult)(
|
||||
ChainResultK[TestContext](parseIntResult)(eff),
|
||||
)
|
||||
|
||||
result, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value: 42", result)
|
||||
})
|
||||
|
||||
t.Run("integrates with other effect operations", func(t *testing.T) {
|
||||
parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
|
||||
eff := Map[TestContext](func(x int) string {
|
||||
return "final: " + strconv.Itoa(x)
|
||||
})(ChainResultK[TestContext](parseIntResult)(Of[TestContext]("100")))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "final: 100", result)
|
||||
})
|
||||
|
||||
t.Run("works with custom Result functions", func(t *testing.T) {
|
||||
validatePositive := func(x int) result.Result[int] {
|
||||
if x > 0 {
|
||||
return result.Of(x)
|
||||
}
|
||||
return result.Left[int](errors.New("must be positive"))
|
||||
}
|
||||
|
||||
parseIntResult := result.Eitherize1(strconv.Atoi)
|
||||
|
||||
// Test with positive number
|
||||
eff1 := ChainResultK[TestContext](validatePositive)(
|
||||
ChainResultK[TestContext](parseIntResult)(Of[TestContext]("42")),
|
||||
)
|
||||
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, 42, result1)
|
||||
|
||||
// Test with negative number
|
||||
eff2 := ChainResultK[TestContext](validatePositive)(
|
||||
ChainResultK[TestContext](parseIntResult)(Of[TestContext]("-5")),
|
||||
)
|
||||
_, err2 := runEffect(eff2, TestContext{Value: "test"})
|
||||
assert.Error(t, err2)
|
||||
assert.Contains(t, err2.Error(), "must be positive")
|
||||
})
|
||||
|
||||
t.Run("preserves error context", func(t *testing.T) {
|
||||
customError := errors.New("custom validation error")
|
||||
validateFunc := func(s string) result.Result[string] {
|
||||
if len(s) > 0 {
|
||||
return result.Of(s)
|
||||
}
|
||||
return result.Left[string](customError)
|
||||
}
|
||||
|
||||
eff := ChainResultK[TestContext](validateFunc)(Of[TestContext](""))
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, customError, err)
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
208
v2/effect/eitherize.go
Normal file
208
v2/effect/eitherize.go
Normal file
@@ -0,0 +1,208 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
)
|
||||
|
||||
// Eitherize converts a function that returns a value and error into an Effect.
|
||||
//
|
||||
// This function takes a function that accepts a context C and context.Context,
|
||||
// returning a value T and an error, and converts it into an Effect[C, T].
|
||||
// The error is automatically converted into a failure, while successful
|
||||
// values become successes.
|
||||
//
|
||||
// This is particularly useful for integrating standard Go error-handling patterns into
|
||||
// the effect system. It is especially helpful for adapting interface member functions
|
||||
// that accept a context. When you have an interface method with signature
|
||||
// (receiver, context.Context) (T, error), you can use Eitherize to convert it into
|
||||
// an Effect where the receiver becomes the context C.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - T: The success value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes C and context.Context and returns (T, error)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, T]: An effect that depends on C, performs IO, and produces T
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // A function using standard Go error handling
|
||||
// func fetchUser(cfg AppConfig, ctx context.Context) (*User, error) {
|
||||
// // Implementation that may return an error
|
||||
// return &User{ID: 1, Name: "Alice"}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to Effect
|
||||
// fetchUserEffect := effect.Eitherize(fetchUser)
|
||||
//
|
||||
// // Use in functional composition
|
||||
// pipeline := F.Pipe1(
|
||||
// fetchUserEffect,
|
||||
// effect.Map[AppConfig](func(u *User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// // Execute with config
|
||||
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
|
||||
// result, err := effect.RunSync(effect.Provide[*User](cfg)(pipeline))(context.Background())
|
||||
//
|
||||
// # Adapting Interface Methods
|
||||
//
|
||||
// Eitherize is particularly useful for adapting interface member functions:
|
||||
//
|
||||
// type UserRepository interface {
|
||||
// GetUser(ctx context.Context, id int) (*User, error)
|
||||
// }
|
||||
//
|
||||
// type UserRepo struct {
|
||||
// db *sql.DB
|
||||
// }
|
||||
//
|
||||
// func (r *UserRepo) GetUser(ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation
|
||||
// return &User{ID: id}, nil
|
||||
// }
|
||||
//
|
||||
// // Adapt the method by binding the first parameter (receiver)
|
||||
// repo := &UserRepo{db: db}
|
||||
// getUserEffect := effect.Eitherize(func(id int, ctx context.Context) (*User, error) {
|
||||
// return repo.GetUser(ctx, id)
|
||||
// })
|
||||
//
|
||||
// // Now getUserEffect has type: Effect[int, *User]
|
||||
// // The receiver (repo) is captured in the closure
|
||||
// // The id becomes the context C
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Eitherize1: For functions that take an additional parameter
|
||||
// - readerreaderioresult.Eitherize: The underlying implementation
|
||||
//
|
||||
//go:inline
|
||||
func Eitherize[C, T any](f func(C, context.Context) (T, error)) Effect[C, T] {
|
||||
return readerreaderioresult.Eitherize(f)
|
||||
}
|
||||
|
||||
// Eitherize1 converts a function that takes an additional parameter and returns a value
|
||||
// and error into a Kleisli arrow.
|
||||
//
|
||||
// This function takes a function that accepts a context C, context.Context, and
|
||||
// an additional parameter A, returning a value T and an error, and converts it into a
|
||||
// Kleisli arrow (A -> Effect[C, T]). The error is automatically converted into a failure,
|
||||
// while successful values become successes.
|
||||
//
|
||||
// This is useful for creating composable operations that depend on context and
|
||||
// an input value, following standard Go error-handling patterns. It is especially helpful
|
||||
// for adapting interface member functions that accept a context and additional parameters.
|
||||
// When you have an interface method with signature (receiver, context.Context, A) (T, error),
|
||||
// you can use Eitherize1 to convert it into a Kleisli arrow where the receiver becomes
|
||||
// the context C and A becomes the input parameter.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input parameter type
|
||||
// - T: The success value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes C, context.Context, and A, returning (T, error)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Kleisli[C, A, T]: A function from A to Effect[C, T]
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // A function using standard Go error handling
|
||||
// func fetchUserByID(cfg AppConfig, ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation that may return an error
|
||||
// return &User{ID: id, Name: "Alice"}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to Kleisli arrow
|
||||
// fetchUserKleisli := effect.Eitherize1(fetchUserByID)
|
||||
//
|
||||
// // Use in functional composition with Chain
|
||||
// pipeline := F.Pipe1(
|
||||
// effect.Succeed[AppConfig](123),
|
||||
// effect.Chain[AppConfig](fetchUserKleisli),
|
||||
// )
|
||||
//
|
||||
// // Execute with config
|
||||
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
|
||||
// result, err := effect.RunSync(effect.Provide[*User](cfg)(pipeline))(context.Background())
|
||||
//
|
||||
// # Adapting Interface Methods
|
||||
//
|
||||
// Eitherize1 is particularly useful for adapting interface member functions with parameters:
|
||||
//
|
||||
// type UserRepository interface {
|
||||
// GetUserByID(ctx context.Context, id int) (*User, error)
|
||||
// UpdateUser(ctx context.Context, user *User) error
|
||||
// }
|
||||
//
|
||||
// type UserRepo struct {
|
||||
// db *sql.DB
|
||||
// }
|
||||
//
|
||||
// func (r *UserRepo) GetUserByID(ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation
|
||||
// return &User{ID: id}, nil
|
||||
// }
|
||||
//
|
||||
// // Adapt the method - receiver becomes C, id becomes A
|
||||
// repo := &UserRepo{db: db}
|
||||
// getUserKleisli := effect.Eitherize1(func(r *UserRepo, ctx context.Context, id int) (*User, error) {
|
||||
// return r.GetUserByID(ctx, id)
|
||||
// })
|
||||
//
|
||||
// // Now getUserKleisli has type: Kleisli[*UserRepo, int, *User]
|
||||
// // Which is: func(int) Effect[*UserRepo, *User]
|
||||
// // Use it in composition:
|
||||
// pipeline := F.Pipe1(
|
||||
// effect.Succeed[*UserRepo](123),
|
||||
// effect.Chain[*UserRepo](getUserKleisli),
|
||||
// )
|
||||
// result, err := effect.RunSync(effect.Provide[*User](repo)(pipeline))(context.Background())
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Eitherize: For functions without an additional parameter
|
||||
// - Chain: For composing Kleisli arrows
|
||||
// - readerreaderioresult.Eitherize1: The underlying implementation
|
||||
//
|
||||
//go:inline
|
||||
func Eitherize1[C, A, T any](f func(C, context.Context, A) (T, error)) Kleisli[C, A, T] {
|
||||
return readerreaderioresult.Eitherize1(f)
|
||||
}
|
||||
507
v2/effect/eitherize_test.go
Normal file
507
v2/effect/eitherize_test.go
Normal file
@@ -0,0 +1,507 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestEitherize_Success tests successful conversion with Eitherize
|
||||
func TestEitherize_Success(t *testing.T) {
|
||||
t.Run("converts successful function to Effect", func(t *testing.T) {
|
||||
// Arrange
|
||||
successFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return cfg.Prefix + "-success", nil
|
||||
}
|
||||
eff := Eitherize(successFunc)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG-success", result)
|
||||
})
|
||||
|
||||
t.Run("preserves context values", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ctxKey string
|
||||
key := ctxKey("testKey")
|
||||
expectedValue := "contextValue"
|
||||
|
||||
contextFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
value := ctx.Value(key)
|
||||
if value == nil {
|
||||
return "", errors.New("context value not found")
|
||||
}
|
||||
return value.(string), nil
|
||||
}
|
||||
eff := Eitherize(contextFunc)
|
||||
|
||||
// Act
|
||||
ioResult := Provide[string](testConfig)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
ctx := context.WithValue(context.Background(), key, expectedValue)
|
||||
result, err := readerResult(ctx)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedValue, result)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// Arrange
|
||||
intFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return cfg.Multiplier, nil
|
||||
}
|
||||
eff := Eitherize(intFunc)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_Failure tests error handling with Eitherize
|
||||
func TestEitherize_Failure(t *testing.T) {
|
||||
t.Run("converts error to failure", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := errors.New("operation failed")
|
||||
failFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return "", expectedErr
|
||||
}
|
||||
eff := Eitherize(failFunc)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("preserves error message", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := fmt.Errorf("validation error: field is required")
|
||||
failFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return 0, expectedErr
|
||||
}
|
||||
eff := Eitherize(failFunc)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_EdgeCases tests edge cases for Eitherize
|
||||
func TestEitherize_EdgeCases(t *testing.T) {
|
||||
t.Run("handles nil context", func(t *testing.T) {
|
||||
// Arrange
|
||||
nilCtxFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
if ctx == nil {
|
||||
return "nil-context", nil
|
||||
}
|
||||
return "non-nil-context", nil
|
||||
}
|
||||
eff := Eitherize(nilCtxFunc)
|
||||
|
||||
// Act
|
||||
ioResult := Provide[string](testConfig)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(nil)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nil-context", result)
|
||||
})
|
||||
|
||||
t.Run("handles zero value config", func(t *testing.T) {
|
||||
// Arrange
|
||||
zeroFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return cfg.Prefix, nil
|
||||
}
|
||||
eff := Eitherize(zeroFunc)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(eff, TestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("handles pointer types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context) (*User, error) {
|
||||
return &User{Name: cfg.Prefix}, nil
|
||||
}
|
||||
eff := Eitherize(ptrFunc)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "LOG", result.Name)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_Integration tests integration with other operations
|
||||
func TestEitherize_Integration(t *testing.T) {
|
||||
t.Run("composes with Map", func(t *testing.T) {
|
||||
// Arrange
|
||||
baseFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return cfg.Multiplier, nil
|
||||
}
|
||||
eff := Eitherize(baseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
eff,
|
||||
Map[TestConfig](func(n int) string { return strconv.Itoa(n) }),
|
||||
)
|
||||
result, err := runEffect(pipeline, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "3", result)
|
||||
})
|
||||
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
firstFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return cfg.Multiplier, nil
|
||||
}
|
||||
secondFunc := func(n int) Effect[TestConfig, string] {
|
||||
return Succeed[TestConfig](fmt.Sprintf("value: %d", n))
|
||||
}
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
Eitherize(firstFunc),
|
||||
Chain[TestConfig](secondFunc),
|
||||
)
|
||||
result, err := runEffect(pipeline, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value: 3", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Success tests successful conversion with Eitherize1
|
||||
func TestEitherize1_Success(t *testing.T) {
|
||||
t.Run("converts successful function to Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
multiplyFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
return n * cfg.Multiplier, nil
|
||||
}
|
||||
kleisli := Eitherize1(multiplyFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(10)
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result)
|
||||
})
|
||||
|
||||
t.Run("works with string input", func(t *testing.T) {
|
||||
// Arrange
|
||||
concatFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
|
||||
return cfg.Prefix + "-" + s, nil
|
||||
}
|
||||
kleisli := Eitherize1(concatFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli("input")
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG-input", result)
|
||||
})
|
||||
|
||||
t.Run("preserves context in Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ctxKey string
|
||||
key := ctxKey("factor")
|
||||
|
||||
scaleFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
factor := ctx.Value(key)
|
||||
if factor == nil {
|
||||
return n * cfg.Multiplier, nil
|
||||
}
|
||||
return n * factor.(int), nil
|
||||
}
|
||||
kleisli := Eitherize1(scaleFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(5)
|
||||
ioResult := Provide[int](testConfig)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
ctx := context.WithValue(context.Background(), key, 7)
|
||||
result, err := readerResult(ctx)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 35, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Failure tests error handling with Eitherize1
|
||||
func TestEitherize1_Failure(t *testing.T) {
|
||||
t.Run("converts error to failure in Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := errors.New("division by zero")
|
||||
divideFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
if n == 0 {
|
||||
return 0, expectedErr
|
||||
}
|
||||
return 100 / n, nil
|
||||
}
|
||||
kleisli := Eitherize1(divideFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(0)
|
||||
_, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("preserves error context", func(t *testing.T) {
|
||||
// Arrange
|
||||
validateFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
|
||||
if len(s) > 10 {
|
||||
return "", fmt.Errorf("string too long: %d > 10", len(s))
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
kleisli := Eitherize1(validateFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli("this-string-is-too-long")
|
||||
_, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "string too long")
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_EdgeCases tests edge cases for Eitherize1
|
||||
func TestEitherize1_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero value input", func(t *testing.T) {
|
||||
// Arrange
|
||||
zeroFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
kleisli := Eitherize1(zeroFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(0)
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("handles pointer input", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
Value int
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
|
||||
if in == nil {
|
||||
return 0, errors.New("nil input")
|
||||
}
|
||||
return in.Value * cfg.Multiplier, nil
|
||||
}
|
||||
kleisli := Eitherize1(ptrFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(&Input{Value: 7})
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 21, result)
|
||||
})
|
||||
|
||||
t.Run("handles nil pointer input", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
Value int
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
|
||||
if in == nil {
|
||||
return 0, errors.New("nil input")
|
||||
}
|
||||
return in.Value, nil
|
||||
}
|
||||
kleisli := Eitherize1(ptrFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli((*Input)(nil))
|
||||
_, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "nil input")
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Integration tests integration with other operations
|
||||
func TestEitherize1_Integration(t *testing.T) {
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
doubleFunc := func(n int) Effect[TestConfig, int] {
|
||||
return Succeed[TestConfig](n * 2)
|
||||
}
|
||||
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe2(
|
||||
Succeed[TestConfig]("42"),
|
||||
Chain[TestConfig](parseKleisli),
|
||||
Chain[TestConfig](doubleFunc),
|
||||
)
|
||||
result, err := runEffect(pipeline, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 84, result)
|
||||
})
|
||||
|
||||
t.Run("handles error in chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
Succeed[TestConfig]("not-a-number"),
|
||||
Chain[TestConfig](parseKleisli),
|
||||
)
|
||||
_, err := runEffect(pipeline, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("composes multiple Kleisli arrows", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
formatFunc := func(cfg TestConfig, ctx context.Context, n int) (string, error) {
|
||||
return fmt.Sprintf("%s-%d", cfg.Prefix, n), nil
|
||||
}
|
||||
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
formatKleisli := Eitherize1(formatFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe2(
|
||||
Succeed[TestConfig]("123"),
|
||||
Chain[TestConfig](parseKleisli),
|
||||
Chain[TestConfig](formatKleisli),
|
||||
)
|
||||
result, err := runEffect(pipeline, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG-123", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_TypeSafety tests type safety across different scenarios
|
||||
func TestEitherize_TypeSafety(t *testing.T) {
|
||||
t.Run("Eitherize with complex types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ComplexResult struct {
|
||||
Data map[string]int
|
||||
Count int
|
||||
}
|
||||
|
||||
complexFunc := func(cfg TestConfig, ctx context.Context) (ComplexResult, error) {
|
||||
return ComplexResult{
|
||||
Data: map[string]int{cfg.Prefix: cfg.Multiplier},
|
||||
Count: cfg.Multiplier,
|
||||
}, nil
|
||||
}
|
||||
eff := Eitherize(complexFunc)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, result.Data["LOG"])
|
||||
assert.Equal(t, 3, result.Count)
|
||||
})
|
||||
|
||||
t.Run("Eitherize1 with different input and output types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
ID int
|
||||
}
|
||||
type Output struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
convertFunc := func(cfg TestConfig, ctx context.Context, in Input) (Output, error) {
|
||||
return Output{Name: fmt.Sprintf("%s-%d", cfg.Prefix, in.ID)}, nil
|
||||
}
|
||||
kleisli := Eitherize1(convertFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(Input{ID: 99})
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG-99", result.Name)
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
@@ -5,10 +20,66 @@ import (
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
// ApplicativeMonoid creates a monoid for effects using applicative semantics.
|
||||
// This combines effects by running both and combining their results using the provided monoid.
|
||||
// If either effect fails, the combined effect fails.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effects
|
||||
// - A: The value type that has a monoid instance
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: The monoid instance for combining values of type A
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Monoid[Effect[C, A]]: A monoid for combining effects
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// stringMonoid := monoid.MakeMonoid(
|
||||
// func(a, b string) string { return a + b },
|
||||
// "",
|
||||
// )
|
||||
// effectMonoid := effect.ApplicativeMonoid[MyContext](stringMonoid)
|
||||
// eff1 := effect.Of[MyContext]("Hello")
|
||||
// eff2 := effect.Of[MyContext](" World")
|
||||
// combined := effectMonoid.Concat(eff1, eff2)
|
||||
// // combined produces "Hello World"
|
||||
func ApplicativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
|
||||
return readerreaderioresult.ApplicativeMonoid[C](m)
|
||||
}
|
||||
|
||||
// AlternativeMonoid creates a monoid for effects using alternative semantics.
|
||||
// This tries the first effect, and if it fails, tries the second effect.
|
||||
// If both succeed, their results are combined using the provided monoid.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effects
|
||||
// - A: The value type that has a monoid instance
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: The monoid instance for combining values of type A
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Monoid[Effect[C, A]]: A monoid for combining effects with fallback behavior
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// stringMonoid := monoid.MakeMonoid(
|
||||
// func(a, b string) string { return a + b },
|
||||
// "",
|
||||
// )
|
||||
// effectMonoid := effect.AlternativeMonoid[MyContext](stringMonoid)
|
||||
// eff1 := effect.Fail[MyContext, string](errors.New("failed"))
|
||||
// eff2 := effect.Of[MyContext]("fallback")
|
||||
// combined := effectMonoid.Concat(eff1, eff2)
|
||||
// // combined produces "fallback" (first failed, so second is used)
|
||||
func AlternativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
|
||||
return readerreaderioresult.AlternativeMonoid[C](m)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
@@ -5,6 +20,39 @@ import (
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
)
|
||||
|
||||
// Retrying executes an effect with retry logic based on a policy and check predicate.
|
||||
// The effect is retried according to the policy until either:
|
||||
// - The effect succeeds and the check predicate returns false
|
||||
// - The retry policy is exhausted
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The type of the success value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - policy: The retry policy defining retry limits and delays
|
||||
// - action: An effectful computation that receives retry status and produces a value
|
||||
// - check: A predicate that determines if the result should trigger a retry
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, A]: An effect that retries according to the policy
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// policy := retry.LimitRetries(3)
|
||||
// eff := effect.Retrying[MyContext, string](
|
||||
// policy,
|
||||
// func(status retry.RetryStatus) Effect[MyContext, string] {
|
||||
// return fetchData() // may fail
|
||||
// },
|
||||
// func(result Result[string]) bool {
|
||||
// return result.IsLeft() // retry on error
|
||||
// },
|
||||
// )
|
||||
// // Retries up to 3 times if fetchData fails
|
||||
func Retrying[C, A any](
|
||||
policy retry.RetryPolicy,
|
||||
action Kleisli[C, retry.RetryStatus, A],
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
@@ -8,10 +23,64 @@ import (
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func Provide[C, A any](c C) func(Effect[C, A]) ReaderIOResult[A] {
|
||||
// Provide supplies a context to an effect, converting it to a Thunk.
|
||||
// This is the first step in running an effect - it eliminates the context dependency
|
||||
// by providing the required context value.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The type of the success value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - c: The context value to provide to the effect
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(Effect[C, A]) ReaderIOResult[A]: A function that converts an effect to a thunk
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// ctx := MyContext{APIKey: "secret"}
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// thunk := effect.Provide[MyContext, int](ctx)(eff)
|
||||
// // thunk is now a ReaderIOResult[int] that can be run
|
||||
func Provide[A, C any](c C) func(Effect[C, A]) ReaderIOResult[A] {
|
||||
return readerreaderioresult.Read[A](c)
|
||||
}
|
||||
|
||||
// RunSync executes a Thunk synchronously, converting it to a standard Go function.
|
||||
// This is the final step in running an effect - it executes the IO operations
|
||||
// and returns the result as a standard (value, error) tuple.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the success value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fa: The thunk to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - readerresult.ReaderResult[A]: A function that takes a context.Context and returns (A, error)
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// ctx := MyContext{APIKey: "secret"}
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// thunk := effect.Provide[MyContext, int](ctx)(eff)
|
||||
// readerResult := effect.RunSync(thunk)
|
||||
// value, err := readerResult(context.Background())
|
||||
// // value == 42, err == nil
|
||||
//
|
||||
// # Complete Example
|
||||
//
|
||||
// // Typical usage pattern:
|
||||
// result, err := effect.RunSync(
|
||||
// effect.Provide[MyContext, string](myContext)(myEffect),
|
||||
// )(context.Background())
|
||||
func RunSync[A any](fa ReaderIOResult[A]) readerresult.ReaderResult[A] {
|
||||
return func(ctx context.Context) (A, error) {
|
||||
return result.Unwrap(fa(ctx)())
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestProvide(t *testing.T) {
|
||||
ctx := TestContext{Value: "test-value"}
|
||||
eff := Of[TestContext]("result")
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
ioResult := Provide[string](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestProvide(t *testing.T) {
|
||||
cfg := Config{Host: "localhost", Port: 8080}
|
||||
eff := Of[Config]("connected")
|
||||
|
||||
ioResult := Provide[Config, string](cfg)(eff)
|
||||
ioResult := Provide[string](cfg)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestProvide(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Fail[TestContext, string](expectedErr)
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
ioResult := Provide[string](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestProvide(t *testing.T) {
|
||||
ctx := SimpleContext{ID: 42}
|
||||
eff := Of[SimpleContext](100)
|
||||
|
||||
ioResult := Provide[SimpleContext, int](ctx)(eff)
|
||||
ioResult := Provide[int](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestProvide(t *testing.T) {
|
||||
return Of[TestContext]("result")
|
||||
})(Of[TestContext](42))
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
ioResult := Provide[string](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -104,7 +104,7 @@ func TestProvide(t *testing.T) {
|
||||
return "mapped"
|
||||
})(Of[TestContext](42))
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
ioResult := Provide[string](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestRunSync(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext](42)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
ioResult := Provide[int](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestRunSync(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext]("hello")
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
ioResult := Provide[string](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
|
||||
bgCtx := context.Background()
|
||||
@@ -145,7 +145,7 @@ func TestRunSync(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
ioResult := Provide[int](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
@@ -162,7 +162,7 @@ func TestRunSync(t *testing.T) {
|
||||
return Of[TestContext](x + 10)
|
||||
})(Of[TestContext](5)))
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
ioResult := Provide[int](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestRunSync(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext](42)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
ioResult := Provide[int](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
|
||||
// Run multiple times
|
||||
@@ -200,7 +200,7 @@ func TestRunSync(t *testing.T) {
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
eff := Of[TestContext](user)
|
||||
|
||||
ioResult := Provide[TestContext, User](ctx)(eff)
|
||||
ioResult := Provide[User](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
@@ -222,7 +222,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
eff := Of[AppConfig]("API call successful")
|
||||
|
||||
// Provide config and run
|
||||
result, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
|
||||
result, err := RunSync(Provide[string](cfg)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "API call successful", result)
|
||||
@@ -238,7 +238,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
|
||||
eff := Fail[AppConfig, string](expectedErr)
|
||||
|
||||
_, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
|
||||
_, err := RunSync(Provide[string](cfg)(eff))(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
@@ -253,7 +253,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
return Of[TestContext](x * 2)
|
||||
})(Of[TestContext](21)))
|
||||
|
||||
result, err := RunSync(Provide[TestContext, string](ctx)(eff))(context.Background())
|
||||
result, err := RunSync(Provide[string](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "final", result)
|
||||
@@ -281,7 +281,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
return State{X: x}
|
||||
})(Of[TestContext](10)))
|
||||
|
||||
result, err := RunSync(Provide[TestContext, State](ctx)(eff))(context.Background())
|
||||
result, err := RunSync(Provide[State](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result.X)
|
||||
@@ -300,11 +300,11 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
innerEff := Of[InnerCtx]("inner result")
|
||||
|
||||
// Transform context
|
||||
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
|
||||
transformedEff := Local[string](func(outer OuterCtx) InnerCtx {
|
||||
return InnerCtx{Data: outer.Value + "-transformed"}
|
||||
})(innerEff)
|
||||
|
||||
result, err := RunSync(Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
|
||||
result, err := RunSync(Provide[string](outerCtx)(transformedEff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "inner result", result)
|
||||
@@ -318,7 +318,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
return Of[TestContext](x * 2)
|
||||
})(input)
|
||||
|
||||
result, err := RunSync(Provide[TestContext, []int](ctx)(eff))(context.Background())
|
||||
result, err := RunSync(Provide[[]int](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
|
||||
@@ -1,7 +1,55 @@
|
||||
// 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 effect
|
||||
|
||||
import "github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
|
||||
// TraverseArray applies an effectful function to each element of an array,
|
||||
// collecting the results into a new array. If any effect fails, the entire
|
||||
// traversal fails and returns the first error encountered.
|
||||
//
|
||||
// This is useful for performing effectful operations on collections while
|
||||
// maintaining the sequential order of results.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effects
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: An effectful function to apply to each element
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Kleisli[C, []A, []B]: A function that transforms an array of A to an effect producing an array of B
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// parseIntEff := func(s string) Effect[MyContext, int] {
|
||||
// val, err := strconv.Atoi(s)
|
||||
// if err != nil {
|
||||
// return effect.Fail[MyContext, int](err)
|
||||
// }
|
||||
// return effect.Of[MyContext](val)
|
||||
// }
|
||||
// input := []string{"1", "2", "3"}
|
||||
// eff := effect.TraverseArray[MyContext](parseIntEff)(input)
|
||||
// // eff produces []int{1, 2, 3}
|
||||
func TraverseArray[C, A, B any](f Kleisli[C, A, B]) Kleisli[C, []A, []B] {
|
||||
return readerreaderioresult.TraverseArray(f)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// 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 effect
|
||||
|
||||
import (
|
||||
@@ -17,21 +32,61 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
IO[A any] = io.IO[A]
|
||||
IOEither[E, A any] = ioeither.IOEither[E, A]
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
|
||||
Thunk[A any] = ReaderIOResult[A]
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
Result[A any] = result.Result[A]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
// Either represents a value that can be either a Left (error) or Right (success).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
|
||||
// Reader represents a computation that depends on a context R and produces a value A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// ReaderIO represents a computation that depends on a context R and produces an IO action returning A.
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
|
||||
// IO represents a synchronous side effect that produces a value A.
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// IOEither represents a synchronous side effect that can fail with error E or succeed with value A.
|
||||
IOEither[E, A any] = ioeither.IOEither[E, A]
|
||||
|
||||
// Lazy represents a lazily evaluated computation that produces a value A.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// IOResult represents a synchronous side effect that can fail with an error or succeed with value A.
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
|
||||
// ReaderIOResult represents a computation that depends on context and performs IO with error handling.
|
||||
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
|
||||
|
||||
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
|
||||
// Effect represents an effectful computation that:
|
||||
// - Requires a context of type C
|
||||
// - Can perform I/O operations
|
||||
// - Can fail with an error
|
||||
// - Produces a value of type A on success
|
||||
//
|
||||
// This is the core type of the effect package, providing a complete effect system
|
||||
// for managing dependencies, errors, and side effects in a composable way.
|
||||
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
|
||||
|
||||
// Thunk represents a computation that performs IO with error handling but doesn't require context.
|
||||
// It's equivalent to ReaderIOResult and is used as an intermediate step when providing context to an Effect.
|
||||
Thunk[A any] = ReaderIOResult[A]
|
||||
|
||||
// Predicate represents a function that tests a value of type A and returns a boolean.
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Result represents a computation result that can be either an error (Left) or a success value (Right).
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
// Lens represents an optic for focusing on a field T within a structure S.
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Kleisli represents a function from A to Effect[C, B], enabling monadic composition.
|
||||
// It's the fundamental building block for chaining effectful computations.
|
||||
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
|
||||
|
||||
// Operator represents a function that transforms Effect[C, A] to Effect[C, B].
|
||||
// It's used for lifting operations over effects.
|
||||
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
|
||||
)
|
||||
|
||||
@@ -188,6 +188,81 @@ func MonadChain[E, A, B any](fa Either[E, A], f Kleisli[E, A, B]) Either[E, B] {
|
||||
return f(fa.r)
|
||||
}
|
||||
|
||||
// MonadChainLeft sequences a computation on the Left (error) value, allowing error recovery or transformation.
|
||||
// If the Either is Left, applies the provided function to the error value, which returns a new Either.
|
||||
// If the Either is Right, returns the Right value unchanged with the new error type.
|
||||
//
|
||||
// This is the dual of [MonadChain] - while MonadChain operates on Right values (success),
|
||||
// MonadChainLeft operates on Left values (errors). It's useful for error recovery, error transformation,
|
||||
// or chaining alternative computations when an error occurs.
|
||||
//
|
||||
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// The error type can be transformed from EA to EB, allowing flexible error type conversions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Error recovery: convert specific errors to success
|
||||
// result := either.MonadChainLeft(
|
||||
// either.Left[int](errors.New("not found")),
|
||||
// func(err error) either.Either[string, int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return either.Right[string](0) // default value
|
||||
// }
|
||||
// return either.Left[int](err.Error()) // transform error
|
||||
// },
|
||||
// ) // Right(0)
|
||||
//
|
||||
// // Error transformation: change error type
|
||||
// result := either.MonadChainLeft(
|
||||
// either.Left[int](404),
|
||||
// func(code int) either.Either[string, int] {
|
||||
// return either.Left[int](fmt.Sprintf("Error code: %d", code))
|
||||
// },
|
||||
// ) // Left("Error code: 404")
|
||||
//
|
||||
// // Right values pass through unchanged
|
||||
// result := either.MonadChainLeft(
|
||||
// either.Right[error](42),
|
||||
// func(err error) either.Either[string, int] {
|
||||
// return either.Left[int]("error")
|
||||
// },
|
||||
// ) // Right(42)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[EA, EB, A any](fa Either[EA, A], f Kleisli[EB, EA, A]) Either[EB, A] {
|
||||
return MonadFold(fa, f, Of[EB])
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// Returns a function that sequences a computation on the Left (error) value.
|
||||
//
|
||||
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for creating reusable error handlers or transformers that can be
|
||||
// composed with other Either operations using pipes or function composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a reusable error handler
|
||||
// handleNotFound := either.ChainLeft[error, string](func(err error) either.Either[string, int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return either.Right[string](0)
|
||||
// }
|
||||
// return either.Left[int](err.Error())
|
||||
// })
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// result := F.Pipe1(
|
||||
// either.Left[int](errors.New("not found")),
|
||||
// handleNotFound,
|
||||
// ) // Right(0)
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[EA, EB, A any](f Kleisli[EB, EA, A]) Kleisli[EB, Either[EA, A], A] {
|
||||
return Fold(f, Of[EB])
|
||||
}
|
||||
|
||||
// MonadChainFirst executes a side-effect computation but returns the original value.
|
||||
// Useful for performing actions (like logging) without changing the value.
|
||||
//
|
||||
@@ -471,6 +546,8 @@ func Alt[E, A any](that Lazy[Either[E, A]]) Operator[E, A, A] {
|
||||
// If the Either is Left, it applies the provided function to the error value,
|
||||
// which returns a new Either that replaces the original.
|
||||
//
|
||||
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
|
||||
@@ -124,79 +124,52 @@ func TestStringer(t *testing.T) {
|
||||
func TestZeroWithIntegers(t *testing.T) {
|
||||
e := Zero[error, int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, 0, value, "Right value should be zero for int")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](0), e, "Zero should create a Right value with zero for int")
|
||||
}
|
||||
|
||||
// TestZeroWithStrings tests Zero function with string types
|
||||
func TestZeroWithStrings(t *testing.T) {
|
||||
e := Zero[error, string]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, "", value, "Right value should be empty string")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](""), e, "Zero should create a Right value with empty string")
|
||||
}
|
||||
|
||||
// TestZeroWithBooleans tests Zero function with boolean types
|
||||
func TestZeroWithBooleans(t *testing.T) {
|
||||
e := Zero[error, bool]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, false, value, "Right value should be false for bool")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](false), e, "Zero should create a Right value with false for bool")
|
||||
}
|
||||
|
||||
// TestZeroWithFloats tests Zero function with float types
|
||||
func TestZeroWithFloats(t *testing.T) {
|
||||
e := Zero[error, float64]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, 0.0, value, "Right value should be 0.0 for float64")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](0.0), e, "Zero should create a Right value with 0.0 for float64")
|
||||
}
|
||||
|
||||
// TestZeroWithPointers tests Zero function with pointer types
|
||||
func TestZeroWithPointers(t *testing.T) {
|
||||
e := Zero[error, *int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for pointer type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilPtr *int
|
||||
assert.Equal(t, Of[error](nilPtr), e, "Zero should create a Right value with nil pointer")
|
||||
}
|
||||
|
||||
// TestZeroWithSlices tests Zero function with slice types
|
||||
func TestZeroWithSlices(t *testing.T) {
|
||||
e := Zero[error, []int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for slice type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilSlice []int
|
||||
assert.Equal(t, Of[error](nilSlice), e, "Zero should create a Right value with nil slice")
|
||||
}
|
||||
|
||||
// TestZeroWithMaps tests Zero function with map types
|
||||
func TestZeroWithMaps(t *testing.T) {
|
||||
e := Zero[error, map[string]int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for map type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilMap map[string]int
|
||||
assert.Equal(t, Of[error](nilMap), e, "Zero should create a Right value with nil map")
|
||||
}
|
||||
|
||||
// TestZeroWithStructs tests Zero function with struct types
|
||||
@@ -208,23 +181,16 @@ func TestZeroWithStructs(t *testing.T) {
|
||||
|
||||
e := Zero[error, TestStruct]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
expected := TestStruct{Field1: 0, Field2: ""}
|
||||
assert.Equal(t, expected, value, "Right value should be zero value for struct")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](expected), e, "Zero should create a Right value with zero value for struct")
|
||||
}
|
||||
|
||||
// TestZeroWithInterfaces tests Zero function with interface types
|
||||
func TestZeroWithInterfaces(t *testing.T) {
|
||||
e := Zero[error, interface{}]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for interface type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilInterface interface{}
|
||||
assert.Equal(t, Of[error](nilInterface), e, "Zero should create a Right value with nil interface")
|
||||
}
|
||||
|
||||
// TestZeroWithCustomErrorType tests Zero function with custom error types
|
||||
@@ -236,12 +202,7 @@ func TestZeroWithCustomErrorType(t *testing.T) {
|
||||
|
||||
e := Zero[CustomError, string]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, "", value, "Right value should be empty string")
|
||||
assert.Equal(t, CustomError{Code: 0, Message: ""}, err, "Error should be zero value for CustomError")
|
||||
assert.Equal(t, Of[CustomError](""), e, "Zero should create a Right value with empty string")
|
||||
}
|
||||
|
||||
// TestZeroCanBeUsedWithOtherFunctions tests that Zero Eithers work with other either functions
|
||||
@@ -252,17 +213,13 @@ func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
|
||||
mapped := MonadMap(e, func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
})
|
||||
assert.True(t, IsRight(mapped), "Mapped Zero should still be Right")
|
||||
value, _ := Unwrap(mapped)
|
||||
assert.Equal(t, "0", value, "Mapped value should be '0'")
|
||||
assert.Equal(t, Of[error]("0"), mapped, "Mapped Zero should be Right with '0'")
|
||||
|
||||
// Test with Chain
|
||||
chained := MonadChain(e, func(n int) Either[error, string] {
|
||||
return Right[error](fmt.Sprintf("value: %d", n))
|
||||
})
|
||||
assert.True(t, IsRight(chained), "Chained Zero should still be Right")
|
||||
chainedValue, _ := Unwrap(chained)
|
||||
assert.Equal(t, "value: 0", chainedValue, "Chained value should be 'value: 0'")
|
||||
assert.Equal(t, Of[error]("value: 0"), chained, "Chained Zero should be Right with 'value: 0'")
|
||||
|
||||
// Test with Fold
|
||||
folded := MonadFold(e,
|
||||
@@ -295,23 +252,15 @@ func TestZeroWithComplexTypes(t *testing.T) {
|
||||
|
||||
e := Zero[error, ComplexType]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
expected := ComplexType{Nested: nil, Ptr: nil}
|
||||
assert.Equal(t, expected, value, "Right value should be zero value for complex struct")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](expected), e, "Zero should create a Right value with zero value for complex struct")
|
||||
}
|
||||
|
||||
// TestZeroWithOption tests Zero with Option type
|
||||
func TestZeroWithOption(t *testing.T) {
|
||||
e := Zero[error, O.Option[int]]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.True(t, O.IsNone(value), "Right value should be None for Option type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](O.None[int]()), e, "Zero should create a Right value with None option")
|
||||
}
|
||||
|
||||
// TestZeroIsNotLeft tests that Zero never creates a Left value
|
||||
@@ -343,3 +292,211 @@ func TestZeroEqualsDefaultInitialization(t *testing.T) {
|
||||
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
|
||||
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
|
||||
}
|
||||
|
||||
// TestMonadChainLeft tests the MonadChainLeft function with various scenarios
|
||||
func TestMonadChainLeft(t *testing.T) {
|
||||
t.Run("Left value is transformed by function", func(t *testing.T) {
|
||||
// Transform error to success
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("not found")),
|
||||
func(err error) Either[string, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[string](0) // default value
|
||||
}
|
||||
return Left[int](err.Error())
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
})
|
||||
|
||||
t.Run("Left value error type is transformed", func(t *testing.T) {
|
||||
// Transform error type from int to string
|
||||
result := MonadChainLeft(
|
||||
Left[int](404),
|
||||
func(code int) Either[string, int] {
|
||||
return Left[int](fmt.Sprintf("Error code: %d", code))
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Left[int]("Error code: 404"), result)
|
||||
})
|
||||
|
||||
t.Run("Right value passes through unchanged", func(t *testing.T) {
|
||||
// Right value should not be affected
|
||||
result := MonadChainLeft(
|
||||
Right[error](42),
|
||||
func(err error) Either[string, int] {
|
||||
return Left[int]("should not be called")
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("Chain multiple error transformations", func(t *testing.T) {
|
||||
// First transformation
|
||||
step1 := MonadChainLeft(
|
||||
Left[int](errors.New("error1")),
|
||||
func(err error) Either[error, int] {
|
||||
return Left[int](errors.New("error2"))
|
||||
},
|
||||
)
|
||||
// Second transformation
|
||||
step2 := MonadChainLeft(
|
||||
step1,
|
||||
func(err error) Either[string, int] {
|
||||
return Left[int](err.Error())
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Left[int]("error2"), step2)
|
||||
})
|
||||
|
||||
t.Run("Error recovery with fallback", func(t *testing.T) {
|
||||
// Recover from specific errors
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("timeout")),
|
||||
func(err error) Either[error, int] {
|
||||
if err.Error() == "timeout" {
|
||||
return Right[error](999) // fallback value
|
||||
}
|
||||
return Left[int](err)
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of[error](999), result)
|
||||
})
|
||||
|
||||
t.Run("Transform error to different Left", func(t *testing.T) {
|
||||
// Transform one error to another
|
||||
result := MonadChainLeft(
|
||||
Left[string]("original error"),
|
||||
func(s string) Either[int, string] {
|
||||
return Left[string](len(s))
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Left[string](14), result) // length of "original error"
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainLeft tests the curried ChainLeft function
|
||||
func TestChainLeft(t *testing.T) {
|
||||
t.Run("Curried function transforms Left value", func(t *testing.T) {
|
||||
// Create a reusable error handler
|
||||
handleNotFound := ChainLeft(func(err error) Either[string, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[string](0)
|
||||
}
|
||||
return Left[int](err.Error())
|
||||
})
|
||||
|
||||
result := handleNotFound(Left[int](errors.New("not found")))
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
})
|
||||
|
||||
t.Run("Curried function with Right value", func(t *testing.T) {
|
||||
handler := ChainLeft(func(err error) Either[string, int] {
|
||||
return Left[int]("should not be called")
|
||||
})
|
||||
|
||||
result := handler(Right[error](42))
|
||||
assert.Equal(t, Of[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("Use in pipeline with Pipe", func(t *testing.T) {
|
||||
// Create error transformer
|
||||
toStringError := ChainLeft(func(code int) Either[string, string] {
|
||||
return Left[string](fmt.Sprintf("Error: %d", code))
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[string](404),
|
||||
toStringError,
|
||||
)
|
||||
assert.Equal(t, Left[string]("Error: 404"), result)
|
||||
})
|
||||
|
||||
t.Run("Compose multiple ChainLeft operations", func(t *testing.T) {
|
||||
// First handler: convert error to string
|
||||
handler1 := ChainLeft(func(err error) Either[string, int] {
|
||||
return Left[int](err.Error())
|
||||
})
|
||||
|
||||
// Second handler: add prefix to string error
|
||||
handler2 := ChainLeft(func(s string) Either[string, int] {
|
||||
return Left[int]("Handled: " + s)
|
||||
})
|
||||
|
||||
result := F.Pipe2(
|
||||
Left[int](errors.New("original")),
|
||||
handler1,
|
||||
handler2,
|
||||
)
|
||||
assert.Equal(t, Left[int]("Handled: original"), result)
|
||||
})
|
||||
|
||||
t.Run("Error recovery in pipeline", func(t *testing.T) {
|
||||
// Handler that recovers from specific errors
|
||||
recoverFromTimeout := ChainLeft(func(err error) Either[error, int] {
|
||||
if err.Error() == "timeout" {
|
||||
return Right[error](0) // recovered value
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
|
||||
// Test with timeout error
|
||||
result1 := F.Pipe1(
|
||||
Left[int](errors.New("timeout")),
|
||||
recoverFromTimeout,
|
||||
)
|
||||
assert.Equal(t, Of[error](0), result1)
|
||||
|
||||
// Test with other error
|
||||
result2 := F.Pipe1(
|
||||
Left[int](errors.New("other error")),
|
||||
recoverFromTimeout,
|
||||
)
|
||||
assert.True(t, IsLeft(result2))
|
||||
})
|
||||
|
||||
t.Run("Transform error type in pipeline", func(t *testing.T) {
|
||||
// Convert numeric error codes to descriptive strings
|
||||
codeToMessage := ChainLeft(func(code int) Either[string, string] {
|
||||
messages := map[int]string{
|
||||
404: "Not Found",
|
||||
500: "Internal Server Error",
|
||||
}
|
||||
if msg, ok := messages[code]; ok {
|
||||
return Left[string](msg)
|
||||
}
|
||||
return Left[string](fmt.Sprintf("Unknown error: %d", code))
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[string](404),
|
||||
codeToMessage,
|
||||
)
|
||||
assert.Equal(t, Left[string]("Not Found"), result)
|
||||
})
|
||||
|
||||
t.Run("ChainLeft with Map combination", func(t *testing.T) {
|
||||
// Combine ChainLeft with Map to handle both channels
|
||||
errorHandler := ChainLeft(func(err error) Either[string, int] {
|
||||
return Left[int]("Error: " + err.Error())
|
||||
})
|
||||
|
||||
valueMapper := Map[string](S.Format[int]("Value: %d"))
|
||||
|
||||
// Test with Left
|
||||
result1 := F.Pipe2(
|
||||
Left[int](errors.New("fail")),
|
||||
errorHandler,
|
||||
valueMapper,
|
||||
)
|
||||
assert.Equal(t, Left[string]("Error: fail"), result1)
|
||||
|
||||
// Test with Right
|
||||
result2 := F.Pipe2(
|
||||
Right[error](42),
|
||||
errorHandler,
|
||||
valueMapper,
|
||||
)
|
||||
assert.Equal(t, Of[string]("Value: 42"), result2)
|
||||
})
|
||||
}
|
||||
|
||||
351
v2/either/filterable.go
Normal file
351
v2/either/filterable.go
Normal file
@@ -0,0 +1,351 @@
|
||||
// Copyright (c) 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 either provides implementations of the Either type and related operations.
|
||||
//
|
||||
// This package implements several Fantasy Land algebraic structures:
|
||||
// - Filterable: https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The Filterable specification defines operations for filtering and partitioning
|
||||
// data structures based on predicates and mapping functions.
|
||||
package either
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Partition separates an [Either] value into a [Pair] based on a predicate function.
|
||||
// It returns a function that takes an Either and produces a Pair of Either values,
|
||||
// where the first element contains values that fail the predicate and the second
|
||||
// contains values that pass the predicate.
|
||||
//
|
||||
// This function implements the Filterable specification's partition operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is Left, both elements of the resulting Pair will be the same Left value
|
||||
// - If the input is Right and the predicate returns true, the result is (Left(empty), Right(value))
|
||||
// - If the input is Right and the predicate returns false, the result is (Right(value), Left(empty))
|
||||
//
|
||||
// This function is useful for separating Either values into two categories based on
|
||||
// a condition, commonly used in filtering operations where you want to keep track of
|
||||
// both the values that pass and fail a test.
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A predicate function that tests values of type A
|
||||
// - empty: The default Left value to use when creating Left instances for partitioning
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A function that takes an Either[E, A] and returns a Pair where:
|
||||
// - First element: Either values that fail the predicate (or original Left)
|
||||
// - Second element: Either values that pass the predicate (or original Left)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// E "github.com/IBM/fp-go/v2/either"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// P "github.com/IBM/fp-go/v2/pair"
|
||||
// )
|
||||
//
|
||||
// // Partition positive and non-positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// partition := E.Partition(isPositive, "not positive")
|
||||
//
|
||||
// // Right value that passes predicate
|
||||
// result1 := partition(E.Right[string](5))
|
||||
// // result1 = Pair(Left("not positive"), Right(5))
|
||||
// left1, right1 := P.Unpack(result1)
|
||||
// // left1 = Left("not positive"), right1 = Right(5)
|
||||
//
|
||||
// // Right value that fails predicate
|
||||
// result2 := partition(E.Right[string](-3))
|
||||
// // result2 = Pair(Right(-3), Left("not positive"))
|
||||
// left2, right2 := P.Unpack(result2)
|
||||
// // left2 = Right(-3), right2 = Left("not positive")
|
||||
//
|
||||
// // Left value passes through unchanged in both positions
|
||||
// result3 := partition(E.Left[int]("error"))
|
||||
// // result3 = Pair(Left("error"), Left("error"))
|
||||
// left3, right3 := P.Unpack(result3)
|
||||
// // left3 = Left("error"), right3 = Left("error")
|
||||
func Partition[E, A any](p Predicate[A], empty E) func(Either[E, A]) Pair[Either[E, A], Either[E, A]] {
|
||||
l := Left[A](empty)
|
||||
return func(e Either[E, A]) Pair[Either[E, A], Either[E, A]] {
|
||||
if e.isLeft {
|
||||
return pair.Of(e)
|
||||
}
|
||||
if p(e.r) {
|
||||
return pair.MakePair(l, e)
|
||||
}
|
||||
return pair.MakePair(e, l)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter creates a filtering operation for [Either] values based on a predicate function.
|
||||
// It returns a function that takes an Either and produces an Either, where Right values
|
||||
// that fail the predicate are converted to Left values with the provided empty value.
|
||||
//
|
||||
// This function implements the Filterable specification's filter operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is Left, it passes through unchanged
|
||||
// - If the input is Right and the predicate returns true, the Right value passes through unchanged
|
||||
// - If the input is Right and the predicate returns false, it's converted to Left(empty)
|
||||
//
|
||||
// This function is useful for conditional validation or filtering of Either values,
|
||||
// where you want to reject Right values that don't meet certain criteria by converting
|
||||
// them to Left values with a default error.
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A predicate function that tests values of type A
|
||||
// - empty: The default Left value to use when filtering out Right values that fail the predicate
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator function that takes an Either[E, A] and returns an Either[E, A] where:
|
||||
// - Left values pass through unchanged
|
||||
// - Right values that pass the predicate remain as Right
|
||||
// - Right values that fail the predicate become Left(empty)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// E "github.com/IBM/fp-go/v2/either"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// )
|
||||
//
|
||||
// // Filter to keep only positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// filterPositive := E.Filter(isPositive, "not positive")
|
||||
//
|
||||
// // Right value that passes predicate - remains Right
|
||||
// result1 := filterPositive(E.Right[string](5))
|
||||
// // result1 = Right(5)
|
||||
//
|
||||
// // Right value that fails predicate - becomes Left
|
||||
// result2 := filterPositive(E.Right[string](-3))
|
||||
// // result2 = Left("not positive")
|
||||
//
|
||||
// // Left value passes through unchanged
|
||||
// result3 := filterPositive(E.Left[int]("original error"))
|
||||
// // result3 = Left("original error")
|
||||
//
|
||||
// // Chaining filters
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// filterEven := E.Filter(isEven, "not even")
|
||||
//
|
||||
// // Apply multiple filters in sequence
|
||||
// result4 := filterEven(filterPositive(E.Right[string](4)))
|
||||
// // result4 = Right(4) - passes both filters
|
||||
//
|
||||
// result5 := filterEven(filterPositive(E.Right[string](3)))
|
||||
// // result5 = Left("not even") - passes first, fails second
|
||||
func Filter[E, A any](p Predicate[A], empty E) Operator[E, A, A] {
|
||||
l := Left[A](empty)
|
||||
return func(e Either[E, A]) Either[E, A] {
|
||||
if e.isLeft || p(e.r) {
|
||||
return e
|
||||
}
|
||||
return l
|
||||
}
|
||||
}
|
||||
|
||||
// FilterMap combines filtering and mapping operations for [Either] values using an [Option]-returning function.
|
||||
// It returns a function that takes an Either[E, A] and produces an Either[E, B], where Right values
|
||||
// are transformed by applying the function f. If f returns Some(B), the result is Right(B). If f returns
|
||||
// None, the result is Left(empty).
|
||||
//
|
||||
// This function implements the Filterable specification's filterMap operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is Left, it passes through with its error value preserved as Left[B]
|
||||
// - If the input is Right and f returns Some(B), the result is Right(B)
|
||||
// - If the input is Right and f returns None, the result is Left(empty)
|
||||
//
|
||||
// This function is useful for operations that combine validation/filtering with transformation,
|
||||
// such as parsing strings to numbers (where invalid strings result in None), or extracting
|
||||
// optional fields from structures.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An Option Kleisli function that transforms values of type A to Option[B]
|
||||
// - empty: The default Left value to use when f returns None
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator function that takes an Either[E, A] and returns an Either[E, B] where:
|
||||
// - Left values pass through with error preserved
|
||||
// - Right values are transformed by f: Some(B) becomes Right(B), None becomes Left(empty)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// E "github.com/IBM/fp-go/v2/either"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// "strconv"
|
||||
// )
|
||||
//
|
||||
// // Parse string to int, filtering out invalid values
|
||||
// parseInt := func(s string) O.Option[int] {
|
||||
// if n, err := strconv.Atoi(s); err == nil {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
// filterMapInt := E.FilterMap(parseInt, "invalid number")
|
||||
//
|
||||
// // Valid number string - transforms to Right(int)
|
||||
// result1 := filterMapInt(E.Right[string]("42"))
|
||||
// // result1 = Right(42)
|
||||
//
|
||||
// // Invalid number string - becomes Left
|
||||
// result2 := filterMapInt(E.Right[string]("abc"))
|
||||
// // result2 = Left("invalid number")
|
||||
//
|
||||
// // Left value passes through with error preserved
|
||||
// result3 := filterMapInt(E.Left[string]("original error"))
|
||||
// // result3 = Left("original error")
|
||||
//
|
||||
// // Extract optional field from struct
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Email O.Option[string]
|
||||
// }
|
||||
// extractEmail := func(p Person) O.Option[string] { return p.Email }
|
||||
// filterMapEmail := E.FilterMap(extractEmail, "no email")
|
||||
//
|
||||
// result4 := filterMapEmail(E.Right[string](Person{Name: "Alice", Email: O.Some("alice@example.com")}))
|
||||
// // result4 = Right("alice@example.com")
|
||||
//
|
||||
// result5 := filterMapEmail(E.Right[string](Person{Name: "Bob", Email: O.None[string]()}))
|
||||
// // result5 = Left("no email")
|
||||
func FilterMap[E, A, B any](f option.Kleisli[A, B], empty E) Operator[E, A, B] {
|
||||
l := Left[B](empty)
|
||||
return func(e Either[E, A]) Either[E, B] {
|
||||
if e.isLeft {
|
||||
return Left[B](e.l)
|
||||
}
|
||||
if b, ok := option.Unwrap(f(e.r)); ok {
|
||||
return Right[E](b)
|
||||
}
|
||||
return l
|
||||
}
|
||||
}
|
||||
|
||||
// PartitionMap separates and transforms an [Either] value into a [Pair] of Either values using a mapping function.
|
||||
// It returns a function that takes an Either[E, A] and produces a Pair of Either values, where the mapping
|
||||
// function f transforms the Right value into Either[B, C]. The result is partitioned based on whether f
|
||||
// produces a Left or Right value.
|
||||
//
|
||||
// This function implements the Filterable specification's partitionMap operation:
|
||||
// https://github.com/fantasyland/fantasy-land#filterable
|
||||
//
|
||||
// The behavior is as follows:
|
||||
// - If the input is Left, both elements of the resulting Pair will be Left with the original error
|
||||
// - If the input is Right and f returns Left(B), the result is (Right(B), Left(empty))
|
||||
// - If the input is Right and f returns Right(C), the result is (Left(empty), Right(C))
|
||||
//
|
||||
// This function is useful for operations that need to categorize and transform values simultaneously,
|
||||
// such as separating valid and invalid data while applying different transformations to each category.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that transforms values of type A to Either[B, C]
|
||||
// - empty: The default error value to use when creating Left instances for partitioning
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A function that takes an Either[E, A] and returns a Pair[Either[E, B], Either[E, C]] where:
|
||||
// - If input is Left: (Left(original_error), Left(original_error))
|
||||
// - If f returns Left(B): (Right(B), Left(empty))
|
||||
// - If f returns Right(C): (Left(empty), Right(C))
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// E "github.com/IBM/fp-go/v2/either"
|
||||
// P "github.com/IBM/fp-go/v2/pair"
|
||||
// )
|
||||
//
|
||||
// // Classify and transform numbers: negative -> error message, positive -> squared value
|
||||
// classifyNumber := func(n int) E.Either[string, int] {
|
||||
// if n < 0 {
|
||||
// return E.Left[int]("negative: " + strconv.Itoa(n))
|
||||
// }
|
||||
// return E.Right[string](n * n)
|
||||
// }
|
||||
// partitionMap := E.PartitionMap(classifyNumber, "not classified")
|
||||
//
|
||||
// // Positive number - goes to right side as squared value
|
||||
// result1 := partitionMap(E.Right[string](5))
|
||||
// // result1 = Pair(Left("not classified"), Right(25))
|
||||
// left1, right1 := P.Unpack(result1)
|
||||
// // left1 = Left("not classified"), right1 = Right(25)
|
||||
//
|
||||
// // Negative number - goes to left side with error message
|
||||
// result2 := partitionMap(E.Right[string](-3))
|
||||
// // result2 = Pair(Right("negative: -3"), Left("not classified"))
|
||||
// left2, right2 := P.Unpack(result2)
|
||||
// // left2 = Right("negative: -3"), right2 = Left("not classified")
|
||||
//
|
||||
// // Original Left value - appears in both positions
|
||||
// result3 := partitionMap(E.Left[int]("original error"))
|
||||
// // result3 = Pair(Left("original error"), Left("original error"))
|
||||
// left3, right3 := P.Unpack(result3)
|
||||
// // left3 = Left("original error"), right3 = Left("original error")
|
||||
//
|
||||
// // Validate and transform user input
|
||||
// type ValidationError struct{ Field, Message string }
|
||||
// type User struct{ Name string; Age int }
|
||||
//
|
||||
// validateUser := func(input map[string]string) E.Either[ValidationError, User] {
|
||||
// name, hasName := input["name"]
|
||||
// ageStr, hasAge := input["age"]
|
||||
// if !hasName {
|
||||
// return E.Left[User](ValidationError{"name", "missing"})
|
||||
// }
|
||||
// if !hasAge {
|
||||
// return E.Left[User](ValidationError{"age", "missing"})
|
||||
// }
|
||||
// age, err := strconv.Atoi(ageStr)
|
||||
// if err != nil {
|
||||
// return E.Left[User](ValidationError{"age", "invalid"})
|
||||
// }
|
||||
// return E.Right[ValidationError](User{name, age})
|
||||
// }
|
||||
// partitionUsers := E.PartitionMap(validateUser, ValidationError{"", "not processed"})
|
||||
//
|
||||
// validInput := map[string]string{"name": "Alice", "age": "30"}
|
||||
// result4 := partitionUsers(E.Right[string](validInput))
|
||||
// // result4 = Pair(Left(ValidationError{"", "not processed"}), Right(User{"Alice", 30}))
|
||||
//
|
||||
// invalidInput := map[string]string{"name": "Bob"}
|
||||
// result5 := partitionUsers(E.Right[string](invalidInput))
|
||||
// // result5 = Pair(Right(ValidationError{"age", "missing"}), Left(ValidationError{"", "not processed"}))
|
||||
func PartitionMap[E, A, B, C any](f Kleisli[B, A, C], empty E) func(Either[E, A]) Pair[Either[E, B], Either[E, C]] {
|
||||
return func(e Either[E, A]) Pair[Either[E, B], Either[E, C]] {
|
||||
if e.isLeft {
|
||||
return pair.MakePair(Left[B](e.l), Left[C](e.l))
|
||||
}
|
||||
res := f(e.r)
|
||||
if res.isLeft {
|
||||
return pair.MakePair(Right[E](res.l), Left[C](empty))
|
||||
}
|
||||
return pair.MakePair(Left[B](empty), Right[E](res.r))
|
||||
}
|
||||
}
|
||||
1433
v2/either/filterable_test.go
Normal file
1433
v2/either/filterable_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
@@ -53,4 +54,8 @@ type (
|
||||
// Predicate represents a function that tests a value of type A and returns a boolean.
|
||||
// It's commonly used for filtering and conditional operations.
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Pair represents a tuple of two values of types L and R.
|
||||
// It's commonly used to return multiple values from functions or to group related data.
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
)
|
||||
|
||||
@@ -489,6 +489,8 @@ func After[E, A any](timestamp time.Time) Operator[E, A, A] {
|
||||
// If the input is a Left value, it applies the function f to transform the error and potentially
|
||||
// change the error type from EA to EB. If the input is a Right value, it passes through unchanged.
|
||||
//
|
||||
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery or error transformation scenarios where you want to handle
|
||||
// errors by performing another computation that may also fail.
|
||||
//
|
||||
@@ -523,6 +525,8 @@ func MonadChainLeft[EA, EB, A any](fa IOEither[EA, A], f Kleisli[EB, EA, A]) IOE
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// It returns a function that chains a computation on the left (error) side of an [IOEither].
|
||||
//
|
||||
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is particularly useful in functional composition pipelines where you want to handle
|
||||
// errors by performing another computation that may also fail.
|
||||
//
|
||||
@@ -644,6 +648,8 @@ func TapLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
|
||||
// If the IOEither is Left, it applies the provided function to the error value,
|
||||
// which returns a new IOEither that replaces the original.
|
||||
//
|
||||
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative IO computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
|
||||
@@ -490,3 +490,148 @@ func TestOrElseW(t *testing.T) {
|
||||
preserved := preserveRecover(preservedRight)()
|
||||
assert.Equal(t, E.Right[AppError](42), preserved)
|
||||
}
|
||||
|
||||
// TestChainLeftIdenticalToOrElse proves that ChainLeft and OrElse are identical functions.
|
||||
// This test verifies that both functions produce the same results for all scenarios:
|
||||
// - Left values with error recovery
|
||||
// - Left values with error transformation
|
||||
// - Right values passing through unchanged
|
||||
func TestChainLeftIdenticalToOrElse(t *testing.T) {
|
||||
// Test 1: Left value with error recovery - both should recover to Right
|
||||
t.Run("Left value recovery - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
recoveryFn := func(e string) IOEither[string, int] {
|
||||
if e == "recoverable" {
|
||||
return Right[string](42)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
input := Left[int]("recoverable")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(recoveryFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(recoveryFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](42), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 2: Left value with error transformation - both should transform error
|
||||
t.Run("Left value transformation - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
transformFn := func(e string) IOEither[string, int] {
|
||||
return Left[int]("transformed: " + e)
|
||||
}
|
||||
|
||||
input := Left[int]("original error")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(transformFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(transformFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Left[int]("transformed: original error"), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 3: Right value - both should pass through unchanged
|
||||
t.Run("Right value passthrough - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
handlerFn := func(e string) IOEither[string, int] {
|
||||
return Left[int]("should not be called")
|
||||
}
|
||||
|
||||
input := Right[string](100)
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(handlerFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(handlerFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](100), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 4: Error type widening - both should handle type transformation
|
||||
t.Run("Error type widening - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
widenFn := func(e string) IOEither[int, int] {
|
||||
return Left[int](404)
|
||||
}
|
||||
|
||||
input := Left[int]("not found")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(widenFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(widenFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Left[int](404), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 5: Composition in pipeline - both should work identically in F.Pipe
|
||||
t.Run("Pipeline composition - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
recoveryFn := func(e string) IOEither[string, int] {
|
||||
if e == "network error" {
|
||||
return Right[string](0)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
input := Left[int]("network error")
|
||||
|
||||
// Using ChainLeft in pipeline
|
||||
resultChainLeft := F.Pipe1(input, ChainLeft(recoveryFn))()
|
||||
|
||||
// Using OrElse in pipeline
|
||||
resultOrElse := F.Pipe1(input, OrElse(recoveryFn))()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](0), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 6: Multiple chained operations - both should behave identically
|
||||
t.Run("Multiple operations - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
handler1 := func(e string) IOEither[string, int] {
|
||||
if e == "error1" {
|
||||
return Right[string](1)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
handler2 := func(e string) IOEither[string, int] {
|
||||
if e == "error2" {
|
||||
return Right[string](2)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
input := Left[int]("error2")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := F.Pipe2(
|
||||
input,
|
||||
ChainLeft(handler1),
|
||||
ChainLeft(handler2),
|
||||
)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := F.Pipe2(
|
||||
input,
|
||||
OrElse(handler1),
|
||||
OrElse(handler2),
|
||||
)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](2), resultChainLeft)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -466,6 +466,11 @@ func Chain[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return F.Bind2nd(MonadChain[A, B], f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
// Flatten flattens a sequence of sequences into a single sequence.
|
||||
//
|
||||
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll
|
||||
|
||||
158
v2/iterator/iter/option.go
Normal file
158
v2/iterator/iter/option.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// 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 iter
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// MonadChainOptionK chains a function that returns an Option into a sequence,
|
||||
// filtering out None values and unwrapping Some values.
|
||||
//
|
||||
// This is useful for operations that may or may not produce a value for each element
|
||||
// in the sequence. Only the successful (Some) results are included in the output sequence,
|
||||
// while None values are filtered out.
|
||||
//
|
||||
// This is the monadic form that takes the sequence as the first parameter.
|
||||
//
|
||||
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: The element type of the input sequence
|
||||
// - B: The element type of the output sequence (wrapped in Option by the function)
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input sequence to transform
|
||||
// - f: A function that takes an element and returns an Option[B]
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// A new sequence containing only the unwrapped Some values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "strconv"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// I "github.com/IBM/fp-go/v2/iterator/iter"
|
||||
// )
|
||||
//
|
||||
// // Parse strings to integers, filtering out invalid ones
|
||||
// parseNum := func(s string) O.Option[int] {
|
||||
// if n, err := strconv.Atoi(s); err == nil {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// seq := I.From("1", "invalid", "2", "3", "bad")
|
||||
// result := I.MonadChainOptionK(seq, parseNum)
|
||||
// // yields: 1, 2, 3 (invalid strings are filtered out)
|
||||
func MonadChainOptionK[A, B any](as Seq[A], f option.Kleisli[A, B]) Seq[B] {
|
||||
return MonadFilterMap(as, f)
|
||||
}
|
||||
|
||||
// ChainOptionK returns an operator that chains a function returning an Option into a sequence,
|
||||
// filtering out None values and unwrapping Some values.
|
||||
//
|
||||
// This is the curried version of [MonadChainOptionK], useful for function composition
|
||||
// and creating reusable transformations.
|
||||
//
|
||||
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: The element type of the input sequence
|
||||
// - B: The element type of the output sequence (wrapped in Option by the function)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an element and returns an Option[B]
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator that transforms Seq[A] to Seq[B], filtering out None values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "strconv"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// I "github.com/IBM/fp-go/v2/iterator/iter"
|
||||
// )
|
||||
//
|
||||
// // Create a reusable parser operator
|
||||
// parsePositive := I.ChainOptionK(func(x int) O.Option[int] {
|
||||
// if x > 0 {
|
||||
// return O.Some(x)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// I.From(-1, 2, -3, 4, 5),
|
||||
// parsePositive,
|
||||
// )
|
||||
// // yields: 2, 4, 5 (negative numbers are filtered out)
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
|
||||
return FilterMap(f)
|
||||
}
|
||||
|
||||
// FlatMapOptionK is an alias for [ChainOptionK].
|
||||
//
|
||||
// This provides a more familiar name for developers coming from other functional
|
||||
// programming languages or libraries where "flatMap" is the standard terminology
|
||||
// for the monadic bind operation.
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: The element type of the input sequence
|
||||
// - B: The element type of the output sequence (wrapped in Option by the function)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an element and returns an Option[B]
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// An Operator that transforms Seq[A] to Seq[B], filtering out None values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// O "github.com/IBM/fp-go/v2/option"
|
||||
// I "github.com/IBM/fp-go/v2/iterator/iter"
|
||||
// )
|
||||
//
|
||||
// // Validate and transform data
|
||||
// validateAge := I.FlatMapOptionK(func(age int) O.Option[string] {
|
||||
// if age >= 18 && age <= 120 {
|
||||
// return O.Some(fmt.Sprintf("Valid age: %d", age))
|
||||
// }
|
||||
// return O.None[string]()
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// I.From(15, 25, 150, 30),
|
||||
// validateAge,
|
||||
// )
|
||||
// // yields: "Valid age: 25", "Valid age: 30"
|
||||
//
|
||||
//go:inline
|
||||
func FlatMapOptionK[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
|
||||
return ChainOptionK(f)
|
||||
}
|
||||
387
v2/iterator/iter/option_test.go
Normal file
387
v2/iterator/iter/option_test.go
Normal file
@@ -0,0 +1,387 @@
|
||||
// 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 iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestMonadChainOptionK_AllSome tests MonadChainOptionK when all values produce Some
|
||||
func TestMonadChainOptionK_AllSome(t *testing.T) {
|
||||
// Function that always returns Some
|
||||
double := func(x int) O.Option[int] {
|
||||
return O.Some(x * 2)
|
||||
}
|
||||
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := MonadChainOptionK(seq, double)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(2, 4, 6, 8, 10)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_AllNone tests MonadChainOptionK when all values produce None
|
||||
func TestMonadChainOptionK_AllNone(t *testing.T) {
|
||||
// Function that always returns None
|
||||
alwaysNone := func(x int) O.Option[int] {
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := MonadChainOptionK(seq, alwaysNone)
|
||||
values := slices.Collect(result)
|
||||
|
||||
assert.Empty(t, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_MixedSomeNone tests MonadChainOptionK with mixed Some and None
|
||||
func TestMonadChainOptionK_MixedSomeNone(t *testing.T) {
|
||||
// Function that returns Some for even numbers, None for odd
|
||||
evenOnly := func(x int) O.Option[int] {
|
||||
if x%2 == 0 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
seq := From(1, 2, 3, 4, 5, 6)
|
||||
result := MonadChainOptionK(seq, evenOnly)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(2, 4, 6)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_ParseStrings tests parsing strings to integers
|
||||
func TestMonadChainOptionK_ParseStrings(t *testing.T) {
|
||||
// Parse strings to integers, returning None for invalid strings
|
||||
parseNum := func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
seq := From("1", "invalid", "2", "3", "bad", "4")
|
||||
result := MonadChainOptionK(seq, parseNum)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(1, 2, 3, 4)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_EmptySequence tests MonadChainOptionK with empty sequence
|
||||
func TestMonadChainOptionK_EmptySequence(t *testing.T) {
|
||||
double := func(x int) O.Option[int] {
|
||||
return O.Some(x * 2)
|
||||
}
|
||||
|
||||
seq := From[int]()
|
||||
result := MonadChainOptionK(seq, double)
|
||||
values := slices.Collect(result)
|
||||
|
||||
assert.Empty(t, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_TypeTransformation tests transforming types
|
||||
func TestMonadChainOptionK_TypeTransformation(t *testing.T) {
|
||||
// Convert integers to strings, only for positive numbers
|
||||
positiveToString := func(x int) O.Option[string] {
|
||||
if x > 0 {
|
||||
return O.Some(fmt.Sprintf("num_%d", x))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
|
||||
seq := From(-2, -1, 0, 1, 2, 3)
|
||||
result := MonadChainOptionK(seq, positiveToString)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From("num_1", "num_2", "num_3")
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_ComplexType tests with complex types
|
||||
func TestMonadChainOptionK_ComplexType(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
// Extract age only for adults
|
||||
getAdultAge := func(p Person) O.Option[int] {
|
||||
if p.Age >= 18 {
|
||||
return O.Some(p.Age)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
seq := From(
|
||||
Person{"Alice", 25},
|
||||
Person{"Bob", 15},
|
||||
Person{"Charlie", 30},
|
||||
Person{"David", 12},
|
||||
)
|
||||
result := MonadChainOptionK(seq, getAdultAge)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(25, 30)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestChainOptionK_BasicUsage tests ChainOptionK basic functionality
|
||||
func TestChainOptionK_BasicUsage(t *testing.T) {
|
||||
// Create a reusable operator
|
||||
parsePositive := ChainOptionK(func(x int) O.Option[int] {
|
||||
if x > 0 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
seq := From(-1, 2, -3, 4, 5, -6)
|
||||
result := parsePositive(seq)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(2, 4, 5)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestChainOptionK_WithPipe tests ChainOptionK in a pipeline
|
||||
func TestChainOptionK_WithPipe(t *testing.T) {
|
||||
// Validate and transform in a pipeline
|
||||
validateRange := ChainOptionK(func(x int) O.Option[int] {
|
||||
if x >= 0 && x <= 100 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
result := F.Pipe2(
|
||||
From(-10, 20, 150, 50, 200, 75),
|
||||
validateRange,
|
||||
Map(func(x int) int { return x * 2 }),
|
||||
)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(40, 100, 150)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestChainOptionK_Composition tests composing multiple ChainOptionK operations
|
||||
func TestChainOptionK_Composition(t *testing.T) {
|
||||
// First filter: only positive
|
||||
onlyPositive := ChainOptionK(func(x int) O.Option[int] {
|
||||
if x > 0 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
// Second filter: only even
|
||||
onlyEven := ChainOptionK(func(x int) O.Option[int] {
|
||||
if x%2 == 0 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
result := F.Pipe2(
|
||||
From(-2, -1, 0, 1, 2, 3, 4, 5, 6),
|
||||
onlyPositive,
|
||||
onlyEven,
|
||||
)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(2, 4, 6)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestChainOptionK_StringParsing tests parsing with ChainOptionK
|
||||
func TestChainOptionK_StringParsing(t *testing.T) {
|
||||
// Create a reusable string parser
|
||||
parseInt := ChainOptionK(func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
From("10", "abc", "20", "xyz", "30"),
|
||||
parseInt,
|
||||
)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(10, 20, 30)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestFlatMapOptionK_Equivalence tests that FlatMapOptionK is equivalent to ChainOptionK
|
||||
func TestFlatMapOptionK_Equivalence(t *testing.T) {
|
||||
validate := func(x int) O.Option[int] {
|
||||
if x >= 0 && x <= 10 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
seq := From(-5, 0, 5, 10, 15)
|
||||
|
||||
// Using ChainOptionK
|
||||
result1 := ChainOptionK(validate)(seq)
|
||||
values1 := slices.Collect(result1)
|
||||
|
||||
// Using FlatMapOptionK
|
||||
result2 := FlatMapOptionK(validate)(seq)
|
||||
values2 := slices.Collect(result2)
|
||||
|
||||
// Both should produce the same result
|
||||
assert.Equal(t, values1, values2)
|
||||
assert.Equal(t, A.From(0, 5, 10), values1)
|
||||
}
|
||||
|
||||
// TestFlatMapOptionK_WithMap tests FlatMapOptionK combined with Map
|
||||
func TestFlatMapOptionK_WithMap(t *testing.T) {
|
||||
// Validate age and convert to category
|
||||
validateAge := FlatMapOptionK(func(age int) O.Option[string] {
|
||||
if age >= 18 && age <= 120 {
|
||||
return O.Some(fmt.Sprintf("Valid age: %d", age))
|
||||
}
|
||||
return O.None[string]()
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
From(15, 25, 150, 30, 200),
|
||||
validateAge,
|
||||
)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From("Valid age: 25", "Valid age: 30")
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestChainOptionK_LookupOperation tests using ChainOptionK for lookup operations
|
||||
func TestChainOptionK_LookupOperation(t *testing.T) {
|
||||
// Simulate a lookup table
|
||||
lookup := map[string]int{
|
||||
"one": 1,
|
||||
"two": 2,
|
||||
"three": 3,
|
||||
}
|
||||
|
||||
lookupValue := ChainOptionK(func(key string) O.Option[int] {
|
||||
if val, ok := lookup[key]; ok {
|
||||
return O.Some(val)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
From("one", "invalid", "two", "missing", "three"),
|
||||
lookupValue,
|
||||
)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(1, 2, 3)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// TestMonadChainOptionK_EarlyTermination tests that iteration stops when yield returns false
|
||||
func TestMonadChainOptionK_EarlyTermination(t *testing.T) {
|
||||
callCount := 0
|
||||
countCalls := func(x int) O.Option[int] {
|
||||
callCount++
|
||||
return O.Some(x)
|
||||
}
|
||||
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := MonadChainOptionK(seq, countCalls)
|
||||
|
||||
// Collect only first 3 elements
|
||||
collected := make([]int, 0)
|
||||
for v := range result {
|
||||
collected = append(collected, v)
|
||||
if len(collected) >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Should have called the function only 3 times due to early termination
|
||||
assert.Equal(t, 3, callCount)
|
||||
assert.Equal(t, A.From(1, 2, 3), collected)
|
||||
}
|
||||
|
||||
// TestChainOptionK_WithReduce tests ChainOptionK with reduction
|
||||
func TestChainOptionK_WithReduce(t *testing.T) {
|
||||
// Parse and sum valid numbers
|
||||
parseInt := ChainOptionK(func(s string) O.Option[int] {
|
||||
if n, err := strconv.Atoi(s); err == nil {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
From("10", "invalid", "20", "bad", "30"),
|
||||
parseInt,
|
||||
)
|
||||
|
||||
sum := MonadReduce(result, func(acc, x int) int {
|
||||
return acc + x
|
||||
}, 0)
|
||||
|
||||
assert.Equal(t, 60, sum)
|
||||
}
|
||||
|
||||
// TestFlatMapOptionK_NestedOptions tests FlatMapOptionK with nested option handling
|
||||
func TestFlatMapOptionK_NestedOptions(t *testing.T) {
|
||||
type Result struct {
|
||||
Value int
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// Extract value only if valid
|
||||
extractValid := FlatMapOptionK(func(r Result) O.Option[int] {
|
||||
if r.Valid {
|
||||
return O.Some(r.Value)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
|
||||
seq := From(
|
||||
Result{10, true},
|
||||
Result{20, false},
|
||||
Result{30, true},
|
||||
Result{40, false},
|
||||
Result{50, true},
|
||||
)
|
||||
|
||||
result := F.Pipe1(seq, extractValid)
|
||||
values := slices.Collect(result)
|
||||
|
||||
expected := A.From(10, 30, 50)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
99
v2/llms.txt
Normal file
99
v2/llms.txt
Normal file
@@ -0,0 +1,99 @@
|
||||
# fp-go
|
||||
|
||||
> A comprehensive functional programming library for Go, bringing type-safe monads, functors, applicatives, optics, and composable abstractions inspired by fp-ts and Haskell to the Go ecosystem. Created by IBM, licensed under Apache-2.0.
|
||||
|
||||
fp-go v2 requires Go 1.24+ and leverages generic type aliases for a cleaner API.
|
||||
|
||||
Key concepts: `Option` for nullable values, `Either`/`Result` for error handling, `IO` for lazy side effects, `Reader` for dependency injection, `IOResult` for effectful error handling, `ReaderIOResult` for the full monad stack, and `Optics` (lens, prism, traversal, iso) for immutable data manipulation.
|
||||
|
||||
## Core Documentation
|
||||
|
||||
- [API Reference (pkg.go.dev)](https://pkg.go.dev/github.com/IBM/fp-go/v2): Complete API documentation for all packages
|
||||
- [README](https://github.com/IBM/fp-go/blob/main/v2/README.md): Overview, quick start, installation, and migration guide from v1 to v2
|
||||
- [Design Decisions](https://github.com/IBM/fp-go/blob/main/v2/DESIGN.md): Key design principles and patterns
|
||||
- [Functional I/O Guide](https://github.com/IBM/fp-go/blob/main/v2/FUNCTIONAL_IO.md): Understanding Context, errors, and the Reader pattern for I/O operations
|
||||
- [Idiomatic vs Standard Comparison](https://github.com/IBM/fp-go/blob/main/v2/IDIOMATIC_COMPARISON.md): Performance comparison and when to use each approach
|
||||
- [Optics README](https://github.com/IBM/fp-go/blob/main/v2/optics/README.md): Guide to lens, prism, optional, and traversal optics
|
||||
|
||||
## Standard Packages (struct-based)
|
||||
|
||||
- [option](https://pkg.go.dev/github.com/IBM/fp-go/v2/option): Option monad — represent optional values without nil
|
||||
- [either](https://pkg.go.dev/github.com/IBM/fp-go/v2/either): Either monad — type-safe error handling with Left/Right values
|
||||
- [result](https://pkg.go.dev/github.com/IBM/fp-go/v2/result): Result monad — simplified Either with `error` as Left type (recommended for error handling)
|
||||
- [io](https://pkg.go.dev/github.com/IBM/fp-go/v2/io): IO monad — lazy evaluation and side effect management
|
||||
- [iooption](https://pkg.go.dev/github.com/IBM/fp-go/v2/iooption): IOOption — IO combined with Option
|
||||
- [ioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/ioeither): IOEither — IO combined with Either for effectful error handling
|
||||
- [ioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/ioresult): IOResult — IO combined with Result (recommended over IOEither)
|
||||
- [reader](https://pkg.go.dev/github.com/IBM/fp-go/v2/reader): Reader monad — dependency injection pattern
|
||||
- [readeroption](https://pkg.go.dev/github.com/IBM/fp-go/v2/readeroption): ReaderOption — Reader combined with Option
|
||||
- [readeriooption](https://pkg.go.dev/github.com/IBM/fp-go/v2/readeriooption): ReaderIOOption — Reader + IO + Option
|
||||
- [readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/readerioresult): ReaderIOResult — Reader + IO + Result for complex workflows
|
||||
- [readerioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/readerioeither): ReaderIOEither — Reader + IO + Either
|
||||
- [statereaderioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/statereaderioeither): StateReaderIOEither — State + Reader + IO + Either
|
||||
|
||||
## Idiomatic Packages (tuple-based, high performance)
|
||||
|
||||
- [idiomatic/option](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/option): Option using native Go `(value, bool)` tuples
|
||||
- [idiomatic/result](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/result): Result using native Go `(value, error)` tuples
|
||||
- [idiomatic/ioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/ioresult): IOResult using `func() (value, error)`
|
||||
- [idiomatic/readerresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/readerresult): ReaderResult with tuple-based results
|
||||
- [idiomatic/readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/readerioresult): ReaderIOResult with tuple-based results
|
||||
|
||||
## Context Packages (context.Context specializations)
|
||||
|
||||
- [context/readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult): ReaderIOResult specialized for context.Context
|
||||
- [context/readerioresult/http](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult/http): Functional HTTP client utilities
|
||||
- [context/readerioresult/http/builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult/http/builder): Functional HTTP request builder
|
||||
- [context/statereaderioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/statereaderioresult): State + Reader + IO + Result for context.Context
|
||||
|
||||
## Optics
|
||||
|
||||
- [optics](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics): Core optics package
|
||||
- [optics/lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens): Lenses for focusing on fields in product types
|
||||
- [optics/prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism): Prisms for focusing on variants in sum types
|
||||
- [optics/iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso): Isomorphisms for bidirectional transformations
|
||||
- [optics/optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional): Optionals for values that may not exist
|
||||
- [optics/traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal): Traversals for focusing on multiple values
|
||||
- [optics/codec](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/codec): Codecs for encoding/decoding with validation
|
||||
|
||||
## Utility Packages
|
||||
|
||||
- [array](https://pkg.go.dev/github.com/IBM/fp-go/v2/array): Functional array/slice operations (map, filter, fold, etc.)
|
||||
- [record](https://pkg.go.dev/github.com/IBM/fp-go/v2/record): Functional operations for maps
|
||||
- [function](https://pkg.go.dev/github.com/IBM/fp-go/v2/function): Function composition, pipe, flow, curry, identity
|
||||
- [pair](https://pkg.go.dev/github.com/IBM/fp-go/v2/pair): Strongly-typed pair/tuple data structure
|
||||
- [tuple](https://pkg.go.dev/github.com/IBM/fp-go/v2/tuple): Type-safe heterogeneous tuples
|
||||
- [predicate](https://pkg.go.dev/github.com/IBM/fp-go/v2/predicate): Predicate combinators (and, or, not, etc.)
|
||||
- [endomorphism](https://pkg.go.dev/github.com/IBM/fp-go/v2/endomorphism): Endomorphism operations (compose, chain)
|
||||
- [eq](https://pkg.go.dev/github.com/IBM/fp-go/v2/eq): Type-safe equality comparisons
|
||||
- [ord](https://pkg.go.dev/github.com/IBM/fp-go/v2/ord): Total ordering type class
|
||||
- [semigroup](https://pkg.go.dev/github.com/IBM/fp-go/v2/semigroup): Semigroup algebraic structure
|
||||
- [monoid](https://pkg.go.dev/github.com/IBM/fp-go/v2/monoid): Monoid algebraic structure
|
||||
- [number](https://pkg.go.dev/github.com/IBM/fp-go/v2/number): Algebraic structures for numeric types
|
||||
- [string](https://pkg.go.dev/github.com/IBM/fp-go/v2/string): Functional string utilities
|
||||
- [boolean](https://pkg.go.dev/github.com/IBM/fp-go/v2/boolean): Functional boolean utilities
|
||||
- [bytes](https://pkg.go.dev/github.com/IBM/fp-go/v2/bytes): Functional byte slice utilities
|
||||
- [json](https://pkg.go.dev/github.com/IBM/fp-go/v2/json): Functional JSON encoding/decoding
|
||||
- [lazy](https://pkg.go.dev/github.com/IBM/fp-go/v2/lazy): Lazy evaluation without side effects
|
||||
- [identity](https://pkg.go.dev/github.com/IBM/fp-go/v2/identity): Identity monad
|
||||
- [retry](https://pkg.go.dev/github.com/IBM/fp-go/v2/retry): Retry policies with configurable backoff
|
||||
- [tailrec](https://pkg.go.dev/github.com/IBM/fp-go/v2/tailrec): Trampoline for tail-call optimization
|
||||
- [di](https://pkg.go.dev/github.com/IBM/fp-go/v2/di): Dependency injection utilities
|
||||
- [effect](https://pkg.go.dev/github.com/IBM/fp-go/v2/effect): Functional effect system
|
||||
- [circuitbreaker](https://pkg.go.dev/github.com/IBM/fp-go/v2/circuitbreaker): Circuit breaker error types
|
||||
- [builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/builder): Generic builder pattern with validation
|
||||
|
||||
## Code Samples
|
||||
|
||||
- [samples/builder](https://github.com/IBM/fp-go/tree/main/v2/samples/builder): Functional builder pattern example
|
||||
- [samples/http](https://github.com/IBM/fp-go/tree/main/v2/samples/http): HTTP client examples
|
||||
- [samples/lens](https://github.com/IBM/fp-go/tree/main/v2/samples/lens): Optics/lens examples
|
||||
- [samples/mostly-adequate](https://github.com/IBM/fp-go/tree/main/v2/samples/mostly-adequate): Examples adapted from "Mostly Adequate Guide to Functional Programming"
|
||||
- [samples/tuples](https://github.com/IBM/fp-go/tree/main/v2/samples/tuples): Tuple usage examples
|
||||
|
||||
## Optional
|
||||
|
||||
- [Source Code](https://github.com/IBM/fp-go): GitHub repository
|
||||
- [Issues](https://github.com/IBM/fp-go/issues): Bug reports and feature requests
|
||||
- [Go Report Card](https://goreportcard.com/report/github.com/IBM/fp-go/v2): Code quality report
|
||||
- [Coverage](https://coveralls.io/github/IBM/fp-go?branch=main): Test coverage report
|
||||
@@ -13,6 +13,50 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package monoid provides an implementation of the Monoid algebraic structure.
|
||||
//
|
||||
// # Monoid
|
||||
//
|
||||
// A Monoid is an algebraic structure that extends [Semigroup] by adding an identity element.
|
||||
// It consists of:
|
||||
// - A type A
|
||||
// - An associative binary operation Concat: (A, A) → A
|
||||
// - An identity element Empty: () → A
|
||||
//
|
||||
// # Laws
|
||||
//
|
||||
// A Monoid must satisfy the following laws:
|
||||
//
|
||||
// 1. Associativity (from Semigroup):
|
||||
// Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
//
|
||||
// 2. Left Identity:
|
||||
// Concat(Empty(), x) = x
|
||||
//
|
||||
// 3. Right Identity:
|
||||
// Concat(x, Empty()) = x
|
||||
//
|
||||
// # Common Examples
|
||||
//
|
||||
// - Integer addition: Concat = (+), Empty = 0
|
||||
// - Integer multiplication: Concat = (*), Empty = 1
|
||||
// - String concatenation: Concat = (++), Empty = ""
|
||||
// - List concatenation: Concat = (++), Empty = []
|
||||
// - Boolean AND: Concat = (&&), Empty = true
|
||||
// - Boolean OR: Concat = (||), Empty = false
|
||||
// - Function composition: Concat = (∘), Empty = id
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Data.Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
|
||||
// - Fantasy Land Monoid: https://github.com/fantasyland/fantasy-land#monoid
|
||||
// - Semigroup: https://github.com/IBM/fp-go/v2/semigroup
|
||||
//
|
||||
// # Related Concepts
|
||||
//
|
||||
// - [Semigroup]: A Monoid without the identity element requirement
|
||||
// - Magma: A set with a binary operation (no laws required)
|
||||
// - Group: A Monoid where every element has an inverse
|
||||
package monoid
|
||||
|
||||
import (
|
||||
@@ -21,20 +65,31 @@ import (
|
||||
|
||||
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
|
||||
//
|
||||
// A Monoid extends Semigroup by adding an identity element (Empty) that satisfies:
|
||||
// A Monoid extends [Semigroup] by adding an identity element (Empty) that satisfies:
|
||||
// - Left identity: Concat(Empty(), x) = x
|
||||
// - Right identity: Concat(x, Empty()) = x
|
||||
//
|
||||
// The Monoid must also satisfy the associativity law from Semigroup:
|
||||
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
//
|
||||
// Common examples:
|
||||
// # Methods
|
||||
//
|
||||
// - Concat(x, y A) A: Inherited from Semigroup, combines two values associatively
|
||||
// - Empty() A: Returns the identity element for the monoid
|
||||
//
|
||||
// # Common Examples
|
||||
//
|
||||
// - Integer addition with 0 as identity
|
||||
// - Integer multiplication with 1 as identity
|
||||
// - String concatenation with "" as identity
|
||||
// - List concatenation with [] as identity
|
||||
// - Boolean AND with true as identity
|
||||
// - Boolean OR with false as identity
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Monoid typeclass: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Monoid
|
||||
// - Fantasy Land Monoid specification: https://github.com/fantasyland/fantasy-land#monoid
|
||||
type Monoid[A any] interface {
|
||||
S.Semigroup[A]
|
||||
Empty() A
|
||||
@@ -58,16 +113,22 @@ func (m monoid[A]) Empty() A {
|
||||
// The provided concat function must be associative, and the empty element must
|
||||
// satisfy the identity laws (left and right identity).
|
||||
//
|
||||
// Parameters:
|
||||
// - c: An associative binary operation func(A, A) A
|
||||
// - e: The identity element of type A
|
||||
// This is the primary constructor for creating custom monoid instances. It's the
|
||||
// equivalent of defining a Monoid instance in Haskell or implementing the Fantasy Land
|
||||
// Monoid specification.
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[A] instance
|
||||
// # Parameters
|
||||
//
|
||||
// Example:
|
||||
// - c: An associative binary operation func(A, A) A (equivalent to Haskell's mappend or <>)
|
||||
// - e: The identity element of type A (equivalent to Haskell's mempty)
|
||||
//
|
||||
// // Integer addition monoid
|
||||
// # Returns
|
||||
//
|
||||
// - A [Monoid][A] instance
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Integer addition monoid (Sum in Haskell)
|
||||
// addMonoid := MakeMonoid(
|
||||
// func(a, b int) int { return a + b },
|
||||
// 0, // identity element
|
||||
@@ -81,6 +142,11 @@ func (m monoid[A]) Empty() A {
|
||||
// "", // identity element
|
||||
// )
|
||||
// result := stringMonoid.Concat("Hello", " World") // "Hello World"
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Monoid instance: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Monoid
|
||||
// - Fantasy Land Monoid.empty: https://github.com/fantasyland/fantasy-land#monoid
|
||||
func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
|
||||
return monoid[A]{c: c, e: e}
|
||||
}
|
||||
@@ -91,13 +157,18 @@ func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
|
||||
// operation in the opposite order. This is useful for operations that are
|
||||
// not commutative.
|
||||
//
|
||||
// Parameters:
|
||||
// This corresponds to the Dual newtype wrapper in Haskell's Data.Monoid, which
|
||||
// provides a Monoid instance with reversed operation order.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: The monoid to reverse
|
||||
//
|
||||
// Returns:
|
||||
// - A new Monoid[A] with reversed operation order
|
||||
// # Returns
|
||||
//
|
||||
// Example:
|
||||
// - A new [Monoid][A] with reversed operation order
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Subtraction monoid (not commutative)
|
||||
// subMonoid := MakeMonoid(
|
||||
@@ -116,6 +187,10 @@ func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
|
||||
// )
|
||||
// reversed := Reverse(stringMonoid)
|
||||
// result := reversed.Concat("Hello", "World") // "WorldHello"
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Data.Monoid.Dual: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Dual
|
||||
func Reverse[A any](m Monoid[A]) Monoid[A] {
|
||||
return MakeMonoid(S.Reverse(m).Concat, m.Empty())
|
||||
}
|
||||
@@ -125,13 +200,19 @@ func Reverse[A any](m Monoid[A]) Monoid[A] {
|
||||
// This is useful when you need to use a monoid in a context that only requires
|
||||
// a semigroup (associative binary operation without identity).
|
||||
//
|
||||
// Parameters:
|
||||
// Since every Monoid is also a Semigroup (Monoid extends Semigroup), this conversion
|
||||
// is always safe. This reflects the mathematical relationship where monoids form a
|
||||
// subset of semigroups.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: The monoid to convert
|
||||
//
|
||||
// Returns:
|
||||
// - A Semigroup[A] that uses the same Concat operation
|
||||
// # Returns
|
||||
//
|
||||
// Example:
|
||||
// - A [Semigroup][A] that uses the same Concat operation
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// addMonoid := MakeMonoid(
|
||||
// func(a, b int) int { return a + b },
|
||||
@@ -139,6 +220,11 @@ func Reverse[A any](m Monoid[A]) Monoid[A] {
|
||||
// )
|
||||
// sg := ToSemigroup(addMonoid)
|
||||
// result := sg.Concat(5, 3) // 8 (identity not available)
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Semigroup: https://hackage.haskell.org/package/base/docs/Data-Semigroup.html
|
||||
// - Fantasy Land Semigroup: https://github.com/fantasyland/fantasy-land#semigroup
|
||||
func ToSemigroup[A any](m Monoid[A]) S.Semigroup[A] {
|
||||
return S.Semigroup[A](m)
|
||||
}
|
||||
|
||||
52
v2/monoid/types.go
Normal file
52
v2/monoid/types.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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 monoid
|
||||
|
||||
import "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// Void is an alias for function.Void, representing the unit type.
|
||||
//
|
||||
// The Void type (also known as Unit in functional programming) has exactly one value,
|
||||
// making it useful for representing the absence of meaningful information. It's similar
|
||||
// to void in other languages, but as a value rather than the absence of a value.
|
||||
//
|
||||
// This type alias is provided in the monoid package for convenience when working with
|
||||
// VoidMonoid and other monoid operations that may use the unit type.
|
||||
//
|
||||
// Common use cases:
|
||||
// - As a return type for functions that perform side effects but don't return meaningful data
|
||||
// - As a placeholder type parameter when a type is required but no data needs to be passed
|
||||
// - In monoid operations where you need to track that operations occurred without caring about results
|
||||
//
|
||||
// See also:
|
||||
// - function.Void: The underlying type definition
|
||||
// - function.VOID: The single inhabitant of the Void type
|
||||
// - VoidMonoid: A monoid instance for the Void type
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Function that performs an action but returns no meaningful data
|
||||
// func logMessage(msg string) Void {
|
||||
// fmt.Println(msg)
|
||||
// return function.VOID
|
||||
// }
|
||||
//
|
||||
// // Using Void in monoid operations
|
||||
// m := VoidMonoid()
|
||||
// result := m.Concat(function.VOID, function.VOID) // function.VOID
|
||||
type (
|
||||
Void = function.Void
|
||||
)
|
||||
65
v2/monoid/void.go
Normal file
65
v2/monoid/void.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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 monoid
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// VoidMonoid creates a Monoid for the Void (unit) type.
|
||||
//
|
||||
// The Void type has exactly one value (function.VOID), making it trivial to define
|
||||
// a monoid. This monoid uses the Last semigroup, which always returns the second
|
||||
// argument, though since all Void values are identical, the choice of semigroup
|
||||
// doesn't affect the result.
|
||||
//
|
||||
// This monoid is useful in contexts where:
|
||||
// - A monoid instance is required but no meaningful data needs to be combined
|
||||
// - You need to track that an operation occurred without caring about its result
|
||||
// - Building generic abstractions that work with any monoid, including the trivial case
|
||||
//
|
||||
// # Monoid Laws
|
||||
//
|
||||
// The VoidMonoid satisfies all monoid laws trivially:
|
||||
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always VOID
|
||||
// - Left Identity: Concat(Empty(), x) = x - always VOID
|
||||
// - Right Identity: Concat(x, Empty()) = x - always VOID
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid[Void] instance
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// m := VoidMonoid()
|
||||
// result := m.Concat(function.VOID, function.VOID) // function.VOID
|
||||
// empty := m.Empty() // function.VOID
|
||||
//
|
||||
// // Useful for tracking operations without data
|
||||
// type Action = func() Void
|
||||
// actions := []Action{
|
||||
// func() Void { fmt.Println("Action 1"); return function.VOID },
|
||||
// func() Void { fmt.Println("Action 2"); return function.VOID },
|
||||
// }
|
||||
// // Execute all actions and combine results
|
||||
// results := A.Map(func(a Action) Void { return a() })(actions)
|
||||
// _ = ConcatAll(m)(results) // All actions executed, result is VOID
|
||||
func VoidMonoid() Monoid[Void] {
|
||||
return MakeMonoid(
|
||||
S.Last[Void]().Concat,
|
||||
function.VOID,
|
||||
)
|
||||
}
|
||||
290
v2/monoid/void_test.go
Normal file
290
v2/monoid/void_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
// 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 monoid
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestVoidMonoid_Basic tests basic VoidMonoid functionality
|
||||
func TestVoidMonoid_Basic(t *testing.T) {
|
||||
m := VoidMonoid()
|
||||
|
||||
// Test Empty returns VOID
|
||||
empty := m.Empty()
|
||||
assert.Equal(t, function.VOID, empty)
|
||||
|
||||
// Test Concat returns VOID (since all Void values are identical)
|
||||
result := m.Concat(function.VOID, function.VOID)
|
||||
assert.Equal(t, function.VOID, result)
|
||||
}
|
||||
|
||||
// TestVoidMonoid_Laws verifies VoidMonoid satisfies monoid laws
|
||||
func TestVoidMonoid_Laws(t *testing.T) {
|
||||
m := VoidMonoid()
|
||||
|
||||
// Since Void has only one value, we test with that value
|
||||
v := function.VOID
|
||||
|
||||
// Left Identity: Concat(Empty(), x) = x
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
result := m.Concat(m.Empty(), v)
|
||||
assert.Equal(t, v, result, "Left identity law failed")
|
||||
})
|
||||
|
||||
// Right Identity: Concat(x, Empty()) = x
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
result := m.Concat(v, m.Empty())
|
||||
assert.Equal(t, v, result, "Right identity law failed")
|
||||
})
|
||||
|
||||
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
left := m.Concat(m.Concat(v, v), v)
|
||||
right := m.Concat(v, m.Concat(v, v))
|
||||
assert.Equal(t, left, right, "Associativity law failed")
|
||||
})
|
||||
|
||||
// All results should be VOID
|
||||
t.Run("all operations return VOID", func(t *testing.T) {
|
||||
assert.Equal(t, function.VOID, m.Concat(v, v))
|
||||
assert.Equal(t, function.VOID, m.Empty())
|
||||
assert.Equal(t, function.VOID, m.Concat(m.Empty(), v))
|
||||
assert.Equal(t, function.VOID, m.Concat(v, m.Empty()))
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMonoid_ConcatAll tests combining multiple Void values
|
||||
func TestVoidMonoid_ConcatAll(t *testing.T) {
|
||||
m := VoidMonoid()
|
||||
concatAll := ConcatAll(m)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input []Void
|
||||
expected Void
|
||||
}{
|
||||
{
|
||||
name: "empty slice",
|
||||
input: []Void{},
|
||||
expected: function.VOID,
|
||||
},
|
||||
{
|
||||
name: "single element",
|
||||
input: []Void{function.VOID},
|
||||
expected: function.VOID,
|
||||
},
|
||||
{
|
||||
name: "multiple elements",
|
||||
input: []Void{function.VOID, function.VOID, function.VOID},
|
||||
expected: function.VOID,
|
||||
},
|
||||
{
|
||||
name: "many elements",
|
||||
input: make([]Void, 100),
|
||||
expected: function.VOID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Initialize slice with VOID values
|
||||
for i := range tt.input {
|
||||
tt.input[i] = function.VOID
|
||||
}
|
||||
result := concatAll(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestVoidMonoid_Fold tests the Fold function with VoidMonoid
|
||||
func TestVoidMonoid_Fold(t *testing.T) {
|
||||
m := VoidMonoid()
|
||||
fold := Fold(m)
|
||||
|
||||
// Fold should behave identically to ConcatAll
|
||||
voids := []Void{function.VOID, function.VOID, function.VOID}
|
||||
result := fold(voids)
|
||||
assert.Equal(t, function.VOID, result)
|
||||
|
||||
// Empty fold
|
||||
emptyResult := fold([]Void{})
|
||||
assert.Equal(t, function.VOID, emptyResult)
|
||||
}
|
||||
|
||||
// TestVoidMonoid_Reverse tests that Reverse doesn't affect VoidMonoid
|
||||
func TestVoidMonoid_Reverse(t *testing.T) {
|
||||
m := VoidMonoid()
|
||||
reversed := Reverse(m)
|
||||
|
||||
// Since all Void values are identical, reverse should have no effect
|
||||
v := function.VOID
|
||||
|
||||
assert.Equal(t, m.Concat(v, v), reversed.Concat(v, v))
|
||||
assert.Equal(t, m.Empty(), reversed.Empty())
|
||||
|
||||
// Test identity laws still hold
|
||||
assert.Equal(t, v, reversed.Concat(reversed.Empty(), v))
|
||||
assert.Equal(t, v, reversed.Concat(v, reversed.Empty()))
|
||||
}
|
||||
|
||||
// TestVoidMonoid_ToSemigroup tests conversion to Semigroup
|
||||
func TestVoidMonoid_ToSemigroup(t *testing.T) {
|
||||
m := VoidMonoid()
|
||||
sg := ToSemigroup(m)
|
||||
|
||||
// Should work as a semigroup
|
||||
result := sg.Concat(function.VOID, function.VOID)
|
||||
assert.Equal(t, function.VOID, result)
|
||||
|
||||
// Verify it's the same underlying operation
|
||||
assert.Equal(t, m.Concat(function.VOID, function.VOID), sg.Concat(function.VOID, function.VOID))
|
||||
}
|
||||
|
||||
// TestVoidMonoid_FunctionMonoid tests VoidMonoid with FunctionMonoid
|
||||
func TestVoidMonoid_FunctionMonoid(t *testing.T) {
|
||||
m := VoidMonoid()
|
||||
funcMonoid := FunctionMonoid[string](m)
|
||||
|
||||
// Create functions that return Void
|
||||
f1 := func(s string) Void { return function.VOID }
|
||||
f2 := func(s string) Void { return function.VOID }
|
||||
|
||||
// Combine functions
|
||||
combined := funcMonoid.Concat(f1, f2)
|
||||
|
||||
// Test combined function
|
||||
result := combined("test")
|
||||
assert.Equal(t, function.VOID, result)
|
||||
|
||||
// Test empty function
|
||||
emptyFunc := funcMonoid.Empty()
|
||||
assert.Equal(t, function.VOID, emptyFunc("anything"))
|
||||
}
|
||||
|
||||
// TestVoidMonoid_PracticalUsage demonstrates practical usage patterns
|
||||
func TestVoidMonoid_PracticalUsage(t *testing.T) {
|
||||
m := VoidMonoid()
|
||||
|
||||
// Simulate tracking that operations occurred without caring about results
|
||||
type Action func() Void
|
||||
|
||||
actions := []Action{
|
||||
func() Void { return function.VOID }, // Action 1
|
||||
func() Void { return function.VOID }, // Action 2
|
||||
func() Void { return function.VOID }, // Action 3
|
||||
}
|
||||
|
||||
// Execute all actions and collect results
|
||||
results := make([]Void, len(actions))
|
||||
for i, action := range actions {
|
||||
results[i] = action()
|
||||
}
|
||||
|
||||
// Combine all results (all are VOID)
|
||||
finalResult := ConcatAll(m)(results)
|
||||
assert.Equal(t, function.VOID, finalResult)
|
||||
}
|
||||
|
||||
// TestVoidMonoid_EdgeCases tests edge cases
|
||||
func TestVoidMonoid_EdgeCases(t *testing.T) {
|
||||
m := VoidMonoid()
|
||||
|
||||
t.Run("multiple concatenations", func(t *testing.T) {
|
||||
// Chain multiple Concat operations
|
||||
result := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(function.VOID, function.VOID),
|
||||
function.VOID,
|
||||
),
|
||||
function.VOID,
|
||||
)
|
||||
assert.Equal(t, function.VOID, result)
|
||||
})
|
||||
|
||||
t.Run("concat with empty", func(t *testing.T) {
|
||||
// Various combinations with Empty()
|
||||
assert.Equal(t, function.VOID, m.Concat(m.Empty(), m.Empty()))
|
||||
assert.Equal(t, function.VOID, m.Concat(m.Concat(m.Empty(), function.VOID), m.Empty()))
|
||||
})
|
||||
|
||||
t.Run("large slice", func(t *testing.T) {
|
||||
// Test with a large number of elements
|
||||
largeSlice := make([]Void, 10000)
|
||||
for i := range largeSlice {
|
||||
largeSlice[i] = function.VOID
|
||||
}
|
||||
result := ConcatAll(m)(largeSlice)
|
||||
assert.Equal(t, function.VOID, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMonoid_TypeSafety verifies type safety
|
||||
func TestVoidMonoid_TypeSafety(t *testing.T) {
|
||||
m := VoidMonoid()
|
||||
|
||||
// Verify it implements Monoid interface
|
||||
var _ Monoid[Void] = m
|
||||
|
||||
// Verify Empty returns correct type
|
||||
empty := m.Empty()
|
||||
var _ Void = empty
|
||||
|
||||
// Verify Concat returns correct type
|
||||
result := m.Concat(function.VOID, function.VOID)
|
||||
var _ Void = result
|
||||
}
|
||||
|
||||
// BenchmarkVoidMonoid_Concat benchmarks the Concat operation
|
||||
func BenchmarkVoidMonoid_Concat(b *testing.B) {
|
||||
m := VoidMonoid()
|
||||
v := function.VOID
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = m.Concat(v, v)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkVoidMonoid_ConcatAll benchmarks combining multiple Void values
|
||||
func BenchmarkVoidMonoid_ConcatAll(b *testing.B) {
|
||||
m := VoidMonoid()
|
||||
concatAll := ConcatAll(m)
|
||||
|
||||
voids := make([]Void, 1000)
|
||||
for i := range voids {
|
||||
voids[i] = function.VOID
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = concatAll(voids)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkVoidMonoid_Empty benchmarks the Empty operation
|
||||
func BenchmarkVoidMonoid_Empty(b *testing.B) {
|
||||
m := VoidMonoid()
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = m.Empty()
|
||||
}
|
||||
}
|
||||
480
v2/optics/codec/alt.go
Normal file
480
v2/optics/codec/alt.go
Normal file
@@ -0,0 +1,480 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// validateAlt creates a validation function that tries the first codec's validation,
|
||||
// and if it fails, tries the second codec's validation as a fallback.
|
||||
//
|
||||
// This is an internal helper function that implements the Alternative pattern for
|
||||
// codec validation. It combines two codec validators using the validate.Alt operation,
|
||||
// which provides error recovery and fallback logic.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that both codecs decode to
|
||||
// - O: The output type that both codecs encode to
|
||||
// - I: The input type that both codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - first: The primary codec whose validation is tried first
|
||||
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
|
||||
// first validation fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, A] function that tries the first codec's validation, falling back
|
||||
// to the second if needed. If both fail, errors from both are aggregated.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The second codec is lazily evaluated for efficiency
|
||||
// - This function is used internally by MonadAlt and Alt
|
||||
// - The validation context is threaded through both validators
|
||||
// - Errors are accumulated using the validation error monoid
|
||||
func validateAlt[A, O, I any](
|
||||
first Type[A, O, I],
|
||||
second Lazy[Type[A, O, I]],
|
||||
) Validate[I, A] {
|
||||
|
||||
return F.Pipe1(
|
||||
first.Validate,
|
||||
validate.Alt(F.Pipe1(
|
||||
second,
|
||||
lazy.Map(F.Flip(reader.Curry(Type[A, O, I].Validate))),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAlt creates a new codec that tries the first codec, and if it fails during
|
||||
// validation, tries the second codec as a fallback.
|
||||
//
|
||||
// This function implements the Alternative typeclass pattern for codecs, enabling
|
||||
// "try this codec, or else try that codec" logic. It's particularly useful for:
|
||||
// - Handling multiple valid input formats
|
||||
// - Providing backward compatibility with legacy formats
|
||||
// - Implementing graceful degradation in parsing
|
||||
// - Supporting union types or polymorphic data
|
||||
//
|
||||
// The resulting codec uses the first codec's encoder and combines both validators
|
||||
// using the Alternative pattern. If both validations fail, errors from both are
|
||||
// aggregated for comprehensive error reporting.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that both codecs decode to
|
||||
// - O: The output type that both codecs encode to
|
||||
// - I: The input type that both codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - first: The primary codec to try first. Its encoder is used for the result.
|
||||
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
|
||||
// first validation fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A new Type[A, O, I] that combines both codecs with Alternative semantics.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// **Validation**:
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
//
|
||||
// **Encoding**:
|
||||
// - Always uses the first codec's encoder
|
||||
// - This assumes both codecs encode to the same output format
|
||||
//
|
||||
// **Type Checking**:
|
||||
// - Uses the generic Is[A]() type checker
|
||||
// - Validates that values are of type A
|
||||
//
|
||||
// # Example: Multiple Input Formats
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// )
|
||||
//
|
||||
// // Accept integers as either strings or numbers
|
||||
// intFromString := codec.IntFromString()
|
||||
// intFromNumber := codec.Int()
|
||||
//
|
||||
// // Try parsing as string first, fall back to number
|
||||
// flexibleInt := codec.MonadAlt(
|
||||
// intFromString,
|
||||
// func() codec.Type[int, any, any] { return intFromNumber },
|
||||
// )
|
||||
//
|
||||
// // Can now decode both "42" and 42
|
||||
// result1 := flexibleInt.Decode("42") // Success(42)
|
||||
// result2 := flexibleInt.Decode(42) // Success(42)
|
||||
//
|
||||
// # Example: Backward Compatibility
|
||||
//
|
||||
// // Support both old and new configuration formats
|
||||
// newConfigCodec := codec.Struct(/* new format */)
|
||||
// oldConfigCodec := codec.Struct(/* old format */)
|
||||
//
|
||||
// // Try new format first, fall back to old format
|
||||
// configCodec := codec.MonadAlt(
|
||||
// newConfigCodec,
|
||||
// func() codec.Type[Config, any, any] { return oldConfigCodec },
|
||||
// )
|
||||
//
|
||||
// // Automatically handles both formats
|
||||
// config := configCodec.Decode(input)
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// // Both validations will fail for invalid input
|
||||
// result := flexibleInt.Decode("not a number")
|
||||
// // Result contains errors from both string and number parsing attempts
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The second codec is lazily evaluated for efficiency
|
||||
// - First success short-circuits evaluation (second not called)
|
||||
// - Errors are aggregated when both fail
|
||||
// - The resulting codec's name is "Alt[<first codec name>]"
|
||||
// - Both codecs must have compatible input and output types
|
||||
// - The first codec's encoder is always used
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Alt: The curried, point-free version
|
||||
// - validate.MonadAlt: The underlying validation operation
|
||||
// - Either: For codecs that decode to Either[L, R] types
|
||||
func MonadAlt[A, O, I any](first Type[A, O, I], second Lazy[Type[A, O, I]]) Type[A, O, I] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("Alt[%s]", first.Name()),
|
||||
Is[A](),
|
||||
validateAlt(first, second),
|
||||
first.Encode,
|
||||
)
|
||||
}
|
||||
|
||||
// Alt creates an operator that adds alternative fallback logic to a codec.
|
||||
//
|
||||
// This is the curried, point-free version of MonadAlt. It returns a function that
|
||||
// can be applied to codecs to add fallback behavior. This style is particularly
|
||||
// useful for building codec transformation pipelines using function composition.
|
||||
//
|
||||
// Alt implements the Alternative typeclass pattern, enabling "try this codec, or
|
||||
// else try that codec" logic in a composable way.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that both codecs decode to
|
||||
// - O: The output type that both codecs encode to
|
||||
// - I: The input type that both codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
|
||||
// first codec's validation fails.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[A, A, O, I] that transforms codecs by adding alternative fallback logic.
|
||||
// This operator can be applied to any Type[A, O, I] to create a new codec with
|
||||
// fallback behavior.
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// When the returned operator is applied to a codec:
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
//
|
||||
// # Example: Point-Free Style
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// )
|
||||
//
|
||||
// // Create a reusable fallback operator
|
||||
// withNumberFallback := codec.Alt(func() codec.Type[int, any, any] {
|
||||
// return codec.Int()
|
||||
// })
|
||||
//
|
||||
// // Apply it to different codecs
|
||||
// flexibleInt1 := withNumberFallback(codec.IntFromString())
|
||||
// flexibleInt2 := withNumberFallback(codec.IntFromHex())
|
||||
//
|
||||
// # Example: Pipeline Composition
|
||||
//
|
||||
// // Build a codec pipeline with multiple fallbacks
|
||||
// flexibleCodec := F.Pipe2(
|
||||
// primaryCodec,
|
||||
// codec.Alt(func() codec.Type[T, O, I] { return fallback1 }),
|
||||
// codec.Alt(func() codec.Type[T, O, I] { return fallback2 }),
|
||||
// )
|
||||
// // Tries primary, then fallback1, then fallback2
|
||||
//
|
||||
// # Example: Reusable Transformations
|
||||
//
|
||||
// // Create a transformation that adds JSON fallback
|
||||
// withJSONFallback := codec.Alt(func() codec.Type[Config, string, string] {
|
||||
// return codec.JSONCodec[Config]()
|
||||
// })
|
||||
//
|
||||
// // Apply to multiple codecs
|
||||
// yamlWithFallback := withJSONFallback(yamlCodec)
|
||||
// tomlWithFallback := withJSONFallback(tomlCodec)
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - This is the point-free style version of MonadAlt
|
||||
// - Useful for building transformation pipelines with F.Pipe
|
||||
// - The second codec is lazily evaluated for efficiency
|
||||
// - First success short-circuits evaluation
|
||||
// - Errors are aggregated when both fail
|
||||
// - Can be composed with other codec operators
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - MonadAlt: The direct application version
|
||||
// - validate.Alt: The underlying validation operation
|
||||
// - F.Pipe: For composing multiple operators
|
||||
func Alt[A, O, I any](second Lazy[Type[A, O, I]]) Operator[A, A, O, I] {
|
||||
return F.Bind2nd(MonadAlt, second)
|
||||
}
|
||||
|
||||
// AltMonoid creates a Monoid instance for Type[A, O, I] using alternative semantics
|
||||
// with a provided zero/default codec.
|
||||
//
|
||||
// This function creates a monoid where:
|
||||
// 1. The first successful codec wins (no result combination)
|
||||
// 2. If the first fails during validation, the second is tried as a fallback
|
||||
// 3. If both fail, errors are aggregated
|
||||
// 4. The provided zero codec serves as the identity element
|
||||
//
|
||||
// Unlike other monoid patterns, AltMonoid does NOT combine successful results - it always
|
||||
// returns the first success. This makes it ideal for building fallback chains with default
|
||||
// codecs, configuration loading from multiple sources, and parser combinators with alternatives.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type that all codecs decode to
|
||||
// - O: The output type that all codecs encode to
|
||||
// - I: The input type that all codecs decode from
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - zero: A lazy Type[A, O, I] that serves as the identity element. This is typically
|
||||
// a codec that always succeeds with a default value, but can also be a failing
|
||||
// codec if no default is appropriate.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Monoid[Type[A, O, I]] that combines codecs using alternative semantics where
|
||||
// the first success wins.
|
||||
//
|
||||
// # Behavior Details
|
||||
//
|
||||
// The AltMonoid implements a "first success wins" strategy:
|
||||
//
|
||||
// - **First succeeds**: Returns the first result, second is never evaluated
|
||||
// - **First fails, second succeeds**: Returns the second result
|
||||
// - **Both fail**: Aggregates errors from both validators
|
||||
// - **Concat with Empty**: The zero codec is used as fallback
|
||||
// - **Encoding**: Always uses the first codec's encoder
|
||||
//
|
||||
// # Example: Configuration Loading with Fallbacks
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// "github.com/IBM/fp-go/v2/array"
|
||||
// )
|
||||
//
|
||||
// // Create a monoid with a default configuration
|
||||
// m := codec.AltMonoid(func() codec.Type[Config, string, string] {
|
||||
// return codec.MakeType(
|
||||
// "DefaultConfig",
|
||||
// codec.Is[Config](),
|
||||
// func(s string) codec.Decode[codec.Context, Config] {
|
||||
// return func(c codec.Context) codec.Validation[Config] {
|
||||
// return validation.Success(defaultConfig)
|
||||
// }
|
||||
// },
|
||||
// encodeConfig,
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// // Define codecs for different sources
|
||||
// fileCodec := loadFromFile("config.json")
|
||||
// envCodec := loadFromEnv()
|
||||
// defaultCodec := m.Empty()
|
||||
//
|
||||
// // Try file, then env, then default
|
||||
// configCodec := array.MonadFold(
|
||||
// []codec.Type[Config, string, string]{fileCodec, envCodec, defaultCodec},
|
||||
// m.Empty(),
|
||||
// m.Concat,
|
||||
// )
|
||||
//
|
||||
// // Load configuration - tries each source in order
|
||||
// result := configCodec.Decode(input)
|
||||
//
|
||||
// # Example: Parser with Multiple Formats
|
||||
//
|
||||
// // Create a monoid for parsing dates in multiple formats
|
||||
// m := codec.AltMonoid(func() codec.Type[time.Time, string, string] {
|
||||
// return codec.Date(time.RFC3339) // default format
|
||||
// })
|
||||
//
|
||||
// // Define parsers for different date formats
|
||||
// iso8601 := codec.Date("2006-01-02")
|
||||
// usFormat := codec.Date("01/02/2006")
|
||||
// euroFormat := codec.Date("02/01/2006")
|
||||
//
|
||||
// // Combine: try ISO 8601, then US, then European, then RFC3339
|
||||
// flexibleDate := m.Concat(
|
||||
// m.Concat(
|
||||
// m.Concat(iso8601, usFormat),
|
||||
// euroFormat,
|
||||
// ),
|
||||
// m.Empty(),
|
||||
// )
|
||||
//
|
||||
// // Can parse any of these formats
|
||||
// result1 := flexibleDate.Decode("2024-03-15") // ISO 8601
|
||||
// result2 := flexibleDate.Decode("03/15/2024") // US format
|
||||
// result3 := flexibleDate.Decode("15/03/2024") // European format
|
||||
//
|
||||
// # Example: Integer Parsing with Default
|
||||
//
|
||||
// // Create a monoid with default value of 0
|
||||
// m := codec.AltMonoid(func() codec.Type[int, string, string] {
|
||||
// return codec.MakeType(
|
||||
// "DefaultZero",
|
||||
// codec.Is[int](),
|
||||
// func(s string) codec.Decode[codec.Context, int] {
|
||||
// return func(c codec.Context) codec.Validation[int] {
|
||||
// return validation.Success(0)
|
||||
// }
|
||||
// },
|
||||
// strconv.Itoa,
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// // Try parsing as int, fall back to 0
|
||||
// intOrZero := m.Concat(codec.IntFromString(), m.Empty())
|
||||
//
|
||||
// result1 := intOrZero.Decode("42") // Success(42)
|
||||
// result2 := intOrZero.Decode("invalid") // Success(0) - uses default
|
||||
//
|
||||
// # Example: Error Aggregation
|
||||
//
|
||||
// // Both codecs fail - errors are aggregated
|
||||
// m := codec.AltMonoid(func() codec.Type[int, string, string] {
|
||||
// return codec.MakeType(
|
||||
// "NoDefault",
|
||||
// codec.Is[int](),
|
||||
// func(s string) codec.Decode[codec.Context, int] {
|
||||
// return func(c codec.Context) codec.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](s, "no default available")(c)
|
||||
// }
|
||||
// },
|
||||
// strconv.Itoa,
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// failing1 := codec.MakeType(
|
||||
// "Failing1",
|
||||
// codec.Is[int](),
|
||||
// func(s string) codec.Decode[codec.Context, int] {
|
||||
// return func(c codec.Context) codec.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](s, "error 1")(c)
|
||||
// }
|
||||
// },
|
||||
// strconv.Itoa,
|
||||
// )
|
||||
//
|
||||
// failing2 := codec.MakeType(
|
||||
// "Failing2",
|
||||
// codec.Is[int](),
|
||||
// func(s string) codec.Decode[codec.Context, int] {
|
||||
// return func(c codec.Context) codec.Validation[int] {
|
||||
// return validation.FailureWithMessage[int](s, "error 2")(c)
|
||||
// }
|
||||
// },
|
||||
// strconv.Itoa,
|
||||
// )
|
||||
//
|
||||
// combined := m.Concat(failing1, failing2)
|
||||
// result := combined.Decode("input")
|
||||
// // result contains errors: "error 1", "error 2", and "no default available"
|
||||
//
|
||||
// # Monoid Laws
|
||||
//
|
||||
// AltMonoid satisfies the monoid laws:
|
||||
//
|
||||
// 1. **Left Identity**: m.Concat(m.Empty(), codec) ≡ codec
|
||||
// 2. **Right Identity**: m.Concat(codec, m.Empty()) ≡ codec (tries codec first, falls back to zero)
|
||||
// 3. **Associativity**: m.Concat(m.Concat(a, b), c) ≡ m.Concat(a, m.Concat(b, c))
|
||||
//
|
||||
// Note: Due to the "first success wins" behavior, right identity means the zero is only
|
||||
// used if the codec fails.
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Configuration loading with multiple sources (file, env, default)
|
||||
// - Parsing data in multiple formats with fallbacks
|
||||
// - API versioning (try v2, fall back to v1, then default)
|
||||
// - Content negotiation (try JSON, then XML, then plain text)
|
||||
// - Validation with default values
|
||||
// - Parser combinators with alternative branches
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The zero codec is lazily evaluated, only when needed
|
||||
// - First success short-circuits evaluation (subsequent codecs not tried)
|
||||
// - Error aggregation ensures all validation failures are reported
|
||||
// - Encoding always uses the first codec's encoder
|
||||
// - This follows the alternative functor laws
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - MonadAlt: The underlying alternative operation for two codecs
|
||||
// - Alt: The curried version for pipeline composition
|
||||
// - validate.AltMonoid: The validation-level alternative monoid
|
||||
// - decode.AltMonoid: The decode-level alternative monoid
|
||||
func AltMonoid[A, O, I any](zero Lazy[Type[A, O, I]]) Monoid[Type[A, O, I]] {
|
||||
return monoid.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[A, O, I],
|
||||
)
|
||||
}
|
||||
921
v2/optics/codec/alt_test.go
Normal file
921
v2/optics/codec/alt_test.go
Normal file
@@ -0,0 +1,921 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMonadAltBasicFunctionality tests the basic behavior of MonadAlt
|
||||
func TestMonadAltBasicFunctionality(t *testing.T) {
|
||||
t.Run("uses first codec when it succeeds", func(t *testing.T) {
|
||||
// Create two codecs that both work with strings
|
||||
stringCodec := Id[string]()
|
||||
|
||||
// Create another string codec that only accepts uppercase
|
||||
uppercaseOnly := MakeType(
|
||||
"UppercaseOnly",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
for _, r := range s {
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return validation.FailureWithMessage[string](s, "must be uppercase")(c)
|
||||
}
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Create alt codec that tries uppercase first, then any string
|
||||
altCodec := MonadAlt(
|
||||
uppercaseOnly,
|
||||
func() Type[string, string, string] { return stringCodec },
|
||||
)
|
||||
|
||||
// Test with uppercase string - should succeed with first codec
|
||||
result := altCodec.Decode("HELLO")
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode with first codec")
|
||||
|
||||
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
|
||||
assert.Equal(t, "HELLO", value)
|
||||
})
|
||||
|
||||
t.Run("falls back to second codec when first fails", func(t *testing.T) {
|
||||
// Create a codec that only accepts positive integers
|
||||
positiveInt := MakeType(
|
||||
"PositiveInt",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i <= 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Create a codec that accepts any integer (with same input type)
|
||||
anyInt := MakeType(
|
||||
"AnyInt",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
positiveInt,
|
||||
func() Type[int, int, int] { return anyInt },
|
||||
)
|
||||
|
||||
// Test with negative number - first fails, second succeeds
|
||||
result := altCodec.Decode(-5)
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode with second codec")
|
||||
|
||||
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
|
||||
assert.Equal(t, -5, value)
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when both codecs fail", func(t *testing.T) {
|
||||
// Create two codecs that will both fail
|
||||
positiveInt := MakeType(
|
||||
"PositiveInt",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i <= 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
evenInt := MakeType(
|
||||
"EvenInt",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i%2 != 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be even")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
positiveInt,
|
||||
func() Type[int, int, int] { return evenInt },
|
||||
)
|
||||
|
||||
// Test with -3 (negative and odd) - both should fail
|
||||
result := altCodec.Decode(-3)
|
||||
|
||||
assert.True(t, either.IsLeft(result), "should fail when both codecs fail")
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
// Should have errors from both validation attempts
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "should have errors from both codecs")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAltNaming tests that the codec name is correctly generated
|
||||
func TestMonadAltNaming(t *testing.T) {
|
||||
t.Run("generates correct name", func(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
anotherStringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] { return anotherStringCodec },
|
||||
)
|
||||
|
||||
assert.Equal(t, "Alt[string]", altCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAltEncoding tests that encoding uses the first codec's encoder
|
||||
func TestMonadAltEncoding(t *testing.T) {
|
||||
t.Run("uses first codec's encoder", func(t *testing.T) {
|
||||
// Create a codec that encodes ints as strings with prefix
|
||||
prefixedInt := MakeType(
|
||||
"PrefixedInt",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
var n int
|
||||
_, err := fmt.Sscanf(s, "NUM:%d", &n)
|
||||
if err != nil {
|
||||
return validation.FailureWithError[int](s, "expected NUM:n format")(err)(c)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
},
|
||||
func(n int) string {
|
||||
return fmt.Sprintf("NUM:%d", n)
|
||||
},
|
||||
)
|
||||
|
||||
// Create a standard int from string codec
|
||||
standardInt := IntFromString()
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
prefixedInt,
|
||||
func() Type[int, string, string] { return standardInt },
|
||||
)
|
||||
|
||||
// Encode should use first codec's encoder
|
||||
encoded := altCodec.Encode(42)
|
||||
assert.Equal(t, "NUM:42", encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltOperator tests the curried Alt function
|
||||
func TestAltOperator(t *testing.T) {
|
||||
t.Run("creates reusable operator", func(t *testing.T) {
|
||||
// Create a fallback operator that accepts any string
|
||||
withStringFallback := Alt(func() Type[string, string, string] {
|
||||
return Id[string]()
|
||||
})
|
||||
|
||||
// Create a codec that only accepts "hello"
|
||||
helloOnly := MakeType(
|
||||
"HelloOnly",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s != "hello" {
|
||||
return validation.FailureWithMessage[string](s, "must be 'hello'")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Apply fallback to the codec
|
||||
altCodec := withStringFallback(helloOnly)
|
||||
|
||||
// Test that it works
|
||||
result1 := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
|
||||
result2 := altCodec.Decode("world")
|
||||
assert.True(t, either.IsRight(result2))
|
||||
})
|
||||
|
||||
t.Run("works in pipeline with F.Pipe", func(t *testing.T) {
|
||||
// Create a codec pipeline with multiple fallbacks
|
||||
baseCodec := MakeType(
|
||||
"StrictInt",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if s != "42" {
|
||||
return validation.FailureWithMessage[int](s, "must be exactly '42'")(c)
|
||||
}
|
||||
return validation.Success(42)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
fallback1 := MakeType(
|
||||
"Fallback1",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if s != "100" {
|
||||
return validation.FailureWithMessage[int](s, "must be exactly '100'")(c)
|
||||
}
|
||||
return validation.Success(100)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
fallback2 := MakeType(
|
||||
"AnyInt",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return validation.FailureWithError[int](s, "not an integer")(err)(c)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
// Build pipeline with multiple alternatives
|
||||
pipeline := F.Pipe2(
|
||||
baseCodec,
|
||||
Alt(func() Type[int, string, string] { return fallback1 }),
|
||||
Alt(func() Type[int, string, string] { return fallback2 }),
|
||||
)
|
||||
|
||||
// Test with "42" - should use base codec
|
||||
result1 := pipeline.Decode("42")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
value1 := either.GetOrElse(reader.Of[validation.Errors](0))(result1)
|
||||
assert.Equal(t, 42, value1)
|
||||
|
||||
// Test with "100" - should use fallback1
|
||||
result2 := pipeline.Decode("100")
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2 := either.GetOrElse(reader.Of[validation.Errors](0))(result2)
|
||||
assert.Equal(t, 100, value2)
|
||||
|
||||
// Test with "999" - should use fallback2
|
||||
result3 := pipeline.Decode("999")
|
||||
assert.True(t, either.IsRight(result3))
|
||||
value3 := either.GetOrElse(reader.Of[validation.Errors](0))(result3)
|
||||
assert.Equal(t, 999, value3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltLazyEvaluation tests that the second codec is only evaluated when needed
|
||||
func TestAltLazyEvaluation(t *testing.T) {
|
||||
t.Run("does not evaluate second codec when first succeeds", func(t *testing.T) {
|
||||
evaluated := false
|
||||
|
||||
stringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] {
|
||||
evaluated = true
|
||||
return Id[string]()
|
||||
},
|
||||
)
|
||||
|
||||
// Decode with first codec succeeding
|
||||
result := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Second codec should not have been evaluated
|
||||
assert.False(t, evaluated, "second codec should not be evaluated when first succeeds")
|
||||
})
|
||||
|
||||
t.Run("evaluates second codec when first fails", func(t *testing.T) {
|
||||
evaluated := false
|
||||
|
||||
// Create a codec that always fails
|
||||
failingCodec := MakeType(
|
||||
"Failing",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
return validation.FailureWithMessage[string](s, "always fails")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
altCodec := MonadAlt(
|
||||
failingCodec,
|
||||
func() Type[string, string, string] {
|
||||
evaluated = true
|
||||
return Id[string]()
|
||||
},
|
||||
)
|
||||
|
||||
// Decode with first codec failing
|
||||
result := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Second codec should have been evaluated
|
||||
assert.True(t, evaluated, "second codec should be evaluated when first fails")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltWithComplexTypes tests Alt with more complex codec scenarios
|
||||
func TestAltWithComplexTypes(t *testing.T) {
|
||||
t.Run("works with string length validation", func(t *testing.T) {
|
||||
// Create codec that accepts strings of length 5
|
||||
length5 := MakeType(
|
||||
"Length5",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) != 5 {
|
||||
return validation.FailureWithMessage[string](s, "must be length 5")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Create codec that accepts any string
|
||||
anyString := Id[string]()
|
||||
|
||||
// Create alt codec
|
||||
altCodec := MonadAlt(
|
||||
length5,
|
||||
func() Type[string, string, string] { return anyString },
|
||||
)
|
||||
|
||||
// Test with length 5 - should use first codec
|
||||
result1 := altCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
|
||||
// Test with different length - should fall back to second codec
|
||||
result2 := altCodec.Decode("hi")
|
||||
assert.True(t, either.IsRight(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltTypeChecking tests that type checking works correctly
|
||||
func TestAltTypeChecking(t *testing.T) {
|
||||
t.Run("type checking uses generic Is", func(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
anotherStringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] { return anotherStringCodec },
|
||||
)
|
||||
|
||||
// Test Is with valid type
|
||||
result1 := altCodec.Is("hello")
|
||||
assert.True(t, either.IsRight(result1))
|
||||
|
||||
// Test Is with invalid type
|
||||
result2 := altCodec.Is(42)
|
||||
assert.True(t, either.IsLeft(result2))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltRoundTrip tests encoding and decoding round trips
|
||||
func TestAltRoundTrip(t *testing.T) {
|
||||
t.Run("round-trip with first codec", func(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
anotherStringCodec := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
stringCodec,
|
||||
func() Type[string, string, string] { return anotherStringCodec },
|
||||
)
|
||||
|
||||
original := "hello"
|
||||
|
||||
// Decode
|
||||
decodeResult := altCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
decoded := either.GetOrElse(reader.Of[validation.Errors](""))(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := altCodec.Encode(decoded)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip with second codec", func(t *testing.T) {
|
||||
// Create a codec that only accepts "hello"
|
||||
helloOnly := MakeType(
|
||||
"HelloOnly",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s != "hello" {
|
||||
return validation.FailureWithMessage[string](s, "must be 'hello'")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
anyString := Id[string]()
|
||||
|
||||
altCodec := MonadAlt(
|
||||
helloOnly,
|
||||
func() Type[string, string, string] { return anyString },
|
||||
)
|
||||
|
||||
original := "world"
|
||||
|
||||
// Decode (will use second codec)
|
||||
decodeResult := altCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
decoded := either.GetOrElse(reader.Of[validation.Errors](""))(decodeResult)
|
||||
|
||||
// Encode (uses first codec's encoder, which is identity)
|
||||
encoded := altCodec.Encode(decoded)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltErrorMessages tests that error messages are informative
|
||||
func TestAltErrorMessages(t *testing.T) {
|
||||
t.Run("provides detailed error context", func(t *testing.T) {
|
||||
// Create two codecs with specific error messages
|
||||
codec1 := MakeType(
|
||||
"Codec1",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](i, "codec1 error")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
codec2 := MakeType(
|
||||
"Codec2",
|
||||
Is[int](),
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](i, "codec2 error")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
altCodec := MonadAlt(
|
||||
codec1,
|
||||
func() Type[int, int, int] { return codec2 },
|
||||
)
|
||||
|
||||
result := altCodec.Decode(42)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
require.GreaterOrEqual(t, len(errors), 2)
|
||||
|
||||
// Check that both error messages are present
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
|
||||
hasCodec1Error := false
|
||||
hasCodec2Error := false
|
||||
for _, msg := range messages {
|
||||
if msg == "codec1 error" {
|
||||
hasCodec1Error = true
|
||||
}
|
||||
if msg == "codec2 error" {
|
||||
hasCodec2Error = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasCodec1Error, "should have error from first codec")
|
||||
assert.True(t, hasCodec2Error, "should have error from second codec")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltMonoid tests the AltMonoid function
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
t.Run("with default value as zero", func(t *testing.T) {
|
||||
// Create a monoid with a default value of 0
|
||||
m := AltMonoid(func() Type[int, string, string] {
|
||||
return MakeType(
|
||||
"DefaultZero",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(0)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
})
|
||||
|
||||
// Create codecs
|
||||
intFromString := IntFromString()
|
||||
failing := MakeType(
|
||||
"Failing",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "always fails")(c)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
t.Run("first success wins", func(t *testing.T) {
|
||||
// Combine two successful codecs - first should win
|
||||
codec1 := MakeType(
|
||||
"Returns10",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(10)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
codec2 := MakeType(
|
||||
"Returns20",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(20)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
combined := m.Concat(codec1, codec2)
|
||||
result := combined.Decode("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
|
||||
assert.Equal(t, 10, value, "first success should win")
|
||||
})
|
||||
|
||||
t.Run("falls back to second when first fails", func(t *testing.T) {
|
||||
combined := m.Concat(failing, intFromString)
|
||||
result := combined.Decode("42")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("uses zero when both fail", func(t *testing.T) {
|
||||
combined := m.Concat(failing, m.Empty())
|
||||
result := combined.Decode("invalid")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
|
||||
assert.Equal(t, 0, value, "should use default zero value")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with failing zero", func(t *testing.T) {
|
||||
// Create a monoid with a failing zero
|
||||
m := AltMonoid(func() Type[int, string, string] {
|
||||
return MakeType(
|
||||
"NoDefault",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "no default available")(c)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
})
|
||||
|
||||
failing1 := MakeType(
|
||||
"Failing1",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "error 1")(c)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
failing2 := MakeType(
|
||||
"Failing2",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "error 2")(c)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
t.Run("aggregates all errors when all fail", func(t *testing.T) {
|
||||
combined := m.Concat(m.Concat(failing1, failing2), m.Empty())
|
||||
result := combined.Decode("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
// Should have errors from all three: failing1, failing2, and zero
|
||||
assert.GreaterOrEqual(t, len(errors), 3)
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
|
||||
hasError1 := false
|
||||
hasError2 := false
|
||||
hasNoDefault := false
|
||||
for _, msg := range messages {
|
||||
if msg == "error 1" {
|
||||
hasError1 = true
|
||||
}
|
||||
if msg == "error 2" {
|
||||
hasError2 = true
|
||||
}
|
||||
if msg == "no default available" {
|
||||
hasNoDefault = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasError1, "should have error from failing1")
|
||||
assert.True(t, hasError2, "should have error from failing2")
|
||||
assert.True(t, hasNoDefault, "should have error from zero")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("chaining multiple fallbacks", func(t *testing.T) {
|
||||
m := AltMonoid(func() Type[string, string, string] {
|
||||
return MakeType(
|
||||
"Default",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
return validation.Success("default")
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
})
|
||||
|
||||
primary := MakeType(
|
||||
"Primary",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s == "primary" {
|
||||
return validation.Success("from primary")
|
||||
}
|
||||
return validation.FailureWithMessage[string](s, "not primary")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
secondary := MakeType(
|
||||
"Secondary",
|
||||
Is[string](),
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if s == "secondary" {
|
||||
return validation.Success("from secondary")
|
||||
}
|
||||
return validation.FailureWithMessage[string](s, "not secondary")(c)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Chain: try primary, then secondary, then default
|
||||
combined := m.Concat(m.Concat(primary, secondary), m.Empty())
|
||||
|
||||
t.Run("uses primary when it succeeds", func(t *testing.T) {
|
||||
result := combined.Decode("primary")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
|
||||
assert.Equal(t, "from primary", value)
|
||||
})
|
||||
|
||||
t.Run("uses secondary when primary fails", func(t *testing.T) {
|
||||
result := combined.Decode("secondary")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
|
||||
assert.Equal(t, "from secondary", value)
|
||||
})
|
||||
|
||||
t.Run("uses default when both fail", func(t *testing.T) {
|
||||
result := combined.Decode("other")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
|
||||
assert.Equal(t, "default", value)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := AltMonoid(func() Type[int, string, string] {
|
||||
return MakeType(
|
||||
"DefaultZero",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(0)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
})
|
||||
|
||||
codec1 := MakeType(
|
||||
"Codec1",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(10)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
codec2 := MakeType(
|
||||
"Codec2",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(20)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
codec3 := MakeType(
|
||||
"Codec3",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(30)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
// m.Concat(m.Empty(), codec) should behave like codec
|
||||
// But with AltMonoid, if codec fails, it falls back to empty
|
||||
combined := m.Concat(m.Empty(), codec1)
|
||||
result := combined.Decode("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
|
||||
// Empty (0) comes first, so it wins
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
// m.Concat(codec, m.Empty()) tries codec first, falls back to empty
|
||||
combined := m.Concat(codec1, m.Empty())
|
||||
result := combined.Decode("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
|
||||
assert.Equal(t, 10, value, "codec1 should win")
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
// For AltMonoid, first success wins
|
||||
left := m.Concat(m.Concat(codec1, codec2), codec3)
|
||||
right := m.Concat(codec1, m.Concat(codec2, codec3))
|
||||
|
||||
resultLeft := left.Decode("input")
|
||||
resultRight := right.Decode("input")
|
||||
|
||||
assert.True(t, either.IsRight(resultLeft))
|
||||
assert.True(t, either.IsRight(resultRight))
|
||||
|
||||
valueLeft := either.GetOrElse(reader.Of[validation.Errors](-1))(resultLeft)
|
||||
valueRight := either.GetOrElse(reader.Of[validation.Errors](-1))(resultRight)
|
||||
|
||||
// Both should return 10 (first success)
|
||||
assert.Equal(t, valueLeft, valueRight)
|
||||
assert.Equal(t, 10, valueLeft)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("encoding uses first codec", func(t *testing.T) {
|
||||
m := AltMonoid(func() Type[int, string, string] {
|
||||
return MakeType(
|
||||
"Default",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(0)
|
||||
}
|
||||
},
|
||||
func(n int) string { return "DEFAULT" },
|
||||
)
|
||||
})
|
||||
|
||||
codec1 := MakeType(
|
||||
"Codec1",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(42)
|
||||
}
|
||||
},
|
||||
func(n int) string { return fmt.Sprintf("FIRST:%d", n) },
|
||||
)
|
||||
|
||||
codec2 := MakeType(
|
||||
"Codec2",
|
||||
Is[int](),
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
return validation.Success(100)
|
||||
}
|
||||
},
|
||||
func(n int) string { return fmt.Sprintf("SECOND:%d", n) },
|
||||
)
|
||||
|
||||
combined := m.Concat(codec1, codec2)
|
||||
|
||||
// Encoding should use first codec's encoder
|
||||
encoded := combined.Encode(42)
|
||||
assert.Equal(t, "FIRST:42", encoded)
|
||||
})
|
||||
}
|
||||
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)),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
816
v2/optics/codec/bind_test.go
Normal file
816
v2/optics/codec/bind_test.go
Normal file
@@ -0,0 +1,816 @@
|
||||
// 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")
|
||||
})
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -100,7 +101,7 @@ func (t *typeImpl[A, O, I]) Is(i any) Result[A] {
|
||||
// stringToInt := codec.MakeType(...) // Type[int, string, string]
|
||||
// intToPositive := codec.MakeType(...) // Type[PositiveInt, int, int]
|
||||
// composed := codec.Pipe(intToPositive)(stringToInt) // Type[PositiveInt, string, string]
|
||||
func Pipe[A, B, O, I any](ab Type[B, A, A]) func(Type[A, O, I]) Type[B, O, I] {
|
||||
func Pipe[O, I, A, B any](ab Type[B, A, A]) func(Type[A, O, I]) Type[B, O, I] {
|
||||
return func(this Type[A, O, I]) Type[B, O, I] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("Pipe(%s, %s)", this.Name(), ab.Name()),
|
||||
@@ -747,3 +748,114 @@ func FromRefinement[A, B any](refinement Refinement[A, B]) Type[B, A, A] {
|
||||
refinement.ReverseGet,
|
||||
)
|
||||
}
|
||||
|
||||
// Empty creates a Type codec that ignores input during decoding and uses a default value,
|
||||
// and ignores the value during encoding, using a default output.
|
||||
//
|
||||
// This codec is useful for:
|
||||
// - Providing default values for optional fields
|
||||
// - Creating placeholder codecs in generic contexts
|
||||
// - Implementing constant codecs that always produce the same value
|
||||
// - Building codecs for phantom types or unit-like types
|
||||
//
|
||||
// The codec uses a lazily-evaluated Pair[O, A] to provide both the default output
|
||||
// for encoding and the default value for decoding. The lazy evaluation ensures that
|
||||
// the defaults are only computed when needed.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type (what we decode to and encode from)
|
||||
// - O: The output type (what we encode to)
|
||||
// - I: The input type (what we decode from, but is ignored)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - e: A Lazy[Pair[O, A]] that provides the default values:
|
||||
// - pair.Head(e()): The default output value O used during encoding
|
||||
// - pair.Tail(e()): The default decoded value A used during decoding
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Type[A, O, I] that:
|
||||
// - Decode: Always succeeds and returns the default value A, ignoring input I
|
||||
// - Encode: Always returns the default output O, ignoring the input value A
|
||||
// - Is: Checks if a value is of type A (standard type checking)
|
||||
// - Name: Returns "Empty"
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// Decoding:
|
||||
// - Ignores the input value completely
|
||||
// - Always succeeds with validation.Success
|
||||
// - Returns the default value from pair.Tail(e())
|
||||
//
|
||||
// Encoding:
|
||||
// - Ignores the input value completely
|
||||
// - Always returns the default output from pair.Head(e())
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// Creating a codec with default values:
|
||||
//
|
||||
// // Create a codec that always decodes to 42 and encodes to "default"
|
||||
// defaultCodec := codec.Empty[int, string, any](lazy.Of(pair.MakePair("default", 42)))
|
||||
//
|
||||
// // Decode always returns 42, regardless of input
|
||||
// result := defaultCodec.Decode("anything") // Success: Right(42)
|
||||
// result = defaultCodec.Decode(123) // Success: Right(42)
|
||||
// result = defaultCodec.Decode(nil) // Success: Right(42)
|
||||
//
|
||||
// // Encode always returns "default", regardless of input
|
||||
// encoded := defaultCodec.Encode(100) // Returns: "default"
|
||||
// encoded = defaultCodec.Encode(0) // Returns: "default"
|
||||
//
|
||||
// Using with struct fields for default values:
|
||||
//
|
||||
// type Config struct {
|
||||
// Timeout int
|
||||
// Retries int
|
||||
// }
|
||||
//
|
||||
// // Codec that provides default retries value
|
||||
// defaultRetries := codec.Empty[int, int, any](lazy.Of(pair.MakePair(3, 3)))
|
||||
//
|
||||
// configCodec := F.Pipe2(
|
||||
// codec.Struct[Config]("Config"),
|
||||
// codec.ApSL(S.Monoid, timeoutLens, codec.Int()),
|
||||
// codec.ApSL(S.Monoid, retriesLens, defaultRetries),
|
||||
// )
|
||||
//
|
||||
// Creating a unit-like codec:
|
||||
//
|
||||
// // Codec for a unit type that always produces Void
|
||||
// unitCodec := codec.Empty[function.Void, function.Void, any](
|
||||
// lazy.Of(pair.MakePair(function.VOID, function.VOID)),
|
||||
// )
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Default values: Provide fallback values when decoding optional fields
|
||||
// - Constant codecs: Always produce the same value regardless of input
|
||||
// - Placeholder codecs: Use in generic contexts where a codec is required but not used
|
||||
// - Unit types: Encode/decode unit-like types that carry no information
|
||||
// - Testing: Create simple codecs for testing codec composition
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The lazy evaluation of the Pair ensures defaults are only computed when needed
|
||||
// - Both encoding and decoding always succeed (no validation errors)
|
||||
// - The input values are completely ignored in both directions
|
||||
// - The Is method still performs standard type checking for type A
|
||||
// - This codec is useful in applicative composition where some fields have defaults
|
||||
//
|
||||
// See also:
|
||||
// - Id: For identity codecs that preserve values
|
||||
// - MakeType: For creating custom codecs with validation logic
|
||||
func Empty[I, A, O any](e Lazy[Pair[O, A]]) Type[A, O, I] {
|
||||
return MakeType(
|
||||
"Empty",
|
||||
Is[A](),
|
||||
validate.OfLazy[I](F.Pipe1(e, lazy.Map(pair.Tail[O, A]))),
|
||||
reader.OfLazy[A](F.Pipe1(e, lazy.Map(pair.Head[O, A]))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -19,12 +21,7 @@ func TestString(t *testing.T) {
|
||||
stringType := String()
|
||||
result := stringType.Decode("hello")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "hello", value)
|
||||
assert.Equal(t, validation.Of("hello"), result)
|
||||
})
|
||||
|
||||
t.Run("fails to decode non-string", func(t *testing.T) {
|
||||
@@ -57,12 +54,7 @@ func TestString(t *testing.T) {
|
||||
stringType := String()
|
||||
result := stringType.Decode("")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) string { return "error" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,12 +63,7 @@ func TestInt(t *testing.T) {
|
||||
intType := Int()
|
||||
result := intType.Decode(42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("fails to decode string as int", func(t *testing.T) {
|
||||
@@ -109,24 +96,14 @@ func TestInt(t *testing.T) {
|
||||
intType := Int()
|
||||
result := intType.Decode(-42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, -42, value)
|
||||
assert.Equal(t, validation.Of(-42), result)
|
||||
})
|
||||
|
||||
t.Run("decodes zero", func(t *testing.T) {
|
||||
intType := Int()
|
||||
result := intType.Decode(0)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
assert.Equal(t, validation.Of(0), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,24 +112,14 @@ func TestBool(t *testing.T) {
|
||||
boolType := Bool()
|
||||
result := boolType.Decode(true)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) bool { return false },
|
||||
F.Identity[bool],
|
||||
)
|
||||
assert.Equal(t, true, value)
|
||||
assert.Equal(t, validation.Of(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes false", func(t *testing.T) {
|
||||
boolType := Bool()
|
||||
result := boolType.Decode(false)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) bool { return true },
|
||||
F.Identity[bool],
|
||||
)
|
||||
assert.Equal(t, false, value)
|
||||
assert.Equal(t, validation.Of(false), result)
|
||||
})
|
||||
|
||||
t.Run("fails to decode int as bool", func(t *testing.T) {
|
||||
@@ -189,36 +156,21 @@ func TestArray(t *testing.T) {
|
||||
intArray := Array(Int())
|
||||
result := intArray.Decode([]int{1, 2, 3})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{1, 2, 3}, value)
|
||||
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
|
||||
})
|
||||
|
||||
t.Run("decodes valid string array", func(t *testing.T) {
|
||||
stringArray := Array(String())
|
||||
result := stringArray.Decode([]string{"a", "b", "c"})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []string { return nil },
|
||||
F.Identity[[]string],
|
||||
)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, value)
|
||||
assert.Equal(t, validation.Of([]string{"a", "b", "c"}), result)
|
||||
})
|
||||
|
||||
t.Run("decodes empty array", func(t *testing.T) {
|
||||
intArray := Array(Int())
|
||||
result := intArray.Decode([]int{})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{}, value)
|
||||
assert.Equal(t, validation.Of([]int{}), result)
|
||||
})
|
||||
|
||||
t.Run("fails when array contains invalid element", func(t *testing.T) {
|
||||
@@ -256,12 +208,7 @@ func TestArray(t *testing.T) {
|
||||
nestedArray := Array(Array(Int()))
|
||||
result := nestedArray.Decode([][]int{{1, 2}, {3, 4}})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) [][]int { return nil },
|
||||
F.Identity[[][]int],
|
||||
)
|
||||
assert.Equal(t, [][]int{{1, 2}, {3, 4}}, value)
|
||||
assert.Equal(t, validation.Of([][]int{{1, 2}, {3, 4}}), result)
|
||||
})
|
||||
|
||||
t.Run("fails to decode non-iterable", func(t *testing.T) {
|
||||
@@ -275,12 +222,7 @@ func TestArray(t *testing.T) {
|
||||
boolArray := Array(Bool())
|
||||
result := boolArray.Decode([]bool{true, false, true})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []bool { return nil },
|
||||
F.Identity[[]bool],
|
||||
)
|
||||
assert.Equal(t, []bool{true, false, true}, value)
|
||||
assert.Equal(t, validation.Of([]bool{true, false, true}), result)
|
||||
})
|
||||
|
||||
t.Run("collects multiple validation errors", func(t *testing.T) {
|
||||
@@ -360,24 +302,14 @@ func TestTranscodeArray(t *testing.T) {
|
||||
intTranscode := TranscodeArray(Int())
|
||||
result := intTranscode.Decode([]any{1, 2, 3})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{1, 2, 3}, value)
|
||||
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
|
||||
})
|
||||
|
||||
t.Run("decodes valid string array from string slice", func(t *testing.T) {
|
||||
stringTranscode := TranscodeArray(String())
|
||||
result := stringTranscode.Decode([]any{"a", "b", "c"})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []string { return nil },
|
||||
F.Identity[[]string],
|
||||
)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, value)
|
||||
assert.Equal(t, validation.Of([]string{"a", "b", "c"}), result)
|
||||
})
|
||||
|
||||
t.Run("decodes empty array", func(t *testing.T) {
|
||||
@@ -411,24 +343,14 @@ func TestTranscodeArray(t *testing.T) {
|
||||
nestedTranscode := TranscodeArray(TranscodeArray(Int()))
|
||||
result := nestedTranscode.Decode([][]any{{1, 2}, {3, 4}})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) [][]int { return nil },
|
||||
F.Identity[[][]int],
|
||||
)
|
||||
assert.Equal(t, [][]int{{1, 2}, {3, 4}}, value)
|
||||
assert.Equal(t, validation.Of([][]int{{1, 2}, {3, 4}}), result)
|
||||
})
|
||||
|
||||
t.Run("decodes array of bools", func(t *testing.T) {
|
||||
boolTranscode := TranscodeArray(Bool())
|
||||
result := boolTranscode.Decode([]any{true, false, true})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []bool { return nil },
|
||||
F.Identity[[]bool],
|
||||
)
|
||||
assert.Equal(t, []bool{true, false, true}, value)
|
||||
assert.Equal(t, validation.Of([]bool{true, false, true}), result)
|
||||
})
|
||||
|
||||
t.Run("encodes empty array", func(t *testing.T) {
|
||||
@@ -481,12 +403,7 @@ func TestTranscodeArrayWithTransformation(t *testing.T) {
|
||||
arrayTranscode := TranscodeArray(stringToInt)
|
||||
result := arrayTranscode.Decode([]string{"a", "bb", "ccc"})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{1, 2, 3}, value)
|
||||
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
|
||||
})
|
||||
|
||||
t.Run("encodes int slice to string slice", func(t *testing.T) {
|
||||
@@ -710,6 +627,146 @@ func TestTranscodeEither(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestTranscodeEitherValidation(t *testing.T) {
|
||||
t.Run("validates Left value with context", func(t *testing.T) {
|
||||
eitherCodec := TranscodeEither(String(), Int())
|
||||
result := eitherCodec.Decode(either.Left[any, any](123)) // Invalid: should be string
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(either.Either[string, int]) validation.Errors { return nil },
|
||||
)
|
||||
assert.NotEmpty(t, errors)
|
||||
// Verify error contains type information
|
||||
assert.Contains(t, fmt.Sprintf("%v", errors[0]), "string")
|
||||
})
|
||||
|
||||
t.Run("validates Right value with context", func(t *testing.T) {
|
||||
eitherCodec := TranscodeEither(String(), Int())
|
||||
result := eitherCodec.Decode(either.Right[any, any]("not a number"))
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(either.Either[string, int]) validation.Errors { return nil },
|
||||
)
|
||||
assert.NotEmpty(t, errors)
|
||||
// Verify error contains type information
|
||||
assert.Contains(t, fmt.Sprintf("%v", errors[0]), "int")
|
||||
})
|
||||
|
||||
t.Run("preserves Either structure on validation failure", func(t *testing.T) {
|
||||
eitherCodec := TranscodeEither(String(), Int())
|
||||
|
||||
// Left with wrong type
|
||||
leftResult := eitherCodec.Decode(either.Left[any, any]([]int{1, 2, 3}))
|
||||
assert.True(t, either.IsLeft(leftResult))
|
||||
|
||||
// Right with wrong type
|
||||
rightResult := eitherCodec.Decode(either.Right[any, any](true))
|
||||
assert.True(t, either.IsLeft(rightResult))
|
||||
})
|
||||
|
||||
t.Run("validates with custom codec that can fail", func(t *testing.T) {
|
||||
// Create a codec that only accepts positive integers
|
||||
positiveInt := MakeType(
|
||||
"PositiveInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i <= 0 {
|
||||
return either.Left[int](fmt.Errorf("not a positive integer"))
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i <= 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
eitherCodec := TranscodeEither(String(), positiveInt)
|
||||
|
||||
// Valid positive integer
|
||||
validResult := eitherCodec.Decode(either.Right[any](42))
|
||||
assert.True(t, either.IsRight(validResult))
|
||||
|
||||
// Invalid: zero
|
||||
zeroResult := eitherCodec.Decode(either.Right[any](0))
|
||||
assert.True(t, either.IsLeft(zeroResult))
|
||||
|
||||
// Invalid: negative
|
||||
negativeResult := eitherCodec.Decode(either.Right[any](-5))
|
||||
assert.True(t, either.IsLeft(negativeResult))
|
||||
})
|
||||
|
||||
t.Run("validates both branches independently", func(t *testing.T) {
|
||||
// Create codecs with specific validation rules
|
||||
nonEmptyString := MakeType(
|
||||
"NonEmptyString",
|
||||
func(u any) either.Either[error, string] {
|
||||
s, ok := u.(string)
|
||||
if !ok || len(s) == 0 {
|
||||
return either.Left[string](fmt.Errorf("not a non-empty string"))
|
||||
}
|
||||
return either.Of[error](s)
|
||||
},
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) == 0 {
|
||||
return validation.FailureWithMessage[string](s, "must not be empty")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
evenInt := MakeType(
|
||||
"EvenInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i%2 != 0 {
|
||||
return either.Left[int](fmt.Errorf("not an even integer"))
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i%2 != 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be even")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
eitherCodec := TranscodeEither(nonEmptyString, evenInt)
|
||||
|
||||
// Valid Left: non-empty string
|
||||
validLeft := eitherCodec.Decode(either.Left[int]("hello"))
|
||||
assert.True(t, either.IsRight(validLeft))
|
||||
|
||||
// Invalid Left: empty string
|
||||
invalidLeft := eitherCodec.Decode(either.Left[int](""))
|
||||
assert.True(t, either.IsLeft(invalidLeft))
|
||||
|
||||
// Valid Right: even integer
|
||||
validRight := eitherCodec.Decode(either.Right[string](42))
|
||||
assert.True(t, either.IsRight(validRight))
|
||||
|
||||
// Invalid Right: odd integer
|
||||
invalidRight := eitherCodec.Decode(either.Right[string](43))
|
||||
assert.True(t, either.IsLeft(invalidRight))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTranscodeEitherWithTransformation(t *testing.T) {
|
||||
// Create a codec that transforms strings to their lengths
|
||||
stringToLength := MakeType(
|
||||
@@ -1218,24 +1275,14 @@ func TestId(t *testing.T) {
|
||||
idCodec := Id[string]()
|
||||
result := idCodec.Decode("hello")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "hello", value)
|
||||
assert.Equal(t, validation.Of("hello"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes int successfully", func(t *testing.T) {
|
||||
idCodec := Id[int]()
|
||||
result := idCodec.Decode(42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("encodes with identity function", func(t *testing.T) {
|
||||
@@ -1291,13 +1338,7 @@ func TestId(t *testing.T) {
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
|
||||
result := idCodec.Decode(person)
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Person { return Person{} },
|
||||
F.Identity[Person],
|
||||
)
|
||||
assert.Equal(t, person, value)
|
||||
assert.Equal(t, validation.Of(person), result)
|
||||
|
||||
encoded := idCodec.Encode(person)
|
||||
assert.Equal(t, person, encoded)
|
||||
@@ -1310,13 +1351,7 @@ func TestIdWithTranscodeArray(t *testing.T) {
|
||||
arrayCodec := TranscodeArray(intId)
|
||||
|
||||
result := arrayCodec.Decode([]int{1, 2, 3, 4, 5})
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, value)
|
||||
assert.Equal(t, validation.Of([]int{1, 2, 3, 4, 5}), result)
|
||||
})
|
||||
|
||||
t.Run("Id codec encodes array with identity", func(t *testing.T) {
|
||||
@@ -1333,13 +1368,7 @@ func TestIdWithTranscodeArray(t *testing.T) {
|
||||
|
||||
input := [][]int{{1, 2}, {3, 4}, {5}}
|
||||
result := nestedCodec.Decode(input)
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) [][]int { return nil },
|
||||
F.Identity[[][]int],
|
||||
)
|
||||
assert.Equal(t, input, value)
|
||||
assert.Equal(t, validation.Of(input), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1608,7 +1637,7 @@ func TestFromRefinementComposition(t *testing.T) {
|
||||
positiveCodec := FromRefinement(positiveIntPrism)
|
||||
|
||||
// Compose with Int codec using Pipe
|
||||
composed := Pipe[int, int, int, any](positiveCodec)(Int())
|
||||
composed := Pipe[int, any](positiveCodec)(Int())
|
||||
|
||||
t.Run("ComposedDecodeValid", func(t *testing.T) {
|
||||
result := composed.Decode(42)
|
||||
@@ -1709,3 +1738,416 @@ func TestFromRefinementValidationContext(t *testing.T) {
|
||||
assert.Equal(t, -5, err.Value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_Success tests that Empty always succeeds during decoding
|
||||
func TestEmpty_Success(t *testing.T) {
|
||||
t.Run("decodes any input to default value", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default", 42)))
|
||||
|
||||
// Test with various input types
|
||||
testCases := []struct {
|
||||
name string
|
||||
input any
|
||||
}{
|
||||
{"string input", "anything"},
|
||||
{"int input", 123},
|
||||
{"nil input", nil},
|
||||
{"bool input", true},
|
||||
{"struct input", struct{ X int }{X: 10}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := defaultCodec.Decode(tc.input)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("always returns same default value", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("output", "default")))
|
||||
|
||||
result1 := defaultCodec.Decode(123)
|
||||
result2 := defaultCodec.Decode("different")
|
||||
result3 := defaultCodec.Decode(nil)
|
||||
|
||||
assert.True(t, either.IsRight(result1))
|
||||
assert.True(t, either.IsRight(result2))
|
||||
assert.True(t, either.IsRight(result3))
|
||||
|
||||
value1 := either.MonadFold(result1, func(validation.Errors) string { return "" }, F.Identity[string])
|
||||
value2 := either.MonadFold(result2, func(validation.Errors) string { return "" }, F.Identity[string])
|
||||
value3 := either.MonadFold(result3, func(validation.Errors) string { return "" }, F.Identity[string])
|
||||
|
||||
assert.Equal(t, "default", value1)
|
||||
assert.Equal(t, "default", value2)
|
||||
assert.Equal(t, "default", value3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_Encoding tests that Empty always uses default output during encoding
|
||||
func TestEmpty_Encoding(t *testing.T) {
|
||||
t.Run("encodes any value to default output", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default", 42)))
|
||||
|
||||
// Test with various input values
|
||||
testCases := []struct {
|
||||
name string
|
||||
input int
|
||||
}{
|
||||
{"zero value", 0},
|
||||
{"positive value", 100},
|
||||
{"negative value", -50},
|
||||
{"default value", 42},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
encoded := defaultCodec.Encode(tc.input)
|
||||
assert.Equal(t, "default", encoded)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("always returns same default output", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, string, int](lazy.Of(pair.MakePair(999, "ignored")))
|
||||
|
||||
encoded1 := defaultCodec.Encode("value1")
|
||||
encoded2 := defaultCodec.Encode("value2")
|
||||
encoded3 := defaultCodec.Encode("")
|
||||
|
||||
assert.Equal(t, 999, encoded1)
|
||||
assert.Equal(t, 999, encoded2)
|
||||
assert.Equal(t, 999, encoded3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_Name tests that Empty has correct name
|
||||
func TestEmpty_Name(t *testing.T) {
|
||||
t.Run("has name 'Empty'", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(0, 0)))
|
||||
assert.Equal(t, "Empty", defaultCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_TypeChecking tests that Empty performs standard type checking
|
||||
func TestEmpty_TypeChecking(t *testing.T) {
|
||||
t.Run("Is checks for correct type", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default", 42)))
|
||||
|
||||
// Should succeed for int
|
||||
result := defaultCodec.Is(100)
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Should fail for non-int
|
||||
result = defaultCodec.Is("not an int")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("Is checks for string type", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("out", "in")))
|
||||
|
||||
// Should succeed for string
|
||||
result := defaultCodec.Is("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Should fail for non-string
|
||||
result = defaultCodec.Is(123)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_LazyEvaluation tests that the Pair parameter allows dynamic values
|
||||
func TestEmpty_LazyEvaluation(t *testing.T) {
|
||||
t.Run("lazy pair allows dynamic values", func(t *testing.T) {
|
||||
counter := 0
|
||||
lazyPair := func() pair.Pair[int, int] {
|
||||
counter++
|
||||
return pair.MakePair(counter, counter*10)
|
||||
}
|
||||
|
||||
defaultCodec := Empty[any, int, int](lazyPair)
|
||||
|
||||
// Each decode can get a different value if the lazy function is dynamic
|
||||
result1 := defaultCodec.Decode("input1")
|
||||
value1 := either.MonadFold(result1,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
result2 := defaultCodec.Decode("input2")
|
||||
value2 := either.MonadFold(result2,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Values can be different if lazy function produces different results
|
||||
assert.True(t, value1 > 0)
|
||||
assert.True(t, value2 > 0)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_WithStructs tests Empty with struct types
|
||||
func TestEmpty_WithStructs(t *testing.T) {
|
||||
type Config struct {
|
||||
Timeout int
|
||||
Retries int
|
||||
}
|
||||
|
||||
t.Run("provides default struct value", func(t *testing.T) {
|
||||
defaultConfig := Config{Timeout: 30, Retries: 3}
|
||||
defaultCodec := Empty[any, Config, Config](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Config { return Config{} },
|
||||
F.Identity[Config],
|
||||
)
|
||||
assert.Equal(t, 30, value.Timeout)
|
||||
assert.Equal(t, 3, value.Retries)
|
||||
})
|
||||
|
||||
t.Run("encodes to default struct", func(t *testing.T) {
|
||||
defaultConfig := Config{Timeout: 30, Retries: 3}
|
||||
inputConfig := Config{Timeout: 60, Retries: 5}
|
||||
|
||||
defaultCodec := Empty[any, Config, Config](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
|
||||
|
||||
encoded := defaultCodec.Encode(inputConfig)
|
||||
assert.Equal(t, 30, encoded.Timeout)
|
||||
assert.Equal(t, 3, encoded.Retries)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_WithPointers tests Empty with pointer types
|
||||
func TestEmpty_WithPointers(t *testing.T) {
|
||||
t.Run("provides default pointer value", func(t *testing.T) {
|
||||
defaultValue := 42
|
||||
defaultCodec := Empty[any, *int, *int](lazy.Of(pair.MakePair(&defaultValue, &defaultValue)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) *int { return nil },
|
||||
F.Identity[*int],
|
||||
)
|
||||
require.NotNil(t, value)
|
||||
assert.Equal(t, 42, *value)
|
||||
})
|
||||
|
||||
t.Run("provides nil pointer as default", func(t *testing.T) {
|
||||
var nilPtr *int
|
||||
defaultCodec := Empty[any, *int, *int](lazy.Of(pair.MakePair(nilPtr, nilPtr)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) *int { return new(int) },
|
||||
F.Identity[*int],
|
||||
)
|
||||
assert.Nil(t, value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_WithSlices tests Empty with slice types
|
||||
func TestEmpty_WithSlices(t *testing.T) {
|
||||
t.Run("provides default slice value", func(t *testing.T) {
|
||||
defaultSlice := []int{1, 2, 3}
|
||||
defaultCodec := Empty[any, []int, []int](lazy.Of(pair.MakePair(defaultSlice, defaultSlice)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{1, 2, 3}, value)
|
||||
})
|
||||
|
||||
t.Run("provides empty slice as default", func(t *testing.T) {
|
||||
emptySlice := []int{}
|
||||
defaultCodec := Empty[any, []int, []int](lazy.Of(pair.MakePair(emptySlice, emptySlice)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{}, value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_DifferentInputOutput tests Empty with different input and output types
|
||||
func TestEmpty_DifferentInputOutput(t *testing.T) {
|
||||
t.Run("decodes to int, encodes to string", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default-output", 42)))
|
||||
|
||||
// Decode always returns 42
|
||||
result := defaultCodec.Decode("any input")
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
|
||||
// Encode always returns "default-output"
|
||||
encoded := defaultCodec.Encode(100)
|
||||
assert.Equal(t, "default-output", encoded)
|
||||
})
|
||||
|
||||
t.Run("decodes to string, encodes to int", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, string, int](lazy.Of(pair.MakePair(999, "default-value")))
|
||||
|
||||
// Decode always returns "default-value"
|
||||
result := defaultCodec.Decode(123)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "default-value", value)
|
||||
|
||||
// Encode always returns 999
|
||||
encoded := defaultCodec.Encode("any string")
|
||||
assert.Equal(t, 999, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_EdgeCases tests edge cases for Empty
|
||||
func TestEmpty_EdgeCases(t *testing.T) {
|
||||
t.Run("with zero values", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(0, 0)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
|
||||
encoded := defaultCodec.Encode(100)
|
||||
assert.Equal(t, 0, encoded)
|
||||
})
|
||||
|
||||
t.Run("with empty string", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("", "")))
|
||||
|
||||
result := defaultCodec.Decode("non-empty")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) string { return "error" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
|
||||
encoded := defaultCodec.Encode("non-empty")
|
||||
assert.Equal(t, "", encoded)
|
||||
})
|
||||
|
||||
t.Run("with false boolean", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, bool, bool](lazy.Of(pair.MakePair(false, false)))
|
||||
|
||||
result := defaultCodec.Decode(true)
|
||||
assert.Equal(t, validation.Of(false), result)
|
||||
|
||||
encoded := defaultCodec.Encode(true)
|
||||
assert.Equal(t, false, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_Integration tests Empty in composition scenarios
|
||||
func TestEmpty_Integration(t *testing.T) {
|
||||
t.Run("composes with other codecs using Pipe", func(t *testing.T) {
|
||||
// Create a codec that always provides a default int
|
||||
defaultIntCodec := Empty[any, int, int](lazy.Of(pair.MakePair(42, 42)))
|
||||
|
||||
// Create a refinement that only accepts positive integers
|
||||
positiveIntPrism := prism.MakePrismWithName(
|
||||
func(n int) option.Option[int] {
|
||||
if n > 0 {
|
||||
return option.Some(n)
|
||||
}
|
||||
return option.None[int]()
|
||||
},
|
||||
func(n int) int { return n },
|
||||
"PositiveInt",
|
||||
)
|
||||
|
||||
positiveCodec := FromRefinement(positiveIntPrism)
|
||||
|
||||
// Compose: always decode to 42, then validate it's positive
|
||||
composed := Pipe[int, any](positiveCodec)(defaultIntCodec)
|
||||
|
||||
// Should succeed because 42 is positive
|
||||
result := composed.Decode("anything")
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("used as placeholder in generic contexts", func(t *testing.T) {
|
||||
// Empty can be used where a codec is required but not actually used
|
||||
unitCodec := Empty[any, Void, Void](
|
||||
lazy.Of(pair.MakePair(F.VOID, F.VOID)),
|
||||
)
|
||||
|
||||
result := unitCodec.Decode("ignored")
|
||||
assert.Equal(t, validation.Of(F.VOID), result)
|
||||
|
||||
encoded := unitCodec.Encode(F.VOID)
|
||||
assert.Equal(t, F.VOID, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_RoundTrip tests that Empty maintains consistency
|
||||
func TestEmpty_RoundTrip(t *testing.T) {
|
||||
t.Run("decode then encode returns default output", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("output", 42)))
|
||||
|
||||
// Decode
|
||||
result := defaultCodec.Decode("input")
|
||||
require.True(t, either.IsRight(result))
|
||||
|
||||
decoded := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Encode
|
||||
encoded := defaultCodec.Encode(decoded)
|
||||
|
||||
// Should get default output, not related to decoded value
|
||||
assert.Equal(t, "output", encoded)
|
||||
})
|
||||
|
||||
t.Run("multiple round trips are consistent", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(100, 50)))
|
||||
|
||||
// First round trip
|
||||
result1 := defaultCodec.Decode("input1")
|
||||
decoded1 := either.MonadFold(result1,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
encoded1 := defaultCodec.Encode(decoded1)
|
||||
|
||||
// Second round trip
|
||||
result2 := defaultCodec.Decode("input2")
|
||||
decoded2 := either.MonadFold(result2,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
encoded2 := defaultCodec.Encode(decoded2)
|
||||
|
||||
// All decoded values should be the same
|
||||
assert.Equal(t, 50, decoded1)
|
||||
assert.Equal(t, 50, decoded2)
|
||||
|
||||
// All encoded values should be the same
|
||||
assert.Equal(t, 100, encoded1)
|
||||
assert.Equal(t, 100, encoded2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestURL(t *testing.T) {
|
||||
|
||||
func TestDate(t *testing.T) {
|
||||
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, time.Time](time.Time{}))
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](time.Time{}))
|
||||
|
||||
t.Run("ISO 8601 date format", func(t *testing.T) {
|
||||
dateCodec := Date("2006-01-02")
|
||||
|
||||
321
v2/optics/codec/decode/OrElse_ChainLeft_explanation.md
Normal file
321
v2/optics/codec/decode/OrElse_ChainLeft_explanation.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# ChainLeft and OrElse in the Decode Package
|
||||
|
||||
## Overview
|
||||
|
||||
In [`optics/codec/decode/monad.go`](monad.go:53-62), the [`ChainLeft`](monad.go:53) and [`OrElse`](monad.go:60) functions work with decoders that may fail during decoding operations.
|
||||
|
||||
```go
|
||||
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return readert.Chain[Decode[I, A]](
|
||||
validation.ChainLeft,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
```
|
||||
|
||||
## Key Insight: OrElse is ChainLeft
|
||||
|
||||
**`OrElse` is exactly the same as `ChainLeft`** - they are aliases with identical implementations and behavior. The choice between them is purely about **code readability and semantic intent**.
|
||||
|
||||
## Understanding the Types
|
||||
|
||||
### Decode[I, A]
|
||||
A decoder that takes input of type `I` and produces a `Validation[A]`:
|
||||
```go
|
||||
type Decode[I, A any] = func(I) Validation[A]
|
||||
```
|
||||
|
||||
### Kleisli[I, Errors, A]
|
||||
A function that takes `Errors` and produces a `Decode[I, A]`:
|
||||
```go
|
||||
type Kleisli[I, Errors, A] = func(Errors) Decode[I, A]
|
||||
```
|
||||
|
||||
This allows error handlers to:
|
||||
1. Access the validation errors that occurred
|
||||
2. Access the original input (via the returned Decode function)
|
||||
3. Either recover with a success value or produce new errors
|
||||
|
||||
### Operator[I, A, A]
|
||||
A function that transforms one decoder into another:
|
||||
```go
|
||||
type Operator[I, A, A] = func(Decode[I, A]) Decode[I, A]
|
||||
```
|
||||
|
||||
## Core Behavior
|
||||
|
||||
Both [`ChainLeft`](monad.go:53) and [`OrElse`](monad.go:60) delegate to [`validation.ChainLeft`](../validation/monad.go:304), which provides:
|
||||
|
||||
### 1. Error Aggregation
|
||||
When the transformation function returns a failure, **both the original errors AND the new errors are combined** using the Errors monoid:
|
||||
|
||||
```go
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "original error"},
|
||||
})
|
||||
}
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "additional error"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := handler(failingDecoder)
|
||||
result := decoder("input")
|
||||
// Result contains BOTH errors: ["original error", "additional error"]
|
||||
```
|
||||
|
||||
### 2. Success Pass-Through
|
||||
Success values pass through unchanged - the handler is never called:
|
||||
|
||||
```go
|
||||
successDecoder := Of[string](42)
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "never called"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := handler(successDecoder)
|
||||
result := decoder("input")
|
||||
// Result: Success(42) - unchanged
|
||||
```
|
||||
|
||||
### 3. Error Recovery
|
||||
The handler can recover from failures by returning a successful decoder:
|
||||
|
||||
```go
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "not found"},
|
||||
})
|
||||
}
|
||||
|
||||
recoverFromNotFound := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "not found" {
|
||||
return Of[string](0) // recover with default
|
||||
}
|
||||
}
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
})
|
||||
|
||||
decoder := recoverFromNotFound(failingDecoder)
|
||||
result := decoder("input")
|
||||
// Result: Success(0) - recovered from failure
|
||||
```
|
||||
|
||||
### 4. Access to Original Input
|
||||
The handler returns a `Decode[I, A]` function, giving it access to the original input:
|
||||
|
||||
```go
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
// Can access both errs and input here
|
||||
if input == "special" {
|
||||
return validation.Of(999)
|
||||
}
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Fallback Decoding (OrElse reads better)
|
||||
|
||||
```go
|
||||
// Primary decoder that may fail
|
||||
primaryDecoder := func(input string) Validation[int] {
|
||||
n, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "not a valid integer"},
|
||||
})
|
||||
}
|
||||
return validation.Of(n)
|
||||
}
|
||||
|
||||
// Use OrElse for semantic clarity - "try primary, or else use default"
|
||||
withDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
return Of[string](0) // default to 0 if decoding fails
|
||||
})
|
||||
|
||||
decoder := withDefault(primaryDecoder)
|
||||
|
||||
result1 := decoder("42") // Success(42)
|
||||
result2 := decoder("abc") // Success(0) - fallback
|
||||
```
|
||||
|
||||
### 2. Error Context Addition (ChainLeft reads better)
|
||||
|
||||
```go
|
||||
decodeUserAge := func(data map[string]any) Validation[int] {
|
||||
age, ok := data["age"].(int)
|
||||
if !ok {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: data["age"], Messsage: "invalid type"},
|
||||
})
|
||||
}
|
||||
return validation.Of(age)
|
||||
}
|
||||
|
||||
// Use ChainLeft when emphasizing error transformation
|
||||
addContext := ChainLeft(func(errs Errors) Decode[map[string]any, int] {
|
||||
return func(data map[string]any) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
Messsage: "failed to decode user age",
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := addContext(decodeUserAge)
|
||||
// Errors will include both original error and context
|
||||
```
|
||||
|
||||
### 3. Conditional Recovery Based on Input
|
||||
|
||||
```go
|
||||
decodePort := func(input string) Validation[int] {
|
||||
port, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "invalid port"},
|
||||
})
|
||||
}
|
||||
return validation.Of(port)
|
||||
}
|
||||
|
||||
// Recover with different defaults based on input
|
||||
smartDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
// Check input to determine appropriate default
|
||||
if strings.Contains(input, "http") {
|
||||
return validation.Of(80)
|
||||
}
|
||||
if strings.Contains(input, "https") {
|
||||
return validation.Of(443)
|
||||
}
|
||||
return validation.Of(8080)
|
||||
}
|
||||
})
|
||||
|
||||
decoder := smartDefault(decodePort)
|
||||
result1 := decoder("http-server") // Success(80)
|
||||
result2 := decoder("https-server") // Success(443)
|
||||
result3 := decoder("other") // Success(8080)
|
||||
```
|
||||
|
||||
### 4. Pipeline Composition
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
DatabaseURL string
|
||||
}
|
||||
|
||||
decodeConfig := func(data map[string]any) Validation[Config] {
|
||||
url, ok := data["db_url"].(string)
|
||||
if !ok {
|
||||
return either.Left[Config](validation.Errors{
|
||||
{Messsage: "missing db_url"},
|
||||
})
|
||||
}
|
||||
return validation.Of(Config{DatabaseURL: url})
|
||||
}
|
||||
|
||||
// Build a pipeline with multiple error handlers
|
||||
decoder := F.Pipe2(
|
||||
decodeConfig,
|
||||
OrElse(func(errs Errors) Decode[map[string]any, Config] {
|
||||
// Try environment variable as fallback
|
||||
return func(data map[string]any) Validation[Config] {
|
||||
if url := os.Getenv("DATABASE_URL"); url != "" {
|
||||
return validation.Of(Config{DatabaseURL: url})
|
||||
}
|
||||
return either.Left[Config](errs)
|
||||
}
|
||||
}),
|
||||
OrElse(func(errs Errors) Decode[map[string]any, Config] {
|
||||
// Final fallback to default
|
||||
return Of[map[string]any](Config{
|
||||
DatabaseURL: "localhost:5432",
|
||||
})
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
## Comparison with validation.ChainLeft
|
||||
|
||||
The decode package's [`ChainLeft`](monad.go:53) wraps [`validation.ChainLeft`](../validation/monad.go:304) using the Reader transformer pattern:
|
||||
|
||||
| Aspect | validation.ChainLeft | decode.ChainLeft |
|
||||
|--------|---------------------|------------------|
|
||||
| **Input** | `Validation[A]` | `Decode[I, A]` (function) |
|
||||
| **Handler** | `func(Errors) Validation[A]` | `func(Errors) Decode[I, A]` |
|
||||
| **Output** | `Validation[A]` | `Decode[I, A]` (function) |
|
||||
| **Context** | No input access | Access to original input `I` |
|
||||
| **Use Case** | Pure validation logic | Decoding with input-dependent recovery |
|
||||
|
||||
The key difference is that decode's version gives handlers access to the original input through the returned `Decode[I, A]` function.
|
||||
|
||||
## When to Use Which Name
|
||||
|
||||
### Use **OrElse** when:
|
||||
- Emphasizing fallback/alternative decoding logic
|
||||
- Providing default values on decode failure
|
||||
- The intent is "try this, or else try that"
|
||||
- Code reads more naturally with "or else"
|
||||
|
||||
### Use **ChainLeft** when:
|
||||
- Emphasizing technical error channel transformation
|
||||
- Adding context or enriching error information
|
||||
- The focus is on error handling mechanics
|
||||
- Working with other functional programming concepts
|
||||
|
||||
## Verification
|
||||
|
||||
The test suite in [`monad_test.go`](monad_test.go:385) includes comprehensive tests proving that `OrElse` and `ChainLeft` are equivalent:
|
||||
|
||||
- ✅ Identical behavior for Success values
|
||||
- ✅ Identical behavior for error recovery
|
||||
- ✅ Identical behavior for error aggregation
|
||||
- ✅ Identical behavior in pipeline composition
|
||||
- ✅ Identical behavior for multiple error scenarios
|
||||
- ✅ Both provide access to original input
|
||||
|
||||
Run the tests:
|
||||
```bash
|
||||
go test -v -run "TestChainLeft|TestOrElse" ./optics/codec/decode
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
**`OrElse` is exactly the same as `ChainLeft`** in the decode package - they are aliases with identical implementations and behavior. Both:
|
||||
|
||||
1. **Delegate to validation.ChainLeft** for error handling logic
|
||||
2. **Aggregate errors** when transformations fail
|
||||
3. **Preserve successes** unchanged
|
||||
4. **Enable recovery** from decode failures
|
||||
5. **Provide access** to the original input
|
||||
|
||||
The choice between them is purely about **code readability and semantic intent**:
|
||||
- Use **`OrElse`** when emphasizing fallback/alternative decoding
|
||||
- Use **`ChainLeft`** when emphasizing error transformation
|
||||
|
||||
Both maintain the critical property of **error aggregation**, ensuring all validation failures are preserved and reported together.
|
||||
@@ -19,12 +19,7 @@ func TestDo(t *testing.T) {
|
||||
decoder := Do[string](State{})
|
||||
result := decoder("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{}, value)
|
||||
assert.Equal(t, validation.Of(State{}), result)
|
||||
})
|
||||
|
||||
t.Run("creates decoder with initialized state", func(t *testing.T) {
|
||||
@@ -79,12 +74,7 @@ func TestBind(t *testing.T) {
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 42, y: 10}, value)
|
||||
assert.Equal(t, validation.Of(State{x: 42, y: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("propagates failure", func(t *testing.T) {
|
||||
@@ -216,12 +206,7 @@ func TestLet(t *testing.T) {
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
|
||||
assert.Equal(t, validation.Of(State{x: 60, y: 10, z: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package decode
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
// Of creates a Decode that always succeeds with the given value.
|
||||
@@ -14,7 +15,123 @@ import (
|
||||
// decoder := decode.Of[string](42)
|
||||
// result := decoder("any input") // Always returns validation.Success(42)
|
||||
func Of[I, A any](a A) Decode[I, A] {
|
||||
return reader.Of[I](validation.Of(a))
|
||||
return readereither.Of[I, Errors](a)
|
||||
}
|
||||
|
||||
// OfLazy converts a lazy computation into a Decode that ignores its input.
|
||||
// The resulting Decode will evaluate the lazy computation when executed and wrap
|
||||
// the result in a successful validation, regardless of the input provided.
|
||||
//
|
||||
// This function is intended solely for deferring the computation of a value, NOT for
|
||||
// representing side effects. The lazy computation should be a pure function that
|
||||
// produces the same result each time it's called (referential transparency). For
|
||||
// operations with side effects, use appropriate effect types like IO or IOResult.
|
||||
//
|
||||
// This is useful for lifting deferred computations into the Decode context without
|
||||
// requiring access to the input, while maintaining the validation wrapper for consistency.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - I: The input type (ignored by the resulting Decode)
|
||||
// - A: The result type produced by the lazy computation
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: A lazy computation that produces a value of type A (must be pure, no side effects)
|
||||
//
|
||||
// Returns:
|
||||
// - A Decode that ignores its input, evaluates the lazy computation, and wraps the result in Validation[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lazyValue := func() int { return 42 }
|
||||
// decoder := decode.OfLazy[string](lazyValue)
|
||||
// result := decoder("any input") // validation.Success(42)
|
||||
//
|
||||
// Example - Deferring expensive computation:
|
||||
//
|
||||
// expensiveCalc := func() Config {
|
||||
// // Expensive but pure computation here
|
||||
// return computeDefaultConfig()
|
||||
// }
|
||||
// decoder := decode.OfLazy[map[string]any](expensiveCalc)
|
||||
// // Computation is deferred until the Decode is executed
|
||||
// result := decoder(inputData) // validation.Success(config)
|
||||
func OfLazy[I, A any](fa Lazy[A]) Decode[I, A] {
|
||||
return readereither.OfLazy[I, Errors](fa)
|
||||
}
|
||||
|
||||
// Left creates a Decode that always fails with the given validation errors.
|
||||
// This is the dual of Of - while Of lifts a success value, Left lifts failure errors
|
||||
// into the Decode context.
|
||||
//
|
||||
// Left is useful for:
|
||||
// - Creating decoders that represent known failure states
|
||||
// - Short-circuiting decode pipelines with specific errors
|
||||
// - Building custom validation error responses
|
||||
// - Testing error handling paths
|
||||
//
|
||||
// The returned decoder ignores its input and always returns a validation failure
|
||||
// containing the provided errors. This makes it the identity element for the
|
||||
// Alt/OrElse operations when used as a fallback.
|
||||
//
|
||||
// Type signature: func(Errors) Decode[I, A]
|
||||
// - Takes validation errors
|
||||
// - Returns a decoder that always fails with those errors
|
||||
// - The decoder ignores its input of type I
|
||||
// - The failure type A can be any type (phantom type)
|
||||
//
|
||||
// Example - Creating a failing decoder:
|
||||
//
|
||||
// failDecoder := decode.Left[string, int](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: nil,
|
||||
// Messsage: "operation not supported",
|
||||
// },
|
||||
// })
|
||||
// result := failDecoder("any input") // Always fails with the error
|
||||
//
|
||||
// Example - Short-circuiting with specific errors:
|
||||
//
|
||||
// validateAge := func(age int) Decode[map[string]any, int] {
|
||||
// if age < 0 {
|
||||
// return decode.Left[map[string]any, int](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: age,
|
||||
// Context: validation.Context{{Key: "age", Type: "int"}},
|
||||
// Messsage: "age cannot be negative",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// return decode.Of[map[string]any](age)
|
||||
// }
|
||||
//
|
||||
// Example - Building error responses:
|
||||
//
|
||||
// notFoundError := decode.Left[string, User](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Messsage: "user not found",
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// decoder := decode.MonadAlt(
|
||||
// tryFindUser,
|
||||
// func() Decode[string, User] { return notFoundError },
|
||||
// )
|
||||
//
|
||||
// Example - Testing error paths:
|
||||
//
|
||||
// // Create a decoder that always fails for testing
|
||||
// alwaysFails := decode.Left[string, int](validation.Errors{
|
||||
// &validation.ValidationError{Messsage: "test error"},
|
||||
// })
|
||||
//
|
||||
// // Test error recovery logic
|
||||
// recovered := decode.OrElse(func(errs Errors) Decode[string, int] {
|
||||
// return decode.Of[string](0) // recover with default
|
||||
// })(alwaysFails)
|
||||
//
|
||||
// result := recovered("input") // Success(0)
|
||||
func Left[I, A any](err Errors) Decode[I, A] {
|
||||
return readereither.Left[I, A](err)
|
||||
}
|
||||
|
||||
// MonadChain sequences two decode operations, passing the result of the first to the second.
|
||||
@@ -50,6 +167,212 @@ func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// ChainLeft transforms the error channel of a decoder, enabling error recovery and context addition.
|
||||
// This is the left-biased monadic chain operation that operates on validation failures.
|
||||
//
|
||||
// **Key behaviors**:
|
||||
// - Success values pass through unchanged - the handler is never called
|
||||
// - On failure, the handler receives the errors and can recover or add context
|
||||
// - When the handler also fails, **both original and new errors are aggregated**
|
||||
// - The handler returns a Decode[I, A], giving it access to the original input
|
||||
//
|
||||
// **Error Aggregation**: Unlike standard Either operations, when the transformation function
|
||||
// returns a failure, both the original errors AND the new errors are combined using the
|
||||
// Errors monoid. This ensures no validation errors are lost.
|
||||
//
|
||||
// Use cases:
|
||||
// - Adding contextual information to validation errors
|
||||
// - Recovering from specific error conditions
|
||||
// - Transforming error messages while preserving original errors
|
||||
// - Implementing conditional recovery based on error types
|
||||
//
|
||||
// Example - Error recovery:
|
||||
//
|
||||
// failingDecoder := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not found"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// recoverFromNotFound := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "not found" {
|
||||
// return Of[string](0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](errs)
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// decoder := recoverFromNotFound(failingDecoder)
|
||||
// result := decoder("input") // Success(0) - recovered from failure
|
||||
//
|
||||
// Example - Adding context:
|
||||
//
|
||||
// addContext := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {
|
||||
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to decode user age",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// // Result will contain BOTH original error and context error
|
||||
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return readert.Chain[Decode[I, A]](
|
||||
validation.ChainLeft,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainLeft transforms the error channel of a decoder, enabling error recovery and context addition.
|
||||
// This is the uncurried version of ChainLeft, taking both the decoder and the transformation function directly.
|
||||
//
|
||||
// **Key behaviors**:
|
||||
// - Success values pass through unchanged - the handler is never called
|
||||
// - On failure, the handler receives the errors and can recover or add context
|
||||
// - When the handler also fails, **both original and new errors are aggregated**
|
||||
// - The handler returns a Decode[I, A], giving it access to the original input
|
||||
//
|
||||
// **Error Aggregation**: Unlike standard Either operations, when the transformation function
|
||||
// returns a failure, both the original errors AND the new errors are combined using the
|
||||
// Errors monoid. This ensures no validation errors are lost.
|
||||
//
|
||||
// This function is the direct, uncurried form of ChainLeft. Use ChainLeft when you need
|
||||
// a curried operator for composition pipelines, and use MonadChainLeft when you have both
|
||||
// the decoder and transformation function available at once.
|
||||
//
|
||||
// Use cases:
|
||||
// - Adding contextual information to validation errors
|
||||
// - Recovering from specific error conditions
|
||||
// - Transforming error messages while preserving original errors
|
||||
// - Implementing conditional recovery based on error types
|
||||
//
|
||||
// Example - Error recovery:
|
||||
//
|
||||
// failingDecoder := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not found"},
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// recoverFromNotFound := func(errs Errors) Decode[string, int] {
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "not found" {
|
||||
// return Of[string](0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](errs)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadChainLeft(failingDecoder, recoverFromNotFound)
|
||||
// result := decoder("input") // Success(0) - recovered from failure
|
||||
//
|
||||
// Example - Adding context:
|
||||
//
|
||||
// addContext := func(errs Errors) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {
|
||||
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to decode user age",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadChainLeft(failingDecoder, addContext)
|
||||
// result := decoder("abc")
|
||||
// // Result will contain BOTH original error and context error
|
||||
//
|
||||
// Example - Comparison with ChainLeft:
|
||||
//
|
||||
// // MonadChainLeft - direct application
|
||||
// result1 := MonadChainLeft(decoder, handler)("input")
|
||||
//
|
||||
// // ChainLeft - curried for pipelines
|
||||
// result2 := ChainLeft(handler)(decoder)("input")
|
||||
//
|
||||
// // Both produce identical results
|
||||
func MonadChainLeft[I, A any](fa Decode[I, A], f Kleisli[I, Errors, A]) Decode[I, A] {
|
||||
return readert.MonadChain(
|
||||
validation.MonadChainLeft,
|
||||
fa,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// OrElse provides fallback decoding logic when the primary decoder fails.
|
||||
// This is an alias for ChainLeft with a more semantic name for fallback scenarios.
|
||||
//
|
||||
// **OrElse is exactly the same as ChainLeft** - they are aliases with identical implementations
|
||||
// and behavior. The choice between them is purely about code readability and semantic intent:
|
||||
// - Use **OrElse** when emphasizing fallback/alternative decoding logic
|
||||
// - Use **ChainLeft** when emphasizing technical error channel transformation
|
||||
//
|
||||
// **Key behaviors** (identical to ChainLeft):
|
||||
// - Success values pass through unchanged - the handler is never called
|
||||
// - On failure, the handler receives the errors and can provide an alternative
|
||||
// - When the handler also fails, **both original and new errors are aggregated**
|
||||
// - The handler returns a Decode[I, A], giving it access to the original input
|
||||
//
|
||||
// The name "OrElse" reads naturally in code: "try this decoder, or else try this alternative."
|
||||
// This makes it ideal for expressing fallback logic and default values.
|
||||
//
|
||||
// Use cases:
|
||||
// - Providing default values when decoding fails
|
||||
// - Trying alternative decoding strategies
|
||||
// - Implementing fallback chains with multiple alternatives
|
||||
// - Input-dependent recovery (using access to original input)
|
||||
//
|
||||
// Example - Simple fallback:
|
||||
//
|
||||
// primaryDecoder := func(input string) Validation[int] {
|
||||
// n, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not a valid integer"},
|
||||
// })
|
||||
// }
|
||||
// return validation.Of(n)
|
||||
// }
|
||||
//
|
||||
// withDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
// return Of[string](0) // default to 0 if decoding fails
|
||||
// })
|
||||
//
|
||||
// decoder := withDefault(primaryDecoder)
|
||||
// result1 := decoder("42") // Success(42)
|
||||
// result2 := decoder("abc") // Success(0) - fallback
|
||||
//
|
||||
// Example - Input-dependent fallback:
|
||||
//
|
||||
// smartDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// // Access original input to determine appropriate default
|
||||
// if strings.Contains(input, "http") {
|
||||
// return validation.Of(80)
|
||||
// }
|
||||
// if strings.Contains(input, "https") {
|
||||
// return validation.Of(443)
|
||||
// }
|
||||
// return validation.Of(8080)
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// decoder := smartDefault(decodePort)
|
||||
// result1 := decoder("http-server") // Success(80)
|
||||
// result2 := decoder("https-server") // Success(443)
|
||||
// result3 := decoder("other") // Success(8080)
|
||||
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
|
||||
// MonadMap transforms the decoded value using the provided function.
|
||||
// This is the functor map operation that applies a transformation to successful decode results.
|
||||
//
|
||||
@@ -127,3 +450,155 @@ func Ap[B, I, A any](fa Decode[I, A]) Operator[I, func(A) B, B] {
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadAlt provides alternative/fallback decoding with error aggregation.
|
||||
// This is the Alternative pattern's core operation that tries the first decoder,
|
||||
// and if it fails, tries the second decoder as a fallback.
|
||||
//
|
||||
// **Key behaviors**:
|
||||
// - If first succeeds: returns the first result (second is never evaluated)
|
||||
// - If first fails and second succeeds: returns the second result
|
||||
// - If both fail: **aggregates errors from both decoders**
|
||||
//
|
||||
// **Error Aggregation**: Unlike simple fallback patterns, when both decoders fail,
|
||||
// MonadAlt combines ALL errors from both attempts using the Errors monoid. This ensures
|
||||
// complete visibility into why all alternatives failed, which is crucial for debugging
|
||||
// and providing comprehensive error messages to users.
|
||||
//
|
||||
// The name "Alt" comes from the Alternative type class in functional programming,
|
||||
// which represents computations with a notion of choice and failure.
|
||||
//
|
||||
// Use cases:
|
||||
// - Trying multiple decoding strategies for the same input
|
||||
// - Providing fallback decoders when primary decoder fails
|
||||
// - Building validation pipelines with multiple alternatives
|
||||
// - Implementing "try this, or else try that" logic
|
||||
//
|
||||
// Example - Simple fallback:
|
||||
//
|
||||
// primaryDecoder := func(input string) Validation[int] {
|
||||
// n, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not a valid integer"},
|
||||
// })
|
||||
// }
|
||||
// return validation.Of(n)
|
||||
// }
|
||||
//
|
||||
// fallbackDecoder := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// // Try parsing as float and converting to int
|
||||
// f, err := strconv.ParseFloat(input, 64)
|
||||
// if err != nil {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Value: input, Messsage: "not a valid number"},
|
||||
// })
|
||||
// }
|
||||
// return validation.Of(int(f))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadAlt(primaryDecoder, fallbackDecoder)
|
||||
// result1 := decoder("42") // Success(42) - primary succeeds
|
||||
// result2 := decoder("42.5") // Success(42) - fallback succeeds
|
||||
// result3 := decoder("abc") // Failures with both errors aggregated
|
||||
//
|
||||
// Example - Multiple alternatives:
|
||||
//
|
||||
// decoder1 := parseAsJSON
|
||||
// decoder2 := func() Decode[string, Config] { return parseAsYAML }
|
||||
// decoder3 := func() Decode[string, Config] { return parseAsINI }
|
||||
//
|
||||
// // Try JSON, then YAML, then INI
|
||||
// decoder := MonadAlt(MonadAlt(decoder1, decoder2), decoder3)
|
||||
// // If all fail, errors from all three attempts are aggregated
|
||||
//
|
||||
// Example - Error aggregation:
|
||||
//
|
||||
// failing1 := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Messsage: "primary decoder failed"},
|
||||
// })
|
||||
// }
|
||||
// failing2 := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {Messsage: "fallback decoder failed"},
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := MonadAlt(failing1, failing2)
|
||||
// result := decoder("input")
|
||||
// // Result contains BOTH errors: ["primary decoder failed", "fallback decoder failed"]
|
||||
func MonadAlt[I, A any](first Decode[I, A], second Lazy[Decode[I, A]]) Decode[I, A] {
|
||||
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
// Alt creates an operator that provides alternative/fallback decoding with error aggregation.
|
||||
// This is the curried version of MonadAlt, useful for composition pipelines.
|
||||
//
|
||||
// **Key behaviors** (identical to MonadAlt):
|
||||
// - If first succeeds: returns the first result (second is never evaluated)
|
||||
// - If first fails and second succeeds: returns the second result
|
||||
// - If both fail: **aggregates errors from both decoders**
|
||||
//
|
||||
// The Alt operator enables building reusable fallback chains that can be applied
|
||||
// to different decoders. It reads naturally in pipelines: "apply this decoder,
|
||||
// with this alternative if it fails."
|
||||
//
|
||||
// Use cases:
|
||||
// - Creating reusable fallback strategies
|
||||
// - Building decoder combinators with alternatives
|
||||
// - Composing multiple fallback layers
|
||||
// - Implementing retry logic with different strategies
|
||||
//
|
||||
// Example - Creating a reusable fallback:
|
||||
//
|
||||
// // Create an operator that falls back to a default value
|
||||
// withDefault := Alt(func() Decode[string, int] {
|
||||
// return Of[string](0)
|
||||
// })
|
||||
//
|
||||
// // Apply to any decoder
|
||||
// decoder1 := withDefault(parseInteger)
|
||||
// decoder2 := withDefault(parseFromJSON)
|
||||
//
|
||||
// result1 := decoder1("42") // Success(42)
|
||||
// result2 := decoder1("abc") // Success(0) - fallback
|
||||
//
|
||||
// Example - Composing multiple alternatives:
|
||||
//
|
||||
// tryYAML := Alt(func() Decode[string, Config] { return parseAsYAML })
|
||||
// tryINI := Alt(func() Decode[string, Config] { return parseAsINI })
|
||||
// useDefault := Alt(func() Decode[string, Config] {
|
||||
// return Of[string](defaultConfig)
|
||||
// })
|
||||
//
|
||||
// // Build a pipeline: try JSON, then YAML, then INI, then default
|
||||
// decoder := useDefault(tryINI(tryYAML(parseAsJSON)))
|
||||
//
|
||||
// Example - Error aggregation in pipeline:
|
||||
//
|
||||
// failing1 := func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{{Messsage: "error 1"}})
|
||||
// }
|
||||
// failing2 := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{{Messsage: "error 2"}})
|
||||
// }
|
||||
// }
|
||||
// failing3 := func() Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// return either.Left[int](validation.Errors{{Messsage: "error 3"}})
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Chain multiple alternatives
|
||||
// decoder := Alt(failing3)(Alt(failing2)(failing1))
|
||||
// result := decoder("input")
|
||||
// // Result contains ALL errors: ["error 1", "error 2", "error 3"]
|
||||
func Alt[I, A any](second Lazy[Decode[I, A]]) Operator[I, A, A] {
|
||||
return ChainLeft(function.Ignore1of1[Errors](second))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user