mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-07 23:03:15 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54d5dbd04a | ||
|
|
51adce0c95 | ||
|
|
aa5e908810 | ||
|
|
b3bd5e9ad3 | ||
|
|
178df09ff7 | ||
|
|
92eb9715bd | ||
|
|
41ebb04ae0 | ||
|
|
b2705e3adf | ||
|
|
b232183e47 | ||
|
|
0f9f89f16d |
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
@@ -41,6 +41,14 @@ jobs:
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./coverage.txt
|
||||
flag-name: v1-go-${{ matrix.go-version }}
|
||||
parallel: true
|
||||
|
||||
# - name: Upload coverage to Codecov
|
||||
# uses: codecov/codecov-action@v5
|
||||
# with:
|
||||
@@ -72,6 +80,14 @@ jobs:
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./v2/coverage.txt
|
||||
flag-name: v2-go-${{ matrix.go-version }}
|
||||
parallel: true
|
||||
|
||||
# - name: Upload coverage to Codecov
|
||||
# uses: codecov/codecov-action@v5
|
||||
# with:
|
||||
@@ -82,9 +98,22 @@ jobs:
|
||||
# env:
|
||||
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
coveralls-finish:
|
||||
name: Finish Coveralls
|
||||
needs:
|
||||
- build-v1
|
||||
- build-v2
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel-finished: true
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs:
|
||||
needs:
|
||||
- build-v1
|
||||
- build-v2
|
||||
if: github.repository == 'IBM/fp-go' && github.event_name != 'pull_request'
|
||||
|
||||
347
README.md
347
README.md
@@ -1,207 +1,312 @@
|
||||
# Functional programming library for golang
|
||||
# fp-go: Functional Programming Library for Go
|
||||
|
||||
**🚧 Work in progress! 🚧** Despite major version 1 because of <https://github.com/semantic-release/semantic-release/issues/1507>. Trying to not make breaking changes, but devil is in the details.
|
||||
[](https://pkg.go.dev/github.com/IBM/fp-go)
|
||||
[](https://coveralls.io/github/IBM/fp-go?branch=main)
|
||||
|
||||
**🚧 Work in progress! 🚧** Despite major version 1 (due to [semantic-release limitations](https://github.com/semantic-release/semantic-release/issues/1507)), we're working to minimize breaking changes.
|
||||
|
||||

|
||||
|
||||
This library is strongly influenced by the awesome [fp-ts](https://github.com/gcanti/fp-ts).
|
||||
A comprehensive functional programming library for Go, strongly influenced by the excellent [fp-ts](https://github.com/gcanti/fp-ts) library for TypeScript.
|
||||
|
||||
## Getting started
|
||||
## 📚 Table of Contents
|
||||
|
||||
- [Getting Started](#-getting-started)
|
||||
- [Design Goals](#-design-goals)
|
||||
- [Core Concepts](#-core-concepts)
|
||||
- [Comparison to Idiomatic Go](#comparison-to-idiomatic-go)
|
||||
- [Implementation Notes](#implementation-notes)
|
||||
- [Common Operations](#common-operations)
|
||||
- [Resources](#-resources)
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
go get github.com/IBM/fp-go
|
||||
```
|
||||
|
||||
Refer to the [samples](./samples/).
|
||||
### Quick Example
|
||||
|
||||
Find API documentation [here](https://pkg.go.dev/github.com/IBM/fp-go)
|
||||
```go
|
||||
import (
|
||||
"errors"
|
||||
"github.com/IBM/fp-go/either"
|
||||
"github.com/IBM/fp-go/function"
|
||||
)
|
||||
|
||||
## Design Goal
|
||||
// Pure function that can fail
|
||||
func divide(a, b int) either.Either[error, int] {
|
||||
if b == 0 {
|
||||
return either.Left[int](errors.New("division by zero"))
|
||||
}
|
||||
return either.Right[error](a / b)
|
||||
}
|
||||
|
||||
This library aims to provide a set of data types and functions that make it easy and fun to write maintainable and testable code in golang. It encourages the following patterns:
|
||||
// Compose operations safely
|
||||
result := function.Pipe2(
|
||||
divide(10, 2),
|
||||
either.Map(func(x int) int { return x * 2 }),
|
||||
either.GetOrElse(func() int { return 0 }),
|
||||
)
|
||||
// result = 10
|
||||
```
|
||||
|
||||
- write many small, testable and pure functions, i.e. functions that produce output only depending on their input and that do not execute side effects
|
||||
- offer helpers to isolate side effects into lazily executed functions (IO)
|
||||
- expose a consistent set of composition to create new functions from existing ones
|
||||
- for each data type there exists a small set of composition functions
|
||||
- these functions are called the same across all data types, so you only have to learn a small number of function names
|
||||
- the semantic of functions of the same name is consistent across all data types
|
||||
### Resources
|
||||
|
||||
### How does this play with the [🧘🏽 Zen Of Go](https://the-zen-of-go.netlify.app/)?
|
||||
- 📖 [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go)
|
||||
- 💡 [Code Samples](./samples/)
|
||||
- 🆕 [V2 Documentation](./v2/README.md) (requires Go 1.24+)
|
||||
|
||||
#### 🧘🏽 Each package fulfills a single purpose
|
||||
## 🎯 Design Goals
|
||||
|
||||
✔️ Each of the top level packages (e.g. Option, Either, ReaderIOEither, ...) fulfills the purpose of defining the respective data type and implementing the set of common operations for this data type.
|
||||
This library aims to provide a set of data types and functions that make it easy and fun to write maintainable and testable code in Go. It encourages the following patterns:
|
||||
|
||||
#### 🧘🏽 Handle errors explicitly
|
||||
### Core Principles
|
||||
|
||||
✔️ The library makes a clear distinction between that operations that cannot fail by design and operations that can fail. Failure is represented via the `Either` type and errors are handled explicitly by using `Either`'s monadic set of operations.
|
||||
- **Pure Functions**: Write many small, testable, and pure functions that produce output only depending on their input and execute no side effects
|
||||
- **Side Effect Isolation**: Isolate side effects into lazily executed functions using the `IO` monad
|
||||
- **Consistent Composition**: Expose a consistent set of composition functions across all data types
|
||||
- Each data type has a small set of composition functions
|
||||
- Functions are named consistently across all data types
|
||||
- Semantics of same-named functions are consistent across data types
|
||||
|
||||
#### 🧘🏽 Return early rather than nesting deeply
|
||||
### 🧘🏽 Alignment with the Zen of Go
|
||||
|
||||
✔️ We recommend to implement simple, small functions that implement one feature and that would typically not invoke other functions. Interaction with other functions is done by function composition and the composition makes sure to run one function after the other. In the error case the `Either` monad makes sure to skip the error path.
|
||||
This library respects and aligns with [The Zen of Go](https://the-zen-of-go.netlify.app/):
|
||||
|
||||
#### 🧘🏽 Leave concurrency to the caller
|
||||
| Principle | Alignment | Explanation |
|
||||
|-----------|-----------|-------------|
|
||||
| 🧘🏽 Each package fulfills a single purpose | ✔️ | Each top-level package (Option, Either, ReaderIOEither, etc.) defines one data type and its operations |
|
||||
| 🧘🏽 Handle errors explicitly | ✔️ | Clear distinction between operations that can/cannot fail; failures represented via `Either` type |
|
||||
| 🧘🏽 Return early rather than nesting deeply | ✔️ | Small, focused functions composed together; `Either` monad handles error paths automatically |
|
||||
| 🧘🏽 Leave concurrency to the caller | ✔️ | Pure functions are synchronous; I/O operations are asynchronous by default |
|
||||
| 🧘🏽 Before you launch a goroutine, know when it will stop | 🤷🏽 | Library doesn't start goroutines; Task monad supports cancellation via context |
|
||||
| 🧘🏽 Avoid package level state | ✔️ | No package-level state anywhere |
|
||||
| 🧘🏽 Simplicity matters | ✔️ | Small, consistent interface across data types; focus on business logic |
|
||||
| 🧘🏽 Write tests to lock in behaviour | 🟡 | Programming pattern encourages testing; library has growing test coverage |
|
||||
| 🧘🏽 If you think it's slow, first prove it with a benchmark | ✔️ | Performance claims should be backed by benchmarks |
|
||||
| 🧘🏽 Moderation is a virtue | ✔️ | No custom goroutines or expensive synchronization; atomic counters for coordination |
|
||||
| 🧘🏽 Maintainability counts | ✔️ | Small, concise operations; pure functions with clear type signatures |
|
||||
|
||||
✔️ All pure are synchronous by default. The I/O operations are asynchronous per default.
|
||||
## 💡 Core Concepts
|
||||
|
||||
#### 🧘🏽 Before you launch a goroutine, know when it will stop
|
||||
### Data Types
|
||||
|
||||
🤷🏽 This is left to the user of the library since the library itself will not start goroutines on its own. The Task monad offers support for cancellation via the golang context, though.
|
||||
The library provides several key functional data types:
|
||||
|
||||
#### 🧘🏽 Avoid package level state
|
||||
- **`Option[A]`**: Represents an optional value (Some or None)
|
||||
- **`Either[E, A]`**: Represents a value that can be one of two types (Left for errors, Right for success)
|
||||
- **`IO[A]`**: Represents a lazy computation that produces a value
|
||||
- **`IOEither[E, A]`**: Represents a lazy computation that can fail
|
||||
- **`Reader[R, A]`**: Represents a computation that depends on an environment
|
||||
- **`ReaderIOEither[R, E, A]`**: Combines Reader, IO, and Either for effectful computations with dependencies
|
||||
- **`Task[A]`**: Represents an asynchronous computation
|
||||
- **`State[S, A]`**: Represents a stateful computation
|
||||
|
||||
✔️ No package level state anywhere, this would be a significant anti-pattern
|
||||
### Monadic Operations
|
||||
|
||||
#### 🧘🏽 Simplicity matters
|
||||
All data types support common monadic operations:
|
||||
|
||||
✔️ The library is simple in the sense that it offers a small, consistent interface to a variety of data types. Users can concentrate on implementing business logic rather than dealing with low level data structures.
|
||||
|
||||
#### 🧘🏽 Write tests to lock in the behaviour of your package’s API
|
||||
|
||||
🟡 The programming pattern suggested by this library encourages writing test cases. The library itself also has a growing number of tests, but not enough, yet. TBD
|
||||
|
||||
#### 🧘🏽 If you think it’s slow, first prove it with a benchmark
|
||||
|
||||
✔️ Absolutely. If you think the function composition offered by this library is too slow, please provide a benchmark.
|
||||
|
||||
#### 🧘🏽 Moderation is a virtue
|
||||
|
||||
✔️ The library does not implement its own goroutines and also does not require any expensive synchronization primitives. Coordination of IO operations is implemented via atomic counters without additional primitives.
|
||||
|
||||
#### 🧘🏽 Maintainability counts
|
||||
|
||||
✔️ Code that consumes this library is easy to maintain because of the small and concise set of operations exposed. Also the suggested programming paradigm to decompose an application into small functions increases maintainability, because these functions are easy to understand and if they are pure, it's often sufficient to look at the type signature to understand the purpose.
|
||||
|
||||
The library itself also comprises many small functions, but it's admittedly harder to maintain than code that uses it. However this asymmetry is intended because it offloads complexity from users into a central component.
|
||||
- **`Map`**: Transform the value inside a context
|
||||
- **`Chain`** (FlatMap): Transform and flatten nested contexts
|
||||
- **`Ap`**: Apply a function in a context to a value in a context
|
||||
- **`Of`**: Wrap a value in a context
|
||||
- **`Fold`**: Extract a value from a context
|
||||
|
||||
## Comparison to Idiomatic Go
|
||||
|
||||
In this section we discuss how the functional APIs differ from idiomatic go function signatures and how to convert back and forth.
|
||||
This section explains how functional APIs differ from idiomatic Go and how to convert between them.
|
||||
|
||||
### Pure functions
|
||||
### Pure Functions
|
||||
|
||||
Pure functions are functions that take input parameters and that compute an output without changing any global state and without mutating the input parameters. They will always return the same output for the same input.
|
||||
Pure functions take input parameters and compute output without changing global state or mutating inputs. They always return the same output for the same input.
|
||||
|
||||
#### Without Errors
|
||||
|
||||
If your pure function does not return an error, the idiomatic signature is just fine and no changes are required.
|
||||
If your pure function doesn't return an error, the idiomatic signature works as-is:
|
||||
|
||||
```go
|
||||
func add(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
```
|
||||
|
||||
#### With Errors
|
||||
|
||||
If your pure function can return an error, then it will have a `(T, error)` return value in idiomatic go. In functional style the return value is [Either[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/either) because function composition is easier with such a return type. Use the `EitherizeXXX` methods in ["github.com/IBM/fp-go/either"](https://pkg.go.dev/github.com/IBM/fp-go/either) to convert from idiomatic to functional style and `UneitherizeXXX` to convert from functional to idiomatic style.
|
||||
**Idiomatic Go:**
|
||||
```go
|
||||
func divide(a, b int) (int, error) {
|
||||
if b == 0 {
|
||||
return 0, errors.New("division by zero")
|
||||
}
|
||||
return a / b, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Effectful functions
|
||||
**Functional Style:**
|
||||
```go
|
||||
func divide(a, b int) either.Either[error, int] {
|
||||
if b == 0 {
|
||||
return either.Left[int](errors.New("division by zero"))
|
||||
}
|
||||
return either.Right[error](a / b)
|
||||
}
|
||||
```
|
||||
|
||||
An effectful function (or function with a side effect) is one that changes data outside the scope of the function or that does not always produce the same output for the same input (because it depends on some external, mutable state). There is no special way in idiomatic go to identify such a function other than documentation. In functional style we represent them as functions that do not take an input but that produce an output. The base type for these functions is [IO[T]](https://pkg.go.dev/github.com/IBM/fp-go/io) because in many cases such functions represent `I/O` operations.
|
||||
**Conversion:**
|
||||
- Use `either.EitherizeXXX` to convert from idiomatic to functional style
|
||||
- Use `either.UneitherizeXXX` to convert from functional to idiomatic style
|
||||
|
||||
### Effectful Functions
|
||||
|
||||
An effectful function changes data outside its scope or doesn't always produce the same output for the same input.
|
||||
|
||||
#### Without Errors
|
||||
|
||||
If your effectful function does not return an error, the functional signature is [IO[T]](https://pkg.go.dev/github.com/IBM/fp-go/io)
|
||||
**Functional signature:** `IO[T]`
|
||||
|
||||
```go
|
||||
func getCurrentTime() io.IO[time.Time] {
|
||||
return func() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### With Errors
|
||||
|
||||
If your effectful function can return an error, the functional signature is [IOEither[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/ioeither). Use `EitherizeXXX` from ["github.com/IBM/fp-go/ioeither"](https://pkg.go.dev/github.com/IBM/fp-go/ioeither) to convert an idiomatic go function to functional style.
|
||||
**Functional signature:** `IOEither[error, T]`
|
||||
|
||||
```go
|
||||
func readFile(path string) ioeither.IOEither[error, []byte] {
|
||||
return func() either.Either[error, []byte] {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return either.Left[[]byte](err)
|
||||
}
|
||||
return either.Right[error](data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Conversion:**
|
||||
- Use `ioeither.EitherizeXXX` to convert idiomatic Go functions to functional style
|
||||
|
||||
### Go Context
|
||||
|
||||
Functions that take a [context](https://pkg.go.dev/context) are per definition effectful because they depend on the context parameter that is designed to be mutable (it can e.g. be used to cancel a running operation). Furthermore in idiomatic go the parameter is typically passed as the first parameter to a function.
|
||||
Functions that take a `context.Context` are effectful because they depend on mutable context.
|
||||
|
||||
In functional style we isolate the [context](https://pkg.go.dev/context) and represent the nature of the effectful function as an [IOEither[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/ioeither). The resulting type is [ReaderIOEither[T]](https://pkg.go.dev/github.com/IBM/fp-go/context/readerioeither), a function taking a [context](https://pkg.go.dev/context) that returns a function without parameters returning an [Either[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/either). Use the `EitherizeXXX` methods from ["github.com/IBM/fp-go/context/readerioeither"](https://pkg.go.dev/github.com/IBM/fp-go/context/readerioeither) to convert an idiomatic go function with a [context](https://pkg.go.dev/context) to functional style.
|
||||
**Idiomatic Go:**
|
||||
```go
|
||||
func fetchData(ctx context.Context, url string) ([]byte, error) {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
**Functional Style:**
|
||||
```go
|
||||
func fetchData(url string) readerioeither.ReaderIOEither[context.Context, error, []byte] {
|
||||
return func(ctx context.Context) ioeither.IOEither[error, []byte] {
|
||||
return func() either.Either[error, []byte] {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Conversion:**
|
||||
- Use `readerioeither.EitherizeXXX` to convert idiomatic Go functions with context to functional style
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Generics
|
||||
|
||||
All monadic operations are implemented via generics, i.e. they offer a type safe way to compose operations. This allows for convenient IDE support and also gives confidence about the correctness of the composition at compile time.
|
||||
All monadic operations use Go generics for type safety:
|
||||
|
||||
Downside is that this will result in different versions of each operation per type, these versions are generated by the golang compiler at build time (unlike type erasure in languages such as Java of TypeScript). This might lead to large binaries for codebases with many different types. If this is a concern, you can always implement type erasure on top, i.e. use the monadic operations with the `any` type as if generics were not supported. You loose type safety, but this might result in smaller binaries.
|
||||
- ✅ **Pros**: Type-safe composition, IDE support, compile-time correctness
|
||||
- ⚠️ **Cons**: May result in larger binaries (different versions per type)
|
||||
- 💡 **Tip**: For binary size concerns, use type erasure with `any` type
|
||||
|
||||
### Ordering of Generic Type Parameters
|
||||
|
||||
In go we need to specify all type parameters of a function on the global function definition, even if the function returns a higher order function and some of the type parameters are only applicable to the higher order function. So the following is not possible:
|
||||
Go requires all type parameters on the global function definition. Parameters that cannot be auto-detected come first:
|
||||
|
||||
```go
|
||||
func Map[A, B any](f func(A) B) [R, E any]func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
|
||||
// Map: B cannot be auto-detected, so it comes first
|
||||
func Map[R, E, A, B any](f func(A) B) func(ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
|
||||
|
||||
// Ap: B cannot be auto-detected from the argument
|
||||
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
|
||||
```
|
||||
|
||||
Note that the parameters `R` and `E` are not needed by the first level of `Map` but only by the resulting higher order function. Instead we need to specify the following:
|
||||
This ordering maximizes type inference where possible.
|
||||
|
||||
```go
|
||||
func Map[R, E, A, B any](f func(A) B) func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
|
||||
```
|
||||
### Use of the ~ Operator
|
||||
|
||||
which overspecifies `Map` on the global scope. As a result the go compiler will not be able to auto-detect these parameters, it can only auto detect `A` and `B` since they appear in the argument of `Map`. We need to explicitly pass values for these type parameters when `Map` is being used.
|
||||
|
||||
Because of this limitation the order of parameters on a function matters. We want to make sure that we define those parameters that cannot be auto-detected, first, and the parameters that can be auto-detected, last. This can lead to inconsistencies in parameter ordering, but we believe that the gain in convenience is worth it. The parameter order of `Ap` is e.g. different from that of `Map`:
|
||||
|
||||
```go
|
||||
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(fab ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
|
||||
```
|
||||
|
||||
because `R`, `E` and `A` can be determined from the argument to `Ap` but `B` cannot.
|
||||
|
||||
### Use of the [~ Operator](https://go.googlesource.com/proposal/+/master/design/47781-parameterized-go-ast.md)
|
||||
|
||||
The FP library attempts to be easy to consume and one aspect of this is the definition of higher level type definitions instead of having to use their low level equivalent. It is e.g. more convenient and readable to use
|
||||
|
||||
```go
|
||||
ReaderIOEither[R, E, A]
|
||||
```
|
||||
|
||||
than
|
||||
|
||||
```go
|
||||
func(R) func() Either.Either[E, A]
|
||||
```
|
||||
|
||||
although both are logically equivalent. At the time of this writing the go type system does not support generic type aliases, only generic type definition, i.e. it is not possible to write:
|
||||
|
||||
```go
|
||||
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
only
|
||||
Go doesn't support generic type aliases (until Go 1.24), only type definitions. The `~` operator allows generic implementations to work with compatible types:
|
||||
|
||||
```go
|
||||
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
This makes a big difference, because in the second case the type `ReaderIOEither[R, E, A any]` is considered a completely new type, not compatible to its right hand side, so it's not just a shortcut but a fully new type.
|
||||
**Generic Subpackages:**
|
||||
- Each higher-level type has a `generic` subpackage with fully generic implementations
|
||||
- These are for library extensions, not end-users
|
||||
- Main packages specialize generic implementations for convenience
|
||||
|
||||
From the implementation perspective however there is no reason to restrict the implementation to the new type, it can be generic for all compatible types. The way to express this in go is the [~](https://go.googlesource.com/proposal/+/master/design/47781-parameterized-go-ast.md) operator. This comes with some quite complicated type declarations in some cases, which undermines the goal of the library to be easy to use.
|
||||
### Higher Kinded Types (HKT)
|
||||
|
||||
For that reason there exist sub-packages called `Generic` for all higher level types. These packages contain the fully generic implementation of the operations, preferring abstraction over usability. These packages are not meant to be used by end-users but are meant to be used by library extensions. The implementation for the convenient higher level types specializes the generic implementation for the particular higher level type, i.e. this layer does not contain any business logic but only *type magic*.
|
||||
Go doesn't support HKT natively. This library addresses this by:
|
||||
|
||||
### Higher Kinded Types
|
||||
- Introducing HKTs as individual types (e.g., `HKTA` for `HKT[A]`)
|
||||
- Implementing generic algorithms in the `internal` package
|
||||
- Keeping complexity hidden from end-users
|
||||
|
||||
Go does not support higher kinded types (HKT). Such types occur if a generic type itself is parametrized by another generic type. Example:
|
||||
## Common Operations
|
||||
|
||||
The `Map` operation for `ReaderIOEither` is defined as:
|
||||
### Map/Chain/Ap/Flap
|
||||
|
||||
| Operator | Parameter | Monad | Result | Use Case |
|
||||
| -------- | ---------------- | --------------- | -------- | -------- |
|
||||
| Map | `func(A) B` | `HKT[A]` | `HKT[B]` | Transform value in context |
|
||||
| Chain | `func(A) HKT[B]` | `HKT[A]` | `HKT[B]` | Transform and flatten |
|
||||
| Ap | `HKT[A]` | `HKT[func(A)B]` | `HKT[B]` | Apply function in context |
|
||||
| Flap | `A` | `HKT[func(A)B]` | `HKT[B]` | Apply value to function in context |
|
||||
|
||||
### Example: Chaining Operations
|
||||
|
||||
```go
|
||||
func Map[R, E, A, B any](f func(A) B) func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
|
||||
import (
|
||||
"github.com/IBM/fp-go/either"
|
||||
"github.com/IBM/fp-go/function"
|
||||
)
|
||||
|
||||
result := function.Pipe3(
|
||||
either.Right[error](10),
|
||||
either.Map(func(x int) int { return x * 2 }),
|
||||
either.Chain(func(x int) either.Either[error, int] {
|
||||
if x > 15 {
|
||||
return either.Right[error](x)
|
||||
}
|
||||
return either.Left[int](errors.New("too small"))
|
||||
}),
|
||||
either.GetOrElse(func() int { return 0 }),
|
||||
)
|
||||
```
|
||||
|
||||
and in fact the equivalent operations for all other monads follow the same pattern, we could try to introduce a new type for `ReaderIOEither` (without a parameter) as a HKT, e.g. like so (made-up syntax, does not work in go):
|
||||
## 📚 Resources
|
||||
|
||||
```go
|
||||
func Map[HKT, R, E, A, B any](f func(A) B) func(HKT[R, E, A]) HKT[R, E, B]
|
||||
```
|
||||
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go)
|
||||
- [Code Samples](./samples/)
|
||||
- [V2 Documentation](./v2/README.md) - New features in Go 1.24+
|
||||
- [fp-ts](https://github.com/gcanti/fp-ts) - Original TypeScript inspiration
|
||||
|
||||
this would be the completely generic method signature for all possible monads. In particular in many cases it is possible to compose functions independent of the concrete knowledge of the actual `HKT`. From the perspective of a library this is the ideal situation because then a particular algorithm only has to be implemented and tested once.
|
||||
## 🤝 Contributing
|
||||
|
||||
This FP library addresses this by introducing the HKTs as individual types, e.g. `HKT[A]` would be represented as a new generic type `HKTA`. This loses the correlation to the type `A` but allows to implement generic algorithms, at the price of readability.
|
||||
Contributions are welcome! Please feel free to submit issues or pull requests.
|
||||
|
||||
For that reason these implementations are kept in the `internal` package. These are meant to be used by the library itself or by extensions, not by end users.
|
||||
## 📄 License
|
||||
|
||||
## Map/Ap/Flap
|
||||
|
||||
The following table lists the relationship between some selected operators
|
||||
|
||||
| Operator | Parameter | Monad | Result |
|
||||
| -------- | ---------------- | --------------- | -------- |
|
||||
| Map | `func(A) B` | `HKT[A]` | `HKT[B]` |
|
||||
| Chain | `func(A) HKT[B]` | `HKT[A]` | `HKT[B]` |
|
||||
| Ap | `HKT[A]` | `HKT[func(A)B]` | `HKT[B]` |
|
||||
| Flap | `A` | `HKT[func(A)B]` | `HKT[B]` |
|
||||
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
"config:recommended",
|
||||
":dependencyDashboard"
|
||||
],
|
||||
"rangeStrategy": "bump",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDatasources": [
|
||||
"golang-version"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"go"
|
||||
],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"gomod"
|
||||
@@ -25,15 +34,6 @@
|
||||
],
|
||||
"automerge": true,
|
||||
"groupName": "go dependencies"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"conventional-changelog-conventionalcommits"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"major"
|
||||
],
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
310
v2/README.md
310
v2/README.md
@@ -1,43 +1,319 @@
|
||||
# Functional programming library for golang V2
|
||||
# fp-go V2: Enhanced Functional Programming for Go 1.24+
|
||||
|
||||
Go 1.24 introduces [generic type aliases](https://github.com/golang/go/issues/46477) which are leveraged by V2.
|
||||
[](https://pkg.go.dev/github.com/IBM/fp-go/v2)
|
||||
[](https://coveralls.io/github/IBM/fp-go?branch=main)
|
||||
|
||||
Version 2 of fp-go leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
|
||||
|
||||
## 📚 Table of Contents
|
||||
|
||||
- [Requirements](#-requirements)
|
||||
- [Breaking Changes](#-breaking-changes)
|
||||
- [Key Improvements](#-key-improvements)
|
||||
- [Migration Guide](#-migration-guide)
|
||||
- [Installation](#-installation)
|
||||
- [What's New](#-whats-new)
|
||||
|
||||
## 🔧 Requirements
|
||||
|
||||
- **Go 1.24 or later** (for generic type alias support)
|
||||
|
||||
## ⚠️ Breaking Changes
|
||||
|
||||
- use of [generic type aliases](https://github.com/golang/go/issues/46477) which requires [go1.24](https://tip.golang.org/doc/go1.24)
|
||||
- order of generic type arguments adjusted such that types that _cannot_ be inferred by the method argument come first, e.g. in the `Ap` methods
|
||||
- monadic operations for `Pair` operate on the second argument, to be compatible with the [Haskell](https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/Data-Pair.html) definition
|
||||
### 1. Generic Type Aliases
|
||||
|
||||
## Simplifications
|
||||
V2 uses [generic type aliases](https://github.com/golang/go/issues/46477) which require Go 1.24+. This is the most significant change and enables cleaner type definitions.
|
||||
|
||||
- use type aliases to get rid of namespace imports for type declarations, e.g. instead of
|
||||
**V1:**
|
||||
```go
|
||||
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
**V2:**
|
||||
```go
|
||||
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
|
||||
```
|
||||
|
||||
### 2. Generic Type Parameter Ordering
|
||||
|
||||
Type parameters that **cannot** be inferred from function arguments now come first, improving type inference.
|
||||
|
||||
**V1:**
|
||||
```go
|
||||
// Ap in V1 - less intuitive ordering
|
||||
func Ap[R, E, A, B any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
|
||||
```
|
||||
|
||||
**V2:**
|
||||
```go
|
||||
// Ap in V2 - B comes first as it cannot be inferred
|
||||
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
|
||||
```
|
||||
|
||||
This change allows the Go compiler to infer more types automatically, reducing the need for explicit type parameters.
|
||||
|
||||
### 3. Pair Monad Semantics
|
||||
|
||||
Monadic operations for `Pair` now operate on the **second argument** to align with the [Haskell definition](https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/Data-Pair.html).
|
||||
|
||||
**V1:**
|
||||
```go
|
||||
// Operations on first element
|
||||
pair := MakePair(1, "hello")
|
||||
result := Map(func(x int) int { return x * 2 })(pair) // Pair(2, "hello")
|
||||
```
|
||||
|
||||
**V2:**
|
||||
```go
|
||||
// Operations on second element (Haskell-compatible)
|
||||
pair := MakePair(1, "hello")
|
||||
result := Map(func(s string) string { return s + "!" })(pair) // Pair(1, "hello!")
|
||||
```
|
||||
|
||||
## ✨ Key Improvements
|
||||
|
||||
### 1. Simplified Type Declarations
|
||||
|
||||
Generic type aliases eliminate the need for namespace imports in type declarations.
|
||||
|
||||
**V1 Approach:**
|
||||
```go
|
||||
import (
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
ET "github.com/IBM/fp-go/either"
|
||||
OPT "github.com/IBM/fp-go/option"
|
||||
)
|
||||
|
||||
func doSth() ET.Either[error, string] {
|
||||
...
|
||||
func processData(input string) ET.Either[error, OPT.Option[int]] {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
you can declare your type once
|
||||
**V2 Approach:**
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Define type aliases once
|
||||
type Either[A any] = either.Either[error, A]
|
||||
type Option[A any] = option.Option[A]
|
||||
|
||||
// Use them throughout your codebase
|
||||
func processData(input string) Either[Option[int]] {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
### 2. No More `generic` Subpackages
|
||||
|
||||
The library implementation no longer requires separate `generic` subpackages, making the codebase simpler and easier to understand.
|
||||
|
||||
**V1 Structure:**
|
||||
```
|
||||
either/
|
||||
either.go
|
||||
generic/
|
||||
either.go // Generic implementation
|
||||
```
|
||||
|
||||
**V2 Structure:**
|
||||
```
|
||||
either/
|
||||
either.go // Single, clean implementation
|
||||
```
|
||||
|
||||
### 3. Better Type Inference
|
||||
|
||||
The reordered type parameters allow the Go compiler to infer more types automatically:
|
||||
|
||||
**V1:**
|
||||
```go
|
||||
// Often need explicit type parameters
|
||||
result := Map[Context, error, int, string](transform)(value)
|
||||
```
|
||||
|
||||
**V2:**
|
||||
```go
|
||||
// Compiler can infer more types
|
||||
result := Map(transform)(value) // Cleaner!
|
||||
```
|
||||
|
||||
## 🚀 Migration Guide
|
||||
|
||||
### Step 1: Update Go Version
|
||||
|
||||
Ensure you're using Go 1.24 or later:
|
||||
|
||||
```bash
|
||||
go version # Should show go1.24 or higher
|
||||
```
|
||||
|
||||
### Step 2: Update Import Paths
|
||||
|
||||
Change all import paths from `github.com/IBM/fp-go` to `github.com/IBM/fp-go/v2`:
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/either"
|
||||
"github.com/IBM/fp-go/option"
|
||||
)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3: Remove `generic` Subpackage Imports
|
||||
|
||||
If you were using generic subpackages, remove them:
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
import (
|
||||
E "github.com/IBM/fp-go/either/generic"
|
||||
)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
type Either[A any] = either.Either[error, A]
|
||||
```
|
||||
|
||||
and then use it across your codebase
|
||||
### Step 4: Update Type Parameter Order
|
||||
|
||||
Review functions like `Ap` where type parameter order has changed. The compiler will help identify these:
|
||||
|
||||
**Before:**
|
||||
```go
|
||||
result := Ap[Context, error, int, string](value)(funcInContext)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```go
|
||||
result := Ap[string, Context, error, int](value)(funcInContext)
|
||||
// Or better yet, let the compiler infer:
|
||||
result := Ap(value)(funcInContext)
|
||||
```
|
||||
|
||||
### Step 5: Update Pair Operations
|
||||
|
||||
If you're using `Pair`, update operations to work on the second element:
|
||||
|
||||
**Before (V1):**
|
||||
```go
|
||||
pair := MakePair(42, "data")
|
||||
// Map operates on first element
|
||||
result := Map(func(x int) int { return x * 2 })(pair)
|
||||
```
|
||||
|
||||
**After (V2):**
|
||||
```go
|
||||
pair := MakePair(42, "data")
|
||||
// Map operates on second element
|
||||
result := Map(func(s string) string { return s + "!" })(pair)
|
||||
```
|
||||
|
||||
### Step 6: Simplify Type Aliases
|
||||
|
||||
Create project-wide type aliases for common patterns:
|
||||
|
||||
```go
|
||||
func doSth() Either[string] {
|
||||
...
|
||||
// types.go - Define once, use everywhere
|
||||
package myapp
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
type Either[A any] = either.Either[error, A]
|
||||
type Option[A any] = option.Option[A]
|
||||
type IOEither[A any] = ioeither.IOEither[error, A]
|
||||
```
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
go get github.com/IBM/fp-go/v2
|
||||
```
|
||||
|
||||
## 🆕 What's New
|
||||
|
||||
### Cleaner API Surface
|
||||
|
||||
The elimination of `generic` subpackages means:
|
||||
- Fewer imports to manage
|
||||
- Simpler package structure
|
||||
- Easier to navigate documentation
|
||||
- More intuitive API
|
||||
|
||||
### Example: Before and After
|
||||
|
||||
**V1 Complex Example:**
|
||||
```go
|
||||
import (
|
||||
ET "github.com/IBM/fp-go/either"
|
||||
EG "github.com/IBM/fp-go/either/generic"
|
||||
IOET "github.com/IBM/fp-go/ioeither"
|
||||
IOEG "github.com/IBM/fp-go/ioeither/generic"
|
||||
)
|
||||
|
||||
func process() IOET.IOEither[error, string] {
|
||||
return IOEG.Map[error, int, string](
|
||||
func(x int) string { return fmt.Sprintf("%d", x) },
|
||||
)(fetchData())
|
||||
}
|
||||
```
|
||||
|
||||
- library implementation does no longer need to use the `generic` subpackage, this simplifies reading and understanding of the code
|
||||
**V2 Simplified Example:**
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
type IOEither[A any] = ioeither.IOEither[error, A]
|
||||
|
||||
func process() IOEither[string] {
|
||||
return ioeither.Map(
|
||||
func(x int) string { return fmt.Sprintf("%d", x) },
|
||||
)(fetchData())
|
||||
}
|
||||
```
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- [Main README](../README.md) - Core concepts and design philosophy
|
||||
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)
|
||||
- [Code Samples](../samples/)
|
||||
- [Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)
|
||||
|
||||
## 🤔 Should I Migrate?
|
||||
|
||||
**Migrate to V2 if:**
|
||||
- ✅ You can use Go 1.24+
|
||||
- ✅ You want cleaner, more maintainable code
|
||||
- ✅ You want better type inference
|
||||
- ✅ You're starting a new project
|
||||
|
||||
**Stay on V1 if:**
|
||||
- ⚠️ You're locked to Go < 1.24
|
||||
- ⚠️ Migration effort outweighs benefits for your project
|
||||
- ⚠️ You need stability in production (V2 is newer)
|
||||
|
||||
## 🐛 Issues and Feedback
|
||||
|
||||
Found a bug or have a suggestion? Please [open an issue](https://github.com/IBM/fp-go/issues) on GitHub.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
|
||||
@@ -21,14 +21,56 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
// result := generic.Do[[]State, State](State{})
|
||||
func Do[GS ~[]S, S any](
|
||||
empty S,
|
||||
) GS {
|
||||
return Of[GS](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
// For arrays, this produces the cartesian product where later steps can use values from earlier steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// generic.Do[[]State, State](State{}),
|
||||
// generic.Bind[[]State, []State, []int, State, State, int](
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// func(s State) []int {
|
||||
// return []int{1, 2, 3}
|
||||
// },
|
||||
// ),
|
||||
// generic.Bind[[]State, []State, []int, State, State, int](
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) []int {
|
||||
// // This can access s.X from the previous step
|
||||
// return []int{s.X * 10, s.X * 20}
|
||||
// },
|
||||
// ),
|
||||
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
|
||||
func Bind[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) GT,
|
||||
@@ -75,7 +117,39 @@ func BindTo[GS1 ~[]S1, GT ~[]T, S1, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel. For arrays, this produces the cartesian product.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y string
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// xValues := []int{1, 2}
|
||||
// yValues := []string{"a", "b"}
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// generic.Do[[]State, State](State{}),
|
||||
// generic.ApS[[]State, []State, []int, State, State, int](
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// xValues,
|
||||
// ),
|
||||
// generic.ApS[[]State, []State, []string, State, State, string](
|
||||
// func(y string) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// yValues,
|
||||
// ),
|
||||
// ) // [{1,"a"}, {1,"b"}, {2,"a"}, {2,"b"}]
|
||||
func ApS[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa GT,
|
||||
|
||||
@@ -403,5 +403,3 @@ func TestSlicePropertyBased(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -35,5 +35,6 @@ func Commands() []*C.Command {
|
||||
IOCommand(),
|
||||
IOOptionCommand(),
|
||||
DICommand(),
|
||||
LensCommand(),
|
||||
}
|
||||
}
|
||||
|
||||
524
v2/cli/lens.go
Normal file
524
v2/cli/lens.go
Normal file
@@ -0,0 +1,524 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
keyLensDir = "dir"
|
||||
keyVerbose = "verbose"
|
||||
lensAnnotation = "fp-go:Lens"
|
||||
)
|
||||
|
||||
var (
|
||||
flagLensDir = &C.StringFlag{
|
||||
Name: keyLensDir,
|
||||
Value: ".",
|
||||
Usage: "Directory to scan for Go files",
|
||||
}
|
||||
|
||||
flagVerbose = &C.BoolFlag{
|
||||
Name: keyVerbose,
|
||||
Aliases: []string{"v"},
|
||||
Value: false,
|
||||
Usage: "Enable verbose output",
|
||||
}
|
||||
)
|
||||
|
||||
// structInfo holds information about a struct that needs lens generation
|
||||
type structInfo struct {
|
||||
Name string
|
||||
Fields []fieldInfo
|
||||
Imports map[string]string // package path -> alias
|
||||
}
|
||||
|
||||
// fieldInfo holds information about a struct field
|
||||
type fieldInfo struct {
|
||||
Name string
|
||||
TypeName string
|
||||
BaseType string // TypeName without leading * for pointer types
|
||||
IsOptional bool // true if field is a pointer or has json omitempty tag
|
||||
}
|
||||
|
||||
// templateData holds data for template rendering
|
||||
type templateData struct {
|
||||
PackageName string
|
||||
Structs []structInfo
|
||||
}
|
||||
|
||||
const lensStructTemplate = `
|
||||
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
|
||||
type {{.Name}}Lenses struct {
|
||||
{{- range .Fields}}
|
||||
{{.Name}} {{if .IsOptional}}LO.LensO[{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[{{$.Name}}, {{.TypeName}}]{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
|
||||
type {{.Name}}RefLenses struct {
|
||||
{{- range .Fields}}
|
||||
{{.Name}} {{if .IsOptional}}LO.LensO[*{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[*{{$.Name}}, {{.TypeName}}]{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
`
|
||||
|
||||
const lensConstructorTemplate = `
|
||||
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
|
||||
func Make{{.Name}}Lenses() {{.Name}}Lenses {
|
||||
{{- range .Fields}}
|
||||
{{- if .IsOptional}}
|
||||
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}Lenses{
|
||||
{{- range .Fields}}
|
||||
{{- if .IsOptional}}
|
||||
{{.Name}}: L.MakeLens(
|
||||
func(s {{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
|
||||
func(s {{$.Name}}, v O.Option[{{.TypeName}}]) {{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
|
||||
),
|
||||
{{- else}}
|
||||
{{.Name}}: L.MakeLens(
|
||||
func(s {{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s {{$.Name}}, v {{.TypeName}}) {{$.Name}} { s.{{.Name}} = v; return s },
|
||||
),
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
|
||||
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
|
||||
func Make{{.Name}}RefLenses() {{.Name}}RefLenses {
|
||||
{{- range .Fields}}
|
||||
{{- if .IsOptional}}
|
||||
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}RefLenses{
|
||||
{{- range .Fields}}
|
||||
{{- if .IsOptional}}
|
||||
{{.Name}}: L.MakeLensRef(
|
||||
func(s *{{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
|
||||
func(s *{{$.Name}}, v O.Option[{{.TypeName}}]) *{{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
|
||||
),
|
||||
{{- else}}
|
||||
{{.Name}}: L.MakeLensRef(
|
||||
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
|
||||
),
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var (
|
||||
structTmpl *template.Template
|
||||
constructorTmpl *template.Template
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
structTmpl, err = template.New("struct").Parse(lensStructTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
constructorTmpl, err = template.New("constructor").Parse(lensConstructorTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// hasLensAnnotation checks if a comment group contains the lens annotation
|
||||
func hasLensAnnotation(doc *ast.CommentGroup) bool {
|
||||
if doc == nil {
|
||||
return false
|
||||
}
|
||||
for _, comment := range doc.List {
|
||||
if strings.Contains(comment.Text, lensAnnotation) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getTypeName extracts the type name from a field type expression
|
||||
func getTypeName(expr ast.Expr) string {
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
case *ast.StarExpr:
|
||||
return "*" + getTypeName(t.X)
|
||||
case *ast.ArrayType:
|
||||
return "[]" + getTypeName(t.Elt)
|
||||
case *ast.MapType:
|
||||
return "map[" + getTypeName(t.Key) + "]" + getTypeName(t.Value)
|
||||
case *ast.SelectorExpr:
|
||||
return getTypeName(t.X) + "." + t.Sel.Name
|
||||
case *ast.InterfaceType:
|
||||
return "interface{}"
|
||||
case *ast.IndexExpr:
|
||||
// Generic type with single type parameter (Go 1.18+)
|
||||
// e.g., Option[string]
|
||||
return getTypeName(t.X) + "[" + getTypeName(t.Index) + "]"
|
||||
case *ast.IndexListExpr:
|
||||
// Generic type with multiple type parameters (Go 1.18+)
|
||||
// e.g., Map[string, int]
|
||||
var params []string
|
||||
for _, index := range t.Indices {
|
||||
params = append(params, getTypeName(index))
|
||||
}
|
||||
return getTypeName(t.X) + "[" + strings.Join(params, ", ") + "]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
|
||||
// extractImports extracts package imports from a type expression
|
||||
// Returns a map of package path -> package name
|
||||
func extractImports(expr ast.Expr, imports map[string]string) {
|
||||
switch t := expr.(type) {
|
||||
case *ast.StarExpr:
|
||||
extractImports(t.X, imports)
|
||||
case *ast.ArrayType:
|
||||
extractImports(t.Elt, imports)
|
||||
case *ast.MapType:
|
||||
extractImports(t.Key, imports)
|
||||
extractImports(t.Value, imports)
|
||||
case *ast.SelectorExpr:
|
||||
// This is a qualified identifier like "option.Option"
|
||||
if ident, ok := t.X.(*ast.Ident); ok {
|
||||
// ident.Name is the package name (e.g., "option")
|
||||
// We need to track this for import resolution
|
||||
imports[ident.Name] = ident.Name
|
||||
}
|
||||
case *ast.IndexExpr:
|
||||
// Generic type with single type parameter
|
||||
extractImports(t.X, imports)
|
||||
extractImports(t.Index, imports)
|
||||
case *ast.IndexListExpr:
|
||||
// Generic type with multiple type parameters
|
||||
extractImports(t.X, imports)
|
||||
for _, index := range t.Indices {
|
||||
extractImports(index, imports)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasOmitEmpty checks if a struct tag contains json omitempty
|
||||
func hasOmitEmpty(tag *ast.BasicLit) bool {
|
||||
if tag == nil {
|
||||
return false
|
||||
}
|
||||
// Parse the struct tag
|
||||
tagValue := strings.Trim(tag.Value, "`")
|
||||
structTag := reflect.StructTag(tagValue)
|
||||
jsonTag := structTag.Get("json")
|
||||
|
||||
// Check if omitempty is present
|
||||
parts := strings.Split(jsonTag, ",")
|
||||
for _, part := range parts {
|
||||
if strings.TrimSpace(part) == "omitempty" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isPointerType checks if a type expression is a pointer
|
||||
func isPointerType(expr ast.Expr) bool {
|
||||
_, ok := expr.(*ast.StarExpr)
|
||||
return ok
|
||||
}
|
||||
|
||||
// parseFile parses a Go file and extracts structs with lens annotations
|
||||
func parseFile(filename string) ([]structInfo, string, error) {
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
var structs []structInfo
|
||||
packageName := node.Name.Name
|
||||
|
||||
// Build import map: package name -> import path
|
||||
fileImports := make(map[string]string)
|
||||
for _, imp := range node.Imports {
|
||||
path := strings.Trim(imp.Path.Value, `"`)
|
||||
var name string
|
||||
if imp.Name != nil {
|
||||
name = imp.Name.Name
|
||||
} else {
|
||||
// Extract package name from path (last component)
|
||||
parts := strings.Split(path, "/")
|
||||
name = parts[len(parts)-1]
|
||||
}
|
||||
fileImports[name] = path
|
||||
}
|
||||
|
||||
// First pass: collect all GenDecls with their doc comments
|
||||
declMap := make(map[*ast.TypeSpec]*ast.CommentGroup)
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
if gd, ok := n.(*ast.GenDecl); ok {
|
||||
for _, spec := range gd.Specs {
|
||||
if ts, ok := spec.(*ast.TypeSpec); ok {
|
||||
declMap[ts] = gd.Doc
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Second pass: process type specs
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
// Look for type declarations
|
||||
typeSpec, ok := n.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if it's a struct type
|
||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Get the doc comment from our map
|
||||
doc := declMap[typeSpec]
|
||||
if !hasLensAnnotation(doc) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Extract field information and collect imports
|
||||
var fields []fieldInfo
|
||||
structImports := make(map[string]string)
|
||||
|
||||
for _, field := range structType.Fields.List {
|
||||
if len(field.Names) == 0 {
|
||||
// Embedded field, skip for now
|
||||
continue
|
||||
}
|
||||
for _, name := range field.Names {
|
||||
// Only export lenses for exported fields
|
||||
if name.IsExported() {
|
||||
typeName := getTypeName(field.Type)
|
||||
isOptional := false
|
||||
baseType := typeName
|
||||
|
||||
// Check if field is optional:
|
||||
// 1. Pointer types are always optional
|
||||
// 2. Non-pointer types with json omitempty tag are optional
|
||||
if isPointerType(field.Type) {
|
||||
isOptional = true
|
||||
// Strip leading * for base type
|
||||
baseType = strings.TrimPrefix(typeName, "*")
|
||||
} else if hasOmitEmpty(field.Tag) {
|
||||
// Non-pointer type with omitempty is also optional
|
||||
isOptional = true
|
||||
}
|
||||
|
||||
// Extract imports from this field's type
|
||||
fieldImports := make(map[string]string)
|
||||
extractImports(field.Type, fieldImports)
|
||||
|
||||
// Resolve package names to full import paths
|
||||
for pkgName := range fieldImports {
|
||||
if importPath, ok := fileImports[pkgName]; ok {
|
||||
structImports[importPath] = pkgName
|
||||
}
|
||||
}
|
||||
|
||||
fields = append(fields, fieldInfo{
|
||||
Name: name.Name,
|
||||
TypeName: typeName,
|
||||
BaseType: baseType,
|
||||
IsOptional: isOptional,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) > 0 {
|
||||
structs = append(structs, structInfo{
|
||||
Name: typeSpec.Name.Name,
|
||||
Fields: fields,
|
||||
Imports: structImports,
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return structs, packageName, nil
|
||||
}
|
||||
|
||||
// generateLensHelpers scans a directory for Go files and generates lens code
|
||||
func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
// Get absolute path
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Scanning directory: %s", absDir)
|
||||
}
|
||||
|
||||
// Find all Go files in the directory
|
||||
files, err := filepath.Glob(filepath.Join(absDir, "*.go"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Found %d Go files", len(files))
|
||||
}
|
||||
|
||||
// Parse all files and collect structs
|
||||
var allStructs []structInfo
|
||||
var packageName string
|
||||
|
||||
for _, file := range files {
|
||||
// Skip generated files and test files
|
||||
if strings.HasSuffix(file, "_test.go") || strings.Contains(file, "gen.go") {
|
||||
if verbose {
|
||||
log.Printf("Skipping file: %s", filepath.Base(file))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Parsing file: %s", filepath.Base(file))
|
||||
}
|
||||
|
||||
structs, pkg, err := parseFile(file)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to parse %s: %v", file, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if verbose && len(structs) > 0 {
|
||||
log.Printf("Found %d annotated struct(s) in %s", len(structs), filepath.Base(file))
|
||||
for _, s := range structs {
|
||||
log.Printf(" - %s (%d fields)", s.Name, len(s.Fields))
|
||||
}
|
||||
}
|
||||
|
||||
if packageName == "" {
|
||||
packageName = pkg
|
||||
}
|
||||
|
||||
allStructs = append(allStructs, structs...)
|
||||
}
|
||||
|
||||
if len(allStructs) == 0 {
|
||||
log.Printf("No structs with %s annotation found in %s", lensAnnotation, absDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect all unique imports from all structs
|
||||
allImports := make(map[string]string) // import path -> alias
|
||||
for _, s := range allStructs {
|
||||
for importPath, alias := range s.Imports {
|
||||
allImports[importPath] = alias
|
||||
}
|
||||
}
|
||||
|
||||
// Create output file
|
||||
outPath := filepath.Join(absDir, filename)
|
||||
f, err := os.Create(filepath.Clean(outPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(allStructs))
|
||||
|
||||
// Write header
|
||||
writePackage(f, packageName)
|
||||
|
||||
// Write imports
|
||||
f.WriteString("import (\n")
|
||||
// Standard fp-go imports always needed
|
||||
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
|
||||
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
|
||||
f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
|
||||
f.WriteString("\tI \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
|
||||
|
||||
// Add additional imports collected from field types
|
||||
for importPath, alias := range allImports {
|
||||
f.WriteString("\t" + alias + " \"" + importPath + "\"\n")
|
||||
}
|
||||
|
||||
f.WriteString(")\n")
|
||||
|
||||
// Generate lens code for each struct using templates
|
||||
for _, s := range allStructs {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Generate struct type
|
||||
if err := structTmpl.Execute(&buf, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate constructor
|
||||
if err := constructorTmpl.Execute(&buf, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write to file
|
||||
if _, err := f.Write(buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LensCommand creates the CLI command for lens generation
|
||||
func LensCommand() *C.Command {
|
||||
return &C.Command{
|
||||
Name: "lens",
|
||||
Usage: "generate lens code for annotated structs",
|
||||
Description: "Scans Go files for structs annotated with 'fp-go:Lens' and generates lens types. Pointer types and non-pointer types with json omitempty tag generate LensO (optional lens).",
|
||||
Flags: []C.Flag{
|
||||
flagLensDir,
|
||||
flagFilename,
|
||||
flagVerbose,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
return generateLensHelpers(
|
||||
ctx.String(keyLensDir),
|
||||
ctx.String(keyFilename),
|
||||
ctx.Bool(keyVerbose),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
503
v2/cli/lens_test.go
Normal file
503
v2/cli/lens_test.go
Normal file
@@ -0,0 +1,503 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHasLensAnnotation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
comment string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "has annotation",
|
||||
comment: "// fp-go:Lens",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "has annotation with other text",
|
||||
comment: "// This is a struct with fp-go:Lens annotation",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no annotation",
|
||||
comment: "// This is just a regular comment",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nil comment",
|
||||
comment: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var doc *ast.CommentGroup
|
||||
if tt.comment != "" {
|
||||
doc = &ast.CommentGroup{
|
||||
List: []*ast.Comment{
|
||||
{Text: tt.comment},
|
||||
},
|
||||
}
|
||||
}
|
||||
result := hasLensAnnotation(doc)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTypeName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple type",
|
||||
code: "type T struct { F string }",
|
||||
expected: "string",
|
||||
},
|
||||
{
|
||||
name: "pointer type",
|
||||
code: "type T struct { F *string }",
|
||||
expected: "*string",
|
||||
},
|
||||
{
|
||||
name: "slice type",
|
||||
code: "type T struct { F []int }",
|
||||
expected: "[]int",
|
||||
},
|
||||
{
|
||||
name: "map type",
|
||||
code: "type T struct { F map[string]int }",
|
||||
expected: "map[string]int",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
var fieldType ast.Expr
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
|
||||
fieldType = field.Type
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
require.NotNil(t, fieldType)
|
||||
result := getTypeName(fieldType)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPointerType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "pointer type",
|
||||
code: "type T struct { F *string }",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "non-pointer type",
|
||||
code: "type T struct { F string }",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "slice type",
|
||||
code: "type T struct { F []string }",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
var fieldType ast.Expr
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
|
||||
fieldType = field.Type
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
require.NotNil(t, fieldType)
|
||||
result := isPointerType(fieldType)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasOmitEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "has omitempty",
|
||||
tag: "`json:\"field,omitempty\"`",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "has omitempty with other options",
|
||||
tag: "`json:\"field,omitempty,string\"`",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no omitempty",
|
||||
tag: "`json:\"field\"`",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no tag",
|
||||
tag: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "different tag",
|
||||
tag: "`xml:\"field\"`",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var tag *ast.BasicLit
|
||||
if tt.tag != "" {
|
||||
tag = &ast.BasicLit{
|
||||
Value: tt.tag,
|
||||
}
|
||||
}
|
||||
result := hasOmitEmpty(tag)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFile(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Phone *string
|
||||
}
|
||||
|
||||
// fp-go:Lens
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
// Not annotated
|
||||
type Other struct {
|
||||
Field string
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 2)
|
||||
|
||||
// Check Person struct
|
||||
person := structs[0]
|
||||
assert.Equal(t, "Person", person.Name)
|
||||
assert.Len(t, person.Fields, 3)
|
||||
|
||||
assert.Equal(t, "Name", person.Fields[0].Name)
|
||||
assert.Equal(t, "string", person.Fields[0].TypeName)
|
||||
assert.False(t, person.Fields[0].IsOptional)
|
||||
|
||||
assert.Equal(t, "Age", person.Fields[1].Name)
|
||||
assert.Equal(t, "int", person.Fields[1].TypeName)
|
||||
assert.False(t, person.Fields[1].IsOptional)
|
||||
|
||||
assert.Equal(t, "Phone", person.Fields[2].Name)
|
||||
assert.Equal(t, "*string", person.Fields[2].TypeName)
|
||||
assert.True(t, person.Fields[2].IsOptional)
|
||||
|
||||
// Check Address struct
|
||||
address := structs[1]
|
||||
assert.Equal(t, "Address", address.Name)
|
||||
assert.Len(t, address.Fields, 2)
|
||||
|
||||
assert.Equal(t, "Street", address.Fields[0].Name)
|
||||
assert.Equal(t, "City", address.Fields[1].Name)
|
||||
}
|
||||
|
||||
func TestParseFileWithOmitEmpty(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type Config struct {
|
||||
Name string
|
||||
Value string ` + "`json:\"value,omitempty\"`" + `
|
||||
Count int ` + "`json:\",omitempty\"`" + `
|
||||
Optional *string ` + "`json:\"optional,omitempty\"`" + `
|
||||
Required int ` + "`json:\"required\"`" + `
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 1)
|
||||
|
||||
// Check Config struct
|
||||
config := structs[0]
|
||||
assert.Equal(t, "Config", config.Name)
|
||||
assert.Len(t, config.Fields, 5)
|
||||
|
||||
// Name - no tag, not optional
|
||||
assert.Equal(t, "Name", config.Fields[0].Name)
|
||||
assert.Equal(t, "string", config.Fields[0].TypeName)
|
||||
assert.False(t, config.Fields[0].IsOptional)
|
||||
|
||||
// Value - has omitempty, should be optional
|
||||
assert.Equal(t, "Value", config.Fields[1].Name)
|
||||
assert.Equal(t, "string", config.Fields[1].TypeName)
|
||||
assert.True(t, config.Fields[1].IsOptional, "Value field with omitempty should be optional")
|
||||
|
||||
// Count - has omitempty (no field name in tag), should be optional
|
||||
assert.Equal(t, "Count", config.Fields[2].Name)
|
||||
assert.Equal(t, "int", config.Fields[2].TypeName)
|
||||
assert.True(t, config.Fields[2].IsOptional, "Count field with omitempty should be optional")
|
||||
|
||||
// Optional - pointer with omitempty, should be optional
|
||||
assert.Equal(t, "Optional", config.Fields[3].Name)
|
||||
assert.Equal(t, "*string", config.Fields[3].TypeName)
|
||||
assert.True(t, config.Fields[3].IsOptional)
|
||||
|
||||
// Required - has json tag but no omitempty, not optional
|
||||
assert.Equal(t, "Required", config.Fields[4].Name)
|
||||
assert.Equal(t, "int", config.Fields[4].TypeName)
|
||||
assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional")
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpers(t *testing.T) {
|
||||
// Create a temporary directory with test files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Value *int
|
||||
}
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
genPath := filepath.Join(tmpDir, outputFile)
|
||||
_, err = os.Stat(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read and verify the generated content
|
||||
content, err := os.ReadFile(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// Check for expected content
|
||||
assert.Contains(t, contentStr, "package testpkg")
|
||||
assert.Contains(t, contentStr, "Code generated by go generate")
|
||||
assert.Contains(t, contentStr, "TestStructLens")
|
||||
assert.Contains(t, contentStr, "MakeTestStructLens")
|
||||
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
|
||||
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
|
||||
assert.Contains(t, contentStr, "I.FromZero")
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
|
||||
// Create a temporary directory with test files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// No annotation
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
}
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code (should not create file)
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file does not exist
|
||||
genPath := filepath.Join(tmpDir, outputFile)
|
||||
_, err = os.Stat(genPath)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
}
|
||||
|
||||
func TestLensTemplates(t *testing.T) {
|
||||
s := structInfo{
|
||||
Name: "TestStruct",
|
||||
Fields: []fieldInfo{
|
||||
{Name: "Name", TypeName: "string", IsOptional: false},
|
||||
{Name: "Value", TypeName: "*int", IsOptional: true},
|
||||
},
|
||||
}
|
||||
|
||||
// Test struct template
|
||||
var structBuf bytes.Buffer
|
||||
err := structTmpl.Execute(&structBuf, s)
|
||||
require.NoError(t, err)
|
||||
|
||||
structStr := structBuf.String()
|
||||
assert.Contains(t, structStr, "type TestStructLenses struct")
|
||||
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
|
||||
assert.Contains(t, structStr, "Value LO.LensO[TestStruct, *int]")
|
||||
|
||||
// Test constructor template
|
||||
var constructorBuf bytes.Buffer
|
||||
err = constructorTmpl.Execute(&constructorBuf, s)
|
||||
require.NoError(t, err)
|
||||
|
||||
constructorStr := constructorBuf.String()
|
||||
assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses")
|
||||
assert.Contains(t, constructorStr, "return TestStructLenses{")
|
||||
assert.Contains(t, constructorStr, "Name: L.MakeLens(")
|
||||
assert.Contains(t, constructorStr, "Value: L.MakeLens(")
|
||||
assert.Contains(t, constructorStr, "I.FromZero")
|
||||
}
|
||||
|
||||
func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
||||
s := structInfo{
|
||||
Name: "ConfigStruct",
|
||||
Fields: []fieldInfo{
|
||||
{Name: "Name", TypeName: "string", IsOptional: false},
|
||||
{Name: "Value", TypeName: "string", IsOptional: true}, // non-pointer with omitempty
|
||||
{Name: "Count", TypeName: "int", IsOptional: true}, // non-pointer with omitempty
|
||||
{Name: "Pointer", TypeName: "*string", IsOptional: true}, // pointer
|
||||
},
|
||||
}
|
||||
|
||||
// Test struct template
|
||||
var structBuf bytes.Buffer
|
||||
err := structTmpl.Execute(&structBuf, s)
|
||||
require.NoError(t, err)
|
||||
|
||||
structStr := structBuf.String()
|
||||
assert.Contains(t, structStr, "type ConfigStructLenses struct")
|
||||
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "Value LO.LensO[ConfigStruct, string]", "non-pointer with omitempty should use LensO")
|
||||
assert.Contains(t, structStr, "Count LO.LensO[ConfigStruct, int]", "non-pointer with omitempty should use LensO")
|
||||
assert.Contains(t, structStr, "Pointer LO.LensO[ConfigStruct, *string]")
|
||||
|
||||
// Test constructor template
|
||||
var constructorBuf bytes.Buffer
|
||||
err = constructorTmpl.Execute(&constructorBuf, s)
|
||||
require.NoError(t, err)
|
||||
|
||||
constructorStr := constructorBuf.String()
|
||||
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
|
||||
assert.Contains(t, constructorStr, "isoValue := I.FromZero[string]()")
|
||||
assert.Contains(t, constructorStr, "isoCount := I.FromZero[int]()")
|
||||
assert.Contains(t, constructorStr, "isoPointer := I.FromZero[*string]()")
|
||||
}
|
||||
|
||||
func TestLensCommandFlags(t *testing.T) {
|
||||
cmd := LensCommand()
|
||||
|
||||
assert.Equal(t, "lens", cmd.Name)
|
||||
assert.Equal(t, "generate lens code for annotated structs", cmd.Usage)
|
||||
assert.Contains(t, strings.ToLower(cmd.Description), "fp-go:lens")
|
||||
assert.Contains(t, strings.ToLower(cmd.Description), "lenso")
|
||||
|
||||
// Check flags
|
||||
assert.Len(t, cmd.Flags, 3)
|
||||
|
||||
var hasDir, hasFilename, hasVerbose bool
|
||||
for _, flag := range cmd.Flags {
|
||||
switch flag.Names()[0] {
|
||||
case "dir":
|
||||
hasDir = true
|
||||
case "filename":
|
||||
hasFilename = true
|
||||
case "verbose":
|
||||
hasVerbose = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasDir, "should have dir flag")
|
||||
assert.True(t, hasFilename, "should have filename flag")
|
||||
assert.True(t, hasVerbose, "should have verbose flag")
|
||||
}
|
||||
@@ -18,12 +18,12 @@ package readereither
|
||||
import "github.com/IBM/fp-go/v2/readereither"
|
||||
|
||||
// TraverseArray transforms an array
|
||||
func TraverseArray[A, B any](f func(A) ReaderEither[B]) func([]A) ReaderEither[[]B] {
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return readereither.TraverseArray(f)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex transforms an array
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderEither[B]) func([]A) ReaderEither[[]B] {
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderEither[B]) Kleisli[[]A, []B] {
|
||||
return readereither.TraverseArrayWithIndex(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,21 +18,71 @@ package readereither
|
||||
import (
|
||||
"context"
|
||||
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// UserID string
|
||||
// TenantID string
|
||||
// }
|
||||
// result := readereither.Do(State{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderEither[S] {
|
||||
return G.Do[ReaderEither[S], context.Context, error, S](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access the context.Context from the environment.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// UserID string
|
||||
// TenantID string
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do(State{}),
|
||||
// readereither.Bind(
|
||||
// func(uid string) func(State) State {
|
||||
// return func(s State) State { s.UserID = uid; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderEither[string] {
|
||||
// return func(ctx context.Context) either.Either[error, string] {
|
||||
// if uid, ok := ctx.Value("userID").(string); ok {
|
||||
// return either.Right[error](uid)
|
||||
// }
|
||||
// return either.Left[string](errors.New("no userID"))
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// readereither.Bind(
|
||||
// func(tid string) func(State) State {
|
||||
// return func(s State) State { s.TenantID = tid; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderEither[string] {
|
||||
// // This can access s.UserID from the previous step
|
||||
// return func(ctx context.Context) either.Either[error, string] {
|
||||
// return either.Right[error]("tenant-" + s.UserID)
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) ReaderEither[T],
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[ReaderEither[S1], S2] {
|
||||
return G.Bind[ReaderEither[S1], ReaderEither[S2], ReaderEither[T], context.Context, error, S1, S2, T](setter, f)
|
||||
}
|
||||
|
||||
@@ -40,7 +90,7 @@ func Bind[S1, S2, T any](
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
) Kleisli[ReaderEither[S1], S2] {
|
||||
return G.Let[ReaderEither[S1], ReaderEither[S2], context.Context, error, S1, S2, T](setter, f)
|
||||
}
|
||||
|
||||
@@ -48,21 +98,212 @@ func Let[S1, S2, T any](
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
) Kleisli[ReaderEither[S1], S2] {
|
||||
return G.LetTo[ReaderEither[S1], ReaderEither[S2], context.Context, error, S1, S2, T](setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(ReaderEither[T]) ReaderEither[S1] {
|
||||
) Kleisli[ReaderEither[T], S1] {
|
||||
return G.BindTo[ReaderEither[S1], ReaderEither[T], context.Context, error, S1, T](setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// UserID string
|
||||
// TenantID string
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUserID := func(ctx context.Context) either.Either[error, string] {
|
||||
// return either.Right[error](ctx.Value("userID").(string))
|
||||
// }
|
||||
// getTenantID := func(ctx context.Context) either.Either[error, string] {
|
||||
// return either.Right[error](ctx.Value("tenantID").(string))
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do(State{}),
|
||||
// readereither.ApS(
|
||||
// func(uid string) func(State) State {
|
||||
// return func(s State) State { s.UserID = uid; return s }
|
||||
// },
|
||||
// getUserID,
|
||||
// ),
|
||||
// readereither.ApS(
|
||||
// func(tid string) func(State) State {
|
||||
// return func(s State) State { s.TenantID = tid; return s }
|
||||
// },
|
||||
// getTenantID,
|
||||
// ),
|
||||
// )
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderEither[T],
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
) Kleisli[ReaderEither[S1], S2] {
|
||||
return G.ApS[ReaderEither[S1], ReaderEither[S2], ReaderEither[T], context.Context, error, S1, S2, T](setter, fa)
|
||||
}
|
||||
|
||||
// ApSL is a variant of ApS that uses a lens to focus on a specific field in the state.
|
||||
// Instead of providing a setter function, you provide a lens that knows how to get and set
|
||||
// the field. This is more convenient when working with nested structures.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens that focuses on a field of type T within state S
|
||||
// - fa: A ReaderEither computation that produces a value of type T
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms ReaderEither[S] to ReaderEither[S] by setting the focused field
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(p Person) int { return p.Age },
|
||||
// func(p Person, a int) Person { p.Age = a; return p },
|
||||
// )
|
||||
//
|
||||
// getAge := func(ctx context.Context) either.Either[error, int] {
|
||||
// return either.Right[error](30)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readereither.Do(Person{Name: "Alice", Age: 25}),
|
||||
// readereither.ApSL(ageLens, getAge),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderEither[T],
|
||||
) Kleisli[ReaderEither[S], S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific field in the state.
|
||||
// It combines the lens-based field access with monadic composition, allowing you to:
|
||||
// 1. Extract a field value using the lens
|
||||
// 2. Use that value in a computation that may fail
|
||||
// 3. Update the field with the result
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens that focuses on a field of type T within state S
|
||||
// - f: A function that takes the current field value and returns a ReaderEither computation
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms ReaderEither[S] to ReaderEither[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// increment := func(v int) readereither.ReaderEither[int] {
|
||||
// return func(ctx context.Context) either.Either[error, int] {
|
||||
// if v >= 100 {
|
||||
// return either.Left[int](errors.New("value too large"))
|
||||
// }
|
||||
// return either.Right[error](v + 1)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readereither.Of[error](Counter{Value: 42}),
|
||||
// readereither.BindL(valueLens, increment),
|
||||
// )
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Kleisli[ReaderEither[S], S] {
|
||||
return Bind[S, S, T](lens.Set, func(s S) ReaderEither[T] {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
|
||||
// It applies a pure transformation to the focused field without any effects.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens that focuses on a field of type T within state S
|
||||
// - f: A pure function that transforms the field value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms ReaderEither[S] to ReaderEither[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readereither.Of[error](Counter{Value: 21}),
|
||||
// readereither.LetL(valueLens, double),
|
||||
// )
|
||||
// // result when executed will be Right(Counter{Value: 42})
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Kleisli[ReaderEither[S], S] {
|
||||
return Let[S, S, T](lens.Set, func(s S) T {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.
|
||||
// It sets the focused field to a constant value.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens that focuses on a field of type T within state S
|
||||
// - b: The constant value to set
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms ReaderEither[S] to ReaderEither[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readereither.Of[error](Config{Debug: true, Timeout: 30}),
|
||||
// readereither.LetToL(debugLens, false),
|
||||
// )
|
||||
// // result when executed will be Right(Config{Debug: false, Timeout: 30})
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Kleisli[ReaderEither[S], S] {
|
||||
return LetTo[S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -28,26 +28,26 @@ func Curry0[A any](f func(context.Context) (A, error)) ReaderEither[A] {
|
||||
return readereither.Curry0(f)
|
||||
}
|
||||
|
||||
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderEither[A] {
|
||||
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) Kleisli[T1, A] {
|
||||
return readereither.Curry1(f)
|
||||
}
|
||||
|
||||
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) func(T2) ReaderEither[A] {
|
||||
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) Kleisli[T2, A] {
|
||||
return readereither.Curry2(f)
|
||||
}
|
||||
|
||||
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) func(T3) ReaderEither[A] {
|
||||
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) Kleisli[T3, A] {
|
||||
return readereither.Curry3(f)
|
||||
}
|
||||
|
||||
func Uncurry1[T1, A any](f func(T1) ReaderEither[A]) func(context.Context, T1) (A, error) {
|
||||
func Uncurry1[T1, A any](f Kleisli[T1, A]) func(context.Context, T1) (A, error) {
|
||||
return readereither.Uncurry1(f)
|
||||
}
|
||||
|
||||
func Uncurry2[T1, T2, A any](f func(T1) func(T2) ReaderEither[A]) func(context.Context, T1, T2) (A, error) {
|
||||
func Uncurry2[T1, T2, A any](f func(T1) Kleisli[T2, A]) func(context.Context, T1, T2) (A, error) {
|
||||
return readereither.Uncurry2(f)
|
||||
}
|
||||
|
||||
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderEither[A]) func(context.Context, T1, T2, T3) (A, error) {
|
||||
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) Kleisli[T3, A]) func(context.Context, T1, T2, T3) (A, error) {
|
||||
return readereither.Uncurry3(f)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func From0[A any](f func(context.Context) (A, error)) func() ReaderEither[A] {
|
||||
return readereither.From0(f)
|
||||
}
|
||||
|
||||
func From1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderEither[A] {
|
||||
func From1[T1, A any](f func(context.Context, T1) (A, error)) Kleisli[T1, A] {
|
||||
return readereither.From1(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,11 +41,11 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return readereither.Map[context.Context, error](f)
|
||||
}
|
||||
|
||||
func MonadChain[A, B any](ma ReaderEither[A], f func(A) ReaderEither[B]) ReaderEither[B] {
|
||||
func MonadChain[A, B any](ma ReaderEither[A], f Kleisli[A, B]) ReaderEither[B] {
|
||||
return readereither.MonadChain(ma, f)
|
||||
}
|
||||
|
||||
func Chain[A, B any](f func(A) ReaderEither[B]) Operator[A, B] {
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return readereither.Chain(f)
|
||||
}
|
||||
|
||||
@@ -61,11 +61,11 @@ func Ap[A, B any](fa ReaderEither[A]) func(ReaderEither[func(A) B]) ReaderEither
|
||||
return readereither.Ap[B](fa)
|
||||
}
|
||||
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) ReaderEither[A] {
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
|
||||
return readereither.FromPredicate[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
func OrElse[A any](onLeft func(error) ReaderEither[A]) func(ReaderEither[A]) ReaderEither[A] {
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderEither[A], A] {
|
||||
return readereither.OrElse(onLeft)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,5 +31,6 @@ type (
|
||||
// ReaderEither is a specialization of the Reader monad for the typical golang scenario
|
||||
ReaderEither[A any] = readereither.ReaderEither[context.Context, error, A]
|
||||
|
||||
Operator[A, B any] = reader.Reader[ReaderEither[A], ReaderEither[B]]
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderEither[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderEither[A], B]
|
||||
)
|
||||
|
||||
@@ -19,20 +19,75 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// result := readerioeither.Do(State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderIOEither[S] {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access the context.Context from the environment.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readerioeither.ReaderIOEither[User] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// readerioeither.Bind(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// func(s State) readerioeither.ReaderIOEither[Config] {
|
||||
// // This can access s.User from the previous step
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, Config] {
|
||||
// return ioeither.TryCatch(func() (Config, error) {
|
||||
// return fetchConfigForUser(ctx, s.User.ID)
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) ReaderIOEither[T],
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return chain.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[T, S2],
|
||||
@@ -42,10 +97,12 @@ func Bind[S1, S2, T any](
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
) Operator[S1, S2] {
|
||||
return functor.Let(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
@@ -54,10 +111,12 @@ func Let[S1, S2, T any](
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
) Operator[S1, S2] {
|
||||
return functor.LetTo(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
@@ -66,6 +125,8 @@ func LetTo[S1, S2, T any](
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[T, S1] {
|
||||
@@ -75,11 +136,53 @@ func BindTo[S1, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// getConfig := func(ctx context.Context) ioeither.IOEither[error, Config] {
|
||||
// return ioeither.TryCatch(func() (Config, error) {
|
||||
// return fetchConfig(ctx)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.ApS(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// readerioeither.ApS(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// getConfig,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderIOEither[T],
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
) Operator[S1, S2] {
|
||||
return apply.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
@@ -87,3 +190,152 @@ func ApS[S1, S2, T any](
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Of(State{}),
|
||||
// readerioeither.ApSL(userLens, getUser),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderIOEither[T],
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a ReaderIOEither computation that produces an updated value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.BindL(userLens, func(user User) readerioeither.ReaderIOEither[User] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return Bind[S, S, T](lens.Set, func(s S) ReaderIOEither[T] {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new value (without wrapping in a ReaderIOEither).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{User: User{Name: "Alice"}}),
|
||||
// readerioeither.LetL(userLens, func(user User) User {
|
||||
// user.Name = "Bob"
|
||||
// return user
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[S, S] {
|
||||
return Let[S, S, T](lens.Set, func(s S) T {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The value b is set directly to the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// newUser := User{Name: "Bob", ID: 123}
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.LetToL(userLens, newUser),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return LetTo[S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -42,7 +43,7 @@ func TestBind(t *testing.T) {
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, res(context.Background())(), E.Of[error]("John Doe"))
|
||||
assert.Equal(t, res(t.Context())(), E.Of[error]("John Doe"))
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
@@ -54,5 +55,221 @@ func TestApS(t *testing.T) {
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, res(context.Background())(), E.Of[error]("John Doe"))
|
||||
assert.Equal(t, res(t.Context())(), E.Of[error]("John Doe"))
|
||||
}
|
||||
|
||||
func TestApS_WithError(t *testing.T) {
|
||||
// Test that ApS propagates errors correctly
|
||||
testErr := assert.AnError
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(utils.Empty),
|
||||
ApS(utils.SetLastName, Left[string](testErr)),
|
||||
ApS(utils.SetGivenName, Of("John")),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
assert.Equal(t, testErr, E.ToError(result))
|
||||
}
|
||||
|
||||
func TestApS_WithSecondError(t *testing.T) {
|
||||
// Test that ApS propagates errors from the second operation
|
||||
testErr := assert.AnError
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(utils.Empty),
|
||||
ApS(utils.SetLastName, Of("Doe")),
|
||||
ApS(utils.SetGivenName, Left[string](testErr)),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
assert.Equal(t, testErr, E.ToError(result))
|
||||
}
|
||||
|
||||
func TestApS_MultipleFields(t *testing.T) {
|
||||
// Test ApS with more than two fields
|
||||
type Person struct {
|
||||
FirstName string
|
||||
MiddleName string
|
||||
LastName string
|
||||
Age int
|
||||
}
|
||||
|
||||
setFirstName := func(s string) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.FirstName = s
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
setMiddleName := func(s string) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.MiddleName = s
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
setLastName := func(s string) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.LastName = s
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
setAge := func(a int) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.Age = a
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe5(
|
||||
Do(Person{}),
|
||||
ApS(setFirstName, Of("John")),
|
||||
ApS(setMiddleName, Of("Q")),
|
||||
ApS(setLastName, Of("Doe")),
|
||||
ApS(setAge, Of(42)),
|
||||
Map(func(p Person) Person { return p }),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
person := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(person))
|
||||
p, _ := O.Unwrap(person)
|
||||
assert.Equal(t, "John", p.FirstName)
|
||||
assert.Equal(t, "Q", p.MiddleName)
|
||||
assert.Equal(t, "Doe", p.LastName)
|
||||
assert.Equal(t, 42, p.Age)
|
||||
}
|
||||
|
||||
func TestApS_WithDifferentTypes(t *testing.T) {
|
||||
// Test ApS with different value types
|
||||
type State struct {
|
||||
Name string
|
||||
Count int
|
||||
Flag bool
|
||||
}
|
||||
|
||||
setName := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Name = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
setCount := func(c int) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Count = c
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
setFlag := func(f bool) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Flag = f
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe4(
|
||||
Do(State{}),
|
||||
ApS(setName, Of("test")),
|
||||
ApS(setCount, Of(100)),
|
||||
ApS(setFlag, Of(true)),
|
||||
Map(func(s State) State { return s }),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
stateOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(stateOpt))
|
||||
state, _ := O.Unwrap(stateOpt)
|
||||
assert.Equal(t, "test", state.Name)
|
||||
assert.Equal(t, 100, state.Count)
|
||||
assert.True(t, state.Flag)
|
||||
}
|
||||
|
||||
func TestApS_EmptyState(t *testing.T) {
|
||||
// Test ApS starting with an empty state
|
||||
type Empty struct{}
|
||||
|
||||
res := Do(Empty{})
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
emptyOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(emptyOpt))
|
||||
empty, _ := O.Unwrap(emptyOpt)
|
||||
assert.Equal(t, Empty{}, empty)
|
||||
}
|
||||
|
||||
func TestApS_ChainedWithBind(t *testing.T) {
|
||||
// Test mixing ApS with Bind operations
|
||||
type State struct {
|
||||
Independent string
|
||||
Dependent string
|
||||
}
|
||||
|
||||
setIndependent := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Independent = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
setDependent := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Dependent = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
getDependentValue := func(s State) ReaderIOEither[string] {
|
||||
// This depends on the Independent field
|
||||
return Of(s.Independent + "-dependent")
|
||||
}
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(State{}),
|
||||
ApS(setIndependent, Of("value")),
|
||||
Bind(setDependent, getDependentValue),
|
||||
Map(func(s State) State { return s }),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
stateOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(stateOpt))
|
||||
state, _ := O.Unwrap(stateOpt)
|
||||
assert.Equal(t, "value", state.Independent)
|
||||
assert.Equal(t, "value-dependent", state.Dependent)
|
||||
}
|
||||
|
||||
func TestApS_WithContextCancellation(t *testing.T) {
|
||||
// Test that ApS respects context cancellation
|
||||
type State struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
setValue := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Value = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
// Create a computation that would succeed
|
||||
computation := ApS(setValue, Of("test"))(Do(State{}))
|
||||
|
||||
// Create a cancelled context
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := computation(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func Bracket[
|
||||
A, B, ANY any](
|
||||
|
||||
acquire ReaderIOEither[A],
|
||||
use func(A) ReaderIOEither[B],
|
||||
use Kleisli[A, B],
|
||||
release func(A, Either[B]) ReaderIOEither[ANY],
|
||||
) ReaderIOEither[B] {
|
||||
return bracket.Bracket[ReaderIOEither[A], ReaderIOEither[B], ReaderIOEither[ANY], Either[B], A, B](
|
||||
|
||||
@@ -39,6 +39,8 @@ import (
|
||||
// eqRIE := Eq(eqInt)
|
||||
// ctx := context.Background()
|
||||
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
|
||||
//
|
||||
//go:inline
|
||||
func Eq[A any](eq eq.Eq[Either[A]]) func(context.Context) eq.Eq[ReaderIOEither[A]] {
|
||||
return RIOE.Eq[context.Context](eq)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,36 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package builder provides utilities for building HTTP requests in a functional way
|
||||
// using the ReaderIOEither monad. It integrates with the http/builder package to
|
||||
// create composable, type-safe HTTP request builders with proper error handling
|
||||
// and context support.
|
||||
//
|
||||
// The main function, Requester, converts a Builder from the http/builder package
|
||||
// into a ReaderIOEither that produces HTTP requests. This allows for:
|
||||
// - Immutable request building with method chaining
|
||||
// - Automatic header management including Content-Length
|
||||
// - Support for requests with and without bodies
|
||||
// - Proper error handling wrapped in Either
|
||||
// - Context propagation for cancellation and timeouts
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// import (
|
||||
// "context"
|
||||
// B "github.com/IBM/fp-go/v2/http/builder"
|
||||
// RB "github.com/IBM/fp-go/v2/context/readerioeither/http/builder"
|
||||
// )
|
||||
//
|
||||
// builder := F.Pipe3(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("POST"),
|
||||
// B.WithJSONBody(userData),
|
||||
// )
|
||||
//
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
package builder
|
||||
|
||||
import (
|
||||
@@ -31,6 +61,59 @@ import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Requester converts an http/builder.Builder into a ReaderIOEither that produces HTTP requests.
|
||||
// It handles both requests with and without bodies, automatically managing headers including
|
||||
// Content-Length for requests with bodies.
|
||||
//
|
||||
// The function performs the following operations:
|
||||
// 1. Extracts the request body (if present) from the builder
|
||||
// 2. Creates appropriate request constructor (with or without body)
|
||||
// 3. Applies the target URL from the builder
|
||||
// 4. Applies the HTTP method from the builder
|
||||
// 5. Merges headers from the builder into the request
|
||||
// 6. Handles any errors that occur during request construction
|
||||
//
|
||||
// For requests with a body:
|
||||
// - Sets the Content-Length header automatically
|
||||
// - Uses bytes.NewReader to create the request body
|
||||
// - Merges builder headers into the request
|
||||
//
|
||||
// For requests without a body:
|
||||
// - Creates a request with nil body
|
||||
// - Merges builder headers into the request
|
||||
//
|
||||
// Parameters:
|
||||
// - builder: A pointer to an http/builder.Builder containing request configuration
|
||||
//
|
||||
// Returns:
|
||||
// - A Requester (ReaderIOEither[*http.Request]) that, when executed with a context,
|
||||
// produces either an error or a configured *http.Request
|
||||
//
|
||||
// Example with body:
|
||||
//
|
||||
// import (
|
||||
// B "github.com/IBM/fp-go/v2/http/builder"
|
||||
// RB "github.com/IBM/fp-go/v2/context/readerioeither/http/builder"
|
||||
// )
|
||||
//
|
||||
// builder := F.Pipe3(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("POST"),
|
||||
// B.WithJSONBody(map[string]string{"name": "John"}),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
//
|
||||
// Example without body:
|
||||
//
|
||||
// builder := F.Pipe2(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("GET"),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
func Requester(builder *R.Builder) RIOEH.Requester {
|
||||
|
||||
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOEither[*http.Request] {
|
||||
|
||||
@@ -57,3 +57,231 @@ func TestBuilderWithQuery(t *testing.T) {
|
||||
|
||||
assert.True(t, E.IsRight(req(context.Background())()))
|
||||
}
|
||||
|
||||
// TestBuilderWithoutBody tests creating a request without a body
|
||||
func TestBuilderWithoutBody(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
R.WithMethod("GET"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "GET", req.Method)
|
||||
assert.Equal(t, "https://api.example.com/users", req.URL.String())
|
||||
assert.Nil(t, req.Body, "Expected nil body for GET request")
|
||||
}
|
||||
|
||||
// TestBuilderWithBody tests creating a request with a body
|
||||
func TestBuilderWithBody(t *testing.T) {
|
||||
bodyData := []byte(`{"name":"John","age":30}`)
|
||||
|
||||
builder := F.Pipe3(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
R.WithMethod("POST"),
|
||||
R.WithBytes(bodyData),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://api.example.com/users", req.URL.String())
|
||||
assert.NotNil(t, req.Body, "Expected non-nil body for POST request")
|
||||
assert.Equal(t, "24", req.Header.Get("Content-Length"))
|
||||
}
|
||||
|
||||
// TestBuilderWithHeaders tests that headers are properly set
|
||||
func TestBuilderWithHeaders(t *testing.T) {
|
||||
builder := F.Pipe3(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/data"),
|
||||
R.WithHeader("Authorization")("Bearer token123"),
|
||||
R.WithHeader("Accept")("application/json"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "Bearer token123", req.Header.Get("Authorization"))
|
||||
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
||||
}
|
||||
|
||||
// TestBuilderWithInvalidURL tests error handling for invalid URLs
|
||||
func TestBuilderWithInvalidURL(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
R.Default,
|
||||
R.WithURL("://invalid-url"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestBuilderWithEmptyMethod tests creating a request with empty method
|
||||
func TestBuilderWithEmptyMethod(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
R.WithMethod(""),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
// Empty method should still work (defaults to GET in http.NewRequest)
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
}
|
||||
|
||||
// TestBuilderWithMultipleHeaders tests setting multiple headers
|
||||
func TestBuilderWithMultipleHeaders(t *testing.T) {
|
||||
builder := F.Pipe4(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/data"),
|
||||
R.WithHeader("X-Custom-Header-1")("value1"),
|
||||
R.WithHeader("X-Custom-Header-2")("value2"),
|
||||
R.WithHeader("X-Custom-Header-3")("value3"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "value1", req.Header.Get("X-Custom-Header-1"))
|
||||
assert.Equal(t, "value2", req.Header.Get("X-Custom-Header-2"))
|
||||
assert.Equal(t, "value3", req.Header.Get("X-Custom-Header-3"))
|
||||
}
|
||||
|
||||
// TestBuilderWithBodyAndHeaders tests combining body and headers
|
||||
func TestBuilderWithBodyAndHeaders(t *testing.T) {
|
||||
bodyData := []byte(`{"test":"data"}`)
|
||||
|
||||
builder := F.Pipe4(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/submit"),
|
||||
R.WithMethod("PUT"),
|
||||
R.WithBytes(bodyData),
|
||||
R.WithHeader("X-Request-ID")("12345"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "PUT", req.Method)
|
||||
assert.NotNil(t, req.Body, "Expected non-nil body")
|
||||
assert.Equal(t, "12345", req.Header.Get("X-Request-ID"))
|
||||
assert.Equal(t, "15", req.Header.Get("Content-Length"))
|
||||
}
|
||||
|
||||
// TestBuilderContextCancellation tests that context cancellation is respected
|
||||
func TestBuilderContextCancellation(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
|
||||
// Create a cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
result := requester(ctx)()
|
||||
|
||||
// The request should still be created (cancellation affects execution, not creation)
|
||||
// But we verify the context is properly passed
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
if req != nil {
|
||||
assert.Equal(t, ctx, req.Context(), "Expected context to be set in request")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuilderWithDifferentMethods tests various HTTP methods
|
||||
func TestBuilderWithDifferentMethods(t *testing.T) {
|
||||
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range methods {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/resource"),
|
||||
R.WithMethod(method),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result for method %s", method)
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request for method %s", method)
|
||||
assert.Equal(t, method, req.Method)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuilderWithJSON tests creating a request with JSON body
|
||||
func TestBuilderWithJSON(t *testing.T) {
|
||||
data := map[string]string{"username": "testuser", "email": "test@example.com"}
|
||||
|
||||
builder := F.Pipe3(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/v1/users"),
|
||||
R.WithMethod("POST"),
|
||||
R.WithJSON(data),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://api.example.com/v1/users", req.URL.String())
|
||||
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||
assert.NotNil(t, req.Body)
|
||||
}
|
||||
|
||||
// TestBuilderWithBearer tests adding Bearer token
|
||||
func TestBuilderWithBearer(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/protected"),
|
||||
R.WithBearer("my-secret-token"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "Bearer my-secret-token", req.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
15
v2/context/readerioeither/http/builder/coverage.out
Normal file
15
v2/context/readerioeither/http/builder/coverage.out
Normal file
@@ -0,0 +1,15 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:117.52,119.103 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:119.103,120.80 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:120.80,121.41 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:121.41,123.19 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:123.19,126.6 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:127.5,127.20 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:132.2,132.93 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:132.93,133.80 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:133.80,134.41 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:134.41,136.19 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:136.19,138.6 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:139.5,139.20 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:144.2,150.50 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:150.50,153.4 2 1
|
||||
11
v2/context/readerioeither/http/coverage.out
Normal file
11
v2/context/readerioeither/http/coverage.out
Normal file
@@ -0,0 +1,11 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:111.76,116.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:134.49,136.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:161.90,162.65 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:162.65,166.76 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:166.76,176.5 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:198.73,203.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:222.74,227.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:234.76,236.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:245.74,254.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:281.76,286.2 1 1
|
||||
@@ -13,6 +13,22 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package http provides functional HTTP client utilities built on top of ReaderIOEither monad.
|
||||
// It offers a composable way to make HTTP requests with context support, error handling,
|
||||
// and response parsing capabilities. The package follows functional programming principles
|
||||
// to ensure type-safe, testable, and maintainable HTTP operations.
|
||||
//
|
||||
// The main abstractions include:
|
||||
// - Requester: A reader that constructs HTTP requests with context
|
||||
// - Client: An interface for executing HTTP requests
|
||||
// - Response readers: Functions to parse responses as bytes, text, or JSON
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// result := ReadJSON[MyType](client)(request)
|
||||
// response := result(context.Background())()
|
||||
package http
|
||||
|
||||
import (
|
||||
@@ -30,14 +46,31 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Requester is a reader that constructs a request
|
||||
// Requester is a reader that constructs an HTTP request with context support.
|
||||
// It represents a computation that, given a context, produces either an error
|
||||
// or an HTTP request. This allows for composable request building with proper
|
||||
// error handling and context propagation.
|
||||
Requester = RIOE.ReaderIOEither[*http.Request]
|
||||
|
||||
// Client is an interface for executing HTTP requests in a functional way.
|
||||
// It wraps the standard http.Client and provides a Do method that works
|
||||
// with the ReaderIOEither monad for composable, type-safe HTTP operations.
|
||||
Client interface {
|
||||
// Do can send an HTTP request considering a context
|
||||
// Do executes an HTTP request and returns the response wrapped in a ReaderIOEither.
|
||||
// It takes a Requester (which builds the request) and returns a computation that,
|
||||
// when executed with a context, performs the HTTP request and returns either
|
||||
// an error or the HTTP response.
|
||||
//
|
||||
// Parameters:
|
||||
// - req: A Requester that builds the HTTP request
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderIOEither that produces either an error or an *http.Response
|
||||
Do(Requester) RIOE.ReaderIOEither[*http.Response]
|
||||
}
|
||||
|
||||
// client is the internal implementation of the Client interface.
|
||||
// It wraps a standard http.Client and provides functional HTTP operations.
|
||||
client struct {
|
||||
delegate *http.Client
|
||||
doIOE func(*http.Request) IOE.IOEither[error, *http.Response]
|
||||
@@ -45,11 +78,33 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
// MakeRequest is an eitherized version of [http.NewRequestWithContext]
|
||||
// MakeRequest is an eitherized version of http.NewRequestWithContext.
|
||||
// It creates a Requester that builds an HTTP request with the given method, URL, and body.
|
||||
// This function properly handles errors and wraps them in the Either monad.
|
||||
//
|
||||
// Parameters:
|
||||
// - method: HTTP method (GET, POST, PUT, DELETE, etc.)
|
||||
// - url: The target URL for the request
|
||||
// - body: Optional request body (can be nil)
|
||||
//
|
||||
// Returns:
|
||||
// - A Requester that produces either an error or an *http.Request
|
||||
MakeRequest = RIOE.Eitherize3(http.NewRequestWithContext)
|
||||
|
||||
// makeRequest is a partially applied version of MakeRequest with the context parameter bound.
|
||||
makeRequest = F.Bind13of3(MakeRequest)
|
||||
|
||||
// specialize
|
||||
// MakeGetRequest creates a GET request for the specified URL.
|
||||
// It's a convenience function that specializes MakeRequest for GET requests with no body.
|
||||
//
|
||||
// Parameters:
|
||||
// - url: The target URL for the GET request
|
||||
//
|
||||
// Returns:
|
||||
// - A Requester that produces either an error or an *http.Request
|
||||
//
|
||||
// Example:
|
||||
// req := MakeGetRequest("https://api.example.com/users")
|
||||
MakeGetRequest = makeRequest("GET", nil)
|
||||
)
|
||||
|
||||
@@ -60,12 +115,49 @@ func (client client) Do(req Requester) RIOE.ReaderIOEither[*http.Response] {
|
||||
)
|
||||
}
|
||||
|
||||
// MakeClient creates an HTTP client proxy
|
||||
// MakeClient creates a functional HTTP client wrapper around a standard http.Client.
|
||||
// The returned Client provides methods for executing HTTP requests in a functional,
|
||||
// composable way using the ReaderIOEither monad.
|
||||
//
|
||||
// Parameters:
|
||||
// - httpClient: A standard *http.Client to wrap (e.g., http.DefaultClient)
|
||||
//
|
||||
// Returns:
|
||||
// - A Client that can execute HTTP requests functionally
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// // or with custom client
|
||||
// customClient := &http.Client{Timeout: 10 * time.Second}
|
||||
// client := MakeClient(customClient)
|
||||
func MakeClient(httpClient *http.Client) Client {
|
||||
return client{delegate: httpClient, doIOE: IOE.Eitherize1(httpClient.Do)}
|
||||
}
|
||||
|
||||
// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple
|
||||
// ReadFullResponse sends an HTTP request, reads the complete response body as a byte array,
|
||||
// and returns both the response and body as a tuple (FullResponse).
|
||||
// It validates the HTTP status code and handles errors appropriately.
|
||||
//
|
||||
// The function performs the following steps:
|
||||
// 1. Executes the HTTP request using the provided client
|
||||
// 2. Validates the response status code (checks for HTTP errors)
|
||||
// 3. Reads the entire response body into a byte array
|
||||
// 4. Returns a tuple containing the response and body
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOEither[FullResponse]
|
||||
// where FullResponse is a tuple of (*http.Response, []byte)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// fullResp := ReadFullResponse(client)(request)
|
||||
// result := fullResp(context.Background())()
|
||||
func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOEither[H.FullResponse] {
|
||||
return func(req Requester) RIOE.ReaderIOEither[H.FullResponse] {
|
||||
return F.Flow3(
|
||||
@@ -86,7 +178,23 @@ func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOEither[H.FullR
|
||||
}
|
||||
}
|
||||
|
||||
// ReadAll sends a request and reads the response as bytes
|
||||
// ReadAll sends an HTTP request and reads the complete response body as a byte array.
|
||||
// It validates the HTTP status code and returns the raw response body bytes.
|
||||
// This is useful when you need to process the response body in a custom way.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOEither[[]byte]
|
||||
// containing the response body as bytes
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// readBytes := ReadAll(client)
|
||||
// result := readBytes(request)(context.Background())()
|
||||
func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
@@ -94,7 +202,23 @@ func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
)
|
||||
}
|
||||
|
||||
// ReadText sends a request, reads the response and represents the response as a text string
|
||||
// ReadText sends an HTTP request, reads the response body, and converts it to a string.
|
||||
// It validates the HTTP status code and returns the response body as a UTF-8 string.
|
||||
// This is convenient for APIs that return plain text responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOEither[string]
|
||||
// containing the response body as a string
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/text")
|
||||
// readText := ReadText(client)
|
||||
// result := readText(request)(context.Background())()
|
||||
func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
@@ -102,13 +226,22 @@ func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
|
||||
)
|
||||
}
|
||||
|
||||
// ReadJson sends a request, reads the response and parses the response as JSON
|
||||
// ReadJson sends an HTTP request, reads the response, and parses it as JSON.
|
||||
//
|
||||
// Deprecated: use [ReadJSON] instead
|
||||
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
|
||||
// but will be removed in a future version. The capitalized version follows Go naming
|
||||
// conventions for acronyms.
|
||||
func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
|
||||
return ReadJSON[A](client)
|
||||
}
|
||||
|
||||
// readJSON is an internal helper that reads the response body and validates JSON content type.
|
||||
// It performs the following validations:
|
||||
// 1. Validates HTTP status code
|
||||
// 2. Validates that the response Content-Type is application/json
|
||||
// 3. Reads the response body as bytes
|
||||
//
|
||||
// This function is used internally by ReadJSON to ensure proper JSON response handling.
|
||||
func readJSON(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
return F.Flow3(
|
||||
ReadFullResponse(client),
|
||||
@@ -120,7 +253,31 @@ func readJSON(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
)
|
||||
}
|
||||
|
||||
// ReadJSON sends a request, reads the response and parses the response as JSON
|
||||
// ReadJSON sends an HTTP request, reads the response, and parses it as JSON into type A.
|
||||
// It validates both the HTTP status code and the Content-Type header to ensure the
|
||||
// response is valid JSON before attempting to unmarshal.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type to unmarshal the JSON response into
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOEither[A]
|
||||
// containing the parsed JSON data
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// }
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/user/1")
|
||||
// readUser := ReadJSON[User](client)
|
||||
// result := readUser(request)(context.Background())()
|
||||
func ReadJSON[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
|
||||
@@ -88,7 +88,7 @@ func TestSendSingleRequest(t *testing.T) {
|
||||
|
||||
resp1 := readItem(req1)
|
||||
|
||||
resE := resp1(context.TODO())()
|
||||
resE := resp1(t.Context())()
|
||||
|
||||
fmt.Println(resE)
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func TestSendSingleRequestWithHeaderUnsafe(t *testing.T) {
|
||||
)
|
||||
|
||||
res := F.Pipe1(
|
||||
resp1(context.TODO())(),
|
||||
resp1(t.Context())(),
|
||||
E.GetOrElse(errors.ToString),
|
||||
)
|
||||
|
||||
@@ -149,9 +149,167 @@ func TestSendSingleRequestWithHeaderSafe(t *testing.T) {
|
||||
)
|
||||
|
||||
res := F.Pipe1(
|
||||
response(context.TODO())(),
|
||||
response(t.Context())(),
|
||||
E.GetOrElse(errors.ToString),
|
||||
)
|
||||
|
||||
assert.Equal(t, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", res)
|
||||
}
|
||||
|
||||
// TestReadAll tests the ReadAll function which reads response as bytes
|
||||
func TestReadAll(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readBytes := ReadAll(client)
|
||||
|
||||
result := readBytes(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
bytes := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.NotNil(t, bytes, "Expected non-nil bytes")
|
||||
assert.Greater(t, len(bytes), 0, "Expected non-empty byte array")
|
||||
|
||||
// Verify it contains expected JSON content
|
||||
content := string(bytes)
|
||||
assert.Contains(t, content, "userId")
|
||||
assert.Contains(t, content, "title")
|
||||
}
|
||||
|
||||
// TestReadText tests the ReadText function which reads response as string
|
||||
func TestReadText(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readText := ReadText(client)
|
||||
|
||||
result := readText(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
text := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.NotEmpty(t, text, "Expected non-empty text")
|
||||
|
||||
// Verify it contains expected JSON content as text
|
||||
assert.Contains(t, text, "userId")
|
||||
assert.Contains(t, text, "title")
|
||||
assert.Contains(t, text, "sunt aut facere")
|
||||
}
|
||||
|
||||
// TestReadJson tests the deprecated ReadJson function
|
||||
func TestReadJson(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readItem := ReadJson[PostItem](client)
|
||||
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
item := E.GetOrElse(func(error) PostItem { return PostItem{} })(result)
|
||||
assert.Equal(t, uint(1), item.UserID, "Expected UserID to be 1")
|
||||
assert.Equal(t, uint(1), item.Id, "Expected Id to be 1")
|
||||
assert.NotEmpty(t, item.Title, "Expected non-empty title")
|
||||
assert.NotEmpty(t, item.Body, "Expected non-empty body")
|
||||
}
|
||||
|
||||
// TestReadAllWithInvalidURL tests ReadAll with an invalid URL
|
||||
func TestReadAllWithInvalidURL(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
|
||||
readBytes := ReadAll(client)
|
||||
|
||||
result := readBytes(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestReadTextWithInvalidURL tests ReadText with an invalid URL
|
||||
func TestReadTextWithInvalidURL(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
|
||||
readText := ReadText(client)
|
||||
|
||||
result := readText(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestReadJSONWithInvalidURL tests ReadJSON with an invalid URL
|
||||
func TestReadJSONWithInvalidURL(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestReadJSONWithInvalidJSON tests ReadJSON with non-JSON response
|
||||
func TestReadJSONWithInvalidJSON(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
// This URL returns HTML, not JSON
|
||||
request := MakeGetRequest("https://www.google.com")
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
// Should fail because content-type is not application/json
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for non-JSON response")
|
||||
}
|
||||
|
||||
// TestMakeClientWithCustomClient tests MakeClient with a custom http.Client
|
||||
func TestMakeClientWithCustomClient(t *testing.T) {
|
||||
customClient := H.DefaultClient
|
||||
|
||||
client := MakeClient(customClient)
|
||||
assert.NotNil(t, client, "Expected non-nil client")
|
||||
|
||||
// Verify it works
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
}
|
||||
|
||||
// TestReadAllComposition tests composing ReadAll with other operations
|
||||
func TestReadAllComposition(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
|
||||
// Compose ReadAll with a map operation to get byte length
|
||||
readBytes := ReadAll(client)(request)
|
||||
readLength := R.Map(func(bytes []byte) int { return len(bytes) })(readBytes)
|
||||
|
||||
result := readLength(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
length := E.GetOrElse(func(error) int { return 0 })(result)
|
||||
assert.Greater(t, length, 0, "Expected positive byte length")
|
||||
}
|
||||
|
||||
// TestReadTextComposition tests composing ReadText with other operations
|
||||
func TestReadTextComposition(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
|
||||
// Compose ReadText with a map operation to get string length
|
||||
readText := ReadText(client)(request)
|
||||
readLength := R.Map(func(text string) int { return len(text) })(readText)
|
||||
|
||||
result := readLength(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
length := E.GetOrElse(func(error) int { return 0 })(result)
|
||||
assert.Greater(t, length, 0, "Expected positive string length")
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ const (
|
||||
// - e: The Either value to lift into ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither that produces the given Either value.
|
||||
//
|
||||
//go:inline
|
||||
func FromEither[A any](e Either[A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromEither[context.Context](e)
|
||||
}
|
||||
@@ -59,6 +61,8 @@ func Left[A any](l error) ReaderIOEither[A] {
|
||||
// - r: The success value
|
||||
//
|
||||
// Returns a ReaderIOEither that always succeeds with the given value.
|
||||
//
|
||||
//go:inline
|
||||
func Right[A any](r A) ReaderIOEither[A] {
|
||||
return readerioeither.Right[context.Context, error](r)
|
||||
}
|
||||
@@ -71,6 +75,8 @@ func Right[A any](r A) ReaderIOEither[A] {
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a new ReaderIOEither with the transformed value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[A, B any](fa ReaderIOEither[A], f func(A) B) ReaderIOEither[B] {
|
||||
return readerioeither.MonadMap(fa, f)
|
||||
}
|
||||
@@ -82,6 +88,8 @@ func MonadMap[A, B any](fa ReaderIOEither[A], f func(A) B) ReaderIOEither[B] {
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a function that transforms a ReaderIOEither.
|
||||
//
|
||||
//go:inline
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return readerioeither.Map[context.Context, error](f)
|
||||
}
|
||||
@@ -94,6 +102,8 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a new ReaderIOEither with the constant value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[A, B any](fa ReaderIOEither[A], b B) ReaderIOEither[B] {
|
||||
return readerioeither.MonadMapTo(fa, b)
|
||||
}
|
||||
@@ -105,6 +115,8 @@ func MonadMapTo[A, B any](fa ReaderIOEither[A], b B) ReaderIOEither[B] {
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a function that transforms a ReaderIOEither.
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return readerioeither.MapTo[context.Context, error, A](b)
|
||||
}
|
||||
@@ -117,7 +129,9 @@ func MapTo[A, B any](b B) Operator[A, B] {
|
||||
// - f: Function that produces the second ReaderIOEither based on the first's result
|
||||
//
|
||||
// Returns a new ReaderIOEither representing the sequenced computation.
|
||||
func MonadChain[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) ReaderIOEither[B] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[A, B any](ma ReaderIOEither[A], f Kleisli[A, B]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadChain(ma, f)
|
||||
}
|
||||
|
||||
@@ -128,7 +142,9 @@ func MonadChain[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) Rea
|
||||
// - f: Function that produces the second ReaderIOEither based on the first's result
|
||||
//
|
||||
// Returns a function that sequences ReaderIOEither computations.
|
||||
func Chain[A, B any](f func(A) ReaderIOEither[B]) Operator[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return readerioeither.Chain(f)
|
||||
}
|
||||
|
||||
@@ -140,7 +156,9 @@ func Chain[A, B any](f func(A) ReaderIOEither[B]) Operator[A, B] {
|
||||
// - f: Function that produces the second ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither with the result of the first computation.
|
||||
func MonadChainFirst[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) ReaderIOEither[A] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirst[A, B any](ma ReaderIOEither[A], f Kleisli[A, B]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadChainFirst(ma, f)
|
||||
}
|
||||
|
||||
@@ -151,7 +169,9 @@ func MonadChainFirst[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]
|
||||
// - f: Function that produces the second ReaderIOEither
|
||||
//
|
||||
// Returns a function that sequences ReaderIOEither computations.
|
||||
func ChainFirst[A, B any](f func(A) ReaderIOEither[B]) Operator[A, A] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return readerioeither.ChainFirst(f)
|
||||
}
|
||||
|
||||
@@ -162,6 +182,8 @@ func ChainFirst[A, B any](f func(A) ReaderIOEither[B]) Operator[A, A] {
|
||||
// - a: The value to wrap
|
||||
//
|
||||
// Returns a ReaderIOEither that always succeeds with the given value.
|
||||
//
|
||||
//go:inline
|
||||
func Of[A any](a A) ReaderIOEither[A] {
|
||||
return readerioeither.Of[context.Context, error](a)
|
||||
}
|
||||
@@ -240,6 +262,8 @@ func MonadAp[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) Read
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a ReaderIOEither with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApSeq[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadApSeq(fab, fa)
|
||||
}
|
||||
@@ -251,6 +275,8 @@ func MonadApSeq[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) R
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOEither function to the value.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadAp[B, A], fa)
|
||||
}
|
||||
@@ -262,6 +288,8 @@ func Ap[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOEither function to the value sequentially.
|
||||
//
|
||||
//go:inline
|
||||
func ApSeq[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApSeq[B, A], fa)
|
||||
}
|
||||
@@ -273,6 +301,8 @@ func ApSeq[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOEither function to the value in parallel.
|
||||
//
|
||||
//go:inline
|
||||
func ApPar[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApPar[B, A], fa)
|
||||
}
|
||||
@@ -285,7 +315,9 @@ func ApPar[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
// - onFalse: Function to generate an error when predicate fails
|
||||
//
|
||||
// Returns a function that converts a value to ReaderIOEither based on the predicate.
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) ReaderIOEither[A] {
|
||||
//
|
||||
//go:inline
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
|
||||
return readerioeither.FromPredicate[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
@@ -296,7 +328,9 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) Read
|
||||
// - onLeft: Function that produces an alternative ReaderIOEither from the error
|
||||
//
|
||||
// Returns a function that provides fallback behavior for failed computations.
|
||||
func OrElse[A any](onLeft func(error) ReaderIOEither[A]) Operator[A, A] {
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
|
||||
return readerioeither.OrElse[context.Context](onLeft)
|
||||
}
|
||||
|
||||
@@ -304,6 +338,8 @@ func OrElse[A any](onLeft func(error) ReaderIOEither[A]) Operator[A, A] {
|
||||
// This is useful for accessing the [context.Context] within a computation.
|
||||
//
|
||||
// Returns a ReaderIOEither that produces the context.
|
||||
//
|
||||
//go:inline
|
||||
func Ask() ReaderIOEither[context.Context] {
|
||||
return readerioeither.Ask[context.Context, error]()
|
||||
}
|
||||
@@ -316,6 +352,8 @@ func Ask() ReaderIOEither[context.Context] {
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a new ReaderIOEither with the chained computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadChainEitherK[context.Context](ma, f)
|
||||
}
|
||||
@@ -327,7 +365,9 @@ func MonadChainEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) Read
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a function that chains the Either-returning function.
|
||||
func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
|
||||
return readerioeither.ChainEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -339,6 +379,8 @@ func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) Read
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a ReaderIOEither with the original value if both computations succeed.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadChainFirstEitherK[context.Context](ma, f)
|
||||
}
|
||||
@@ -350,7 +392,9 @@ func MonadChainFirstEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B])
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a function that chains the Either-returning function.
|
||||
func ChainFirstEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
|
||||
return readerioeither.ChainFirstEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -361,6 +405,8 @@ func ChainFirstEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A])
|
||||
// - onNone: Function to generate an error when Option is None
|
||||
//
|
||||
// Returns a function that chains Option-returning functions into ReaderIOEither.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
|
||||
return readerioeither.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
@@ -372,6 +418,8 @@ func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operato
|
||||
// - t: The IOEither to convert
|
||||
//
|
||||
// Returns a ReaderIOEither that executes the IOEither.
|
||||
//
|
||||
//go:inline
|
||||
func FromIOEither[A any](t ioeither.IOEither[error, A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromIOEither[context.Context](t)
|
||||
}
|
||||
@@ -383,6 +431,8 @@ func FromIOEither[A any](t ioeither.IOEither[error, A]) ReaderIOEither[A] {
|
||||
// - t: The IO to convert
|
||||
//
|
||||
// Returns a ReaderIOEither that executes the IO and wraps the result in Right.
|
||||
//
|
||||
//go:inline
|
||||
func FromIO[A any](t IO[A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromIO[context.Context, error](t)
|
||||
}
|
||||
@@ -395,6 +445,8 @@ func FromIO[A any](t IO[A]) ReaderIOEither[A] {
|
||||
// - t: The Lazy computation to convert
|
||||
//
|
||||
// Returns a ReaderIOEither that executes the Lazy computation and wraps the result in Right.
|
||||
//
|
||||
//go:inline
|
||||
func FromLazy[A any](t Lazy[A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromIO[context.Context, error](t)
|
||||
}
|
||||
@@ -420,6 +472,8 @@ func Never[A any]() ReaderIOEither[A] {
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a new ReaderIOEither with the chained IO computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadChainIOK(ma, f)
|
||||
}
|
||||
@@ -431,7 +485,9 @@ func MonadChainIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEith
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
func ChainIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
|
||||
return readerioeither.ChainIOK[context.Context, error](f)
|
||||
}
|
||||
|
||||
@@ -443,6 +499,8 @@ func ChainIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEith
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a ReaderIOEither with the original value after executing the IO.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
@@ -454,7 +512,9 @@ func MonadChainFirstIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderI
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
func ChainFirstIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
return readerioeither.ChainFirstIOK[context.Context, error](f)
|
||||
}
|
||||
|
||||
@@ -465,7 +525,9 @@ func ChainFirstIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderI
|
||||
// - f: Function that produces an IOEither
|
||||
//
|
||||
// Returns a function that chains the IOEither-returning function.
|
||||
func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) Operator[A, B] {
|
||||
return readerioeither.ChainIOEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -476,7 +538,7 @@ func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) func(ma Rea
|
||||
// - delay: The duration to wait before executing the computation
|
||||
//
|
||||
// Returns a function that delays a ReaderIOEither computation.
|
||||
func Delay[A any](delay time.Duration) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
func Delay[A any](delay time.Duration) Operator[A, A] {
|
||||
return func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return func(ctx context.Context) IOEither[A] {
|
||||
return func() Either[A] {
|
||||
@@ -517,6 +579,8 @@ func Timer(delay time.Duration) ReaderIOEither[time.Time] {
|
||||
// - gen: Lazy generator function that produces a ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither that generates a fresh computation on each execution.
|
||||
//
|
||||
//go:inline
|
||||
func Defer[A any](gen Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
return readerioeither.Defer(gen)
|
||||
}
|
||||
@@ -528,6 +592,8 @@ func Defer[A any](gen Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
// - f: Function that takes a context and returns a function producing (value, error)
|
||||
//
|
||||
// Returns a ReaderIOEither that wraps the error-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOEither[A] {
|
||||
return readerioeither.TryCatch(f, errors.IdentityError)
|
||||
}
|
||||
@@ -540,6 +606,8 @@ func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOEither[A
|
||||
// - second: Lazy alternative ReaderIOEither to use if first fails
|
||||
//
|
||||
// Returns a ReaderIOEither that tries the first, then the second if first fails.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAlt[A any](first ReaderIOEither[A], second Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadAlt(first, second)
|
||||
}
|
||||
@@ -551,6 +619,8 @@ func MonadAlt[A any](first ReaderIOEither[A], second Lazy[ReaderIOEither[A]]) Re
|
||||
// - second: Lazy alternative ReaderIOEither to use if first fails
|
||||
//
|
||||
// Returns a function that provides fallback behavior.
|
||||
//
|
||||
//go:inline
|
||||
func Alt[A any](second Lazy[ReaderIOEither[A]]) Operator[A, A] {
|
||||
return readerioeither.Alt(second)
|
||||
}
|
||||
@@ -563,6 +633,8 @@ func Alt[A any](second Lazy[ReaderIOEither[A]]) Operator[A, A] {
|
||||
// - rdr: The ReaderIOEither to memoize
|
||||
//
|
||||
// Returns a ReaderIOEither that caches its result after the first execution.
|
||||
//
|
||||
//go:inline
|
||||
func Memoize[A any](rdr ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return readerioeither.Memoize(rdr)
|
||||
}
|
||||
@@ -574,6 +646,8 @@ func Memoize[A any](rdr ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
// - rdr: The nested ReaderIOEither to flatten
|
||||
//
|
||||
// Returns a flattened ReaderIOEither.
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[A any](rdr ReaderIOEither[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
return readerioeither.Flatten(rdr)
|
||||
}
|
||||
@@ -586,6 +660,8 @@ func Flatten[A any](rdr ReaderIOEither[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a ReaderIOEither with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[B, A any](fab ReaderIOEither[func(A) B], a A) ReaderIOEither[B] {
|
||||
return readerioeither.MonadFlap(fab, a)
|
||||
}
|
||||
@@ -597,6 +673,8 @@ func MonadFlap[B, A any](fab ReaderIOEither[func(A) B], a A) ReaderIOEither[B] {
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a function that applies the value to a ReaderIOEither function.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return readerioeither.Flap[context.Context, error, B](a)
|
||||
}
|
||||
@@ -609,7 +687,9 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
// - onRight: Handler for success case
|
||||
//
|
||||
// Returns a function that folds a ReaderIOEither into a new ReaderIOEither.
|
||||
func Fold[A, B any](onLeft func(error) ReaderIOEither[B], onRight func(A) ReaderIOEither[B]) Operator[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] {
|
||||
return readerioeither.Fold(onLeft, onRight)
|
||||
}
|
||||
|
||||
@@ -620,6 +700,8 @@ func Fold[A, B any](onLeft func(error) ReaderIOEither[B], onRight func(A) Reader
|
||||
// - onLeft: Function to provide a default value from the error
|
||||
//
|
||||
// Returns a function that converts a ReaderIOEither to a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOEither[A]) ReaderIO[A] {
|
||||
return readerioeither.GetOrElse(onLeft)
|
||||
}
|
||||
@@ -631,6 +713,8 @@ func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOEither[A]) Re
|
||||
// - onLeft: Function to transform the error
|
||||
//
|
||||
// Returns a function that transforms the error of a ReaderIOEither.
|
||||
//
|
||||
//go:inline
|
||||
func OrLeft[A any](onLeft func(error) ReaderIO[error]) Operator[A, A] {
|
||||
return readerioeither.OrLeft[A](onLeft)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
rightVal := E.Right[error](42)
|
||||
@@ -43,7 +43,7 @@ func TestFromEither(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLeftRight(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test Left
|
||||
err := errors.New("test error")
|
||||
@@ -58,13 +58,13 @@ func TestLeftRight(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
result := Of(42)(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadMap(Right(42), func(x int) int { return x * 2 })(ctx)()
|
||||
@@ -77,7 +77,7 @@ func TestMonadMap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadMapTo(Right(42), "hello")(ctx)()
|
||||
@@ -90,7 +90,7 @@ func TestMonadMapTo(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChain(Right(42), func(x int) ReaderIOEither[int] {
|
||||
@@ -113,7 +113,7 @@ func TestMonadChain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainFirst(Right(42), func(x int) ReaderIOEither[string] {
|
||||
@@ -136,7 +136,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with both Right
|
||||
fct := Right(func(x int) int { return x * 2 })
|
||||
@@ -159,7 +159,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadApPar(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with both Right
|
||||
fct := Right(func(x int) int { return x * 2 })
|
||||
@@ -169,7 +169,7 @@ func TestMonadApPar(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
pred := func(x int) bool { return x > 0 }
|
||||
onFalse := func(x int) error { return fmt.Errorf("value %d is not positive", x) }
|
||||
@@ -184,7 +184,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
ctx := context.WithValue(t.Context(), "key", "value")
|
||||
result := Ask()(ctx)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
retrievedCtx, _ := E.Unwrap(result)
|
||||
@@ -192,7 +192,7 @@ func TestAsk(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainEitherK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainEitherK(Right(42), func(x int) E.Either[error, int] {
|
||||
@@ -208,7 +208,7 @@ func TestMonadChainEitherK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainFirstEitherK(Right(42), func(x int) E.Either[error, string] {
|
||||
@@ -224,7 +224,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChainOptionKFunc(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
onNone := func() error { return errors.New("none error") }
|
||||
|
||||
@@ -243,7 +243,7 @@ func TestChainOptionKFunc(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromIOEither(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
ioe := func() E.Either[error, int] {
|
||||
@@ -262,7 +262,7 @@ func TestFromIOEither(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
io := func() int { return 42 }
|
||||
result := FromIO(io)(ctx)()
|
||||
@@ -270,7 +270,7 @@ func TestFromIO(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromLazy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
lazy := func() int { return 42 }
|
||||
result := FromLazy(lazy)(ctx)()
|
||||
@@ -278,7 +278,7 @@ func TestFromLazy(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNeverWithCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
// Start Never in a goroutine
|
||||
done := make(chan E.Either[error, int])
|
||||
@@ -295,7 +295,7 @@ func TestNeverWithCancel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainIOK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainIOK(Right(42), func(x int) func() int {
|
||||
@@ -305,7 +305,7 @@ func TestMonadChainIOK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainFirstIOK(Right(42), func(x int) func() string {
|
||||
@@ -315,7 +315,7 @@ func TestMonadChainFirstIOK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDelayFunc(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
delay := 100 * time.Millisecond
|
||||
|
||||
start := time.Now()
|
||||
@@ -328,7 +328,7 @@ func TestDelayFunc(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDefer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
count := 0
|
||||
|
||||
gen := func() ReaderIOEither[int] {
|
||||
@@ -348,7 +348,7 @@ func TestDefer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTryCatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test success
|
||||
result := TryCatch(func(ctx context.Context) func() (int, error) {
|
||||
@@ -369,7 +369,7 @@ func TestTryCatch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadAlt(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right (alternative not called)
|
||||
result := MonadAlt(Right(42), func() ReaderIOEither[int] {
|
||||
@@ -386,7 +386,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemoize(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
count := 0
|
||||
|
||||
rdr := Memoize(FromLazy(func() int {
|
||||
@@ -404,7 +404,7 @@ func TestMemoize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
nested := Right(Right(42))
|
||||
result := Flatten(nested)(ctx)()
|
||||
@@ -412,7 +412,7 @@ func TestFlatten(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
fab := Right(func(x int) int { return x * 2 })
|
||||
result := MonadFlap(fab, 42)(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
@@ -420,19 +420,19 @@ func TestMonadFlap(t *testing.T) {
|
||||
|
||||
func TestWithContext(t *testing.T) {
|
||||
// Test with non-canceled context
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
result := WithContext(Right(42))(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test with canceled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
result = WithContext(Right(42))(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with both Right
|
||||
fct := Right(func(x int) int { return x * 2 })
|
||||
@@ -443,7 +443,7 @@ func TestMonadAp(t *testing.T) {
|
||||
|
||||
// Test traverse functions
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with all Right
|
||||
arr := []ReaderIOEither[int]{Right(1), Right(2), Right(3)}
|
||||
@@ -460,7 +460,7 @@ func TestSequenceArray(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTraverseArray(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test transformation
|
||||
arr := []int{1, 2, 3}
|
||||
@@ -473,7 +473,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSequenceRecord(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with all Right
|
||||
rec := map[string]ReaderIOEither[int]{
|
||||
@@ -488,7 +488,7 @@ func TestSequenceRecord(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTraverseRecord(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test transformation
|
||||
rec := map[string]int{"a": 1, "b": 2}
|
||||
@@ -503,7 +503,7 @@ func TestTraverseRecord(t *testing.T) {
|
||||
|
||||
// Test monoid functions
|
||||
func TestAltSemigroup(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
sg := AltSemigroup[int]()
|
||||
|
||||
@@ -519,7 +519,7 @@ func TestAltSemigroup(t *testing.T) {
|
||||
|
||||
// Test Do notation
|
||||
func TestDo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
type State struct {
|
||||
Value int
|
||||
|
||||
@@ -55,7 +55,7 @@ import (
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
func WithResource[A, R, ANY any](onCreate ReaderIOEither[R], onRelease func(R) ReaderIOEither[ANY]) func(func(R) ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
func WithResource[A, R, ANY any](onCreate ReaderIOEither[R], onRelease func(R) ReaderIOEither[ANY]) Kleisli[Kleisli[R, A], A] {
|
||||
return function.Flow2(
|
||||
function.Bind2nd(function.Flow2[func(R) ReaderIOEither[A], Operator[A, A], R, ReaderIOEither[A], ReaderIOEither[A]], WithContext[A]),
|
||||
RIE.WithResource[A, context.Context, error, R](WithContext(onCreate), onRelease),
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArray[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -45,7 +45,7 @@ func TraverseArray[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEith
|
||||
// - f: Function that transforms each element with its index into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderIOEither[B]) Kleisli[[]A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -72,7 +72,7 @@ func SequenceArray[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
|
||||
// - f: Function that transforms each value into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms a map into a ReaderIOEither of a map.
|
||||
func TraverseRecord[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -89,7 +89,7 @@ func TraverseRecord[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(ma
|
||||
// - f: Function that transforms each key-value pair into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms a map into a ReaderIOEither of a map.
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -117,7 +117,7 @@ func SequenceRecord[K comparable, A any](ma map[K]ReaderIOEither[A]) ReaderIOEit
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither containing an array of transformed values.
|
||||
func MonadTraverseArraySeq[A, B any](as []A, f func(A) ReaderIOEither[B]) ReaderIOEither[[]B] {
|
||||
func MonadTraverseArraySeq[A, B any](as []A, f Kleisli[A, B]) ReaderIOEither[[]B] {
|
||||
return array.MonadTraverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -134,7 +134,7 @@ func MonadTraverseArraySeq[A, B any](as []A, f func(A) ReaderIOEither[B]) Reader
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArraySeq[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -145,7 +145,7 @@ func TraverseArraySeq[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOE
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndexSeq uses transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]]
|
||||
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) ReaderIOEither[B]) Kleisli[[]A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -167,7 +167,7 @@ func SequenceArraySeq[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
|
||||
}
|
||||
|
||||
// MonadTraverseRecordSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f func(A) ReaderIOEither[B]) ReaderIOEither[map[K]B] {
|
||||
func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f Kleisli[A, B]) ReaderIOEither[map[K]B] {
|
||||
return record.MonadTraverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -178,7 +178,7 @@ func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f func(A) Reader
|
||||
}
|
||||
|
||||
// TraverseRecordSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordSeq[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -189,7 +189,7 @@ func TraverseRecordSeq[K comparable, A, B any](f func(A) ReaderIOEither[B]) func
|
||||
}
|
||||
|
||||
// TraverseRecordWithIndexSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordWithIndexSeq[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecordWithIndexSeq[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -212,7 +212,7 @@ func SequenceRecordSeq[K comparable, A any](ma map[K]ReaderIOEither[A]) ReaderIO
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither containing an array of transformed values.
|
||||
func MonadTraverseArrayPar[A, B any](as []A, f func(A) ReaderIOEither[B]) ReaderIOEither[[]B] {
|
||||
func MonadTraverseArrayPar[A, B any](as []A, f Kleisli[A, B]) ReaderIOEither[[]B] {
|
||||
return array.MonadTraverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -229,7 +229,7 @@ func MonadTraverseArrayPar[A, B any](as []A, f func(A) ReaderIOEither[B]) Reader
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArrayPar[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -240,7 +240,7 @@ func TraverseArrayPar[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOE
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndexPar uses transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]]
|
||||
func TraverseArrayWithIndexPar[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArrayWithIndexPar[A, B any](f func(int, A) ReaderIOEither[B]) Kleisli[[]A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -262,7 +262,7 @@ func SequenceArrayPar[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
|
||||
}
|
||||
|
||||
// TraverseRecordPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordPar[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecordPar[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -273,7 +273,7 @@ func TraverseRecordPar[K comparable, A, B any](f func(A) ReaderIOEither[B]) func
|
||||
}
|
||||
|
||||
// TraverseRecordWithIndexPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -284,7 +284,7 @@ func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) ReaderIOEit
|
||||
}
|
||||
|
||||
// MonadTraverseRecordPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f func(A) ReaderIOEither[B]) ReaderIOEither[map[K]B] {
|
||||
func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f Kleisli[A, B]) ReaderIOEither[map[K]B] {
|
||||
return record.MonadTraverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
|
||||
@@ -99,10 +99,12 @@ type (
|
||||
// result := fetchUser("123")(ctx)()
|
||||
ReaderIOEither[A any] = readerioeither.ReaderIOEither[context.Context, error, A]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderIOEither[B]]
|
||||
|
||||
// Operator represents a transformation from one ReaderIOEither to another.
|
||||
// This is useful for point-free style composition and building reusable transformations.
|
||||
//
|
||||
// Operator[A, B] is equivalent to func(ReaderIOEither[A]) ReaderIOEither[B]
|
||||
// Operator[A, B] is equivalent to Kleisli[ReaderIOEither[A], B]
|
||||
//
|
||||
// Example usage:
|
||||
// // Define a reusable transformation
|
||||
@@ -110,5 +112,5 @@ type (
|
||||
//
|
||||
// // Apply the transformation
|
||||
// result := toUpper(computation)
|
||||
Operator[A, B any] = Reader[ReaderIOEither[A], ReaderIOEither[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderIOEither[A], B]
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package either
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
@@ -171,3 +172,204 @@ func ApS[E, S1, S2, T any](
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions and enables working with
|
||||
// nested fields in a type-safe manner.
|
||||
//
|
||||
// Unlike BindL, ApSL uses applicative semantics, meaning the computation fa is independent
|
||||
// of the current state and can be evaluated concurrently.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - fa: An Either[E, T] computation that produces the value to set
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that updates the focused field in the Either context
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(p Person) int { return p.Age },
|
||||
// func(p Person, a int) Person { p.Age = a; return p },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// either.Right[error](Person{Name: "Alice", Age: 25}),
|
||||
// either.ApSL(ageLens, either.Right[error](30)),
|
||||
// ) // Right(Person{Name: "Alice", Age: 30})
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[E, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Either[E, T],
|
||||
) Endomorphism[Either[E, S]] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// an Either that produces the new value.
|
||||
//
|
||||
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
|
||||
// the current value of the focused field.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - f: A function that takes the current field value and returns an Either[E, T]
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that updates the focused field based on its current value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but fail if it would exceed 100
|
||||
// increment := func(v int) either.Either[error, int] {
|
||||
// if v >= 100 {
|
||||
// return either.Left[int](errors.New("counter overflow"))
|
||||
// }
|
||||
// return either.Right[error](v + 1)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// either.Right[error](Counter{Value: 42}),
|
||||
// either.BindL(valueLens, increment),
|
||||
// ) // Right(Counter{Value: 43})
|
||||
//
|
||||
//go:inline
|
||||
func BindL[E, S, T any](
|
||||
lens Lens[S, T],
|
||||
f func(T) Either[E, T],
|
||||
) Endomorphism[Either[E, S]] {
|
||||
return Bind[E, S, S, T](lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in Either).
|
||||
//
|
||||
// This is useful for pure transformations that cannot fail, such as mathematical operations,
|
||||
// string manipulations, or other deterministic updates.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - f: An endomorphism (T → T) that transforms the current field value
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that updates the focused field with the transformed value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// either.Right[error](Counter{Value: 21}),
|
||||
// either.LetL(valueLens, double),
|
||||
// ) // Right(Counter{Value: 42})
|
||||
//
|
||||
//go:inline
|
||||
func LetL[E, S, T any](
|
||||
lens Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Endomorphism[Either[E, S]] {
|
||||
return Let[E, S, S, T](lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// This is useful for resetting fields, initializing values, or setting fields to
|
||||
// predetermined constants.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - b: The constant value to set the field to
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that sets the focused field to the constant value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// either.Right[error](Config{Debug: true, Timeout: 30}),
|
||||
// either.LetToL(debugLens, false),
|
||||
// ) // Right(Config{Debug: false, Timeout: 30})
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[E, S, T any](
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Endomorphism[Either[E, S]] {
|
||||
return LetTo[E, S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -54,3 +55,307 @@ func TestApS(t *testing.T) {
|
||||
|
||||
assert.Equal(t, res, Of[error]("John Doe"))
|
||||
}
|
||||
|
||||
// Test types for lens-based operations
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Timeout int
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
// Create a lens for the Age field
|
||||
ageLens := L.MakeLens(
|
||||
func(p Person) int { return p.Age },
|
||||
func(p Person, a int) Person { p.Age = a; return p },
|
||||
)
|
||||
|
||||
t.Run("ApSL with Right value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Right[error](Person{Name: "Alice", Age: 25}),
|
||||
ApSL(ageLens, Right[error](30)),
|
||||
)
|
||||
|
||||
expected := Right[error](Person{Name: "Alice", Age: 30})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("ApSL with Left in context", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Left[Person](assert.AnError),
|
||||
ApSL(ageLens, Right[error](30)),
|
||||
)
|
||||
|
||||
expected := Left[Person](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("ApSL with Left in value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Right[error](Person{Name: "Alice", Age: 25}),
|
||||
ApSL(ageLens, Left[int](assert.AnError)),
|
||||
)
|
||||
|
||||
expected := Left[Person](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("ApSL with both Left", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Left[Person](assert.AnError),
|
||||
ApSL(ageLens, Left[int](assert.AnError)),
|
||||
)
|
||||
|
||||
expected := Left[Person](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
// Create a lens for the Value field
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("BindL with successful transformation", func(t *testing.T) {
|
||||
// Increment the counter, but fail if it would exceed 100
|
||||
increment := func(v int) Either[error, int] {
|
||||
if v >= 100 {
|
||||
return Left[int](assert.AnError)
|
||||
}
|
||||
return Right[error](v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](Counter{Value: 42}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 43})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("BindL with failing transformation", func(t *testing.T) {
|
||||
increment := func(v int) Either[error, int] {
|
||||
if v >= 100 {
|
||||
return Left[int](assert.AnError)
|
||||
}
|
||||
return Right[error](v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](Counter{Value: 100}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
expected := Left[Counter](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("BindL with Left input", func(t *testing.T) {
|
||||
increment := func(v int) Either[error, int] {
|
||||
return Right[error](v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[Counter](assert.AnError),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
expected := Left[Counter](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("BindL with multiple operations", func(t *testing.T) {
|
||||
double := func(v int) Either[error, int] {
|
||||
return Right[error](v * 2)
|
||||
}
|
||||
|
||||
addTen := func(v int) Either[error, int] {
|
||||
return Right[error](v + 10)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 5}),
|
||||
BindL(valueLens, double),
|
||||
BindL(valueLens, addTen),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 20})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
// Create a lens for the Value field
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("LetL with pure transformation", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](Counter{Value: 21}),
|
||||
LetL[error](valueLens, double),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 42})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetL with Left input", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[Counter](assert.AnError),
|
||||
LetL[error](valueLens, double),
|
||||
)
|
||||
|
||||
expected := Left[Counter](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetL with multiple transformations", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
addTen := func(v int) int { return v + 10 }
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 5}),
|
||||
LetL[error](valueLens, double),
|
||||
LetL[error](valueLens, addTen),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 20})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetL with identity transformation", func(t *testing.T) {
|
||||
identity := func(v int) int { return v }
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](Counter{Value: 42}),
|
||||
LetL[error](valueLens, identity),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 42})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
// Create a lens for the Debug field
|
||||
debugLens := L.MakeLens(
|
||||
func(c Config) bool { return c.Debug },
|
||||
func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
)
|
||||
|
||||
t.Run("LetToL with constant value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Right[error](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[error](debugLens, false),
|
||||
)
|
||||
|
||||
expected := Right[error](Config{Debug: false, Timeout: 30})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetToL with Left input", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Left[Config](assert.AnError),
|
||||
LetToL[error](debugLens, false),
|
||||
)
|
||||
|
||||
expected := Left[Config](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetToL with multiple fields", func(t *testing.T) {
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) int { return c.Timeout },
|
||||
func(c Config, t int) Config { c.Timeout = t; return c },
|
||||
)
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[error](debugLens, false),
|
||||
LetToL[error](timeoutLens, 60),
|
||||
)
|
||||
|
||||
expected := Right[error](Config{Debug: false, Timeout: 60})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetToL setting same value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Right[error](Config{Debug: false, Timeout: 30}),
|
||||
LetToL[error](debugLens, false),
|
||||
)
|
||||
|
||||
expected := Right[error](Config{Debug: false, Timeout: 30})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLensOperationsCombined(t *testing.T) {
|
||||
// Test combining different lens operations
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("Combine LetToL and LetL", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 100}),
|
||||
LetToL[error](valueLens, 10),
|
||||
LetL[error](valueLens, double),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 20})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Combine LetL and BindL", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
validate := func(v int) Either[error, int] {
|
||||
if v > 100 {
|
||||
return Left[int](assert.AnError)
|
||||
}
|
||||
return Right[error](v)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 25}),
|
||||
LetL[error](valueLens, double),
|
||||
BindL(valueLens, validate),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 50})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Combine ApSL and LetL", func(t *testing.T) {
|
||||
addFive := func(v int) int { return v + 5 }
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 10}),
|
||||
ApSL(valueLens, Right[error](20)),
|
||||
LetL[error](valueLens, addFive),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 25})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func MonadAp[B, E, A any](fab Either[E, func(a A) B], fa Either[E, A]) Either[E,
|
||||
|
||||
// Ap is the curried version of [MonadAp].
|
||||
// Returns a function that applies a wrapped function to the given wrapped value.
|
||||
func Ap[B, E, A any](fa Either[E, A]) func(fab Either[E, func(a A) B]) Either[E, B] {
|
||||
func Ap[B, E, A any](fa Either[E, A]) Operator[E, func(A) B, B] {
|
||||
return F.Bind2nd(MonadAp[B, E, A], fa)
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func MonadMapTo[E, A, B any](fa Either[E, A], b B) Either[E, B] {
|
||||
}
|
||||
|
||||
// MapTo is the curried version of [MonadMapTo].
|
||||
func MapTo[E, A, B any](b B) func(Either[E, A]) Either[E, B] {
|
||||
func MapTo[E, A, B any](b B) Operator[E, A, B] {
|
||||
return Map[E](F.Constant1[A](b))
|
||||
}
|
||||
|
||||
@@ -211,26 +211,26 @@ func MonadChainOptionK[A, B, E any](onNone func() E, ma Either[E, A], f func(A)
|
||||
}
|
||||
|
||||
// ChainOptionK is the curried version of [MonadChainOptionK].
|
||||
func ChainOptionK[A, B, E any](onNone func() E) func(func(A) Option[B]) func(Either[E, A]) Either[E, B] {
|
||||
func ChainOptionK[A, B, E any](onNone func() E) func(func(A) Option[B]) Operator[E, A, B] {
|
||||
from := FromOption[B](onNone)
|
||||
return func(f func(A) Option[B]) func(Either[E, A]) Either[E, B] {
|
||||
return func(f func(A) Option[B]) Operator[E, A, B] {
|
||||
return Chain(F.Flow2(f, from))
|
||||
}
|
||||
}
|
||||
|
||||
// ChainTo is the curried version of [MonadChainTo].
|
||||
func ChainTo[A, E, B any](mb Either[E, B]) func(Either[E, A]) Either[E, B] {
|
||||
func ChainTo[A, E, B any](mb Either[E, B]) Operator[E, A, B] {
|
||||
return F.Constant1[Either[E, A]](mb)
|
||||
}
|
||||
|
||||
// Chain is the curried version of [MonadChain].
|
||||
// Sequences two computations where the second depends on the first.
|
||||
func Chain[E, A, B any](f func(a A) Either[E, B]) func(Either[E, A]) Either[E, B] {
|
||||
func Chain[E, A, B any](f func(a A) Either[E, B]) Operator[E, A, B] {
|
||||
return Fold(Left[B, E], f)
|
||||
}
|
||||
|
||||
// ChainFirst is the curried version of [MonadChainFirst].
|
||||
func ChainFirst[E, A, B any](f func(a A) Either[E, B]) func(Either[E, A]) Either[E, A] {
|
||||
func ChainFirst[E, A, B any](f func(a A) Either[E, B]) Operator[E, A, A] {
|
||||
return C.ChainFirst(
|
||||
Chain[E, A, A],
|
||||
Map[E, B, A],
|
||||
@@ -437,7 +437,7 @@ func AltW[E, E1, A any](that L.Lazy[Either[E1, A]]) func(Either[E, A]) Either[E1
|
||||
// return either.Right[error](99)
|
||||
// })
|
||||
// result := alternative(either.Left[int](errors.New("fail"))) // Right(99)
|
||||
func Alt[E, A any](that L.Lazy[Either[E, A]]) func(Either[E, A]) Either[E, A] {
|
||||
func Alt[E, A any](that L.Lazy[Either[E, A]]) Operator[E, A, A] {
|
||||
return AltW[E](that)
|
||||
}
|
||||
|
||||
@@ -449,7 +449,7 @@ func Alt[E, A any](that L.Lazy[Either[E, A]]) func(Either[E, A]) Either[E, A] {
|
||||
// return either.Right[error](0) // default value
|
||||
// })
|
||||
// result := recover(either.Left[int](errors.New("fail"))) // Right(0)
|
||||
func OrElse[E, A any](onLeft func(e E) Either[E, A]) func(Either[E, A]) Either[E, A] {
|
||||
func OrElse[E, A any](onLeft func(e E) Either[E, A]) Operator[E, A, A] {
|
||||
return Fold(onLeft, Of[E, A])
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ func MonadFlap[E, B, A any](fab Either[E, func(A) B], a A) Either[E, B] {
|
||||
}
|
||||
|
||||
// Flap is the curried version of [MonadFlap].
|
||||
func Flap[E, B, A any](a A) func(Either[E, func(A) B]) Either[E, B] {
|
||||
func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
|
||||
return FC.Flap(Map[E, func(A) B, B], a)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
type eitherFunctor[E, A, B any] struct{}
|
||||
|
||||
func (o *eitherFunctor[E, A, B]) Map(f func(A) B) func(Either[E, A]) Either[E, B] {
|
||||
func (o *eitherFunctor[E, A, B]) Map(f func(A) B) Operator[E, A, B] {
|
||||
return Map[E, A, B](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
L "github.com/IBM/fp-go/v2/logging"
|
||||
)
|
||||
|
||||
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) func(Either[E, A]) Either[E, A] {
|
||||
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[E, A, A] {
|
||||
return Fold(
|
||||
func(e E) Either[E, A] {
|
||||
left("%s: %v", prefix, e)
|
||||
@@ -50,9 +50,9 @@ func _log[E, A any](left func(string, ...any), right func(string, ...any), prefi
|
||||
// )
|
||||
// // Logs: "Processing: 42"
|
||||
// // result is Right(84)
|
||||
func Logger[E, A any](loggers ...*log.Logger) func(string) func(Either[E, A]) Either[E, A] {
|
||||
func Logger[E, A any](loggers ...*log.Logger) func(string) Operator[E, A, A] {
|
||||
left, right := L.LoggingCallbacks(loggers...)
|
||||
return func(prefix string) func(Either[E, A]) Either[E, A] {
|
||||
return func(prefix string) Operator[E, A, A] {
|
||||
delegate := _log[E, A](left, right, prefix)
|
||||
return func(ma Either[E, A]) Either[E, A] {
|
||||
return F.Pipe1(
|
||||
|
||||
@@ -25,15 +25,15 @@ func (o *eitherMonad[E, A, B]) Of(a A) Either[E, A] {
|
||||
return Of[E, A](a)
|
||||
}
|
||||
|
||||
func (o *eitherMonad[E, A, B]) Map(f func(A) B) func(Either[E, A]) Either[E, B] {
|
||||
func (o *eitherMonad[E, A, B]) Map(f func(A) B) Operator[E, A, B] {
|
||||
return Map[E, A, B](f)
|
||||
}
|
||||
|
||||
func (o *eitherMonad[E, A, B]) Chain(f func(A) Either[E, B]) func(Either[E, A]) Either[E, B] {
|
||||
func (o *eitherMonad[E, A, B]) Chain(f func(A) Either[E, B]) Operator[E, A, B] {
|
||||
return Chain[E, A, B](f)
|
||||
}
|
||||
|
||||
func (o *eitherMonad[E, A, B]) Ap(fa Either[E, A]) func(Either[E, func(A) B]) Either[E, B] {
|
||||
func (o *eitherMonad[E, A, B]) Ap(fa Either[E, A]) Operator[E, func(A) B, B] {
|
||||
return Ap[B, E, A](fa)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
// m := either.AlternativeMonoid[error](intAdd)
|
||||
// result := m.Concat(either.Right[error](1), either.Right[error](2))
|
||||
// // result is Right(3)
|
||||
func AlternativeMonoid[E, A any](m M.Monoid[A]) M.Monoid[Either[E, A]] {
|
||||
func AlternativeMonoid[E, A any](m M.Monoid[A]) Monoid[E, A] {
|
||||
return M.AlternativeMonoid(
|
||||
Of[E, A],
|
||||
MonadMap[E, A, func(A) A],
|
||||
@@ -51,7 +51,7 @@ func AlternativeMonoid[E, A any](m M.Monoid[A]) M.Monoid[Either[E, A]] {
|
||||
// m := either.AltMonoid[error, int](zero)
|
||||
// result := m.Concat(either.Left[int](errors.New("err1")), either.Right[error](42))
|
||||
// // result is Right(42)
|
||||
func AltMonoid[E, A any](zero L.Lazy[Either[E, A]]) M.Monoid[Either[E, A]] {
|
||||
func AltMonoid[E, A any](zero L.Lazy[Either[E, A]]) Monoid[E, A] {
|
||||
return M.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[E, A],
|
||||
|
||||
@@ -15,10 +15,22 @@
|
||||
|
||||
package either
|
||||
|
||||
import "github.com/IBM/fp-go/v2/option"
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"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/reader"
|
||||
)
|
||||
|
||||
// Option is a type alias for option.Option, provided for convenience
|
||||
// when working with Either and Option together.
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Option[A any] = option.Option[A]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Endomorphism[T any] = endomorphism.Endomorphism[T]
|
||||
|
||||
Kleisli[E, A, B any] = reader.Reader[A, Either[E, B]]
|
||||
Operator[E, A, B any] = Kleisli[E, Either[E, A], B]
|
||||
Monoid[E, A any] = monoid.Monoid[Either[E, A]]
|
||||
)
|
||||
|
||||
@@ -13,6 +13,62 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package builder provides a functional, immutable HTTP request builder with composable operations.
|
||||
// It follows functional programming principles to construct HTTP requests in a type-safe,
|
||||
// testable, and maintainable way.
|
||||
//
|
||||
// The Builder type is immutable - all operations return a new builder instance rather than
|
||||
// modifying the existing one. This ensures thread-safety and makes the code easier to reason about.
|
||||
//
|
||||
// Key Features:
|
||||
// - Immutable builder pattern with method chaining
|
||||
// - Lens-based access to builder properties
|
||||
// - Support for headers, query parameters, request body, and HTTP methods
|
||||
// - JSON and form data encoding
|
||||
// - URL construction with query parameter merging
|
||||
// - Hash generation for caching
|
||||
// - Bearer token authentication helpers
|
||||
//
|
||||
// Basic Usage:
|
||||
//
|
||||
// import (
|
||||
// B "github.com/IBM/fp-go/v2/http/builder"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// )
|
||||
//
|
||||
// // Build a simple GET request
|
||||
// builder := F.Pipe2(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithHeader("Accept")("application/json"),
|
||||
// )
|
||||
//
|
||||
// // Build a POST request with JSON body
|
||||
// builder := F.Pipe3(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("POST"),
|
||||
// B.WithJSON(map[string]string{"name": "John"}),
|
||||
// )
|
||||
//
|
||||
// // Build a request with query parameters
|
||||
// builder := F.Pipe3(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/search"),
|
||||
// B.WithQueryArg("q")("golang"),
|
||||
// B.WithQueryArg("limit")("10"),
|
||||
// )
|
||||
//
|
||||
// The package provides several convenience functions for common HTTP methods:
|
||||
// - WithGet, WithPost, WithPut, WithDelete for setting HTTP methods
|
||||
// - WithBearer for adding Bearer token authentication
|
||||
// - WithJSON for JSON payloads
|
||||
// - WithFormData for form-encoded payloads
|
||||
//
|
||||
// Lenses are provided for advanced use cases:
|
||||
// - URL, Method, Body, Headers, Query for accessing builder properties
|
||||
// - Header(name) for accessing individual headers
|
||||
// - QueryArg(name) for accessing individual query parameters
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -17,8 +17,11 @@ package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
C "github.com/IBM/fp-go/v2/http/content"
|
||||
FD "github.com/IBM/fp-go/v2/http/form"
|
||||
@@ -91,3 +94,351 @@ func TestHash(t *testing.T) {
|
||||
|
||||
fmt.Println(MakeHash(b1))
|
||||
}
|
||||
|
||||
// TestGetTargetURL tests URL construction with query parameters
|
||||
func TestGetTargetURL(t *testing.T) {
|
||||
builder := F.Pipe3(
|
||||
Default,
|
||||
WithURL("http://www.example.com?existing=param"),
|
||||
WithQueryArg("limit")("10"),
|
||||
WithQueryArg("offset")("20"),
|
||||
)
|
||||
|
||||
result := builder.GetTargetURL()
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
url := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Contains(t, url, "limit=10")
|
||||
assert.Contains(t, url, "offset=20")
|
||||
assert.Contains(t, url, "existing=param")
|
||||
}
|
||||
|
||||
// TestGetTargetURLWithInvalidURL tests error handling for invalid URLs
|
||||
func TestGetTargetURLWithInvalidURL(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithURL("://invalid-url"),
|
||||
)
|
||||
|
||||
result := builder.GetTargetURL()
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestGetTargetUrl tests the deprecated GetTargetUrl function
|
||||
func TestGetTargetUrl(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
Default,
|
||||
WithURL("http://www.example.com"),
|
||||
WithQueryArg("test")("value"),
|
||||
)
|
||||
|
||||
result := builder.GetTargetUrl()
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
url := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Contains(t, url, "test=value")
|
||||
}
|
||||
|
||||
// TestSetMethod tests the SetMethod function
|
||||
func TestSetMethod(t *testing.T) {
|
||||
builder := Default.SetMethod("POST")
|
||||
|
||||
assert.Equal(t, "POST", builder.GetMethod())
|
||||
}
|
||||
|
||||
// TestSetQuery tests the SetQuery function
|
||||
func TestSetQuery(t *testing.T) {
|
||||
query := make(url.Values)
|
||||
query.Set("key1", "value1")
|
||||
query.Set("key2", "value2")
|
||||
|
||||
builder := Default.SetQuery(query)
|
||||
|
||||
assert.Equal(t, "value1", builder.GetQuery().Get("key1"))
|
||||
assert.Equal(t, "value2", builder.GetQuery().Get("key2"))
|
||||
}
|
||||
|
||||
// TestSetHeaders tests the SetHeaders function
|
||||
func TestSetHeaders(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("X-Custom-Header", "custom-value")
|
||||
headers.Set("Authorization", "Bearer token")
|
||||
|
||||
builder := Default.SetHeaders(headers)
|
||||
|
||||
assert.Equal(t, "custom-value", builder.GetHeaders().Get("X-Custom-Header"))
|
||||
assert.Equal(t, "Bearer token", builder.GetHeaders().Get("Authorization"))
|
||||
}
|
||||
|
||||
// TestGetHeaderValues tests the GetHeaderValues function
|
||||
func TestGetHeaderValues(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
Default,
|
||||
WithHeader("Accept")("application/json"),
|
||||
WithHeader("Accept")("text/html"),
|
||||
)
|
||||
|
||||
values := builder.GetHeaderValues("Accept")
|
||||
assert.Contains(t, values, "text/html")
|
||||
}
|
||||
|
||||
// TestGetUrl tests the deprecated GetUrl function
|
||||
func TestGetUrl(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithURL("http://www.example.com"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "http://www.example.com", builder.GetUrl())
|
||||
}
|
||||
|
||||
// TestSetUrl tests the deprecated SetUrl function
|
||||
func TestSetUrl(t *testing.T) {
|
||||
builder := Default.SetUrl("http://www.example.com")
|
||||
|
||||
assert.Equal(t, "http://www.example.com", builder.GetURL())
|
||||
}
|
||||
|
||||
// TestWithJson tests the deprecated WithJson function
|
||||
func TestWithJson(t *testing.T) {
|
||||
data := map[string]string{"key": "value"}
|
||||
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithJson(data),
|
||||
)
|
||||
|
||||
contentType := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.ContentType))
|
||||
assert.Equal(t, C.JSON, contentType)
|
||||
assert.True(t, O.IsSome(builder.GetBody()))
|
||||
}
|
||||
|
||||
// TestQueryArg tests the QueryArg lens
|
||||
func TestQueryArg(t *testing.T) {
|
||||
lens := QueryArg("test")
|
||||
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
lens.Set(O.Some("value")),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Some("value"), lens.Get(builder))
|
||||
assert.Equal(t, "value", builder.GetQuery().Get("test"))
|
||||
}
|
||||
|
||||
// TestWithQueryArg tests the WithQueryArg function
|
||||
func TestWithQueryArg(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
Default,
|
||||
WithQueryArg("param1")("value1"),
|
||||
WithQueryArg("param2")("value2"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "value1", builder.GetQuery().Get("param1"))
|
||||
assert.Equal(t, "value2", builder.GetQuery().Get("param2"))
|
||||
}
|
||||
|
||||
// TestWithoutQueryArg tests the WithoutQueryArg function
|
||||
func TestWithoutQueryArg(t *testing.T) {
|
||||
builder := F.Pipe3(
|
||||
Default,
|
||||
WithQueryArg("param1")("value1"),
|
||||
WithQueryArg("param2")("value2"),
|
||||
WithoutQueryArg("param1"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "", builder.GetQuery().Get("param1"))
|
||||
assert.Equal(t, "value2", builder.GetQuery().Get("param2"))
|
||||
}
|
||||
|
||||
// TestGetHash tests the GetHash method
|
||||
func TestGetHash(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
Default,
|
||||
WithURL("http://www.example.com"),
|
||||
WithMethod("POST"),
|
||||
)
|
||||
|
||||
hash := builder.GetHash()
|
||||
assert.NotEmpty(t, hash)
|
||||
assert.Equal(t, MakeHash(builder), hash)
|
||||
}
|
||||
|
||||
// TestWithBytes tests the WithBytes function
|
||||
func TestWithBytes(t *testing.T) {
|
||||
data := []byte("test data")
|
||||
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithBytes(data),
|
||||
)
|
||||
|
||||
body := builder.GetBody()
|
||||
assert.True(t, O.IsSome(body))
|
||||
}
|
||||
|
||||
// TestWithoutBody tests the WithoutBody function
|
||||
func TestWithoutBody(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
Default,
|
||||
WithBytes([]byte("data")),
|
||||
WithoutBody,
|
||||
)
|
||||
|
||||
assert.True(t, O.IsNone(builder.GetBody()))
|
||||
}
|
||||
|
||||
// TestWithGet tests the WithGet convenience function
|
||||
func TestWithGet(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithGet,
|
||||
)
|
||||
|
||||
assert.Equal(t, "GET", builder.GetMethod())
|
||||
}
|
||||
|
||||
// TestWithPost tests the WithPost convenience function
|
||||
func TestWithPost(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithPost,
|
||||
)
|
||||
|
||||
assert.Equal(t, "POST", builder.GetMethod())
|
||||
}
|
||||
|
||||
// TestWithPut tests the WithPut convenience function
|
||||
func TestWithPut(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithPut,
|
||||
)
|
||||
|
||||
assert.Equal(t, "PUT", builder.GetMethod())
|
||||
}
|
||||
|
||||
// TestWithDelete tests the WithDelete convenience function
|
||||
func TestWithDelete(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithDelete,
|
||||
)
|
||||
|
||||
assert.Equal(t, "DELETE", builder.GetMethod())
|
||||
}
|
||||
|
||||
// TestWithBearer tests the WithBearer function
|
||||
func TestWithBearer(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithBearer("my-token"),
|
||||
)
|
||||
|
||||
auth := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.Authorization))
|
||||
assert.Equal(t, "Bearer my-token", auth)
|
||||
}
|
||||
|
||||
// TestWithContentType tests the WithContentType function
|
||||
func TestWithContentType(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithContentType(C.TextPlain),
|
||||
)
|
||||
|
||||
contentType := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.ContentType))
|
||||
assert.Equal(t, C.TextPlain, contentType)
|
||||
}
|
||||
|
||||
// TestWithAuthorization tests the WithAuthorization function
|
||||
func TestWithAuthorization(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithAuthorization("Basic abc123"),
|
||||
)
|
||||
|
||||
auth := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.Authorization))
|
||||
assert.Equal(t, "Basic abc123", auth)
|
||||
}
|
||||
|
||||
// TestBuilderChaining tests that builder operations can be chained
|
||||
func TestBuilderChaining(t *testing.T) {
|
||||
builder := F.Pipe3(
|
||||
Default,
|
||||
WithURL("http://www.example.com"),
|
||||
WithMethod("POST"),
|
||||
WithHeader("X-Test")("test-value"),
|
||||
)
|
||||
|
||||
// Verify all operations were applied
|
||||
assert.Equal(t, "http://www.example.com", builder.GetURL())
|
||||
assert.Equal(t, "POST", builder.GetMethod())
|
||||
|
||||
testHeader := O.GetOrElse(F.Constant(""))(builder.GetHeader("X-Test"))
|
||||
assert.Equal(t, "test-value", testHeader)
|
||||
}
|
||||
|
||||
// TestWithQuery tests the WithQuery function
|
||||
func TestWithQuery(t *testing.T) {
|
||||
query := make(url.Values)
|
||||
query.Set("key1", "value1")
|
||||
query.Set("key2", "value2")
|
||||
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithQuery(query),
|
||||
)
|
||||
|
||||
assert.Equal(t, "value1", builder.GetQuery().Get("key1"))
|
||||
assert.Equal(t, "value2", builder.GetQuery().Get("key2"))
|
||||
}
|
||||
|
||||
// TestWithHeaders tests the WithHeaders function
|
||||
func TestWithHeaders(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("X-Test", "test-value")
|
||||
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithHeaders(headers),
|
||||
)
|
||||
|
||||
assert.Equal(t, "test-value", builder.GetHeaders().Get("X-Test"))
|
||||
}
|
||||
|
||||
// TestWithUrl tests the deprecated WithUrl function
|
||||
func TestWithUrl(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithUrl("http://www.example.com"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "http://www.example.com", builder.GetURL())
|
||||
}
|
||||
|
||||
// TestComplexBuilderComposition tests a complex builder composition
|
||||
func TestComplexBuilderComposition(t *testing.T) {
|
||||
builder := F.Pipe5(
|
||||
Default,
|
||||
WithURL("http://api.example.com/users"),
|
||||
WithPost,
|
||||
WithJSON(map[string]interface{}{
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
}),
|
||||
WithBearer("secret-token"),
|
||||
WithQueryArg("notify")("true"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "http://api.example.com/users", builder.GetURL())
|
||||
assert.Equal(t, "POST", builder.GetMethod())
|
||||
|
||||
contentType := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.ContentType))
|
||||
assert.Equal(t, C.JSON, contentType)
|
||||
|
||||
auth := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.Authorization))
|
||||
assert.Equal(t, "Bearer secret-token", auth)
|
||||
|
||||
assert.Equal(t, "true", builder.GetQuery().Get("notify"))
|
||||
assert.True(t, O.IsSome(builder.GetBody()))
|
||||
}
|
||||
|
||||
36
v2/http/builder/coverage.out
Normal file
36
v2/http/builder/coverage.out
Normal file
@@ -0,0 +1,36 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:208.51,211.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:213.37,215.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:217.42,221.2 3 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:226.64,228.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:231.64,257.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:260.41,262.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:264.41,266.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:268.44,273.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:275.50,277.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:279.47,281.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:283.61,286.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:288.69,290.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:292.59,295.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:298.53,301.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:303.53,306.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:308.66,311.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:313.82,316.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:318.64,321.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:323.57,326.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:328.65,334.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:336.63,338.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:341.42,343.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:346.61,354.75 4 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:354.75,360.3 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:364.62,369.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:372.46,374.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:379.43,381.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:384.43,393.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:396.63,401.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:404.64,409.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:412.48,414.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:416.68,419.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:421.84,424.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:426.35,438.2 7 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:441.34,443.2 1 1
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
ENDO "github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
LA "github.com/IBM/fp-go/v2/optics/lens/array"
|
||||
LO "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
LRG "github.com/IBM/fp-go/v2/optics/lens/record/generic"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
RG "github.com/IBM/fp-go/v2/record/generic"
|
||||
@@ -50,7 +50,7 @@ var (
|
||||
|
||||
composeHead = F.Pipe1(
|
||||
LA.AtHead[string](),
|
||||
L.ComposeOptions[url.Values, string](A.Empty[string]()),
|
||||
LO.Compose[url.Values, string](A.Empty[string]()),
|
||||
)
|
||||
|
||||
// AtValue is a [L.Lens] that focusses on first value in form fields
|
||||
|
||||
@@ -13,6 +13,56 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package headers provides constants and utilities for working with HTTP headers
|
||||
// in a functional programming style. It offers type-safe header name constants,
|
||||
// monoid operations for combining headers, and lens-based access to header values.
|
||||
//
|
||||
// The package follows functional programming principles by providing:
|
||||
// - Immutable operations through lenses
|
||||
// - Monoid for combining header maps
|
||||
// - Type-safe header name constants
|
||||
// - Functional composition of header operations
|
||||
//
|
||||
// Constants:
|
||||
//
|
||||
// The package defines commonly used HTTP header names as constants:
|
||||
// - Accept: The Accept request header
|
||||
// - Authorization: The Authorization request header
|
||||
// - ContentType: The Content-Type header
|
||||
// - ContentLength: The Content-Length header
|
||||
//
|
||||
// Monoid:
|
||||
//
|
||||
// The Monoid provides a way to combine multiple http.Header maps:
|
||||
//
|
||||
// headers1 := make(http.Header)
|
||||
// headers1.Set("X-Custom", "value1")
|
||||
//
|
||||
// headers2 := make(http.Header)
|
||||
// headers2.Set("Authorization", "Bearer token")
|
||||
//
|
||||
// combined := Monoid.Concat(headers1, headers2)
|
||||
// // combined now contains both headers
|
||||
//
|
||||
// Lenses:
|
||||
//
|
||||
// AtValues and AtValue provide lens-based access to header values:
|
||||
//
|
||||
// // AtValues focuses on all values of a header ([]string)
|
||||
// contentTypeLens := AtValues("Content-Type")
|
||||
// values := contentTypeLens.Get(headers)
|
||||
//
|
||||
// // AtValue focuses on the first value of a header (Option[string])
|
||||
// authLens := AtValue("Authorization")
|
||||
// token := authLens.Get(headers) // Returns Option[string]
|
||||
//
|
||||
// The lenses support functional updates:
|
||||
//
|
||||
// // Set a header value
|
||||
// newHeaders := AtValue("Content-Type").Set(O.Some("application/json"))(headers)
|
||||
//
|
||||
// // Remove a header
|
||||
// newHeaders := AtValue("X-Custom").Set(O.None[string]())(headers)
|
||||
package headers
|
||||
|
||||
import (
|
||||
@@ -21,36 +71,94 @@ import (
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
LA "github.com/IBM/fp-go/v2/optics/lens/array"
|
||||
LO "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
LRG "github.com/IBM/fp-go/v2/optics/lens/record/generic"
|
||||
RG "github.com/IBM/fp-go/v2/record/generic"
|
||||
)
|
||||
|
||||
// HTTP headers
|
||||
// Common HTTP header name constants.
|
||||
// These constants provide type-safe access to standard HTTP header names.
|
||||
const (
|
||||
Accept = "Accept"
|
||||
// Accept specifies the media types that are acceptable for the response.
|
||||
// Example: "Accept: application/json"
|
||||
Accept = "Accept"
|
||||
|
||||
// Authorization contains credentials for authenticating the client with the server.
|
||||
// Example: "Authorization: Bearer token123"
|
||||
Authorization = "Authorization"
|
||||
ContentType = "Content-Type"
|
||||
|
||||
// ContentType indicates the media type of the resource or data.
|
||||
// Example: "Content-Type: application/json"
|
||||
ContentType = "Content-Type"
|
||||
|
||||
// ContentLength indicates the size of the entity-body in bytes.
|
||||
// Example: "Content-Length: 348"
|
||||
ContentLength = "Content-Length"
|
||||
)
|
||||
|
||||
var (
|
||||
// Monoid is a [M.Monoid] to concatenate [http.Header] maps
|
||||
// Monoid is a Monoid for combining http.Header maps.
|
||||
// It uses a union operation where values from both headers are preserved.
|
||||
// When the same header exists in both maps, the values are concatenated.
|
||||
//
|
||||
// Example:
|
||||
// h1 := make(http.Header)
|
||||
// h1.Set("X-Custom", "value1")
|
||||
//
|
||||
// h2 := make(http.Header)
|
||||
// h2.Set("Authorization", "Bearer token")
|
||||
//
|
||||
// combined := Monoid.Concat(h1, h2)
|
||||
// // combined contains both X-Custom and Authorization headers
|
||||
Monoid = RG.UnionMonoid[http.Header](A.Semigroup[string]())
|
||||
|
||||
// AtValues is a [L.Lens] that focusses on the values of a header
|
||||
// AtValues is a Lens that focuses on all values of a specific header.
|
||||
// It returns a lens that accesses the []string slice of header values.
|
||||
// The header name is automatically canonicalized using MIME header key rules.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The header name (will be canonicalized)
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[http.Header, []string] focusing on the header's values
|
||||
//
|
||||
// Example:
|
||||
// lens := AtValues("Content-Type")
|
||||
// values := lens.Get(headers) // Returns []string
|
||||
// newHeaders := lens.Set([]string{"application/json"})(headers)
|
||||
AtValues = F.Flow2(
|
||||
textproto.CanonicalMIMEHeaderKey,
|
||||
LRG.AtRecord[http.Header, []string],
|
||||
)
|
||||
|
||||
// composeHead is an internal helper that composes a lens to focus on the first
|
||||
// element of a string array, returning an Option[string].
|
||||
composeHead = F.Pipe1(
|
||||
LA.AtHead[string](),
|
||||
L.ComposeOptions[http.Header, string](A.Empty[string]()),
|
||||
LO.Compose[http.Header, string](A.Empty[string]()),
|
||||
)
|
||||
|
||||
// AtValue is a [L.Lens] that focusses on first value of a header
|
||||
// AtValue is a Lens that focuses on the first value of a specific header.
|
||||
// It returns a lens that accesses an Option[string] representing the first
|
||||
// header value, or None if the header doesn't exist.
|
||||
// The header name is automatically canonicalized using MIME header key rules.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The header name (will be canonicalized)
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[http.Header, Option[string]] focusing on the first header value
|
||||
//
|
||||
// Example:
|
||||
// lens := AtValue("Authorization")
|
||||
// token := lens.Get(headers) // Returns Option[string]
|
||||
//
|
||||
// // Set a header value
|
||||
// newHeaders := lens.Set(O.Some("Bearer token"))(headers)
|
||||
//
|
||||
// // Remove a header
|
||||
// newHeaders := lens.Set(O.None[string]())(headers)
|
||||
AtValue = F.Flow2(
|
||||
AtValues,
|
||||
composeHead,
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/eq"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
LT "github.com/IBM/fp-go/v2/optics/lens/testing"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
RG "github.com/IBM/fp-go/v2/record/generic"
|
||||
@@ -56,3 +57,281 @@ func TestLaws(t *testing.T) {
|
||||
assert.True(t, fieldLaws(v1, s1))
|
||||
assert.True(t, fieldLaws(v2, s1))
|
||||
}
|
||||
|
||||
// TestMonoidEmpty tests the Monoid empty (identity) element
|
||||
func TestMonoidEmpty(t *testing.T) {
|
||||
empty := Monoid.Empty()
|
||||
assert.NotNil(t, empty)
|
||||
assert.Equal(t, 0, len(empty))
|
||||
}
|
||||
|
||||
// TestMonoidConcat tests concatenating two header maps
|
||||
func TestMonoidConcat(t *testing.T) {
|
||||
h1 := make(http.Header)
|
||||
h1.Set("X-Custom-1", "value1")
|
||||
h1.Set("Authorization", "Bearer token1")
|
||||
|
||||
h2 := make(http.Header)
|
||||
h2.Set("X-Custom-2", "value2")
|
||||
h2.Set("Content-Type", "application/json")
|
||||
|
||||
result := Monoid.Concat(h1, h2)
|
||||
|
||||
assert.Equal(t, "value1", result.Get("X-Custom-1"))
|
||||
assert.Equal(t, "value2", result.Get("X-Custom-2"))
|
||||
assert.Equal(t, "Bearer token1", result.Get("Authorization"))
|
||||
assert.Equal(t, "application/json", result.Get("Content-Type"))
|
||||
}
|
||||
|
||||
// TestMonoidConcatWithOverlap tests concatenating headers with overlapping keys
|
||||
func TestMonoidConcatWithOverlap(t *testing.T) {
|
||||
h1 := make(http.Header)
|
||||
h1.Set("X-Custom", "value1")
|
||||
|
||||
h2 := make(http.Header)
|
||||
h2.Add("X-Custom", "value2")
|
||||
|
||||
result := Monoid.Concat(h1, h2)
|
||||
|
||||
// Both values should be present
|
||||
values := result.Values("X-Custom")
|
||||
assert.Contains(t, values, "value1")
|
||||
assert.Contains(t, values, "value2")
|
||||
}
|
||||
|
||||
// TestMonoidIdentity tests that concatenating with empty is identity
|
||||
func TestMonoidIdentity(t *testing.T) {
|
||||
h := make(http.Header)
|
||||
h.Set("X-Test", "value")
|
||||
|
||||
empty := Monoid.Empty()
|
||||
|
||||
// Left identity: empty + h = h
|
||||
leftResult := Monoid.Concat(empty, h)
|
||||
assert.Equal(t, "value", leftResult.Get("X-Test"))
|
||||
|
||||
// Right identity: h + empty = h
|
||||
rightResult := Monoid.Concat(h, empty)
|
||||
assert.Equal(t, "value", rightResult.Get("X-Test"))
|
||||
}
|
||||
|
||||
// TestAtValuesGet tests getting header values using AtValues lens
|
||||
func TestAtValuesGet(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Add("Accept", "application/json")
|
||||
headers.Add("Accept", "text/html")
|
||||
|
||||
// Get Content-Type values
|
||||
ctLens := AtValues("Content-Type")
|
||||
ctValuesOpt := ctLens.Get(headers)
|
||||
assert.True(t, O.IsSome(ctValuesOpt))
|
||||
ctValues := O.GetOrElse(F.Constant([]string{}))(ctValuesOpt)
|
||||
assert.Equal(t, []string{"application/json"}, ctValues)
|
||||
|
||||
// Get Accept values (multiple)
|
||||
acceptLens := AtValues("Accept")
|
||||
acceptValuesOpt := acceptLens.Get(headers)
|
||||
assert.True(t, O.IsSome(acceptValuesOpt))
|
||||
acceptValues := O.GetOrElse(F.Constant([]string{}))(acceptValuesOpt)
|
||||
assert.Equal(t, 2, len(acceptValues))
|
||||
assert.Contains(t, acceptValues, "application/json")
|
||||
assert.Contains(t, acceptValues, "text/html")
|
||||
}
|
||||
|
||||
// TestAtValuesSet tests setting header values using AtValues lens
|
||||
func TestAtValuesSet(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("X-Old", "old-value")
|
||||
|
||||
lens := AtValues("Content-Type")
|
||||
newHeaders := lens.Set(O.Some([]string{"application/json", "text/plain"}))(headers)
|
||||
|
||||
// New header should be set
|
||||
values := newHeaders.Values("Content-Type")
|
||||
assert.Equal(t, 2, len(values))
|
||||
assert.Contains(t, values, "application/json")
|
||||
assert.Contains(t, values, "text/plain")
|
||||
|
||||
// Old header should still exist
|
||||
assert.Equal(t, "old-value", newHeaders.Get("X-Old"))
|
||||
}
|
||||
|
||||
// TestAtValuesCanonical tests that header names are canonicalized
|
||||
func TestAtValuesCanonical(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("content-type", "application/json")
|
||||
|
||||
// Access with different casing
|
||||
lens := AtValues("Content-Type")
|
||||
valuesOpt := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsSome(valuesOpt))
|
||||
values := O.GetOrElse(F.Constant([]string{}))(valuesOpt)
|
||||
assert.Equal(t, []string{"application/json"}, values)
|
||||
}
|
||||
|
||||
// TestAtValueGet tests getting first header value using AtValue lens
|
||||
func TestAtValueGet(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("Authorization", "Bearer token123")
|
||||
|
||||
lens := AtValue("Authorization")
|
||||
value := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsSome(value))
|
||||
token := O.GetOrElse(F.Constant(""))(value)
|
||||
assert.Equal(t, "Bearer token123", token)
|
||||
}
|
||||
|
||||
// TestAtValueGetNone tests getting non-existent header returns None
|
||||
func TestAtValueGetNone(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
|
||||
lens := AtValue("X-Non-Existent")
|
||||
value := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsNone(value))
|
||||
}
|
||||
|
||||
// TestAtValueSet tests setting header value using AtValue lens
|
||||
func TestAtValueSet(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
|
||||
lens := AtValue("Content-Type")
|
||||
newHeaders := lens.Set(O.Some("application/json"))(headers)
|
||||
|
||||
value := lens.Get(newHeaders)
|
||||
assert.True(t, O.IsSome(value))
|
||||
|
||||
ct := O.GetOrElse(F.Constant(""))(value)
|
||||
assert.Equal(t, "application/json", ct)
|
||||
}
|
||||
|
||||
// TestAtValueSetNone tests removing header using AtValue lens
|
||||
func TestAtValueSetNone(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("X-Custom", "value")
|
||||
|
||||
lens := AtValue("X-Custom")
|
||||
newHeaders := lens.Set(O.None[string]())(headers)
|
||||
|
||||
value := lens.Get(newHeaders)
|
||||
assert.True(t, O.IsNone(value))
|
||||
}
|
||||
|
||||
// TestAtValueMultipleValues tests AtValue with multiple header values
|
||||
func TestAtValueMultipleValues(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Add("Accept", "application/json")
|
||||
headers.Add("Accept", "text/html")
|
||||
|
||||
lens := AtValue("Accept")
|
||||
value := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsSome(value))
|
||||
// Should get the first value
|
||||
first := O.GetOrElse(F.Constant(""))(value)
|
||||
assert.Equal(t, "application/json", first)
|
||||
}
|
||||
|
||||
// TestHeaderConstants tests that header constants are correct
|
||||
func TestHeaderConstants(t *testing.T) {
|
||||
assert.Equal(t, "Accept", Accept)
|
||||
assert.Equal(t, "Authorization", Authorization)
|
||||
assert.Equal(t, "Content-Type", ContentType)
|
||||
assert.Equal(t, "Content-Length", ContentLength)
|
||||
}
|
||||
|
||||
// TestHeaderConstantsUsage tests using header constants with http.Header
|
||||
func TestHeaderConstantsUsage(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
|
||||
headers.Set(Accept, "application/json")
|
||||
headers.Set(Authorization, "Bearer token")
|
||||
headers.Set(ContentType, "application/json")
|
||||
headers.Set(ContentLength, "1234")
|
||||
|
||||
assert.Equal(t, "application/json", headers.Get(Accept))
|
||||
assert.Equal(t, "Bearer token", headers.Get(Authorization))
|
||||
assert.Equal(t, "application/json", headers.Get(ContentType))
|
||||
assert.Equal(t, "1234", headers.Get(ContentLength))
|
||||
}
|
||||
|
||||
// TestAtValueWithConstants tests using AtValue with header constants
|
||||
func TestAtValueWithConstants(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set(ContentType, "application/json")
|
||||
|
||||
lens := AtValue(ContentType)
|
||||
value := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsSome(value))
|
||||
ct := O.GetOrElse(F.Constant(""))(value)
|
||||
assert.Equal(t, "application/json", ct)
|
||||
}
|
||||
|
||||
// TestMonoidAssociativity tests that Monoid concatenation is associative
|
||||
func TestMonoidAssociativity(t *testing.T) {
|
||||
h1 := make(http.Header)
|
||||
h1.Set("X-1", "value1")
|
||||
|
||||
h2 := make(http.Header)
|
||||
h2.Set("X-2", "value2")
|
||||
|
||||
h3 := make(http.Header)
|
||||
h3.Set("X-3", "value3")
|
||||
|
||||
// (h1 + h2) + h3
|
||||
left := Monoid.Concat(Monoid.Concat(h1, h2), h3)
|
||||
|
||||
// h1 + (h2 + h3)
|
||||
right := Monoid.Concat(h1, Monoid.Concat(h2, h3))
|
||||
|
||||
// Both should have all three headers
|
||||
assert.Equal(t, "value1", left.Get("X-1"))
|
||||
assert.Equal(t, "value2", left.Get("X-2"))
|
||||
assert.Equal(t, "value3", left.Get("X-3"))
|
||||
|
||||
assert.Equal(t, "value1", right.Get("X-1"))
|
||||
assert.Equal(t, "value2", right.Get("X-2"))
|
||||
assert.Equal(t, "value3", right.Get("X-3"))
|
||||
}
|
||||
|
||||
// TestAtValuesEmptyHeader tests AtValues with empty headers
|
||||
func TestAtValuesEmptyHeader(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
|
||||
lens := AtValues("X-Non-Existent")
|
||||
valuesOpt := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsNone(valuesOpt))
|
||||
}
|
||||
|
||||
// TestComplexHeaderOperations tests complex operations combining lenses and monoid
|
||||
func TestComplexHeaderOperations(t *testing.T) {
|
||||
// Create initial headers
|
||||
h1 := make(http.Header)
|
||||
h1.Set("X-Initial", "initial")
|
||||
|
||||
// Use lens to add Content-Type
|
||||
ctLens := AtValue(ContentType)
|
||||
h2 := ctLens.Set(O.Some("application/json"))(h1)
|
||||
|
||||
// Use lens to add Authorization
|
||||
authLens := AtValue(Authorization)
|
||||
h3 := authLens.Set(O.Some("Bearer token"))(h2)
|
||||
|
||||
// Create additional headers
|
||||
h4 := make(http.Header)
|
||||
h4.Set("X-Additional", "additional")
|
||||
|
||||
// Combine using Monoid
|
||||
final := Monoid.Concat(h3, h4)
|
||||
|
||||
// Verify all headers are present
|
||||
assert.Equal(t, "initial", final.Get("X-Initial"))
|
||||
assert.Equal(t, "application/json", final.Get(ContentType))
|
||||
assert.Equal(t, "Bearer token", final.Get(Authorization))
|
||||
assert.Equal(t, "additional", final.Get("X-Additional"))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,53 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package http provides functional programming utilities for working with HTTP
|
||||
// requests and responses. It offers type-safe abstractions, validation functions,
|
||||
// and utilities for handling HTTP operations in a functional style.
|
||||
//
|
||||
// The package includes:
|
||||
// - Type definitions for HTTP responses with bodies
|
||||
// - Validation functions for HTTP responses
|
||||
// - JSON content type validation
|
||||
// - Error handling with detailed HTTP error information
|
||||
// - Functional utilities for accessing response components
|
||||
//
|
||||
// Types:
|
||||
//
|
||||
// FullResponse represents a complete HTTP response including both the response
|
||||
// object and the body as a byte array. It's implemented as a Pair for functional
|
||||
// composition:
|
||||
//
|
||||
// type FullResponse = Pair[*http.Response, []byte]
|
||||
//
|
||||
// The Response and Body functions provide lens-like access to the components:
|
||||
//
|
||||
// resp := Response(fullResponse) // Get *http.Response
|
||||
// body := Body(fullResponse) // Get []byte
|
||||
//
|
||||
// Validation:
|
||||
//
|
||||
// ValidateResponse checks if an HTTP response has a successful status code (2xx):
|
||||
//
|
||||
// result := ValidateResponse(response)
|
||||
// // Returns Either[error, *http.Response]
|
||||
//
|
||||
// ValidateJSONResponse validates both the status code and Content-Type header:
|
||||
//
|
||||
// result := ValidateJSONResponse(response)
|
||||
// // Returns Either[error, *http.Response]
|
||||
//
|
||||
// Error Handling:
|
||||
//
|
||||
// HttpError provides detailed information about HTTP failures:
|
||||
//
|
||||
// err := StatusCodeError(response)
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// code := httpErr.StatusCode()
|
||||
// headers := httpErr.Headers()
|
||||
// body := httpErr.Body()
|
||||
// url := httpErr.URL()
|
||||
// }
|
||||
package http
|
||||
|
||||
import (
|
||||
@@ -22,11 +69,38 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// FullResponse represents a full http response, including headers and body
|
||||
// FullResponse represents a complete HTTP response including both the
|
||||
// *http.Response object and the response body as a byte slice.
|
||||
//
|
||||
// It's implemented as a Pair to enable functional composition and
|
||||
// transformation of HTTP responses. This allows you to work with both
|
||||
// the response metadata (status, headers) and body content together.
|
||||
//
|
||||
// Example:
|
||||
// fullResp := MakePair(response, bodyBytes)
|
||||
// resp := Response(fullResp) // Extract *http.Response
|
||||
// body := Body(fullResp) // Extract []byte
|
||||
FullResponse = P.Pair[*H.Response, []byte]
|
||||
)
|
||||
|
||||
var (
|
||||
// Response is a lens-like accessor that extracts the *http.Response
|
||||
// from a FullResponse. It provides functional access to the response
|
||||
// metadata including status code, headers, and other HTTP response fields.
|
||||
//
|
||||
// Example:
|
||||
// fullResp := MakePair(response, bodyBytes)
|
||||
// resp := Response(fullResp)
|
||||
// statusCode := resp.StatusCode
|
||||
Response = P.Head[*H.Response, []byte]
|
||||
Body = P.Tail[*H.Response, []byte]
|
||||
|
||||
// Body is a lens-like accessor that extracts the response body bytes
|
||||
// from a FullResponse. It provides functional access to the raw body
|
||||
// content without needing to read from an io.Reader.
|
||||
//
|
||||
// Example:
|
||||
// fullResp := MakePair(response, bodyBytes)
|
||||
// body := Body(fullResp)
|
||||
// content := string(body)
|
||||
Body = P.Tail[*H.Response, []byte]
|
||||
)
|
||||
|
||||
185
v2/http/utils.go
185
v2/http/utils.go
@@ -33,8 +33,29 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// ParsedMediaType represents a parsed MIME media type as a Pair.
|
||||
// The first element is the media type string (e.g., "application/json"),
|
||||
// and the second element is a map of parameters (e.g., {"charset": "utf-8"}).
|
||||
//
|
||||
// Example:
|
||||
// parsed := ParseMediaType("application/json; charset=utf-8")
|
||||
// mediaType := P.Head(parsed) // "application/json"
|
||||
// params := P.Tail(parsed) // map[string]string{"charset": "utf-8"}
|
||||
ParsedMediaType = P.Pair[string, map[string]string]
|
||||
|
||||
// HttpError represents an HTTP error with detailed information about
|
||||
// the failed request. It includes the status code, response headers,
|
||||
// response body, and the URL that was accessed.
|
||||
//
|
||||
// This error type is created by StatusCodeError when an HTTP response
|
||||
// has a non-successful status code (not 2xx).
|
||||
//
|
||||
// Example:
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// fmt.Printf("Status: %d\n", httpErr.StatusCode())
|
||||
// fmt.Printf("URL: %s\n", httpErr.URL())
|
||||
// fmt.Printf("Body: %s\n", string(httpErr.Body()))
|
||||
// }
|
||||
HttpError struct {
|
||||
statusCode int
|
||||
headers H.Header
|
||||
@@ -44,11 +65,28 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
// mime type to check if a media type matches
|
||||
// isJSONMimeType is a regex matcher that checks if a media type is a valid JSON type.
|
||||
// It matches "application/json" and variants like "application/vnd.api+json".
|
||||
isJSONMimeType = regexp.MustCompile(`application/(?:\w+\+)?json`).MatchString
|
||||
// ValidateResponse validates an HTTP response and returns an [E.Either] if the response is not a success
|
||||
|
||||
// ValidateResponse validates an HTTP response and returns an Either.
|
||||
// It checks if the response has a successful status code (2xx range).
|
||||
//
|
||||
// Returns:
|
||||
// - Right(*http.Response) if status code is 2xx
|
||||
// - Left(error) with HttpError if status code is not 2xx
|
||||
//
|
||||
// Example:
|
||||
// result := ValidateResponse(response)
|
||||
// E.Fold(
|
||||
// func(err error) { /* handle error */ },
|
||||
// func(resp *http.Response) { /* handle success */ },
|
||||
// )(result)
|
||||
ValidateResponse = E.FromPredicate(isValidStatus, StatusCodeError)
|
||||
// alidateJsonContentTypeString parses a content type a validates that it is valid JSON
|
||||
|
||||
// validateJSONContentTypeString parses a content type string and validates
|
||||
// that it represents a valid JSON media type. This is an internal helper
|
||||
// used by ValidateJSONResponse.
|
||||
validateJSONContentTypeString = F.Flow2(
|
||||
ParseMediaType,
|
||||
E.ChainFirst(F.Flow2(
|
||||
@@ -56,7 +94,21 @@ var (
|
||||
E.FromPredicate(isJSONMimeType, errors.OnSome[string]("mimetype [%s] is not a valid JSON content type")),
|
||||
)),
|
||||
)
|
||||
// ValidateJSONResponse checks if an HTTP response is a valid JSON response
|
||||
|
||||
// ValidateJSONResponse validates that an HTTP response is a valid JSON response.
|
||||
// It checks both the status code (must be 2xx) and the Content-Type header
|
||||
// (must be a JSON media type like "application/json").
|
||||
//
|
||||
// Returns:
|
||||
// - Right(*http.Response) if response is valid JSON with 2xx status
|
||||
// - Left(error) if status is not 2xx or Content-Type is not JSON
|
||||
//
|
||||
// Example:
|
||||
// result := ValidateJSONResponse(response)
|
||||
// E.Fold(
|
||||
// func(err error) { /* handle non-JSON or error response */ },
|
||||
// func(resp *http.Response) { /* handle valid JSON response */ },
|
||||
// )(result)
|
||||
ValidateJSONResponse = F.Flow2(
|
||||
E.Of[error, *H.Response],
|
||||
E.ChainFirst(F.Flow5(
|
||||
@@ -66,60 +118,175 @@ var (
|
||||
E.FromOption[string](errors.OnNone("unable to access the [%s] header", HeaderContentType)),
|
||||
E.ChainFirst(validateJSONContentTypeString),
|
||||
)))
|
||||
// ValidateJsonResponse checks if an HTTP response is a valid JSON response
|
||||
|
||||
// ValidateJsonResponse checks if an HTTP response is a valid JSON response.
|
||||
//
|
||||
// Deprecated: use [ValidateJSONResponse] instead
|
||||
// Deprecated: use ValidateJSONResponse instead (note the capitalization).
|
||||
ValidateJsonResponse = ValidateJSONResponse
|
||||
)
|
||||
|
||||
const (
|
||||
// HeaderContentType is the standard HTTP Content-Type header name.
|
||||
// It indicates the media type of the resource or data being sent.
|
||||
//
|
||||
// Example values:
|
||||
// - "application/json"
|
||||
// - "text/html; charset=utf-8"
|
||||
// - "application/xml"
|
||||
HeaderContentType = "Content-Type"
|
||||
)
|
||||
|
||||
// ParseMediaType parses a media type into a tuple
|
||||
// ParseMediaType parses a MIME media type string into its components.
|
||||
// It returns a ParsedMediaType (Pair) containing the media type and its parameters.
|
||||
//
|
||||
// Parameters:
|
||||
// - mediaType: A media type string (e.g., "application/json; charset=utf-8")
|
||||
//
|
||||
// Returns:
|
||||
// - Right(ParsedMediaType) with the parsed media type and parameters
|
||||
// - Left(error) if the media type string is invalid
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := ParseMediaType("application/json; charset=utf-8")
|
||||
// E.Map(func(parsed ParsedMediaType) {
|
||||
// mediaType := P.Head(parsed) // "application/json"
|
||||
// params := P.Tail(parsed) // map[string]string{"charset": "utf-8"}
|
||||
// })(result)
|
||||
func ParseMediaType(mediaType string) E.Either[error, ParsedMediaType] {
|
||||
m, p, err := mime.ParseMediaType(mediaType)
|
||||
return E.TryCatchError(P.MakePair(m, p), err)
|
||||
}
|
||||
|
||||
// Error fulfills the error interface
|
||||
// Error implements the error interface for HttpError.
|
||||
// It returns a formatted error message including the status code and URL.
|
||||
func (r *HttpError) Error() string {
|
||||
return fmt.Sprintf("invalid status code [%d] when accessing URL [%s]", r.statusCode, r.url)
|
||||
}
|
||||
|
||||
// String returns the string representation of the HttpError.
|
||||
// It's equivalent to calling Error().
|
||||
func (r *HttpError) String() string {
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// StatusCode returns the HTTP status code from the failed response.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// code := httpErr.StatusCode() // e.g., 404, 500
|
||||
// }
|
||||
func (r *HttpError) StatusCode() int {
|
||||
return r.statusCode
|
||||
}
|
||||
|
||||
// Headers returns a clone of the HTTP headers from the failed response.
|
||||
// The headers are cloned to prevent modification of the original response.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// headers := httpErr.Headers()
|
||||
// contentType := headers.Get("Content-Type")
|
||||
// }
|
||||
func (r *HttpError) Headers() H.Header {
|
||||
return r.headers
|
||||
}
|
||||
|
||||
// URL returns the URL that was accessed when the error occurred.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// url := httpErr.URL()
|
||||
// fmt.Printf("Failed to access: %s\n", url)
|
||||
// }
|
||||
func (r *HttpError) URL() *url.URL {
|
||||
return r.url
|
||||
}
|
||||
|
||||
// Body returns the response body bytes from the failed response.
|
||||
// This can be useful for debugging or displaying error messages from the server.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// body := httpErr.Body()
|
||||
// fmt.Printf("Error response: %s\n", string(body))
|
||||
// }
|
||||
func (r *HttpError) Body() []byte {
|
||||
return r.body
|
||||
}
|
||||
|
||||
// GetHeader extracts the HTTP headers from an http.Response.
|
||||
// This is a functional accessor for the Header field.
|
||||
//
|
||||
// Parameters:
|
||||
// - resp: The HTTP response
|
||||
//
|
||||
// Returns:
|
||||
// - The http.Header map from the response
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// headers := GetHeader(response)
|
||||
// contentType := headers.Get("Content-Type")
|
||||
func GetHeader(resp *H.Response) H.Header {
|
||||
return resp.Header
|
||||
}
|
||||
|
||||
// GetBody extracts the response body reader from an http.Response.
|
||||
// This is a functional accessor for the Body field.
|
||||
//
|
||||
// Parameters:
|
||||
// - resp: The HTTP response
|
||||
//
|
||||
// Returns:
|
||||
// - The io.ReadCloser for reading the response body
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// body := GetBody(response)
|
||||
// defer body.Close()
|
||||
// data, err := io.ReadAll(body)
|
||||
func GetBody(resp *H.Response) io.ReadCloser {
|
||||
return resp.Body
|
||||
}
|
||||
|
||||
// isValidStatus checks if an HTTP response has a successful status code.
|
||||
// A status code is considered valid if it's in the 2xx range (200-299).
|
||||
//
|
||||
// Parameters:
|
||||
// - resp: The HTTP response to check
|
||||
//
|
||||
// Returns:
|
||||
// - true if status code is 2xx, false otherwise
|
||||
func isValidStatus(resp *H.Response) bool {
|
||||
return resp.StatusCode >= H.StatusOK && resp.StatusCode < H.StatusMultipleChoices
|
||||
}
|
||||
|
||||
// StatusCodeError creates an instance of [HttpError] filled with information from the response
|
||||
// StatusCodeError creates an HttpError from an http.Response with a non-successful status code.
|
||||
// It reads the response body and captures all relevant information for debugging.
|
||||
//
|
||||
// The function:
|
||||
// - Reads and stores the response body
|
||||
// - Clones the response headers
|
||||
// - Captures the request URL
|
||||
// - Creates a comprehensive error with all this information
|
||||
//
|
||||
// Parameters:
|
||||
// - resp: The HTTP response with a non-successful status code
|
||||
//
|
||||
// Returns:
|
||||
// - An error (specifically *HttpError) with detailed information
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// if !isValidStatus(response) {
|
||||
// err := StatusCodeError(response)
|
||||
// return err
|
||||
// }
|
||||
func StatusCodeError(resp *H.Response) error {
|
||||
// read the body
|
||||
bodyRdr := GetBody(resp)
|
||||
|
||||
@@ -16,12 +16,18 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
H "net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
C "github.com/IBM/fp-go/v2/http/content"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func NoError[A any](t *testing.T) func(E.Either[error, A]) bool {
|
||||
@@ -37,21 +43,351 @@ func Error[A any](t *testing.T) func(E.Either[error, A]) bool {
|
||||
}
|
||||
|
||||
func TestValidateJsonContentTypeString(t *testing.T) {
|
||||
|
||||
res := F.Pipe1(
|
||||
validateJSONContentTypeString(C.JSON),
|
||||
NoError[ParsedMediaType](t),
|
||||
)
|
||||
|
||||
assert.True(t, res)
|
||||
}
|
||||
|
||||
func TestValidateInvalidJsonContentTypeString(t *testing.T) {
|
||||
|
||||
res := F.Pipe1(
|
||||
validateJSONContentTypeString("application/xml"),
|
||||
Error[ParsedMediaType](t),
|
||||
)
|
||||
|
||||
assert.True(t, res)
|
||||
}
|
||||
|
||||
// TestParseMediaType tests parsing valid media types
|
||||
func TestParseMediaType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mediaType string
|
||||
wantType string
|
||||
wantParam map[string]string
|
||||
}{
|
||||
{
|
||||
name: "simple JSON",
|
||||
mediaType: "application/json",
|
||||
wantType: "application/json",
|
||||
wantParam: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "JSON with charset",
|
||||
mediaType: "application/json; charset=utf-8",
|
||||
wantType: "application/json",
|
||||
wantParam: map[string]string{"charset": "utf-8"},
|
||||
},
|
||||
{
|
||||
name: "HTML with charset",
|
||||
mediaType: "text/html; charset=iso-8859-1",
|
||||
wantType: "text/html",
|
||||
wantParam: map[string]string{"charset": "iso-8859-1"},
|
||||
},
|
||||
{
|
||||
name: "multipart with boundary",
|
||||
mediaType: "multipart/form-data; boundary=----WebKitFormBoundary",
|
||||
wantType: "multipart/form-data",
|
||||
wantParam: map[string]string{"boundary": "----WebKitFormBoundary"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParseMediaType(tt.mediaType)
|
||||
require.True(t, E.IsRight(result), "ParseMediaType should succeed")
|
||||
|
||||
parsed := E.GetOrElse(func(error) ParsedMediaType {
|
||||
return P.MakePair("", map[string]string{})
|
||||
})(result)
|
||||
mediaType := P.Head(parsed)
|
||||
params := P.Tail(parsed)
|
||||
|
||||
assert.Equal(t, tt.wantType, mediaType)
|
||||
assert.Equal(t, tt.wantParam, params)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseMediaTypeInvalid tests parsing invalid media types
|
||||
func TestParseMediaTypeInvalid(t *testing.T) {
|
||||
result := ParseMediaType("invalid media type")
|
||||
assert.True(t, E.IsLeft(result), "ParseMediaType should fail for invalid input")
|
||||
}
|
||||
|
||||
// TestHttpErrorMethods tests all HttpError methods
|
||||
func TestHttpErrorMethods(t *testing.T) {
|
||||
testURL, _ := url.Parse("https://example.com/api/test")
|
||||
headers := make(H.Header)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Set("X-Custom", "value")
|
||||
body := []byte(`{"error": "not found"}`)
|
||||
|
||||
httpErr := &HttpError{
|
||||
statusCode: 404,
|
||||
headers: headers,
|
||||
body: body,
|
||||
url: testURL,
|
||||
}
|
||||
|
||||
// Test StatusCode
|
||||
assert.Equal(t, 404, httpErr.StatusCode())
|
||||
|
||||
// Test Headers
|
||||
returnedHeaders := httpErr.Headers()
|
||||
assert.Equal(t, "application/json", returnedHeaders.Get("Content-Type"))
|
||||
assert.Equal(t, "value", returnedHeaders.Get("X-Custom"))
|
||||
|
||||
// Test Body
|
||||
assert.Equal(t, body, httpErr.Body())
|
||||
assert.Equal(t, `{"error": "not found"}`, string(httpErr.Body()))
|
||||
|
||||
// Test URL
|
||||
assert.Equal(t, testURL, httpErr.URL())
|
||||
assert.Equal(t, "https://example.com/api/test", httpErr.URL().String())
|
||||
|
||||
// Test Error
|
||||
errMsg := httpErr.Error()
|
||||
assert.Contains(t, errMsg, "404")
|
||||
assert.Contains(t, errMsg, "https://example.com/api/test")
|
||||
|
||||
// Test String
|
||||
assert.Equal(t, errMsg, httpErr.String())
|
||||
}
|
||||
|
||||
// TestGetHeader tests the GetHeader function
|
||||
func TestGetHeader(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
resp.Header.Set("Authorization", "Bearer token")
|
||||
|
||||
headers := GetHeader(resp)
|
||||
assert.Equal(t, "application/json", headers.Get("Content-Type"))
|
||||
assert.Equal(t, "Bearer token", headers.Get("Authorization"))
|
||||
}
|
||||
|
||||
// TestGetBody tests the GetBody function
|
||||
func TestGetBody(t *testing.T) {
|
||||
bodyContent := []byte("test body content")
|
||||
resp := &H.Response{
|
||||
Body: io.NopCloser(bytes.NewReader(bodyContent)),
|
||||
}
|
||||
|
||||
body := GetBody(resp)
|
||||
defer body.Close()
|
||||
|
||||
data, err := io.ReadAll(body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, bodyContent, data)
|
||||
}
|
||||
|
||||
// TestIsValidStatus tests the isValidStatus function
|
||||
func TestIsValidStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
want bool
|
||||
}{
|
||||
{"200 OK", H.StatusOK, true},
|
||||
{"201 Created", H.StatusCreated, true},
|
||||
{"204 No Content", H.StatusNoContent, true},
|
||||
{"299 (edge of 2xx)", 299, true},
|
||||
{"300 Multiple Choices", H.StatusMultipleChoices, false},
|
||||
{"301 Moved Permanently", H.StatusMovedPermanently, false},
|
||||
{"400 Bad Request", H.StatusBadRequest, false},
|
||||
{"404 Not Found", H.StatusNotFound, false},
|
||||
{"500 Internal Server Error", H.StatusInternalServerError, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resp := &H.Response{StatusCode: tt.statusCode}
|
||||
assert.Equal(t, tt.want, isValidStatus(resp))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateResponse tests the ValidateResponse function
|
||||
func TestValidateResponse(t *testing.T) {
|
||||
t.Run("successful response", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
|
||||
result := ValidateResponse(resp)
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
validResp := E.GetOrElse(func(error) *H.Response { return nil })(result)
|
||||
assert.Equal(t, resp, validResp)
|
||||
})
|
||||
|
||||
t.Run("error response", func(t *testing.T) {
|
||||
testURL, _ := url.Parse("https://example.com/test")
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusNotFound,
|
||||
Header: make(H.Header),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte("not found"))),
|
||||
Request: &H.Request{URL: testURL},
|
||||
}
|
||||
|
||||
result := ValidateResponse(resp)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
// Extract error using Fold
|
||||
var httpErr *HttpError
|
||||
E.Fold(
|
||||
func(err error) *H.Response {
|
||||
var ok bool
|
||||
httpErr, ok = err.(*HttpError)
|
||||
require.True(t, ok, "error should be *HttpError")
|
||||
return nil
|
||||
},
|
||||
func(r *H.Response) *H.Response { return r },
|
||||
)(result)
|
||||
assert.Equal(t, 404, httpErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
// TestStatusCodeError tests the StatusCodeError function
|
||||
func TestStatusCodeError(t *testing.T) {
|
||||
testURL, _ := url.Parse("https://api.example.com/users/123")
|
||||
bodyContent := []byte(`{"error": "user not found"}`)
|
||||
|
||||
headers := make(H.Header)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Set("X-Request-ID", "abc123")
|
||||
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusNotFound,
|
||||
Header: headers,
|
||||
Body: io.NopCloser(bytes.NewReader(bodyContent)),
|
||||
Request: &H.Request{URL: testURL},
|
||||
}
|
||||
|
||||
err := StatusCodeError(resp)
|
||||
require.Error(t, err)
|
||||
|
||||
httpErr, ok := err.(*HttpError)
|
||||
require.True(t, ok, "error should be *HttpError")
|
||||
|
||||
// Verify all fields
|
||||
assert.Equal(t, 404, httpErr.StatusCode())
|
||||
assert.Equal(t, testURL, httpErr.URL())
|
||||
assert.Equal(t, bodyContent, httpErr.Body())
|
||||
|
||||
// Verify headers are cloned
|
||||
returnedHeaders := httpErr.Headers()
|
||||
assert.Equal(t, "application/json", returnedHeaders.Get("Content-Type"))
|
||||
assert.Equal(t, "abc123", returnedHeaders.Get("X-Request-ID"))
|
||||
|
||||
// Verify error message
|
||||
errMsg := httpErr.Error()
|
||||
assert.Contains(t, errMsg, "404")
|
||||
assert.Contains(t, errMsg, "https://api.example.com/users/123")
|
||||
}
|
||||
|
||||
// TestValidateJSONResponse tests the ValidateJSONResponse function
|
||||
func TestValidateJSONResponse(t *testing.T) {
|
||||
t.Run("valid JSON response", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
assert.True(t, E.IsRight(result), "should accept valid JSON response")
|
||||
})
|
||||
|
||||
t.Run("JSON with charset", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
assert.True(t, E.IsRight(result), "should accept JSON with charset")
|
||||
})
|
||||
|
||||
t.Run("JSON variant (hal+json)", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/hal+json")
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
assert.True(t, E.IsRight(result), "should accept JSON variants")
|
||||
})
|
||||
|
||||
t.Run("non-JSON content type", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "text/html")
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
assert.True(t, E.IsLeft(result), "should reject non-JSON content type")
|
||||
})
|
||||
|
||||
t.Run("missing Content-Type header", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
assert.True(t, E.IsLeft(result), "should reject missing Content-Type")
|
||||
})
|
||||
|
||||
t.Run("valid JSON with error status code", func(t *testing.T) {
|
||||
// Note: ValidateJSONResponse only validates Content-Type, not status code
|
||||
// It wraps the response in Right(response) first, then validates headers
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusInternalServerError,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
// This actually succeeds because ValidateJSONResponse doesn't check status
|
||||
assert.True(t, E.IsRight(result), "ValidateJSONResponse only checks Content-Type, not status")
|
||||
})
|
||||
}
|
||||
|
||||
// TestFullResponseAccessors tests Response and Body accessors
|
||||
func TestFullResponseAccessors(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
bodyContent := []byte(`{"message": "success"}`)
|
||||
fullResp := P.MakePair(resp, bodyContent)
|
||||
|
||||
// Test Response accessor
|
||||
extractedResp := Response(fullResp)
|
||||
assert.Equal(t, resp, extractedResp)
|
||||
assert.Equal(t, H.StatusOK, extractedResp.StatusCode)
|
||||
|
||||
// Test Body accessor
|
||||
extractedBody := Body(fullResp)
|
||||
assert.Equal(t, bodyContent, extractedBody)
|
||||
assert.Equal(t, `{"message": "success"}`, string(extractedBody))
|
||||
}
|
||||
|
||||
// TestHeaderContentTypeConstant tests the HeaderContentType constant
|
||||
func TestHeaderContentTypeConstant(t *testing.T) {
|
||||
assert.Equal(t, "Content-Type", HeaderContentType)
|
||||
|
||||
// Test usage with http.Header
|
||||
headers := make(H.Header)
|
||||
headers.Set(HeaderContentType, "application/json")
|
||||
assert.Equal(t, "application/json", headers.Get(HeaderContentType))
|
||||
}
|
||||
|
||||
@@ -21,14 +21,55 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
// result := identity.Do(State{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) S {
|
||||
return empty
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// identity.Do(State{}),
|
||||
// identity.Bind(
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// func(s State) int {
|
||||
// return 42
|
||||
// },
|
||||
// ),
|
||||
// identity.Bind(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) int {
|
||||
// // This can access s.X from the previous step
|
||||
// return s.X * 2
|
||||
// },
|
||||
// ),
|
||||
// ) // State{X: 42, Y: 84}
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
@@ -75,7 +116,36 @@ func BindTo[S1, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// result := F.Pipe2(
|
||||
// identity.Do(State{}),
|
||||
// identity.ApS(
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// 42,
|
||||
// ),
|
||||
// identity.ApS(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// 100,
|
||||
// ),
|
||||
// ) // State{X: 42, Y: 100}
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa T,
|
||||
|
||||
@@ -49,19 +49,19 @@ func Of[A any](a A) A {
|
||||
return a
|
||||
}
|
||||
|
||||
func MonadChain[A, B any](ma A, f func(A) B) B {
|
||||
func MonadChain[A, B any](ma A, f Kleisli[A, B]) B {
|
||||
return f(ma)
|
||||
}
|
||||
|
||||
func Chain[A, B any](f func(A) B) Operator[A, B] {
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return f
|
||||
}
|
||||
|
||||
func MonadChainFirst[A, B any](fa A, f func(A) B) A {
|
||||
func MonadChainFirst[A, B any](fa A, f Kleisli[A, B]) A {
|
||||
return chain.MonadChainFirst(MonadChain[A, A], MonadMap[B, A], fa, f)
|
||||
}
|
||||
|
||||
func ChainFirst[A, B any](f func(A) B) Operator[A, A] {
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return chain.ChainFirst(Chain[A, A], Map[B, A], f)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,5 +16,6 @@
|
||||
package identity
|
||||
|
||||
type (
|
||||
Operator[A, B any] = func(A) B
|
||||
Kleisli[A, B any] = func(A) B
|
||||
Operator[A, B any] = Kleisli[A, B]
|
||||
)
|
||||
|
||||
136
v2/io/bind.go
136
v2/io/bind.go
@@ -19,6 +19,7 @@ import (
|
||||
INTA "github.com/IBM/fp-go/v2/internal/apply"
|
||||
INTC "github.com/IBM/fp-go/v2/internal/chain"
|
||||
INTF "github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type S to be used with the Bind operation.
|
||||
@@ -58,7 +59,7 @@ func Do[S any](
|
||||
// }, fetchUser)
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) IO[T],
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return INTC.Bind(
|
||||
Chain[S1, S2],
|
||||
@@ -152,3 +153,136 @@ func ApS[S1, S2, T any](
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// portLens := lens.MakeLens(
|
||||
// func(c Config) int { return c.Port },
|
||||
// func(c Config, p int) Config { c.Port = p; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// io.Of(Config{Host: "localhost"}),
|
||||
// io.ApSL(portLens, io.Of(8080)),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa IO[T],
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// an IO that produces the new value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter asynchronously
|
||||
// increment := func(v int) io.IO[int] {
|
||||
// return io.Of(v + 1)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// io.Of(Counter{Value: 42}),
|
||||
// io.BindL(valueLens, increment),
|
||||
// ) // IO[Counter{Value: 43}]
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return Bind[S, S, T](lens.Set, func(s S) IO[T] {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in IO).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// io.Of(Counter{Value: 21}),
|
||||
// io.LetL(valueLens, double),
|
||||
// ) // IO[Counter{Value: 42}]
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[S, S] {
|
||||
return Let[S, S, T](lens.Set, func(s S) T {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// io.Of(Config{Debug: true, Timeout: 30}),
|
||||
// io.LetToL(debugLens, false),
|
||||
// ) // IO[Config{Debug: false, Timeout: 30}]
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return LetTo[S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -54,3 +55,144 @@ func TestApS(t *testing.T) {
|
||||
|
||||
assert.Equal(t, res(), "John Doe")
|
||||
}
|
||||
|
||||
// Test types for lens-based operations
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("BindL with successful transformation", func(t *testing.T) {
|
||||
increment := func(v int) IO[int] {
|
||||
return Of(v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Of(Counter{Value: 42}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
assert.Equal(t, Counter{Value: 43}, result())
|
||||
})
|
||||
|
||||
t.Run("BindL with multiple operations", func(t *testing.T) {
|
||||
double := func(v int) IO[int] {
|
||||
return Of(v * 2)
|
||||
}
|
||||
|
||||
addTen := func(v int) IO[int] {
|
||||
return Of(v + 10)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(Counter{Value: 5}),
|
||||
BindL(valueLens, double),
|
||||
BindL(valueLens, addTen),
|
||||
)
|
||||
|
||||
assert.Equal(t, Counter{Value: 20}, result())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("LetL with pure transformation", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe1(
|
||||
Of(Counter{Value: 21}),
|
||||
LetL(valueLens, double),
|
||||
)
|
||||
|
||||
assert.Equal(t, Counter{Value: 42}, result())
|
||||
})
|
||||
|
||||
t.Run("LetL with multiple transformations", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
addTen := func(v int) int { return v + 10 }
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(Counter{Value: 5}),
|
||||
LetL(valueLens, double),
|
||||
LetL(valueLens, addTen),
|
||||
)
|
||||
|
||||
assert.Equal(t, Counter{Value: 20}, result())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
ageLens := L.MakeLens(
|
||||
func(p Person) int { return p.Age },
|
||||
func(p Person, a int) Person { p.Age = a; return p },
|
||||
)
|
||||
|
||||
t.Run("LetToL with constant value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(Person{Name: "Alice", Age: 25}),
|
||||
LetToL(ageLens, 30),
|
||||
)
|
||||
|
||||
assert.Equal(t, Person{Name: "Alice", Age: 30}, result())
|
||||
})
|
||||
|
||||
t.Run("LetToL with multiple fields", func(t *testing.T) {
|
||||
nameLens := L.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, n string) Person { p.Name = n; return p },
|
||||
)
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(Person{Name: "Alice", Age: 25}),
|
||||
LetToL(ageLens, 30),
|
||||
LetToL(nameLens, "Bob"),
|
||||
)
|
||||
|
||||
assert.Equal(t, Person{Name: "Bob", Age: 30}, result())
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
ageLens := L.MakeLens(
|
||||
func(p Person) int { return p.Age },
|
||||
func(p Person, a int) Person { p.Age = a; return p },
|
||||
)
|
||||
|
||||
t.Run("ApSL with value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(Person{Name: "Alice", Age: 25}),
|
||||
ApSL(ageLens, Of(30)),
|
||||
)
|
||||
|
||||
assert.Equal(t, Person{Name: "Alice", Age: 30}, result())
|
||||
})
|
||||
|
||||
t.Run("ApSL with chaining", func(t *testing.T) {
|
||||
nameLens := L.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, n string) Person { p.Name = n; return p },
|
||||
)
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(Person{Name: "Alice", Age: 25}),
|
||||
ApSL(ageLens, Of(30)),
|
||||
ApSL(nameLens, Of("Bob")),
|
||||
)
|
||||
|
||||
assert.Equal(t, Person{Name: "Bob", Age: 30}, result())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
// whether the body action returns and error or not.
|
||||
func Bracket[A, B, ANY any](
|
||||
acquire IO[A],
|
||||
use func(A) IO[B],
|
||||
use Kleisli[A, B],
|
||||
release func(A, B) IO[ANY],
|
||||
) IO[B] {
|
||||
return INTB.Bracket[IO[A], IO[B], IO[ANY], B, A, B](
|
||||
|
||||
2643
v2/io/gen.go
2643
v2/io/gen.go
File diff suppressed because it is too large
Load Diff
11
v2/io/io.go
11
v2/io/io.go
@@ -44,7 +44,8 @@ type (
|
||||
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioltagt] for more details
|
||||
IO[A any] = func() A
|
||||
|
||||
Operator[A, B any] = R.Reader[IO[A], IO[B]]
|
||||
Kleisli[A, B any] = R.Reader[A, IO[B]]
|
||||
Operator[A, B any] = Kleisli[IO[A], B]
|
||||
Monoid[A any] = M.Monoid[IO[A]]
|
||||
Semigroup[A any] = S.Semigroup[IO[A]]
|
||||
)
|
||||
@@ -121,14 +122,14 @@ func MapTo[A, B any](b B) Operator[A, B] {
|
||||
}
|
||||
|
||||
// MonadChain composes computations in sequence, using the return value of one computation to determine the next computation.
|
||||
func MonadChain[A, B any](fa IO[A], f func(A) IO[B]) IO[B] {
|
||||
func MonadChain[A, B any](fa IO[A], f Kleisli[A, B]) IO[B] {
|
||||
return func() B {
|
||||
return f(fa())()
|
||||
}
|
||||
}
|
||||
|
||||
// Chain composes computations in sequence, using the return value of one computation to determine the next computation.
|
||||
func Chain[A, B any](f func(A) IO[B]) Operator[A, B] {
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return F.Bind2nd(MonadChain[A, B], f)
|
||||
}
|
||||
|
||||
@@ -201,13 +202,13 @@ func Memoize[A any](ma IO[A]) IO[A] {
|
||||
|
||||
// MonadChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
|
||||
// keeping only the result of the first.
|
||||
func MonadChainFirst[A, B any](fa IO[A], f func(A) IO[B]) IO[A] {
|
||||
func MonadChainFirst[A, B any](fa IO[A], f Kleisli[A, B]) IO[A] {
|
||||
return chain.MonadChainFirst(MonadChain[A, A], MonadMap[B, A], fa, f)
|
||||
}
|
||||
|
||||
// ChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
|
||||
// keeping only the result of the first.
|
||||
func ChainFirst[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return chain.ChainFirst(
|
||||
Chain[A, A],
|
||||
Map[B, A],
|
||||
|
||||
@@ -32,9 +32,9 @@ import (
|
||||
// io.ChainFirst(io.Logger[User]()("Fetched user")),
|
||||
// processUser,
|
||||
// )
|
||||
func Logger[A any](loggers ...*log.Logger) func(string) func(A) IO[any] {
|
||||
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
|
||||
_, right := L.LoggingCallbacks(loggers...)
|
||||
return func(prefix string) func(A) IO[any] {
|
||||
return func(prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
right("%s: %v", prefix, a)
|
||||
@@ -53,7 +53,7 @@ func Logger[A any](loggers ...*log.Logger) func(string) func(A) IO[any] {
|
||||
// io.ChainFirst(io.Logf[User]("User: %+v")),
|
||||
// processUser,
|
||||
// )
|
||||
func Logf[A any](prefix string) func(A) IO[any] {
|
||||
func Logf[A any](prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
log.Printf(prefix, a)
|
||||
@@ -72,7 +72,7 @@ func Logf[A any](prefix string) func(A) IO[any] {
|
||||
// io.ChainFirst(io.Printf[User]("User: %+v\n")),
|
||||
// processUser,
|
||||
// )
|
||||
func Printf[A any](prefix string) func(A) IO[any] {
|
||||
func Printf[A any](prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
fmt.Printf(prefix, a)
|
||||
|
||||
@@ -35,7 +35,7 @@ func (o *ioMonad[A, B]) Map(f func(A) B) Operator[A, B] {
|
||||
return Map(f)
|
||||
}
|
||||
|
||||
func (o *ioMonad[A, B]) Chain(f func(A) IO[B]) Operator[A, B] {
|
||||
func (o *ioMonad[A, B]) Chain(f Kleisli[A, B]) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
// return readData(f)
|
||||
// })
|
||||
func WithResource[
|
||||
R, A, ANY any](onCreate IO[R], onRelease func(R) IO[ANY]) func(func(R) IO[A]) IO[A] {
|
||||
R, A, ANY any](onCreate IO[R], onRelease func(R) IO[ANY]) Kleisli[Kleisli[R, A], A] {
|
||||
// simply map to implementation of bracket
|
||||
return function.Bind13of3(Bracket[R, A, ANY])(onCreate, function.Ignore2of2[A](onRelease))
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ type (
|
||||
// )
|
||||
func Retrying[A any](
|
||||
policy R.RetryPolicy,
|
||||
action func(R.RetryStatus) IO[A],
|
||||
action Kleisli[R.RetryStatus, A],
|
||||
check func(A) bool,
|
||||
) IO[A] {
|
||||
// get an implementation for the types
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
// fetchUsers := func(id int) io.IO[User] { return fetchUser(id) }
|
||||
// users := io.MonadTraverseArray([]int{1, 2, 3}, fetchUsers)
|
||||
// result := users() // []User with all fetched users
|
||||
func MonadTraverseArray[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
|
||||
func MonadTraverseArray[A, B any](tas []A, f Kleisli[A, B]) IO[[]B] {
|
||||
return INTA.MonadTraverse(
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -50,7 +50,7 @@ func MonadTraverseArray[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
|
||||
// return fetchUser(id)
|
||||
// })
|
||||
// users := fetchUsers([]int{1, 2, 3})
|
||||
func TraverseArray[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return INTA.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -68,7 +68,7 @@ func TraverseArray[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
|
||||
// numbered := io.TraverseArrayWithIndex(func(i int, s string) io.IO[string] {
|
||||
// return io.Of(fmt.Sprintf("%d: %s", i, s))
|
||||
// })
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) IO[B]) func([]A) IO[[]B] {
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) IO[B]) Kleisli[[]A, []B] {
|
||||
return INTA.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -98,7 +98,7 @@ func SequenceArray[A any](tas []IO[A]) IO[[]A] {
|
||||
// fetchData := func(url string) io.IO[Data] { return fetch(url) }
|
||||
// urls := map[string]string{"a": "http://a.com", "b": "http://b.com"}
|
||||
// data := io.MonadTraverseRecord(urls, fetchData)
|
||||
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) IO[B]) IO[map[K]B] {
|
||||
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) IO[map[K]B] {
|
||||
return INTR.MonadTraverse(
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -112,7 +112,7 @@ func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) IO[B]) I
|
||||
// TraverseRecord returns a function that applies an IO-returning function to each value
|
||||
// in a map and collects the results. This is the curried version of MonadTraverseRecord.
|
||||
// Executes in parallel by default.
|
||||
func TraverseRecord[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO[map[K]B] {
|
||||
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return INTR.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -124,7 +124,7 @@ func TraverseRecord[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO[ma
|
||||
|
||||
// TraverseRecordWithIndex is like TraverseRecord but the function also receives the key.
|
||||
// Executes in parallel by default.
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) IO[B]) func(map[K]A) IO[map[K]B] {
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) IO[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return INTR.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -153,7 +153,7 @@ func SequenceRecord[K comparable, A any](tas map[K]IO[A]) IO[map[K]A] {
|
||||
//
|
||||
// fetchUsers := func(id int) io.IO[User] { return fetchUser(id) }
|
||||
// users := io.MonadTraverseArraySeq([]int{1, 2, 3}, fetchUsers)
|
||||
func MonadTraverseArraySeq[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
|
||||
func MonadTraverseArraySeq[A, B any](tas []A, f Kleisli[A, B]) IO[[]B] {
|
||||
return INTA.MonadTraverse(
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -167,7 +167,7 @@ func MonadTraverseArraySeq[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
|
||||
// TraverseArraySeq returns a function that applies an IO-returning function to each element
|
||||
// of an array and collects the results. Executes sequentially (one after another).
|
||||
// Use this when operations must be performed in order or when parallel execution is not desired.
|
||||
func TraverseArraySeq[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
|
||||
func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return INTA.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -179,7 +179,7 @@ func TraverseArraySeq[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
|
||||
|
||||
// TraverseArrayWithIndexSeq is like TraverseArraySeq but the function also receives the index.
|
||||
// Executes sequentially (one after another).
|
||||
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) IO[B]) func([]A) IO[[]B] {
|
||||
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) IO[B]) Kleisli[[]A, []B] {
|
||||
return INTA.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -197,7 +197,7 @@ func SequenceArraySeq[A any](tas []IO[A]) IO[[]A] {
|
||||
|
||||
// MonadTraverseRecordSeq applies an IO-returning function to each value in a map
|
||||
// and collects the results into an IO of a map. Executes sequentially.
|
||||
func MonadTraverseRecordSeq[K comparable, A, B any](tas map[K]A, f func(A) IO[B]) IO[map[K]B] {
|
||||
func MonadTraverseRecordSeq[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) IO[map[K]B] {
|
||||
return INTR.MonadTraverse(
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -210,7 +210,7 @@ func MonadTraverseRecordSeq[K comparable, A, B any](tas map[K]A, f func(A) IO[B]
|
||||
|
||||
// TraverseRecordSeq returns a function that applies an IO-returning function to each value
|
||||
// in a map and collects the results. Executes sequentially (one after another).
|
||||
func TraverseRecordSeq[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO[map[K]B] {
|
||||
func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return INTR.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -223,7 +223,7 @@ func TraverseRecordSeq[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO
|
||||
// TraverseRecordWithIndeSeq is like TraverseRecordSeq but the function also receives the key.
|
||||
// Executes sequentially (one after another).
|
||||
// Note: There's a typo in the function name (Inde instead of Index) for backward compatibility.
|
||||
func TraverseRecordWithIndeSeq[K comparable, A, B any](f func(K, A) IO[B]) func(map[K]A) IO[map[K]B] {
|
||||
func TraverseRecordWithIndeSeq[K comparable, A, B any](f func(K, A) IO[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return INTR.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
|
||||
@@ -19,16 +19,62 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
// result := ioeither.Do[error](State{})
|
||||
func Do[E, S any](
|
||||
empty S,
|
||||
) IOEither[E, S] {
|
||||
return Of[E](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// ioeither.Do[error](State{}),
|
||||
// ioeither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser()
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// ioeither.Bind(
|
||||
// func(posts []Post) func(State) State {
|
||||
// return func(s State) State { s.Posts = posts; return s }
|
||||
// },
|
||||
// func(s State) ioeither.IOEither[error, []Post] {
|
||||
// // This can access s.User from the previous step
|
||||
// return ioeither.TryCatch(func() ([]Post, error) {
|
||||
// return fetchPostsForUser(s.User.ID)
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) IOEither[E, T],
|
||||
@@ -75,7 +121,39 @@ func BindTo[E, S1, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUser := ioeither.Right[error](User{ID: 1, Name: "Alice"})
|
||||
// getPosts := ioeither.Right[error]([]Post{{ID: 1, Title: "Hello"}})
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// ioeither.Do[error](State{}),
|
||||
// ioeither.ApS(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// ioeither.ApS(
|
||||
// func(posts []Post) func(State) State {
|
||||
// return func(s State) State { s.Posts = posts; return s }
|
||||
// },
|
||||
// getPosts,
|
||||
// ),
|
||||
// )
|
||||
func ApS[E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOEither[E, T],
|
||||
@@ -87,3 +165,139 @@ func ApS[E, S1, S2, T any](
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// portLens := lens.MakeLens(
|
||||
// func(c Config) int { return c.Port },
|
||||
// func(c Config, p int) Config { c.Port = p; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// ioeither.Of[error](Config{Host: "localhost"}),
|
||||
// ioeither.ApSL(portLens, ioeither.Of[error](8080)),
|
||||
// )
|
||||
func ApSL[E, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa IOEither[E, T],
|
||||
) Operator[E, S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// an IOEither that produces the new value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// increment := func(v int) ioeither.IOEither[error, int] {
|
||||
// return ioeither.TryCatch(func() (int, error) {
|
||||
// if v >= 100 {
|
||||
// return 0, errors.New("overflow")
|
||||
// }
|
||||
// return v + 1, nil
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// ioeither.Of[error](Counter{Value: 42}),
|
||||
// ioeither.BindL(valueLens, increment),
|
||||
// )
|
||||
func BindL[E, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) IOEither[E, T],
|
||||
) Operator[E, S, S] {
|
||||
return Bind[E, S, S, T](lens.Set, func(s S) IOEither[E, T] {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in IOEither).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// ioeither.Of[error](Counter{Value: 21}),
|
||||
// ioeither.LetL(valueLens, double),
|
||||
// )
|
||||
func LetL[E, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[E, S, S] {
|
||||
return Let[E, S, S, T](lens.Set, func(s S) T {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// ioeither.Of[error](Config{Debug: true, Timeout: 30}),
|
||||
// ioeither.LetToL(debugLens, false),
|
||||
// )
|
||||
func LetToL[E, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[E, S, S] {
|
||||
return LetTo[E, S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
// TraverseArray transforms an array
|
||||
func TraverseArray[A, B any](f func(A) IOOption[B]) func([]A) IOOption[[]B] {
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return function.Flow2(
|
||||
io.TraverseArray(f),
|
||||
io.Map(option.SequenceArray[B]),
|
||||
@@ -30,7 +30,7 @@ func TraverseArray[A, B any](f func(A) IOOption[B]) func([]A) IOOption[[]B] {
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex transforms an array
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) IOOption[B]) func([]A) IOOption[[]B] {
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) IOOption[B]) Kleisli[[]A, []B] {
|
||||
return function.Flow2(
|
||||
io.TraverseArrayWithIndex(f),
|
||||
io.Map(option.SequenceArray[B]),
|
||||
|
||||
@@ -19,20 +19,62 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
// result := iooption.Do(State{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) IOOption[S] {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// iooption.Do(State{}),
|
||||
// iooption.Bind(
|
||||
// func(name string) func(State) State {
|
||||
// return func(s State) State { s.Name = name; return s }
|
||||
// },
|
||||
// func(s State) iooption.IOOption[string] {
|
||||
// return iooption.FromIO(io.Of("Alice"))
|
||||
// },
|
||||
// ),
|
||||
// iooption.Bind(
|
||||
// func(age int) func(State) State {
|
||||
// return func(s State) State { s.Age = age; return s }
|
||||
// },
|
||||
// func(s State) iooption.IOOption[int] {
|
||||
// // This can access s.Name from the previous step
|
||||
// return iooption.FromIO(io.Of(len(s.Name) * 10))
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) IOOption[T],
|
||||
) func(IOOption[S1]) IOOption[S2] {
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
return chain.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[T, S2],
|
||||
@@ -45,7 +87,7 @@ func Bind[S1, S2, T any](
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(IOOption[S1]) IOOption[S2] {
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
return functor.Let(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
@@ -57,7 +99,7 @@ func Let[S1, S2, T any](
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(IOOption[S1]) IOOption[S2] {
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
return functor.LetTo(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
@@ -68,18 +110,50 @@ func LetTo[S1, S2, T any](
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(IOOption[T]) IOOption[S1] {
|
||||
) Kleisli[IOOption[T], S1] {
|
||||
return chain.BindTo(
|
||||
Map[T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getName := iooption.Some("Alice")
|
||||
// getAge := iooption.Some(30)
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// iooption.Do(State{}),
|
||||
// iooption.ApS(
|
||||
// func(name string) func(State) State {
|
||||
// return func(s State) State { s.Name = name; return s }
|
||||
// },
|
||||
// getName,
|
||||
// ),
|
||||
// iooption.ApS(
|
||||
// func(age int) func(State) State {
|
||||
// return func(s State) State { s.Age = age; return s }
|
||||
// },
|
||||
// getAge,
|
||||
// ),
|
||||
// )
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOOption[T],
|
||||
) func(IOOption[S1]) IOOption[S2] {
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
return apply.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
@@ -87,3 +161,136 @@ func ApS[S1, S2, T any](
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(s State) int { return s.Age },
|
||||
// func(s State, a int) State { s.Age = a; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// iooption.Of(State{Name: "Alice"}),
|
||||
// iooption.ApSL(ageLens, iooption.Some(30)),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa IOOption[T],
|
||||
) Kleisli[IOOption[S], S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// an IOOption that produces the new value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but return None if it would exceed 100
|
||||
// increment := func(v int) iooption.IOOption[int] {
|
||||
// return iooption.FromIO(io.Of(v + 1))
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// iooption.Of(Counter{Value: 42}),
|
||||
// iooption.BindL(valueLens, increment),
|
||||
// ) // IOOption[Counter{Value: 43}]
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Kleisli[IOOption[S], S] {
|
||||
return Bind[S, S, T](lens.Set, func(s S) IOOption[T] {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in IOOption).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// iooption.Of(Counter{Value: 21}),
|
||||
// iooption.LetL(valueLens, double),
|
||||
// ) // IOOption[Counter{Value: 42}]
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Kleisli[IOOption[S], S] {
|
||||
return Let[S, S, T](lens.Set, func(s S) T {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// iooption.Of(Config{Debug: true, Timeout: 30}),
|
||||
// iooption.LetToL(debugLens, false),
|
||||
// ) // IOOption[Config{Debug: false, Timeout: 30}]
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Kleisli[IOOption[S], S] {
|
||||
return LetTo[S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
// whether the body action returns and error or not.
|
||||
func Bracket[A, B, ANY any](
|
||||
acquire IOOption[A],
|
||||
use func(A) IOOption[B],
|
||||
use Kleisli[A, B],
|
||||
release func(A, Option[B]) IOOption[ANY],
|
||||
) IOOption[B] {
|
||||
return G.Bracket[IOOption[A], IOOption[B], IOOption[ANY], Option[B], A, B](
|
||||
|
||||
2643
v2/iooption/gen.go
2643
v2/iooption/gen.go
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ import "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource
|
||||
func WithResource[
|
||||
R, A, ANY any](onCreate IOOption[R], onRelease func(R) IOOption[ANY]) func(func(R) IOOption[A]) IOOption[A] {
|
||||
R, A, ANY any](onCreate IOOption[R], onRelease func(R) IOOption[ANY]) Kleisli[Kleisli[R, A], A] {
|
||||
// simply map to implementation of bracket
|
||||
return function.Bind13of3(Bracket[R, A, ANY])(onCreate, function.Ignore2of2[Option[A]](onRelease))
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
// Retrying will retry the actions according to the check policy
|
||||
func Retrying[A any](
|
||||
policy R.RetryPolicy,
|
||||
action func(R.RetryStatus) IOOption[A],
|
||||
action Kleisli[R.RetryStatus, A],
|
||||
check func(A) bool,
|
||||
) IOOption[A] {
|
||||
// get an implementation for the types
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -31,4 +32,7 @@ type (
|
||||
// IOOption represents a synchronous computation that may fail
|
||||
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioeitherlte-agt] for more details
|
||||
IOOption[A any] = io.IO[Option[A]]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, IOOption[B]]
|
||||
Operator[A, B any] = Kleisli[IOOption[A], B]
|
||||
)
|
||||
|
||||
@@ -19,18 +19,60 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
// result := stateless.Do(State{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) Iterator[S] {
|
||||
return G.Do[Iterator[S]](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
// For iterators, this produces the cartesian product of all values.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// stateless.Do(State{}),
|
||||
// stateless.Bind(
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// func(s State) stateless.Iterator[int] {
|
||||
// return stateless.Of(1, 2, 3)
|
||||
// },
|
||||
// ),
|
||||
// stateless.Bind(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) stateless.Iterator[int] {
|
||||
// // This can access s.X from the previous step
|
||||
// return stateless.Of(s.X * 10, s.X * 20)
|
||||
// },
|
||||
// ),
|
||||
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) Iterator[T],
|
||||
) func(Iterator[S1]) Iterator[S2] {
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[Iterator[S1], S2] {
|
||||
return G.Bind[Iterator[S1], Iterator[S2], Iterator[T], S1, S2, T](setter, f)
|
||||
}
|
||||
|
||||
@@ -38,7 +80,7 @@ func Bind[S1, S2, T any](
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(Iterator[S1]) Iterator[S2] {
|
||||
) Kleisli[Iterator[S1], S2] {
|
||||
return G.Let[Iterator[S1], Iterator[S2], S1, S2, T](setter, f)
|
||||
}
|
||||
|
||||
@@ -46,21 +88,53 @@ func Let[S1, S2, T any](
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(Iterator[S1]) Iterator[S2] {
|
||||
) Kleisli[Iterator[S1], S2] {
|
||||
return G.LetTo[Iterator[S1], Iterator[S2], S1, S2, T](setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Iterator[T]) Iterator[S1] {
|
||||
) Kleisli[Iterator[T], S1] {
|
||||
return G.BindTo[Iterator[S1], Iterator[T], S1, T](setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// xValues := stateless.Of(1, 2, 3)
|
||||
// yValues := stateless.Of(10, 20)
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// stateless.Do(State{}),
|
||||
// stateless.ApS(
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// xValues,
|
||||
// ),
|
||||
// stateless.ApS(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// yValues,
|
||||
// ),
|
||||
// ) // Produces all combinations: {1,10}, {1,20}, {2,10}, {2,20}, {3,10}, {3,20}
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Iterator[T],
|
||||
) func(Iterator[S1]) Iterator[S2] {
|
||||
) Kleisli[Iterator[S1], S2] {
|
||||
return G.ApS[Iterator[func(T) S2], Iterator[S1], Iterator[S2], Iterator[T], S1, S2, T](setter, fa)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,6 @@ import (
|
||||
|
||||
// Compress returns an [Iterator] that filters elements from a data [Iterator] returning only those that have a corresponding element in selector [Iterator] that evaluates to `true`.
|
||||
// Stops when either the data or selectors iterator has been exhausted.
|
||||
func Compress[U any](sel Iterator[bool]) func(Iterator[U]) Iterator[U] {
|
||||
func Compress[U any](sel Iterator[bool]) Kleisli[Iterator[U], U] {
|
||||
return G.Compress[Iterator[U], Iterator[bool], Iterator[P.Pair[U, bool]]](sel)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@ import (
|
||||
|
||||
// DropWhile creates an [Iterator] that drops elements from the [Iterator] as long as the predicate is true; afterwards, returns every element.
|
||||
// Note, the [Iterator] does not produce any output until the predicate first becomes false
|
||||
func DropWhile[U any](pred func(U) bool) func(Iterator[U]) Iterator[U] {
|
||||
func DropWhile[U any](pred func(U) bool) Kleisli[Iterator[U], U] {
|
||||
return G.DropWhile[Iterator[U]](pred)
|
||||
}
|
||||
|
||||
@@ -23,14 +23,56 @@ import (
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
// result := generic.Do[Iterator[State]](State{})
|
||||
func Do[GS ~func() O.Option[P.Pair[GS, S]], S any](
|
||||
empty S,
|
||||
) GS {
|
||||
return Of[GS](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
// For iterators, this produces the cartesian product where later steps can use values from earlier steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// generic.Do[Iterator[State]](State{}),
|
||||
// generic.Bind[Iterator[State], Iterator[State], Iterator[int], State, State, int](
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// func(s State) Iterator[int] {
|
||||
// return generic.Of[Iterator[int]](1, 2, 3)
|
||||
// },
|
||||
// ),
|
||||
// generic.Bind[Iterator[State], Iterator[State], Iterator[int], State, State, int](
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) Iterator[int] {
|
||||
// // This can access s.X from the previous step
|
||||
// return generic.Of[Iterator[int]](s.X * 10, s.X * 20)
|
||||
// },
|
||||
// ),
|
||||
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
|
||||
func Bind[GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], GA ~func() O.Option[P.Pair[GA, A]], S1, S2, A any](
|
||||
setter func(A) func(S1) S2,
|
||||
f func(S1) GA,
|
||||
@@ -78,7 +120,39 @@ func BindTo[GS1 ~func() O.Option[P.Pair[GS1, S1]], GA ~func() O.Option[P.Pair[GA
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel. For iterators, this produces the cartesian product.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y string
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// xIter := generic.Of[Iterator[int]](1, 2, 3)
|
||||
// yIter := generic.Of[Iterator[string]]("a", "b")
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// generic.Do[Iterator[State]](State{}),
|
||||
// generic.ApS[Iterator[func(int) State], Iterator[State], Iterator[State], Iterator[int], State, State, int](
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// xIter,
|
||||
// ),
|
||||
// generic.ApS[Iterator[func(string) State], Iterator[State], Iterator[State], Iterator[string], State, State, string](
|
||||
// func(y string) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// yIter,
|
||||
// ),
|
||||
// ) // Produces: {1,"a"}, {1,"b"}, {2,"a"}, {2,"b"}, {3,"a"}, {3,"b"}
|
||||
func ApS[GAS2 ~func() O.Option[P.Pair[GAS2, func(A) S2]], GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], GA ~func() O.Option[P.Pair[GA, A]], S1, S2, A any](
|
||||
setter func(A) func(S1) S2,
|
||||
fa GA,
|
||||
|
||||
@@ -18,15 +18,11 @@ package stateless
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
|
||||
L "github.com/IBM/fp-go/v2/lazy"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Iterator represents a stateless, pure way to iterate over a sequence
|
||||
type Iterator[U any] L.Lazy[O.Option[pair.Pair[Iterator[U], U]]]
|
||||
|
||||
// Next returns the [Iterator] for the next element in an iterator [pair.Pair]
|
||||
func Next[U any](m pair.Pair[Iterator[U], U]) Iterator[U] {
|
||||
return pair.Head(m)
|
||||
@@ -68,15 +64,15 @@ func MonadMap[U, V any](ma Iterator[U], f func(U) V) Iterator[V] {
|
||||
}
|
||||
|
||||
// Map transforms an [Iterator] of type [U] into an [Iterator] of type [V] via a mapping function
|
||||
func Map[U, V any](f func(U) V) func(ma Iterator[U]) Iterator[V] {
|
||||
func Map[U, V any](f func(U) V) Operator[U, V] {
|
||||
return G.Map[Iterator[V], Iterator[U]](f)
|
||||
}
|
||||
|
||||
func MonadChain[U, V any](ma Iterator[U], f func(U) Iterator[V]) Iterator[V] {
|
||||
func MonadChain[U, V any](ma Iterator[U], f Kleisli[U, V]) Iterator[V] {
|
||||
return G.MonadChain[Iterator[V], Iterator[U]](ma, f)
|
||||
}
|
||||
|
||||
func Chain[U, V any](f func(U) Iterator[V]) func(Iterator[U]) Iterator[V] {
|
||||
func Chain[U, V any](f Kleisli[U, V]) Kleisli[Iterator[U], V] {
|
||||
return G.Chain[Iterator[V], Iterator[U]](f)
|
||||
}
|
||||
|
||||
@@ -101,17 +97,17 @@ func Replicate[U any](a U) Iterator[U] {
|
||||
}
|
||||
|
||||
// FilterMap filters and transforms the content of an iterator
|
||||
func FilterMap[U, V any](f func(U) O.Option[V]) func(ma Iterator[U]) Iterator[V] {
|
||||
func FilterMap[U, V any](f func(U) O.Option[V]) Operator[U, V] {
|
||||
return G.FilterMap[Iterator[V], Iterator[U]](f)
|
||||
}
|
||||
|
||||
// Filter filters the content of an iterator
|
||||
func Filter[U any](f func(U) bool) func(ma Iterator[U]) Iterator[U] {
|
||||
func Filter[U any](f func(U) bool) Operator[U, U] {
|
||||
return G.Filter[Iterator[U]](f)
|
||||
}
|
||||
|
||||
// Ap is the applicative functor for iterators
|
||||
func Ap[V, U any](ma Iterator[U]) func(Iterator[func(U) V]) Iterator[V] {
|
||||
func Ap[V, U any](ma Iterator[U]) Operator[func(U) V, V] {
|
||||
return G.Ap[Iterator[func(U) V], Iterator[V]](ma)
|
||||
}
|
||||
|
||||
@@ -132,7 +128,7 @@ func Count(start int) Iterator[int] {
|
||||
}
|
||||
|
||||
// FilterChain filters and transforms the content of an iterator
|
||||
func FilterChain[U, V any](f func(U) O.Option[Iterator[V]]) func(ma Iterator[U]) Iterator[V] {
|
||||
func FilterChain[U, V any](f func(U) O.Option[Iterator[V]]) Operator[U, V] {
|
||||
return G.FilterChain[Iterator[Iterator[V]], Iterator[V], Iterator[U]](f)
|
||||
}
|
||||
|
||||
@@ -146,10 +142,10 @@ func Fold[U any](m M.Monoid[U]) func(Iterator[U]) U {
|
||||
return G.Fold[Iterator[U]](m)
|
||||
}
|
||||
|
||||
func MonadChainFirst[U, V any](ma Iterator[U], f func(U) Iterator[V]) Iterator[U] {
|
||||
func MonadChainFirst[U, V any](ma Iterator[U], f Kleisli[U, V]) Iterator[U] {
|
||||
return G.MonadChainFirst[Iterator[V], Iterator[U], U, V](ma, f)
|
||||
}
|
||||
|
||||
func ChainFirst[U, V any](f func(U) Iterator[V]) func(Iterator[U]) Iterator[U] {
|
||||
func ChainFirst[U, V any](f Kleisli[U, V]) Operator[U, U] {
|
||||
return G.ChainFirst[Iterator[V], Iterator[U], U, V](f)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,19 @@
|
||||
|
||||
package stateless
|
||||
|
||||
import "github.com/IBM/fp-go/v2/option"
|
||||
import (
|
||||
L "github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Iterator represents a stateless, pure way to iterate over a sequence
|
||||
Iterator[U any] L.Lazy[Option[pair.Pair[Iterator[U], U]]]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, Iterator[B]]
|
||||
Operator[A, B any] = Kleisli[Iterator[A], B]
|
||||
)
|
||||
|
||||
230
v2/lazy/bind.go
230
v2/lazy/bind.go
@@ -16,21 +16,64 @@
|
||||
package lazy
|
||||
|
||||
import (
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data Data
|
||||
// }
|
||||
// result := lazy.Do(State{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) Lazy[S] {
|
||||
return io.Do(empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data Data
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{}),
|
||||
// lazy.Bind(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// func(s State) lazy.Lazy[Config] {
|
||||
// return lazy.MakeLazy(func() Config { return loadConfig() })
|
||||
// },
|
||||
// ),
|
||||
// lazy.Bind(
|
||||
// func(data Data) func(State) State {
|
||||
// return func(s State) State { s.Data = data; return s }
|
||||
// },
|
||||
// func(s State) lazy.Lazy[Data] {
|
||||
// // This can access s.Config from the previous step
|
||||
// return lazy.MakeLazy(func() Data { return loadData(s.Config) })
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) Lazy[T],
|
||||
) func(Lazy[S1]) Lazy[S2] {
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[Lazy[S1], S2] {
|
||||
return io.Bind(setter, f)
|
||||
}
|
||||
|
||||
@@ -38,7 +81,7 @@ func Bind[S1, S2, T any](
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(Lazy[S1]) Lazy[S2] {
|
||||
) Kleisli[Lazy[S1], S2] {
|
||||
return io.Let(setter, f)
|
||||
}
|
||||
|
||||
@@ -46,21 +89,190 @@ func Let[S1, S2, T any](
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(Lazy[S1]) Lazy[S2] {
|
||||
) Kleisli[Lazy[S1], S2] {
|
||||
return io.LetTo(setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Lazy[T]) Lazy[S1] {
|
||||
) Kleisli[Lazy[T], S1] {
|
||||
return io.BindTo(setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data Data
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getConfig := lazy.MakeLazy(func() Config { return loadConfig() })
|
||||
// getData := lazy.MakeLazy(func() Data { return loadData() })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{}),
|
||||
// lazy.ApS(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// getConfig,
|
||||
// ),
|
||||
// lazy.ApS(
|
||||
// func(data Data) func(State) State {
|
||||
// return func(s State) State { s.Data = data; return s }
|
||||
// },
|
||||
// getData,
|
||||
// ),
|
||||
// )
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Lazy[T],
|
||||
) func(Lazy[S1]) Lazy[S2] {
|
||||
) Kleisli[Lazy[S1], S2] {
|
||||
return io.ApS(setter, fa)
|
||||
}
|
||||
|
||||
// ApSL is a variant of ApS that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. This allows you to work with nested fields without manually managing
|
||||
// the update logic.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data string
|
||||
// }
|
||||
//
|
||||
// configLens := L.Prop[State, Config]("Config")
|
||||
// getConfig := lazy.MakeLazy(func() Config { return Config{Host: "localhost", Port: 8080} })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{}),
|
||||
// lazy.ApSL(configLens, getConfig),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Lazy[T],
|
||||
) Kleisli[Lazy[S], S] {
|
||||
return io.ApSL(lens, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new computation that produces an updated value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data string
|
||||
// }
|
||||
//
|
||||
// configLens := L.Prop[State, Config]("Config")
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{Config: Config{Host: "localhost"}}),
|
||||
// lazy.BindL(configLens, func(cfg Config) lazy.Lazy[Config] {
|
||||
// return lazy.MakeLazy(func() Config {
|
||||
// cfg.Port = 8080
|
||||
// return cfg
|
||||
// })
|
||||
// }),
|
||||
// )
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Kleisli[Lazy[S], S] {
|
||||
return io.BindL(lens, f)
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new value (without wrapping in a monad).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data string
|
||||
// }
|
||||
//
|
||||
// configLens := L.Prop[State, Config]("Config")
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{Config: Config{Host: "localhost"}}),
|
||||
// lazy.LetL(configLens, func(cfg Config) Config {
|
||||
// cfg.Port = 8080
|
||||
// return cfg
|
||||
// }),
|
||||
// )
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Kleisli[Lazy[S], S] {
|
||||
return io.LetL(lens, f)
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The value b is set directly to the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data string
|
||||
// }
|
||||
//
|
||||
// configLens := L.Prop[State, Config]("Config")
|
||||
// newConfig := Config{Host: "localhost", Port: 8080}
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{}),
|
||||
// lazy.LetToL(configLens, newConfig),
|
||||
// )
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Kleisli[Lazy[S], S] {
|
||||
return io.LetToL(lens, b)
|
||||
}
|
||||
|
||||
@@ -21,9 +21,6 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
// Lazy represents a synchronous computation without side effects
|
||||
type Lazy[A any] = func() A
|
||||
|
||||
func Of[A any](a A) Lazy[A] {
|
||||
return io.Of(a)
|
||||
}
|
||||
@@ -53,17 +50,17 @@ func MonadMapTo[A, B any](fa Lazy[A], b B) Lazy[B] {
|
||||
return io.MonadMapTo(fa, b)
|
||||
}
|
||||
|
||||
func MapTo[A, B any](b B) func(Lazy[A]) Lazy[B] {
|
||||
func MapTo[A, B any](b B) Kleisli[Lazy[A], B] {
|
||||
return io.MapTo[A](b)
|
||||
}
|
||||
|
||||
// MonadChain composes computations in sequence, using the return value of one computation to determine the next computation.
|
||||
func MonadChain[A, B any](fa Lazy[A], f func(A) Lazy[B]) Lazy[B] {
|
||||
func MonadChain[A, B any](fa Lazy[A], f Kleisli[A, B]) Lazy[B] {
|
||||
return io.MonadChain(fa, f)
|
||||
}
|
||||
|
||||
// Chain composes computations in sequence, using the return value of one computation to determine the next computation.
|
||||
func Chain[A, B any](f func(A) Lazy[B]) func(Lazy[A]) Lazy[B] {
|
||||
func Chain[A, B any](f Kleisli[A, B]) Kleisli[Lazy[A], B] {
|
||||
return io.Chain(f)
|
||||
}
|
||||
|
||||
@@ -86,13 +83,13 @@ func Memoize[A any](ma Lazy[A]) Lazy[A] {
|
||||
|
||||
// MonadChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
|
||||
// keeping only the result of the first.
|
||||
func MonadChainFirst[A, B any](fa Lazy[A], f func(A) Lazy[B]) Lazy[A] {
|
||||
func MonadChainFirst[A, B any](fa Lazy[A], f Kleisli[A, B]) Lazy[A] {
|
||||
return io.MonadChainFirst(fa, f)
|
||||
}
|
||||
|
||||
// ChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
|
||||
// keeping only the result of the first.
|
||||
func ChainFirst[A, B any](f func(A) Lazy[B]) func(Lazy[A]) Lazy[A] {
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Kleisli[Lazy[A], A] {
|
||||
return io.ChainFirst(f)
|
||||
}
|
||||
|
||||
@@ -102,7 +99,7 @@ func MonadApFirst[A, B any](first Lazy[A], second Lazy[B]) Lazy[A] {
|
||||
}
|
||||
|
||||
// ApFirst combines two effectful actions, keeping only the result of the first.
|
||||
func ApFirst[A, B any](second Lazy[B]) func(Lazy[A]) Lazy[A] {
|
||||
func ApFirst[A, B any](second Lazy[B]) Kleisli[Lazy[A], A] {
|
||||
return io.ApFirst[A](second)
|
||||
}
|
||||
|
||||
@@ -112,7 +109,7 @@ func MonadApSecond[A, B any](first Lazy[A], second Lazy[B]) Lazy[B] {
|
||||
}
|
||||
|
||||
// ApSecond combines two effectful actions, keeping only the result of the second.
|
||||
func ApSecond[A, B any](second Lazy[B]) func(Lazy[A]) Lazy[B] {
|
||||
func ApSecond[A, B any](second Lazy[B]) Kleisli[Lazy[A], B] {
|
||||
return io.ApSecond[A](second)
|
||||
}
|
||||
|
||||
@@ -122,7 +119,7 @@ func MonadChainTo[A, B any](fa Lazy[A], fb Lazy[B]) Lazy[B] {
|
||||
}
|
||||
|
||||
// ChainTo composes computations in sequence, ignoring the return value of the first computation
|
||||
func ChainTo[A, B any](fb Lazy[B]) func(Lazy[A]) Lazy[B] {
|
||||
func ChainTo[A, B any](fb Lazy[B]) Kleisli[Lazy[A], B] {
|
||||
return io.ChainTo[A](fb)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
// check - checks if the result of the action needs to be retried
|
||||
func Retrying[A any](
|
||||
policy R.RetryPolicy,
|
||||
action func(R.RetryStatus) Lazy[A],
|
||||
action Kleisli[R.RetryStatus, A],
|
||||
check func(A) bool,
|
||||
) Lazy[A] {
|
||||
return io.Retrying(policy, action, check)
|
||||
|
||||
@@ -17,19 +17,19 @@ package lazy
|
||||
|
||||
import "github.com/IBM/fp-go/v2/io"
|
||||
|
||||
func MonadTraverseArray[A, B any](tas []A, f func(A) Lazy[B]) Lazy[[]B] {
|
||||
func MonadTraverseArray[A, B any](tas []A, f Kleisli[A, B]) Lazy[[]B] {
|
||||
return io.MonadTraverseArray(tas, f)
|
||||
}
|
||||
|
||||
// TraverseArray applies a function returning an [IO] to all elements in an array and the
|
||||
// transforms this into an [IO] of that array
|
||||
func TraverseArray[A, B any](f func(A) Lazy[B]) func([]A) Lazy[[]B] {
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return io.TraverseArray(f)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex applies a function returning an [IO] to all elements in an array and the
|
||||
// transforms this into an [IO] of that array
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) Lazy[B]) func([]A) Lazy[[]B] {
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) Lazy[B]) Kleisli[[]A, []B] {
|
||||
return io.TraverseArrayWithIndex(f)
|
||||
}
|
||||
|
||||
@@ -38,19 +38,19 @@ func SequenceArray[A any](tas []Lazy[A]) Lazy[[]A] {
|
||||
return io.SequenceArray(tas)
|
||||
}
|
||||
|
||||
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) Lazy[B]) Lazy[map[K]B] {
|
||||
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) Lazy[map[K]B] {
|
||||
return io.MonadTraverseRecord(tas, f)
|
||||
}
|
||||
|
||||
// TraverseRecord applies a function returning an [IO] to all elements in a record and the
|
||||
// transforms this into an [IO] of that record
|
||||
func TraverseRecord[K comparable, A, B any](f func(A) Lazy[B]) func(map[K]A) Lazy[map[K]B] {
|
||||
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return io.TraverseRecord[K](f)
|
||||
}
|
||||
|
||||
// TraverseRecord applies a function returning an [IO] to all elements in a record and the
|
||||
// transforms this into an [IO] of that record
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) Lazy[B]) func(map[K]A) Lazy[map[K]B] {
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) Lazy[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return io.TraverseRecordWithIndex[K](f)
|
||||
}
|
||||
|
||||
|
||||
9
v2/lazy/types.go
Normal file
9
v2/lazy/types.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package lazy
|
||||
|
||||
type (
|
||||
// Lazy represents a synchronous computation without side effects
|
||||
Lazy[A any] = func() A
|
||||
|
||||
Kleisli[A, B any] = func(A) Lazy[B]
|
||||
Operator[A, B any] = Kleisli[Lazy[A], B]
|
||||
)
|
||||
5
v2/logging/coverage.out
Normal file
5
v2/logging/coverage.out
Normal file
@@ -0,0 +1,5 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/logging/logger.go:54.92,55.22 1 1
|
||||
github.com/IBM/fp-go/v2/logging/logger.go:56.9,58.32 2 1
|
||||
github.com/IBM/fp-go/v2/logging/logger.go:59.9,61.34 2 1
|
||||
github.com/IBM/fp-go/v2/logging/logger.go:62.10,63.46 1 1
|
||||
@@ -13,12 +13,44 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package logging provides utilities for creating logging callbacks from standard log.Logger instances.
|
||||
// It offers a convenient way to configure logging for functional programming patterns where separate
|
||||
// loggers for success and error cases are needed.
|
||||
package logging
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// LoggingCallbacks creates a pair of logging callback functions from the provided loggers.
|
||||
// It returns two functions that can be used for logging messages, typically one for success
|
||||
// cases and one for error cases.
|
||||
//
|
||||
// The behavior depends on the number of loggers provided:
|
||||
// - 0 loggers: Returns two callbacks using log.Default() for both success and error logging
|
||||
// - 1 logger: Returns two callbacks both using the provided logger
|
||||
// - 2+ loggers: Returns callbacks using the first logger for success and second for errors
|
||||
//
|
||||
// Parameters:
|
||||
// - loggers: Variable number of *log.Logger instances (0, 1, or more)
|
||||
//
|
||||
// Returns:
|
||||
// - First function: Callback for success/info logging (signature: func(string, ...any))
|
||||
// - Second function: Callback for error logging (signature: func(string, ...any))
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Using default logger for both
|
||||
// infoLog, errLog := LoggingCallbacks()
|
||||
//
|
||||
// // Using custom logger for both
|
||||
// customLogger := log.New(os.Stdout, "APP: ", log.LstdFlags)
|
||||
// infoLog, errLog := LoggingCallbacks(customLogger)
|
||||
//
|
||||
// // Using separate loggers for info and errors
|
||||
// infoLogger := log.New(os.Stdout, "INFO: ", log.LstdFlags)
|
||||
// errorLogger := log.New(os.Stderr, "ERROR: ", log.LstdFlags)
|
||||
// infoLog, errLog := LoggingCallbacks(infoLogger, errorLogger)
|
||||
func LoggingCallbacks(loggers ...*log.Logger) (func(string, ...any), func(string, ...any)) {
|
||||
switch len(loggers) {
|
||||
case 0:
|
||||
|
||||
288
v2/logging/logger_test.go
Normal file
288
v2/logging/logger_test.go
Normal file
@@ -0,0 +1,288 @@
|
||||
// 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 logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLoggingCallbacks_NoLoggers tests the case when no loggers are provided.
|
||||
// It should return two callbacks using the default logger.
|
||||
func TestLoggingCallbacks_NoLoggers(t *testing.T) {
|
||||
infoLog, errLog := LoggingCallbacks()
|
||||
|
||||
if infoLog == nil {
|
||||
t.Error("Expected infoLog to be non-nil")
|
||||
}
|
||||
if errLog == nil {
|
||||
t.Error("Expected errLog to be non-nil")
|
||||
}
|
||||
|
||||
// Verify both callbacks work
|
||||
var buf bytes.Buffer
|
||||
log.SetOutput(&buf)
|
||||
defer log.SetOutput(nil)
|
||||
|
||||
infoLog("test info: %s", "message")
|
||||
if !strings.Contains(buf.String(), "test info: message") {
|
||||
t.Errorf("Expected log to contain 'test info: message', got: %s", buf.String())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
errLog("test error: %s", "message")
|
||||
if !strings.Contains(buf.String(), "test error: message") {
|
||||
t.Errorf("Expected log to contain 'test error: message', got: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_OneLogger tests the case when one logger is provided.
|
||||
// Both callbacks should use the same logger.
|
||||
func TestLoggingCallbacks_OneLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "TEST: ", 0)
|
||||
|
||||
infoLog, errLog := LoggingCallbacks(logger)
|
||||
|
||||
if infoLog == nil {
|
||||
t.Error("Expected infoLog to be non-nil")
|
||||
}
|
||||
if errLog == nil {
|
||||
t.Error("Expected errLog to be non-nil")
|
||||
}
|
||||
|
||||
// Test info callback
|
||||
infoLog("info message: %d", 42)
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "TEST: info message: 42") {
|
||||
t.Errorf("Expected log to contain 'TEST: info message: 42', got: %s", output)
|
||||
}
|
||||
|
||||
// Test error callback uses same logger
|
||||
buf.Reset()
|
||||
errLog("error message: %s", "failed")
|
||||
output = buf.String()
|
||||
if !strings.Contains(output, "TEST: error message: failed") {
|
||||
t.Errorf("Expected log to contain 'TEST: error message: failed', got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_TwoLoggers tests the case when two loggers are provided.
|
||||
// First callback should use first logger, second callback should use second logger.
|
||||
func TestLoggingCallbacks_TwoLoggers(t *testing.T) {
|
||||
var infoBuf, errBuf bytes.Buffer
|
||||
infoLogger := log.New(&infoBuf, "INFO: ", 0)
|
||||
errorLogger := log.New(&errBuf, "ERROR: ", 0)
|
||||
|
||||
infoLog, errLog := LoggingCallbacks(infoLogger, errorLogger)
|
||||
|
||||
if infoLog == nil {
|
||||
t.Error("Expected infoLog to be non-nil")
|
||||
}
|
||||
if errLog == nil {
|
||||
t.Error("Expected errLog to be non-nil")
|
||||
}
|
||||
|
||||
// Test info callback uses first logger
|
||||
infoLog("success: %s", "operation completed")
|
||||
infoOutput := infoBuf.String()
|
||||
if !strings.Contains(infoOutput, "INFO: success: operation completed") {
|
||||
t.Errorf("Expected info log to contain 'INFO: success: operation completed', got: %s", infoOutput)
|
||||
}
|
||||
if errBuf.Len() != 0 {
|
||||
t.Errorf("Expected error buffer to be empty, got: %s", errBuf.String())
|
||||
}
|
||||
|
||||
// Test error callback uses second logger
|
||||
errLog("failure: %s", "operation failed")
|
||||
errOutput := errBuf.String()
|
||||
if !strings.Contains(errOutput, "ERROR: failure: operation failed") {
|
||||
t.Errorf("Expected error log to contain 'ERROR: failure: operation failed', got: %s", errOutput)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_MultipleLoggers tests the case when more than two loggers are provided.
|
||||
// Should use first two loggers and ignore the rest.
|
||||
func TestLoggingCallbacks_MultipleLoggers(t *testing.T) {
|
||||
var buf1, buf2, buf3 bytes.Buffer
|
||||
logger1 := log.New(&buf1, "LOG1: ", 0)
|
||||
logger2 := log.New(&buf2, "LOG2: ", 0)
|
||||
logger3 := log.New(&buf3, "LOG3: ", 0)
|
||||
|
||||
infoLog, errLog := LoggingCallbacks(logger1, logger2, logger3)
|
||||
|
||||
if infoLog == nil {
|
||||
t.Error("Expected infoLog to be non-nil")
|
||||
}
|
||||
if errLog == nil {
|
||||
t.Error("Expected errLog to be non-nil")
|
||||
}
|
||||
|
||||
// Test that first logger is used for info
|
||||
infoLog("message 1")
|
||||
if !strings.Contains(buf1.String(), "LOG1: message 1") {
|
||||
t.Errorf("Expected first logger to be used, got: %s", buf1.String())
|
||||
}
|
||||
|
||||
// Test that second logger is used for error
|
||||
errLog("message 2")
|
||||
if !strings.Contains(buf2.String(), "LOG2: message 2") {
|
||||
t.Errorf("Expected second logger to be used, got: %s", buf2.String())
|
||||
}
|
||||
|
||||
// Test that third logger is not used
|
||||
if buf3.Len() != 0 {
|
||||
t.Errorf("Expected third logger to not be used, got: %s", buf3.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_FormattingWithMultipleArgs tests that formatting works correctly
|
||||
// with multiple arguments.
|
||||
func TestLoggingCallbacks_FormattingWithMultipleArgs(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
|
||||
infoLog, _ := LoggingCallbacks(logger)
|
||||
|
||||
infoLog("test %s %d %v", "string", 123, true)
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "test string 123 true") {
|
||||
t.Errorf("Expected formatted output 'test string 123 true', got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_NoFormatting tests logging without format specifiers.
|
||||
func TestLoggingCallbacks_NoFormatting(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "PREFIX: ", 0)
|
||||
|
||||
infoLog, _ := LoggingCallbacks(logger)
|
||||
|
||||
infoLog("simple message")
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "PREFIX: simple message") {
|
||||
t.Errorf("Expected 'PREFIX: simple message', got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_EmptyMessage tests logging with empty message.
|
||||
func TestLoggingCallbacks_EmptyMessage(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
|
||||
infoLog, _ := LoggingCallbacks(logger)
|
||||
|
||||
infoLog("")
|
||||
output := buf.String()
|
||||
// Should still produce output (newline at minimum)
|
||||
if len(output) == 0 {
|
||||
t.Error("Expected some output even with empty message")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_NilLogger tests behavior when nil logger is passed.
|
||||
// This tests edge case handling.
|
||||
func TestLoggingCallbacks_NilLogger(t *testing.T) {
|
||||
// This should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("LoggingCallbacks panicked with nil logger: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
infoLog, errLog := LoggingCallbacks(nil)
|
||||
|
||||
// The callbacks should still be created
|
||||
if infoLog == nil {
|
||||
t.Error("Expected infoLog to be non-nil even with nil logger")
|
||||
}
|
||||
if errLog == nil {
|
||||
t.Error("Expected errLog to be non-nil even with nil logger")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_ConsecutiveCalls tests that callbacks can be called multiple times.
|
||||
func TestLoggingCallbacks_ConsecutiveCalls(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
|
||||
infoLog, errLog := LoggingCallbacks(logger)
|
||||
|
||||
// Multiple calls to info
|
||||
infoLog("call 1")
|
||||
infoLog("call 2")
|
||||
infoLog("call 3")
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "call 1") || !strings.Contains(output, "call 2") || !strings.Contains(output, "call 3") {
|
||||
t.Errorf("Expected all three calls to be logged, got: %s", output)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
|
||||
// Multiple calls to error
|
||||
errLog("error 1")
|
||||
errLog("error 2")
|
||||
|
||||
output = buf.String()
|
||||
if !strings.Contains(output, "error 1") || !strings.Contains(output, "error 2") {
|
||||
t.Errorf("Expected both error calls to be logged, got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLoggingCallbacks_NoLoggers benchmarks the no-logger case.
|
||||
func BenchmarkLoggingCallbacks_NoLoggers(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
LoggingCallbacks()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLoggingCallbacks_OneLogger benchmarks the single-logger case.
|
||||
func BenchmarkLoggingCallbacks_OneLogger(b *testing.B) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
LoggingCallbacks(logger)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLoggingCallbacks_TwoLoggers benchmarks the two-logger case.
|
||||
func BenchmarkLoggingCallbacks_TwoLoggers(b *testing.B) {
|
||||
var buf1, buf2 bytes.Buffer
|
||||
logger1 := log.New(&buf1, "", 0)
|
||||
logger2 := log.New(&buf2, "", 0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
LoggingCallbacks(logger1, logger2)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLoggingCallbacks_Logging benchmarks actual logging operations.
|
||||
func BenchmarkLoggingCallbacks_Logging(b *testing.B) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
infoLog, _ := LoggingCallbacks(logger)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
infoLog("benchmark message %d", i)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Iso is an optic which converts elements of type `S` into elements of type `A` without loss.
|
||||
// Package iso provides isomorphisms - bidirectional transformations between types without loss of information.
|
||||
package iso
|
||||
|
||||
import (
|
||||
@@ -21,21 +21,127 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Iso represents an isomorphism between types S and A.
|
||||
// An isomorphism is a bidirectional transformation that converts between two types
|
||||
// without any loss of information. It consists of two functions that are inverses
|
||||
// of each other.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The target type
|
||||
//
|
||||
// Fields:
|
||||
// - Get: Converts from S to A
|
||||
// - ReverseGet: Converts from A back to S
|
||||
//
|
||||
// Laws:
|
||||
// An Iso must satisfy the round-trip laws:
|
||||
// 1. ReverseGet(Get(s)) == s for all s: S
|
||||
// 2. Get(ReverseGet(a)) == a for all a: A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Isomorphism between Celsius and Fahrenheit
|
||||
// tempIso := Iso[float64, float64]{
|
||||
// Get: func(c float64) float64 { return c*9/5 + 32 },
|
||||
// ReverseGet: func(f float64) float64 { return (f - 32) * 5 / 9 },
|
||||
// }
|
||||
//
|
||||
// fahrenheit := tempIso.Get(20.0) // 68.0
|
||||
// celsius := tempIso.ReverseGet(68.0) // 20.0
|
||||
type Iso[S, A any] struct {
|
||||
Get func(s S) A
|
||||
// Get converts a value from the source type S to the target type A.
|
||||
Get func(s S) A
|
||||
|
||||
// ReverseGet converts a value from the target type A back to the source type S.
|
||||
// This is the inverse of Get.
|
||||
ReverseGet func(a A) S
|
||||
}
|
||||
|
||||
// MakeIso constructs an isomorphism from two functions.
|
||||
// The functions should be inverses of each other to satisfy the isomorphism laws.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The target type
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to convert from S to A
|
||||
// - reverse: Function to convert from A to S (inverse of get)
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[S, A] that uses the provided functions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create an isomorphism between string and []byte
|
||||
// stringBytesIso := MakeIso(
|
||||
// func(s string) []byte { return []byte(s) },
|
||||
// func(b []byte) string { return string(b) },
|
||||
// )
|
||||
//
|
||||
// bytes := stringBytesIso.Get("hello") // []byte("hello")
|
||||
// str := stringBytesIso.ReverseGet([]byte("hi")) // "hi"
|
||||
func MakeIso[S, A any](get func(S) A, reverse func(A) S) Iso[S, A] {
|
||||
return Iso[S, A]{Get: get, ReverseGet: reverse}
|
||||
}
|
||||
|
||||
// Id returns an iso implementing the identity operation
|
||||
// Id returns an identity isomorphism that performs no transformation.
|
||||
// Both Get and ReverseGet are the identity function.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The type for both source and target
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[S, S] where Get and ReverseGet are both identity functions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// idIso := Id[int]()
|
||||
// value := idIso.Get(42) // 42
|
||||
// same := idIso.ReverseGet(42) // 42
|
||||
//
|
||||
// Use cases:
|
||||
// - As a starting point for isomorphism composition
|
||||
// - When you need an isomorphism but don't want to transform the value
|
||||
// - In generic code that requires an isomorphism parameter
|
||||
func Id[S any]() Iso[S, S] {
|
||||
return MakeIso(F.Identity[S], F.Identity[S])
|
||||
}
|
||||
|
||||
// Compose combines an ISO with another ISO
|
||||
// Compose combines two isomorphisms to create a new isomorphism.
|
||||
// Given Iso[S, A] and Iso[A, B], creates Iso[S, B].
|
||||
// The resulting isomorphism first applies the outer iso (S → A),
|
||||
// then the inner iso (A → B).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The outermost source type
|
||||
// - A: The intermediate type
|
||||
// - B: The innermost target type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: The inner isomorphism (A → B)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes the outer isomorphism (S → A) and returns the composed isomorphism (S → B)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// metersToKm := MakeIso(
|
||||
// func(m float64) float64 { return m / 1000 },
|
||||
// func(km float64) float64 { return km * 1000 },
|
||||
// )
|
||||
//
|
||||
// kmToMiles := MakeIso(
|
||||
// func(km float64) float64 { return km * 0.621371 },
|
||||
// func(mi float64) float64 { return mi / 0.621371 },
|
||||
// )
|
||||
//
|
||||
// // Compose: meters → kilometers → miles
|
||||
// metersToMiles := F.Pipe1(metersToKm, Compose[float64](kmToMiles))
|
||||
//
|
||||
// miles := metersToMiles.Get(5000) // ~3.11 miles
|
||||
// meters := metersToMiles.ReverseGet(3.11) // ~5000 meters
|
||||
func Compose[S, A, B any](ab Iso[A, B]) func(Iso[S, A]) Iso[S, B] {
|
||||
return func(sa Iso[S, A]) Iso[S, B] {
|
||||
return MakeIso(
|
||||
@@ -45,7 +151,31 @@ func Compose[S, A, B any](ab Iso[A, B]) func(Iso[S, A]) Iso[S, B] {
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse changes the order of parameters for an iso
|
||||
// Reverse swaps the direction of an isomorphism.
|
||||
// Given Iso[S, A], creates Iso[A, S] where Get and ReverseGet are swapped.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The original source type (becomes target)
|
||||
// - A: The original target type (becomes source)
|
||||
//
|
||||
// Parameters:
|
||||
// - sa: The isomorphism to reverse
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[A, S] with Get and ReverseGet swapped
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// celsiusToFahrenheit := MakeIso(
|
||||
// func(c float64) float64 { return c*9/5 + 32 },
|
||||
// func(f float64) float64 { return (f - 32) * 5 / 9 },
|
||||
// )
|
||||
//
|
||||
// // Reverse to get Fahrenheit to Celsius
|
||||
// fahrenheitToCelsius := Reverse(celsiusToFahrenheit)
|
||||
//
|
||||
// celsius := fahrenheitToCelsius.Get(68.0) // 20.0
|
||||
// fahrenheit := fahrenheitToCelsius.ReverseGet(20.0) // 68.0
|
||||
func Reverse[S, A any](sa Iso[S, A]) Iso[A, S] {
|
||||
return MakeIso(
|
||||
sa.ReverseGet,
|
||||
@@ -53,6 +183,8 @@ func Reverse[S, A any](sa Iso[S, A]) Iso[A, S] {
|
||||
)
|
||||
}
|
||||
|
||||
// modify is an internal helper that applies a transformation function through an isomorphism.
|
||||
// It converts S to A, applies the function, then converts back to S.
|
||||
func modify[FCT ~func(A) A, S, A any](f FCT, sa Iso[S, A], s S) S {
|
||||
return F.Pipe3(
|
||||
s,
|
||||
@@ -62,35 +194,166 @@ func modify[FCT ~func(A) A, S, A any](f FCT, sa Iso[S, A], s S) S {
|
||||
)
|
||||
}
|
||||
|
||||
// Modify applies a transformation
|
||||
// Modify creates a function that applies a transformation in the target space.
|
||||
// It converts the source value to the target type, applies the transformation,
|
||||
// then converts back to the source type.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - FCT: The transformation function type (A → A)
|
||||
// - A: The target type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The transformation function to apply in the target space
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an Iso[S, A] and returns an endomorphism (S → S)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Meters float64
|
||||
// type Kilometers float64
|
||||
//
|
||||
// mToKm := MakeIso(
|
||||
// func(m Meters) Kilometers { return Kilometers(m / 1000) },
|
||||
// func(km Kilometers) Meters { return Meters(km * 1000) },
|
||||
// )
|
||||
//
|
||||
// // Double the distance in kilometers, result in meters
|
||||
// doubled := Modify[Meters](func(km Kilometers) Kilometers {
|
||||
// return km * 2
|
||||
// })(mToKm)(Meters(5000))
|
||||
// // Result: Meters(10000)
|
||||
func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Iso[S, A]) EM.Endomorphism[S] {
|
||||
return EM.Curry3(modify[FCT, S, A])(f)
|
||||
return F.Curry3(modify[FCT, S, A])(f)
|
||||
}
|
||||
|
||||
// Wrap wraps the value
|
||||
// Unwrap extracts the target value from a source value using an isomorphism.
|
||||
// This is a convenience function that applies the Get function of the isomorphism.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type to extract
|
||||
// - S: The source type
|
||||
//
|
||||
// Parameters:
|
||||
// - s: The source value to unwrap
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an Iso[S, A] and returns the unwrapped value of type A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type UserId int
|
||||
//
|
||||
// userIdIso := MakeIso(
|
||||
// func(id UserId) int { return int(id) },
|
||||
// func(i int) UserId { return UserId(i) },
|
||||
// )
|
||||
//
|
||||
// rawId := Unwrap[int](UserId(42))(userIdIso) // 42
|
||||
//
|
||||
// Note: This function is also available as To for semantic clarity.
|
||||
func Unwrap[A, S any](s S) func(Iso[S, A]) A {
|
||||
return func(sa Iso[S, A]) A {
|
||||
return sa.Get(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap unwraps the value
|
||||
// Wrap wraps a target value into a source value using an isomorphism.
|
||||
// This is a convenience function that applies the ReverseGet function of the isomorphism.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type to wrap into
|
||||
// - A: The target type
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The target value to wrap
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an Iso[S, A] and returns the wrapped value of type S
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type UserId int
|
||||
//
|
||||
// userIdIso := MakeIso(
|
||||
// func(id UserId) int { return int(id) },
|
||||
// func(i int) UserId { return UserId(i) },
|
||||
// )
|
||||
//
|
||||
// userId := Wrap[UserId](42)(userIdIso) // UserId(42)
|
||||
//
|
||||
// Note: This function is also available as From for semantic clarity.
|
||||
func Wrap[S, A any](a A) func(Iso[S, A]) S {
|
||||
return func(sa Iso[S, A]) S {
|
||||
return sa.ReverseGet(a)
|
||||
}
|
||||
}
|
||||
|
||||
// From wraps the value
|
||||
// To extracts the target value from a source value using an isomorphism.
|
||||
// This is an alias for Unwrap, provided for semantic clarity when the
|
||||
// direction of conversion is important.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type to convert to
|
||||
// - S: The source type
|
||||
//
|
||||
// Parameters:
|
||||
// - s: The source value to convert
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an Iso[S, A] and returns the converted value of type A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Email string
|
||||
// type ValidatedEmail struct{ value Email }
|
||||
//
|
||||
// emailIso := MakeIso(
|
||||
// func(ve ValidatedEmail) Email { return ve.value },
|
||||
// func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
|
||||
// )
|
||||
//
|
||||
// // Convert to Email
|
||||
// email := To[Email](ValidatedEmail{value: "user@example.com"})(emailIso)
|
||||
// // "user@example.com"
|
||||
func To[A, S any](s S) func(Iso[S, A]) A {
|
||||
return Unwrap[A, S](s)
|
||||
}
|
||||
|
||||
// To unwraps the value
|
||||
// From wraps a target value into a source value using an isomorphism.
|
||||
// This is an alias for Wrap, provided for semantic clarity when the
|
||||
// direction of conversion is important.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type to convert from
|
||||
// - A: The target type
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The target value to convert
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an Iso[S, A] and returns the converted value of type S
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Email string
|
||||
// type ValidatedEmail struct{ value Email }
|
||||
//
|
||||
// emailIso := MakeIso(
|
||||
// func(ve ValidatedEmail) Email { return ve.value },
|
||||
// func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
|
||||
// )
|
||||
//
|
||||
// // Convert from Email
|
||||
// validated := From[ValidatedEmail](Email("admin@example.com"))(emailIso)
|
||||
// // ValidatedEmail{value: "admin@example.com"}
|
||||
func From[S, A any](a A) func(Iso[S, A]) S {
|
||||
return Wrap[S](a)
|
||||
}
|
||||
|
||||
// imap is an internal helper that bidirectionally maps an isomorphism.
|
||||
// It transforms both directions of the isomorphism using the provided functions.
|
||||
func imap[S, A, B any](sa Iso[S, A], ab func(A) B, ba func(B) A) Iso[S, B] {
|
||||
return MakeIso(
|
||||
F.Flow2(sa.Get, ab),
|
||||
@@ -98,7 +361,43 @@ func imap[S, A, B any](sa Iso[S, A], ab func(A) B, ba func(B) A) Iso[S, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// IMap implements a bidirectional mapping of the transform
|
||||
// IMap bidirectionally maps the target type of an isomorphism.
|
||||
// Given Iso[S, A] and functions A → B and B → A, creates Iso[S, B].
|
||||
// This allows you to transform both directions of an isomorphism.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type (unchanged)
|
||||
// - A: The original target type
|
||||
// - B: The new target type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: Function to map from A to B
|
||||
// - ba: Function to map from B to A (inverse of ab)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms Iso[S, A] to Iso[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Celsius float64
|
||||
// type Kelvin float64
|
||||
//
|
||||
// celsiusIso := Id[Celsius]()
|
||||
//
|
||||
// // Create isomorphism to Kelvin
|
||||
// celsiusToKelvin := F.Pipe1(
|
||||
// celsiusIso,
|
||||
// IMap(
|
||||
// func(c Celsius) Kelvin { return Kelvin(c + 273.15) },
|
||||
// func(k Kelvin) Celsius { return Celsius(k - 273.15) },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// kelvin := celsiusToKelvin.Get(Celsius(20)) // 293.15 K
|
||||
// celsius := celsiusToKelvin.ReverseGet(Kelvin(293.15)) // 20°C
|
||||
//
|
||||
// Note: The functions ab and ba must be inverses of each other to maintain
|
||||
// the isomorphism laws.
|
||||
func IMap[S, A, B any](ab func(A) B, ba func(B) A) func(Iso[S, A]) Iso[S, B] {
|
||||
return func(sa Iso[S, A]) Iso[S, B] {
|
||||
return imap(sa, ab, ba)
|
||||
|
||||
187
v2/optics/iso/isos.go
Normal file
187
v2/optics/iso/isos.go
Normal file
@@ -0,0 +1,187 @@
|
||||
// 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 iso
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
B "github.com/IBM/fp-go/v2/bytes"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
)
|
||||
|
||||
// UTF8String creates an isomorphism between byte slices and UTF-8 strings.
|
||||
// This isomorphism provides bidirectional conversion between []byte and string,
|
||||
// treating the byte slice as UTF-8 encoded text.
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[[]byte, string] where:
|
||||
// - Get: Converts []byte to string using UTF-8 encoding
|
||||
// - ReverseGet: Converts string to []byte using UTF-8 encoding
|
||||
//
|
||||
// Behavior:
|
||||
// - Get direction: Interprets the byte slice as UTF-8 and returns the corresponding string
|
||||
// - ReverseGet direction: Encodes the string as UTF-8 bytes
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iso := UTF8String()
|
||||
//
|
||||
// // Convert bytes to string
|
||||
// str := iso.Get([]byte("hello")) // "hello"
|
||||
//
|
||||
// // Convert string to bytes
|
||||
// bytes := iso.ReverseGet("world") // []byte("world")
|
||||
//
|
||||
// // Round-trip conversion
|
||||
// original := []byte("test")
|
||||
// result := iso.ReverseGet(iso.Get(original)) // []byte("test")
|
||||
//
|
||||
// Use cases:
|
||||
// - Converting between string and byte representations
|
||||
// - Working with APIs that use different text representations
|
||||
// - File I/O operations where you need to switch between strings and bytes
|
||||
// - Network protocols that work with byte streams
|
||||
//
|
||||
// Note: This isomorphism assumes valid UTF-8 encoding. Invalid UTF-8 sequences
|
||||
// in the byte slice will be handled according to Go's string conversion rules
|
||||
// (typically replaced with the Unicode replacement character U+FFFD).
|
||||
func UTF8String() Iso[[]byte, string] {
|
||||
return MakeIso(B.ToString, S.ToBytes)
|
||||
}
|
||||
|
||||
// lines creates an isomorphism between a slice of strings and a single string
|
||||
// with lines separated by the specified separator.
|
||||
// This is an internal helper function used by Lines.
|
||||
//
|
||||
// Parameters:
|
||||
// - sep: The separator string to use for joining/splitting lines
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[[]string, string] that joins/splits strings using the separator
|
||||
//
|
||||
// Behavior:
|
||||
// - Get direction: Joins the string slice into a single string with separators
|
||||
// - ReverseGet direction: Splits the string by the separator into a slice
|
||||
func lines(sep string) Iso[[]string, string] {
|
||||
return MakeIso(S.Join(sep), F.Bind2nd(strings.Split, sep))
|
||||
}
|
||||
|
||||
// Lines creates an isomorphism between a slice of strings and a single string
|
||||
// with newline-separated lines.
|
||||
// This is useful for working with multi-line text where you need to convert
|
||||
// between a single string and individual lines.
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[[]string, string] where:
|
||||
// - Get: Joins string slice with newline characters ("\n")
|
||||
// - ReverseGet: Splits string by newline characters into a slice
|
||||
//
|
||||
// Behavior:
|
||||
// - Get direction: Joins each string in the slice with "\n" separator
|
||||
// - ReverseGet direction: Splits the string at each "\n" into a slice
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iso := Lines()
|
||||
//
|
||||
// // Convert lines to single string
|
||||
// lines := []string{"line1", "line2", "line3"}
|
||||
// text := iso.Get(lines) // "line1\nline2\nline3"
|
||||
//
|
||||
// // Convert string to lines
|
||||
// text := "hello\nworld"
|
||||
// lines := iso.ReverseGet(text) // []string{"hello", "world"}
|
||||
//
|
||||
// // Round-trip conversion
|
||||
// original := []string{"a", "b", "c"}
|
||||
// result := iso.ReverseGet(iso.Get(original)) // []string{"a", "b", "c"}
|
||||
//
|
||||
// Use cases:
|
||||
// - Processing multi-line text files
|
||||
// - Converting between text editor representations (array of lines vs single string)
|
||||
// - Working with configuration files that have line-based structure
|
||||
// - Parsing or generating multi-line output
|
||||
//
|
||||
// Note: Empty strings in the slice will result in consecutive newlines in the output.
|
||||
// Splitting a string with trailing newlines will include an empty string at the end.
|
||||
//
|
||||
// Example with edge cases:
|
||||
//
|
||||
// iso := Lines()
|
||||
// lines := []string{"a", "", "b"}
|
||||
// text := iso.Get(lines) // "a\n\nb"
|
||||
// result := iso.ReverseGet(text) // []string{"a", "", "b"}
|
||||
//
|
||||
// text := "a\nb\n"
|
||||
// lines := iso.ReverseGet(text) // []string{"a", "b", ""}
|
||||
func Lines() Iso[[]string, string] {
|
||||
return lines("\n")
|
||||
}
|
||||
|
||||
// UnixMilli creates an isomorphism between Unix millisecond timestamps and time.Time values.
|
||||
// This isomorphism provides bidirectional conversion between int64 milliseconds since
|
||||
// the Unix epoch (January 1, 1970 UTC) and Go's time.Time type.
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[int64, time.Time] where:
|
||||
// - Get: Converts Unix milliseconds (int64) to time.Time
|
||||
// - ReverseGet: Converts time.Time to Unix milliseconds (int64)
|
||||
//
|
||||
// Behavior:
|
||||
// - Get direction: Creates a time.Time from milliseconds since Unix epoch
|
||||
// - ReverseGet direction: Extracts milliseconds since Unix epoch from time.Time
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iso := UnixMilli()
|
||||
//
|
||||
// // Convert milliseconds to time.Time
|
||||
// millis := int64(1609459200000) // 2021-01-01 00:00:00 UTC
|
||||
// t := iso.Get(millis)
|
||||
//
|
||||
// // Convert time.Time to milliseconds
|
||||
// now := time.Now()
|
||||
// millis := iso.ReverseGet(now)
|
||||
//
|
||||
// // Round-trip conversion
|
||||
// original := int64(1234567890000)
|
||||
// result := iso.ReverseGet(iso.Get(original)) // 1234567890000
|
||||
//
|
||||
// Use cases:
|
||||
// - Working with APIs that use Unix millisecond timestamps (e.g., JavaScript Date.now())
|
||||
// - Database storage where timestamps are stored as integers
|
||||
// - JSON serialization/deserialization of timestamps
|
||||
// - Converting between different time representations in distributed systems
|
||||
//
|
||||
// Precision notes:
|
||||
// - Millisecond precision is maintained in both directions
|
||||
// - Sub-millisecond precision in time.Time is lost when converting to int64
|
||||
// - The conversion is timezone-aware (time.Time includes location information)
|
||||
//
|
||||
// Example with precision:
|
||||
//
|
||||
// iso := UnixMilli()
|
||||
// t := time.Date(2021, 1, 1, 12, 30, 45, 123456789, time.UTC)
|
||||
// millis := iso.ReverseGet(t) // Nanoseconds are truncated to milliseconds
|
||||
// restored := iso.Get(millis) // Nanoseconds will be 123000000
|
||||
//
|
||||
// Note: This isomorphism uses UTC for the time.Time values. If you need to preserve
|
||||
// timezone information, consider storing it separately or using a different representation.
|
||||
func UnixMilli() Iso[int64, time.Time] {
|
||||
return MakeIso(time.UnixMilli, time.Time.UnixMilli)
|
||||
}
|
||||
432
v2/optics/iso/isos_test.go
Normal file
432
v2/optics/iso/isos_test.go
Normal file
@@ -0,0 +1,432 @@
|
||||
// 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 iso
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestUTF8String tests the UTF8String isomorphism
|
||||
func TestUTF8String(t *testing.T) {
|
||||
iso := UTF8String()
|
||||
|
||||
t.Run("Get converts bytes to string", func(t *testing.T) {
|
||||
bytes := []byte("hello world")
|
||||
result := iso.Get(bytes)
|
||||
assert.Equal(t, "hello world", result)
|
||||
})
|
||||
|
||||
t.Run("Get handles empty bytes", func(t *testing.T) {
|
||||
bytes := []byte{}
|
||||
result := iso.Get(bytes)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("Get handles UTF-8 characters", func(t *testing.T) {
|
||||
bytes := []byte("Hello 世界 🌍")
|
||||
result := iso.Get(bytes)
|
||||
assert.Equal(t, "Hello 世界 🌍", result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts string to bytes", func(t *testing.T) {
|
||||
str := "hello world"
|
||||
result := iso.ReverseGet(str)
|
||||
assert.Equal(t, []byte("hello world"), result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet handles empty string", func(t *testing.T) {
|
||||
str := ""
|
||||
result := iso.ReverseGet(str)
|
||||
assert.Equal(t, []byte{}, result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet handles UTF-8 characters", func(t *testing.T) {
|
||||
str := "Hello 世界 🌍"
|
||||
result := iso.ReverseGet(str)
|
||||
assert.Equal(t, []byte("Hello 世界 🌍"), result)
|
||||
})
|
||||
|
||||
t.Run("Round-trip bytes to string to bytes", func(t *testing.T) {
|
||||
original := []byte("test data")
|
||||
result := iso.ReverseGet(iso.Get(original))
|
||||
assert.Equal(t, original, result)
|
||||
})
|
||||
|
||||
t.Run("Round-trip string to bytes to string", func(t *testing.T) {
|
||||
original := "test string"
|
||||
result := iso.Get(iso.ReverseGet(original))
|
||||
assert.Equal(t, original, result)
|
||||
})
|
||||
|
||||
t.Run("Handles special characters", func(t *testing.T) {
|
||||
str := "line1\nline2\ttab\r\nwindows"
|
||||
bytes := iso.ReverseGet(str)
|
||||
result := iso.Get(bytes)
|
||||
assert.Equal(t, str, result)
|
||||
})
|
||||
|
||||
t.Run("Handles binary-like data", func(t *testing.T) {
|
||||
bytes := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f} // "Hello"
|
||||
result := iso.Get(bytes)
|
||||
assert.Equal(t, "Hello", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLines tests the Lines isomorphism
|
||||
func TestLines(t *testing.T) {
|
||||
iso := Lines()
|
||||
|
||||
t.Run("Get joins lines with newline", func(t *testing.T) {
|
||||
lines := []string{"line1", "line2", "line3"}
|
||||
result := iso.Get(lines)
|
||||
assert.Equal(t, "line1\nline2\nline3", result)
|
||||
})
|
||||
|
||||
t.Run("Get handles single line", func(t *testing.T) {
|
||||
lines := []string{"single line"}
|
||||
result := iso.Get(lines)
|
||||
assert.Equal(t, "single line", result)
|
||||
})
|
||||
|
||||
t.Run("Get handles empty slice", func(t *testing.T) {
|
||||
lines := []string{}
|
||||
result := iso.Get(lines)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("Get handles empty strings in slice", func(t *testing.T) {
|
||||
lines := []string{"a", "", "b"}
|
||||
result := iso.Get(lines)
|
||||
assert.Equal(t, "a\n\nb", result)
|
||||
})
|
||||
|
||||
t.Run("Get handles slice with only empty strings", func(t *testing.T) {
|
||||
lines := []string{"", "", ""}
|
||||
result := iso.Get(lines)
|
||||
assert.Equal(t, "\n\n", result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet splits string by newline", func(t *testing.T) {
|
||||
str := "line1\nline2\nline3"
|
||||
result := iso.ReverseGet(str)
|
||||
assert.Equal(t, []string{"line1", "line2", "line3"}, result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet handles single line", func(t *testing.T) {
|
||||
str := "single line"
|
||||
result := iso.ReverseGet(str)
|
||||
assert.Equal(t, []string{"single line"}, result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet handles empty string", func(t *testing.T) {
|
||||
str := ""
|
||||
result := iso.ReverseGet(str)
|
||||
assert.Equal(t, []string{""}, result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet handles consecutive newlines", func(t *testing.T) {
|
||||
str := "a\n\nb"
|
||||
result := iso.ReverseGet(str)
|
||||
assert.Equal(t, []string{"a", "", "b"}, result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet handles trailing newline", func(t *testing.T) {
|
||||
str := "a\nb\n"
|
||||
result := iso.ReverseGet(str)
|
||||
assert.Equal(t, []string{"a", "b", ""}, result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet handles leading newline", func(t *testing.T) {
|
||||
str := "\na\nb"
|
||||
result := iso.ReverseGet(str)
|
||||
assert.Equal(t, []string{"", "a", "b"}, result)
|
||||
})
|
||||
|
||||
t.Run("Round-trip lines to string to lines", func(t *testing.T) {
|
||||
original := []string{"line1", "line2", "line3"}
|
||||
result := iso.ReverseGet(iso.Get(original))
|
||||
assert.Equal(t, original, result)
|
||||
})
|
||||
|
||||
t.Run("Round-trip string to lines to string", func(t *testing.T) {
|
||||
original := "line1\nline2\nline3"
|
||||
result := iso.Get(iso.ReverseGet(original))
|
||||
assert.Equal(t, original, result)
|
||||
})
|
||||
|
||||
t.Run("Handles lines with special characters", func(t *testing.T) {
|
||||
lines := []string{"Hello 世界", "🌍 Earth", "tab\there"}
|
||||
text := iso.Get(lines)
|
||||
result := iso.ReverseGet(text)
|
||||
assert.Equal(t, lines, result)
|
||||
})
|
||||
|
||||
t.Run("Preserves whitespace in lines", func(t *testing.T) {
|
||||
lines := []string{" indented", "normal", "\ttabbed"}
|
||||
text := iso.Get(lines)
|
||||
result := iso.ReverseGet(text)
|
||||
assert.Equal(t, lines, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUnixMilli tests the UnixMilli isomorphism
|
||||
func TestUnixMilli(t *testing.T) {
|
||||
iso := UnixMilli()
|
||||
|
||||
t.Run("Get converts milliseconds to time", func(t *testing.T) {
|
||||
millis := int64(1609459200000) // 2021-01-01 00:00:00 UTC
|
||||
result := iso.Get(millis)
|
||||
// Compare Unix timestamps to avoid timezone issues
|
||||
assert.Equal(t, millis, result.UnixMilli())
|
||||
})
|
||||
|
||||
t.Run("Get handles zero milliseconds (Unix epoch)", func(t *testing.T) {
|
||||
millis := int64(0)
|
||||
result := iso.Get(millis)
|
||||
assert.Equal(t, millis, result.UnixMilli())
|
||||
})
|
||||
|
||||
t.Run("Get handles negative milliseconds (before epoch)", func(t *testing.T) {
|
||||
millis := int64(-86400000) // 1 day before epoch
|
||||
result := iso.Get(millis)
|
||||
assert.Equal(t, millis, result.UnixMilli())
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts time to milliseconds", func(t *testing.T) {
|
||||
tm := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
result := iso.ReverseGet(tm)
|
||||
assert.Equal(t, int64(1609459200000), result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet handles Unix epoch", func(t *testing.T) {
|
||||
tm := time.Unix(0, 0).UTC()
|
||||
result := iso.ReverseGet(tm)
|
||||
assert.Equal(t, int64(0), result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet handles time before epoch", func(t *testing.T) {
|
||||
tm := time.Date(1969, 12, 31, 0, 0, 0, 0, time.UTC)
|
||||
result := iso.ReverseGet(tm)
|
||||
assert.Equal(t, int64(-86400000), result)
|
||||
})
|
||||
|
||||
t.Run("Round-trip milliseconds to time to milliseconds", func(t *testing.T) {
|
||||
original := int64(1234567890000)
|
||||
result := iso.ReverseGet(iso.Get(original))
|
||||
assert.Equal(t, original, result)
|
||||
})
|
||||
|
||||
t.Run("Round-trip time to milliseconds to time", func(t *testing.T) {
|
||||
original := time.Date(2021, 6, 15, 12, 30, 45, 0, time.UTC)
|
||||
result := iso.Get(iso.ReverseGet(original))
|
||||
// Compare as Unix timestamps to avoid timezone issues
|
||||
assert.Equal(t, original.UnixMilli(), result.UnixMilli())
|
||||
})
|
||||
|
||||
t.Run("Truncates sub-millisecond precision", func(t *testing.T) {
|
||||
// Time with nanoseconds
|
||||
tm := time.Date(2021, 1, 1, 0, 0, 0, 123456789, time.UTC)
|
||||
millis := iso.ReverseGet(tm)
|
||||
result := iso.Get(millis)
|
||||
|
||||
// Should have millisecond precision only - compare timestamps
|
||||
assert.Equal(t, tm.Truncate(time.Millisecond).UnixMilli(), result.UnixMilli())
|
||||
})
|
||||
|
||||
t.Run("Handles current time", func(t *testing.T) {
|
||||
now := time.Now()
|
||||
millis := iso.ReverseGet(now)
|
||||
result := iso.Get(millis)
|
||||
|
||||
// Should be equal within millisecond precision
|
||||
assert.Equal(t, now.Truncate(time.Millisecond), result.Truncate(time.Millisecond))
|
||||
})
|
||||
|
||||
t.Run("Handles far future date", func(t *testing.T) {
|
||||
future := time.Date(2100, 12, 31, 23, 59, 59, 0, time.UTC)
|
||||
millis := iso.ReverseGet(future)
|
||||
result := iso.Get(millis)
|
||||
assert.Equal(t, future.UnixMilli(), result.UnixMilli())
|
||||
})
|
||||
|
||||
t.Run("Handles far past date", func(t *testing.T) {
|
||||
past := time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
millis := iso.ReverseGet(past)
|
||||
result := iso.Get(millis)
|
||||
assert.Equal(t, past.UnixMilli(), result.UnixMilli())
|
||||
})
|
||||
|
||||
t.Run("Preserves timezone information in round-trip", func(t *testing.T) {
|
||||
// Create time in different timezone
|
||||
loc, _ := time.LoadLocation("America/New_York")
|
||||
tm := time.Date(2021, 6, 15, 12, 0, 0, 0, loc)
|
||||
|
||||
// Convert to millis and back
|
||||
millis := iso.ReverseGet(tm)
|
||||
result := iso.Get(millis)
|
||||
|
||||
// Times should represent the same instant (even if timezone differs)
|
||||
assert.True(t, tm.Equal(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestUTF8StringRoundTripLaws verifies isomorphism laws for UTF8String
|
||||
func TestUTF8StringRoundTripLaws(t *testing.T) {
|
||||
iso := UTF8String()
|
||||
|
||||
t.Run("Law 1: ReverseGet(Get(bytes)) == bytes", func(t *testing.T) {
|
||||
testCases := [][]byte{
|
||||
[]byte("hello"),
|
||||
[]byte(""),
|
||||
[]byte("Hello 世界 🌍"),
|
||||
[]byte{0x48, 0x65, 0x6c, 0x6c, 0x6f},
|
||||
}
|
||||
|
||||
for _, original := range testCases {
|
||||
result := iso.ReverseGet(iso.Get(original))
|
||||
assert.Equal(t, original, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Law 2: Get(ReverseGet(str)) == str", func(t *testing.T) {
|
||||
testCases := []string{
|
||||
"hello",
|
||||
"",
|
||||
"Hello 世界 🌍",
|
||||
"special\nchars\ttab",
|
||||
}
|
||||
|
||||
for _, original := range testCases {
|
||||
result := iso.Get(iso.ReverseGet(original))
|
||||
assert.Equal(t, original, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestLinesRoundTripLaws verifies isomorphism laws for Lines
|
||||
func TestLinesRoundTripLaws(t *testing.T) {
|
||||
iso := Lines()
|
||||
|
||||
t.Run("Law 1: ReverseGet(Get(lines)) == lines", func(t *testing.T) {
|
||||
testCases := [][]string{
|
||||
{"line1", "line2"},
|
||||
{"single"},
|
||||
{"a", "", "b"},
|
||||
{"", "", ""},
|
||||
}
|
||||
|
||||
for _, original := range testCases {
|
||||
result := iso.ReverseGet(iso.Get(original))
|
||||
assert.Equal(t, original, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Law 1: Empty slice special case", func(t *testing.T) {
|
||||
// Empty slice becomes "" which splits to [""]
|
||||
// This is expected behavior of strings.Split
|
||||
original := []string{}
|
||||
text := iso.Get(original) // ""
|
||||
result := iso.ReverseGet(text) // [""]
|
||||
assert.Equal(t, []string{""}, result)
|
||||
})
|
||||
|
||||
t.Run("Law 2: Get(ReverseGet(str)) == str", func(t *testing.T) {
|
||||
testCases := []string{
|
||||
"line1\nline2",
|
||||
"single",
|
||||
"",
|
||||
"a\n\nb",
|
||||
"\n\n",
|
||||
}
|
||||
|
||||
for _, original := range testCases {
|
||||
result := iso.Get(iso.ReverseGet(original))
|
||||
assert.Equal(t, original, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestUnixMilliRoundTripLaws verifies isomorphism laws for UnixMilli
|
||||
func TestUnixMilliRoundTripLaws(t *testing.T) {
|
||||
iso := UnixMilli()
|
||||
|
||||
t.Run("Law 1: ReverseGet(Get(millis)) == millis", func(t *testing.T) {
|
||||
testCases := []int64{
|
||||
0,
|
||||
1609459200000,
|
||||
-86400000,
|
||||
1234567890000,
|
||||
time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
for _, original := range testCases {
|
||||
result := iso.ReverseGet(iso.Get(original))
|
||||
assert.Equal(t, original, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Law 2: Get(ReverseGet(time)) == time (with millisecond precision)", func(t *testing.T) {
|
||||
testCases := []time.Time{
|
||||
time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Unix(0, 0).UTC(),
|
||||
time.Date(1969, 12, 31, 0, 0, 0, 0, time.UTC),
|
||||
time.Now().Truncate(time.Millisecond),
|
||||
}
|
||||
|
||||
for _, original := range testCases {
|
||||
result := iso.Get(iso.ReverseGet(original))
|
||||
// Compare Unix timestamps to avoid timezone issues
|
||||
assert.Equal(t, original.UnixMilli(), result.UnixMilli())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestIsosComposition tests composing the isos functions
|
||||
func TestIsosComposition(t *testing.T) {
|
||||
t.Run("Compose UTF8String with Lines", func(t *testing.T) {
|
||||
utf8Iso := UTF8String()
|
||||
linesIso := Lines()
|
||||
|
||||
// First convert bytes to string, then string to lines
|
||||
bytes := []byte("line1\nline2\nline3")
|
||||
str := utf8Iso.Get(bytes)
|
||||
lines := linesIso.ReverseGet(str)
|
||||
assert.Equal(t, []string{"line1", "line2", "line3"}, lines)
|
||||
|
||||
// Reverse: lines to string to bytes
|
||||
originalLines := []string{"a", "b", "c"}
|
||||
text := linesIso.Get(originalLines)
|
||||
resultBytes := utf8Iso.ReverseGet(text)
|
||||
assert.Equal(t, []byte("a\nb\nc"), resultBytes)
|
||||
})
|
||||
|
||||
t.Run("Chain UTF8String and Lines operations", func(t *testing.T) {
|
||||
utf8Iso := UTF8String()
|
||||
linesIso := Lines()
|
||||
|
||||
// Process: bytes -> string -> lines -> string -> bytes
|
||||
original := []byte("hello\nworld")
|
||||
str := utf8Iso.Get(original)
|
||||
lines := linesIso.ReverseGet(str)
|
||||
text := linesIso.Get(lines)
|
||||
result := utf8Iso.ReverseGet(text)
|
||||
|
||||
assert.Equal(t, original, result)
|
||||
})
|
||||
}
|
||||
83
v2/optics/iso/option/isos.go
Normal file
83
v2/optics/iso/option/isos.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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 option provides isomorphisms for working with Option types.
|
||||
// It offers utilities to convert between regular values and Option-wrapped values,
|
||||
// particularly useful for handling zero values and optional data.
|
||||
package option
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/iso"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// FromZero creates an isomorphism between a comparable type T and Option[T].
|
||||
// The isomorphism treats the zero value of T as None and non-zero values as Some.
|
||||
//
|
||||
// This is particularly useful for types where the zero value has special meaning
|
||||
// (e.g., 0 for numbers, "" for strings, nil for pointers) and you want to represent
|
||||
// the absence of a meaningful value using Option.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: A comparable type (must support == and != operators)
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[T, Option[T]] where:
|
||||
// - Get: Converts T to Option[T] (zero value → None, non-zero → Some)
|
||||
// - ReverseGet: Converts Option[T] to T (None → zero value, Some → unwrapped value)
|
||||
//
|
||||
// Behavior:
|
||||
// - Get direction: If the value equals the zero value of T, returns None; otherwise returns Some(value)
|
||||
// - ReverseGet direction: If the Option is None, returns the zero value; otherwise returns the unwrapped value
|
||||
//
|
||||
// Example with integers:
|
||||
//
|
||||
// isoInt := FromZero[int]()
|
||||
// opt := isoInt.Get(0) // None (0 is the zero value)
|
||||
// opt = isoInt.Get(42) // Some(42)
|
||||
// val := isoInt.ReverseGet(option.None[int]()) // 0
|
||||
// val = isoInt.ReverseGet(option.Some(42)) // 42
|
||||
//
|
||||
// Example with strings:
|
||||
//
|
||||
// isoStr := FromZero[string]()
|
||||
// opt := isoStr.Get("") // None ("" is the zero value)
|
||||
// opt = isoStr.Get("hello") // Some("hello")
|
||||
// val := isoStr.ReverseGet(option.None[string]()) // ""
|
||||
// val = isoStr.ReverseGet(option.Some("world")) // "world"
|
||||
//
|
||||
// Example with pointers:
|
||||
//
|
||||
// isoPtr := FromZero[*int]()
|
||||
// opt := isoPtr.Get(nil) // None (nil is the zero value)
|
||||
// num := 42
|
||||
// opt = isoPtr.Get(&num) // Some(&num)
|
||||
//
|
||||
// Use cases:
|
||||
// - Converting between database nullable columns and Go types
|
||||
// - Handling optional configuration values with defaults
|
||||
// - Working with APIs that use zero values to indicate absence
|
||||
// - Simplifying validation logic for required vs optional fields
|
||||
//
|
||||
// Note: This isomorphism satisfies the round-trip laws:
|
||||
// - ReverseGet(Get(t)) == t for all t: T
|
||||
// - Get(ReverseGet(opt)) == opt for all opt: Option[T]
|
||||
func FromZero[T comparable]() iso.Iso[T, option.Option[T]] {
|
||||
var zero T
|
||||
return iso.MakeIso(
|
||||
option.FromPredicate(func(t T) bool { return t != zero }),
|
||||
option.GetOrElse(func() T { return zero }),
|
||||
)
|
||||
}
|
||||
366
v2/optics/iso/option/isos_test.go
Normal file
366
v2/optics/iso/option/isos_test.go
Normal file
@@ -0,0 +1,366 @@
|
||||
// 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 option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/iso"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestFromZeroInt tests the FromZero isomorphism with integer type
|
||||
func TestFromZeroInt(t *testing.T) {
|
||||
isoInt := FromZero[int]()
|
||||
|
||||
t.Run("Get converts zero to None", func(t *testing.T) {
|
||||
result := isoInt.Get(0)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Get converts non-zero to Some", func(t *testing.T) {
|
||||
result := isoInt.Get(42)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 42, O.MonadGetOrElse(result, func() int { return 0 }))
|
||||
})
|
||||
|
||||
t.Run("Get converts negative to Some", func(t *testing.T) {
|
||||
result := isoInt.Get(-5)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, -5, O.MonadGetOrElse(result, func() int { return 0 }))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts None to zero", func(t *testing.T) {
|
||||
result := isoInt.ReverseGet(O.None[int]())
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts Some to value", func(t *testing.T) {
|
||||
result := isoInt.ReverseGet(O.Some(42))
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroString tests the FromZero isomorphism with string type
|
||||
func TestFromZeroString(t *testing.T) {
|
||||
isoStr := FromZero[string]()
|
||||
|
||||
t.Run("Get converts empty string to None", func(t *testing.T) {
|
||||
result := isoStr.Get("")
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Get converts non-empty string to Some", func(t *testing.T) {
|
||||
result := isoStr.Get("hello")
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, "hello", O.MonadGetOrElse(result, func() string { return "" }))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts None to empty string", func(t *testing.T) {
|
||||
result := isoStr.ReverseGet(O.None[string]())
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts Some to value", func(t *testing.T) {
|
||||
result := isoStr.ReverseGet(O.Some("world"))
|
||||
assert.Equal(t, "world", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroFloat tests the FromZero isomorphism with float64 type
|
||||
func TestFromZeroFloat(t *testing.T) {
|
||||
isoFloat := FromZero[float64]()
|
||||
|
||||
t.Run("Get converts 0.0 to None", func(t *testing.T) {
|
||||
result := isoFloat.Get(0.0)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Get converts non-zero float to Some", func(t *testing.T) {
|
||||
result := isoFloat.Get(3.14)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.InDelta(t, 3.14, O.MonadGetOrElse(result, func() float64 { return 0.0 }), 0.001)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts None to 0.0", func(t *testing.T) {
|
||||
result := isoFloat.ReverseGet(O.None[float64]())
|
||||
assert.Equal(t, 0.0, result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts Some to value", func(t *testing.T) {
|
||||
result := isoFloat.ReverseGet(O.Some(2.718))
|
||||
assert.InDelta(t, 2.718, result, 0.001)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroPointer tests the FromZero isomorphism with pointer type
|
||||
func TestFromZeroPointer(t *testing.T) {
|
||||
isoPtr := FromZero[*int]()
|
||||
|
||||
t.Run("Get converts nil to None", func(t *testing.T) {
|
||||
result := isoPtr.Get(nil)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Get converts non-nil pointer to Some", func(t *testing.T) {
|
||||
num := 42
|
||||
result := isoPtr.Get(&num)
|
||||
assert.True(t, O.IsSome(result))
|
||||
ptr := O.MonadGetOrElse(result, func() *int { return nil })
|
||||
assert.NotNil(t, ptr)
|
||||
assert.Equal(t, 42, *ptr)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts None to nil", func(t *testing.T) {
|
||||
result := isoPtr.ReverseGet(O.None[*int]())
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts Some to pointer", func(t *testing.T) {
|
||||
num := 99
|
||||
result := isoPtr.ReverseGet(O.Some(&num))
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, 99, *result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroBool tests the FromZero isomorphism with bool type
|
||||
func TestFromZeroBool(t *testing.T) {
|
||||
isoBool := FromZero[bool]()
|
||||
|
||||
t.Run("Get converts false to None", func(t *testing.T) {
|
||||
result := isoBool.Get(false)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Get converts true to Some", func(t *testing.T) {
|
||||
result := isoBool.Get(true)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.True(t, O.MonadGetOrElse(result, func() bool { return false }))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts None to false", func(t *testing.T) {
|
||||
result := isoBool.ReverseGet(O.None[bool]())
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts Some to true", func(t *testing.T) {
|
||||
result := isoBool.ReverseGet(O.Some(true))
|
||||
assert.True(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroRoundTripLaws verifies the isomorphism laws
|
||||
func TestFromZeroRoundTripLaws(t *testing.T) {
|
||||
t.Run("Law 1: ReverseGet(Get(t)) == t for integers", func(t *testing.T) {
|
||||
isoInt := FromZero[int]()
|
||||
|
||||
// Test with zero value
|
||||
assert.Equal(t, 0, isoInt.ReverseGet(isoInt.Get(0)))
|
||||
|
||||
// Test with non-zero values
|
||||
assert.Equal(t, 42, isoInt.ReverseGet(isoInt.Get(42)))
|
||||
assert.Equal(t, -10, isoInt.ReverseGet(isoInt.Get(-10)))
|
||||
})
|
||||
|
||||
t.Run("Law 1: ReverseGet(Get(t)) == t for strings", func(t *testing.T) {
|
||||
isoStr := FromZero[string]()
|
||||
|
||||
// Test with zero value
|
||||
assert.Equal(t, "", isoStr.ReverseGet(isoStr.Get("")))
|
||||
|
||||
// Test with non-zero values
|
||||
assert.Equal(t, "hello", isoStr.ReverseGet(isoStr.Get("hello")))
|
||||
})
|
||||
|
||||
t.Run("Law 2: Get(ReverseGet(opt)) == opt for None", func(t *testing.T) {
|
||||
isoInt := FromZero[int]()
|
||||
|
||||
none := O.None[int]()
|
||||
result := isoInt.Get(isoInt.ReverseGet(none))
|
||||
assert.Equal(t, none, result)
|
||||
})
|
||||
|
||||
t.Run("Law 2: Get(ReverseGet(opt)) == opt for Some", func(t *testing.T) {
|
||||
isoInt := FromZero[int]()
|
||||
|
||||
some := O.Some(42)
|
||||
result := isoInt.Get(isoInt.ReverseGet(some))
|
||||
assert.Equal(t, some, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroWithModify tests using FromZero with iso.Modify
|
||||
func TestFromZeroWithModify(t *testing.T) {
|
||||
isoInt := FromZero[int]()
|
||||
|
||||
t.Run("Modify applies transformation to non-zero value", func(t *testing.T) {
|
||||
double := func(opt O.Option[int]) O.Option[int] {
|
||||
return O.MonadMap(opt, func(x int) int { return x * 2 })
|
||||
}
|
||||
|
||||
result := iso.Modify[int](double)(isoInt)(5)
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
|
||||
t.Run("Modify preserves zero value", func(t *testing.T) {
|
||||
double := func(opt O.Option[int]) O.Option[int] {
|
||||
return O.MonadMap(opt, func(x int) int { return x * 2 })
|
||||
}
|
||||
|
||||
result := iso.Modify[int](double)(isoInt)(0)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroWithCompose tests composing FromZero with other isomorphisms
|
||||
func TestFromZeroWithCompose(t *testing.T) {
|
||||
isoInt := FromZero[int]()
|
||||
|
||||
// Create an isomorphism that doubles/halves values
|
||||
doubleIso := iso.MakeIso(
|
||||
func(opt O.Option[int]) O.Option[int] {
|
||||
return O.MonadMap(opt, func(x int) int { return x * 2 })
|
||||
},
|
||||
func(opt O.Option[int]) O.Option[int] {
|
||||
return O.MonadMap(opt, func(x int) int { return x / 2 })
|
||||
},
|
||||
)
|
||||
|
||||
composed := F.Pipe1(isoInt, iso.Compose[int](doubleIso))
|
||||
|
||||
t.Run("Composed isomorphism works with non-zero", func(t *testing.T) {
|
||||
result := composed.Get(5)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 10, O.MonadGetOrElse(result, func() int { return 0 }))
|
||||
})
|
||||
|
||||
t.Run("Composed isomorphism works with zero", func(t *testing.T) {
|
||||
result := composed.Get(0)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Composed isomorphism reverse works", func(t *testing.T) {
|
||||
result := composed.ReverseGet(O.Some(20))
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroWithUnwrapWrap tests using Unwrap and Wrap helpers
|
||||
func TestFromZeroWithUnwrapWrap(t *testing.T) {
|
||||
isoInt := FromZero[int]()
|
||||
|
||||
t.Run("Unwrap extracts Option from value", func(t *testing.T) {
|
||||
result := iso.Unwrap[O.Option[int]](42)(isoInt)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 42, O.MonadGetOrElse(result, func() int { return 0 }))
|
||||
})
|
||||
|
||||
t.Run("Wrap creates value from Option", func(t *testing.T) {
|
||||
result := iso.Wrap[int](O.Some(99))(isoInt)
|
||||
assert.Equal(t, 99, result)
|
||||
})
|
||||
|
||||
t.Run("To is alias for Unwrap", func(t *testing.T) {
|
||||
result := iso.To[O.Option[int]](42)(isoInt)
|
||||
assert.True(t, O.IsSome(result))
|
||||
})
|
||||
|
||||
t.Run("From is alias for Wrap", func(t *testing.T) {
|
||||
result := iso.From[int](O.Some(99))(isoInt)
|
||||
assert.Equal(t, 99, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroWithReverse tests reversing the isomorphism
|
||||
func TestFromZeroWithReverse(t *testing.T) {
|
||||
isoInt := FromZero[int]()
|
||||
reversed := iso.Reverse(isoInt)
|
||||
|
||||
t.Run("Reversed Get is original ReverseGet", func(t *testing.T) {
|
||||
result := reversed.Get(O.Some(42))
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("Reversed ReverseGet is original Get", func(t *testing.T) {
|
||||
result := reversed.ReverseGet(42)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, 42, O.MonadGetOrElse(result, func() int { return 0 }))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroCustomType tests FromZero with a custom comparable type
|
||||
func TestFromZeroCustomType(t *testing.T) {
|
||||
type UserID int
|
||||
|
||||
isoUserID := FromZero[UserID]()
|
||||
|
||||
t.Run("Get converts zero UserID to None", func(t *testing.T) {
|
||||
result := isoUserID.Get(UserID(0))
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Get converts non-zero UserID to Some", func(t *testing.T) {
|
||||
result := isoUserID.Get(UserID(123))
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, UserID(123), O.MonadGetOrElse(result, func() UserID { return 0 }))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts None to zero UserID", func(t *testing.T) {
|
||||
result := isoUserID.ReverseGet(O.None[UserID]())
|
||||
assert.Equal(t, UserID(0), result)
|
||||
})
|
||||
|
||||
t.Run("ReverseGet converts Some to UserID", func(t *testing.T) {
|
||||
result := isoUserID.ReverseGet(O.Some(UserID(456)))
|
||||
assert.Equal(t, UserID(456), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromZeroEdgeCases tests edge cases and boundary conditions
|
||||
func TestFromZeroEdgeCases(t *testing.T) {
|
||||
t.Run("Works with maximum int value", func(t *testing.T) {
|
||||
isoInt := FromZero[int]()
|
||||
maxInt := int(^uint(0) >> 1)
|
||||
|
||||
result := isoInt.Get(maxInt)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, maxInt, isoInt.ReverseGet(result))
|
||||
})
|
||||
|
||||
t.Run("Works with minimum int value", func(t *testing.T) {
|
||||
isoInt := FromZero[int]()
|
||||
minInt := -int(^uint(0)>>1) - 1
|
||||
|
||||
result := isoInt.Get(minInt)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, minInt, isoInt.ReverseGet(result))
|
||||
})
|
||||
|
||||
t.Run("Works with very long strings", func(t *testing.T) {
|
||||
isoStr := FromZero[string]()
|
||||
longStr := string(make([]byte, 10000))
|
||||
for i := range longStr {
|
||||
longStr = longStr[:i] + "a" + longStr[i+1:]
|
||||
}
|
||||
|
||||
result := isoStr.Get(longStr)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, longStr, isoStr.ReverseGet(result))
|
||||
})
|
||||
}
|
||||
@@ -17,10 +17,7 @@
|
||||
package lens
|
||||
|
||||
import (
|
||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
|
||||
@@ -44,32 +41,156 @@ func setCopyCurried[SET ~func(A) Endomorphism[*S], S, A any](setter SET) func(a
|
||||
}
|
||||
}
|
||||
|
||||
// MakeLens creates a [Lens] based on a getter and a setter function. Make sure that the setter creates a (shallow) copy of the
|
||||
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeLensRef`
|
||||
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
|
||||
// MakeLens creates a [Lens] based on a getter and a setter F.
|
||||
//
|
||||
// The setter must create a (shallow) copy of the data structure. This happens automatically
|
||||
// when the data is passed by value. For pointer-based structures, use [MakeLensRef] instead.
|
||||
// For other reference types (slices, maps), ensure the setter creates a copy.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GET: Getter function type (S → A)
|
||||
// - SET: Setter function type (S, A → S)
|
||||
// - S: Source structure type
|
||||
// - A: Focus/field type
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to extract value A from structure S
|
||||
// - set: Function to update value A in structure S, returning a new S
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[S, A] that can get and set values immutably
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// nameLens := lens.MakeLens(
|
||||
// func(p Person) string { return p.Name },
|
||||
// func(p Person, name string) Person {
|
||||
// p.Name = name
|
||||
// return p
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// person := Person{Name: "Alice", Age: 30}
|
||||
// name := nameLens.Get(person) // "Alice"
|
||||
// updated := nameLens.Set("Bob")(person) // Person{Name: "Bob", Age: 30}
|
||||
func MakeLens[GET ~func(S) A, SET ~func(S, A) S, S, A any](get GET, set SET) Lens[S, A] {
|
||||
return MakeLensCurried(get, function.Curry2(F.Swap(set)))
|
||||
return MakeLensCurried(get, F.Curry2(F.Swap(set)))
|
||||
}
|
||||
|
||||
// MakeLensCurried creates a [Lens] based on a getter and a setter function. Make sure that the setter creates a (shallow) copy of the
|
||||
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeLensRef`
|
||||
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
|
||||
// MakeLensCurried creates a [Lens] with a curried setter F.
|
||||
//
|
||||
// This is similar to [MakeLens] but accepts a curried setter (A → S → S) instead of
|
||||
// an uncurried one (S, A → S). The curried form is more composable in functional pipelines.
|
||||
//
|
||||
// The setter must create a (shallow) copy of the data structure. This happens automatically
|
||||
// when the data is passed by value. For pointer-based structures, use [MakeLensRefCurried].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GET: Getter function type (S → A)
|
||||
// - SET: Curried setter function type (A → S → S)
|
||||
// - S: Source structure type
|
||||
// - A: Focus/field type
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to extract value A from structure S
|
||||
// - set: Curried function to update value A in structure S
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[S, A] that can get and set values immutably
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nameLens := lens.MakeLensCurried(
|
||||
// func(p Person) string { return p.Name },
|
||||
// func(name string) func(Person) Person {
|
||||
// return func(p Person) Person {
|
||||
// p.Name = name
|
||||
// return p
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
func MakeLensCurried[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](get GET, set SET) Lens[S, A] {
|
||||
return Lens[S, A]{Get: get, Set: set}
|
||||
}
|
||||
|
||||
// MakeLensRef creates a [Lens] based on a getter and a setter function. The setter passed in does not have to create a shallow
|
||||
// copy, the implementation wraps the setter into one that copies the pointer before modifying it
|
||||
// MakeLensRef creates a [Lens] for pointer-based structures.
|
||||
//
|
||||
// Such a [Lens] assumes that property A of S always exists
|
||||
// Unlike [MakeLens], the setter does not need to create a copy manually. This function
|
||||
// automatically wraps the setter to create a shallow copy of the pointed-to value before
|
||||
// modification, ensuring immutability.
|
||||
//
|
||||
// This lens assumes that property A always exists in structure S (i.e., it's not optional).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GET: Getter function type (*S → A)
|
||||
// - SET: Setter function type (*S, A → *S)
|
||||
// - S: Source structure type (will be used as *S)
|
||||
// - A: Focus/field type
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to extract value A from pointer *S
|
||||
// - set: Function to update value A in pointer *S (copying handled automatically)
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[*S, A] that can get and set values immutably on pointers
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// nameLens := lens.MakeLensRef(
|
||||
// func(p *Person) string { return p.Name },
|
||||
// func(p *Person, name string) *Person {
|
||||
// p.Name = name // No manual copy needed
|
||||
// return p
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// person := &Person{Name: "Alice", Age: 30}
|
||||
// updated := nameLens.Set("Bob")(person)
|
||||
// // person.Name is still "Alice", updated is a new pointer with Name "Bob"
|
||||
func MakeLensRef[GET ~func(*S) A, SET func(*S, A) *S, S, A any](get GET, set SET) Lens[*S, A] {
|
||||
return MakeLens(get, setCopy(set))
|
||||
}
|
||||
|
||||
// MakeLensRefCurried creates a [Lens] based on a getter and a setter function. The setter passed in does not have to create a shallow
|
||||
// copy, the implementation wraps the setter into one that copies the pointer before modifying it
|
||||
// MakeLensRefCurried creates a [Lens] for pointer-based structures with a curried setter.
|
||||
//
|
||||
// Such a [Lens] assumes that property A of S always exists
|
||||
// This combines the benefits of [MakeLensRef] (automatic copying) with [MakeLensCurried]
|
||||
// (curried setter for better composition). The setter does not need to create a copy manually;
|
||||
// this function automatically wraps it to ensure immutability.
|
||||
//
|
||||
// This lens assumes that property A always exists in structure S (i.e., it's not optional).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Source structure type (will be used as *S)
|
||||
// - A: Focus/field type
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to extract value A from pointer *S
|
||||
// - set: Curried function to update value A in pointer *S (copying handled automatically)
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[*S, A] that can get and set values immutably on pointers
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nameLens := lens.MakeLensRefCurried(
|
||||
// func(p *Person) string { return p.Name },
|
||||
// func(name string) func(*Person) *Person {
|
||||
// return func(p *Person) *Person {
|
||||
// p.Name = name // No manual copy needed
|
||||
// return p
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
func MakeLensRefCurried[S, A any](get func(*S) A, set func(A) Endomorphism[*S]) Lens[*S, A] {
|
||||
return MakeLensCurried(get, setCopyCurried(set))
|
||||
}
|
||||
@@ -79,12 +200,54 @@ func id[GET ~func(S) S, SET ~func(S, S) S, S any](creator func(get GET, set SET)
|
||||
return creator(F.Identity[S], F.Second[S, S])
|
||||
}
|
||||
|
||||
// Id returns a [Lens] implementing the identity operation
|
||||
// Id returns an identity [Lens] that focuses on the entire structure.
|
||||
//
|
||||
// The identity lens is useful as a starting point for lens composition or when you need
|
||||
// a lens that doesn't actually focus on a subpart. Get returns the structure unchanged,
|
||||
// and Set replaces the entire structure.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The structure type
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[S, S] where both source and focus are the same type
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// idLens := lens.Id[Person]()
|
||||
// person := Person{Name: "Alice", Age: 30}
|
||||
//
|
||||
// same := idLens.Get(person) // Returns person unchanged
|
||||
// replaced := idLens.Set(Person{Name: "Bob", Age: 25})(person)
|
||||
// // replaced is Person{Name: "Bob", Age: 25}
|
||||
func Id[S any]() Lens[S, S] {
|
||||
return id(MakeLens[Endomorphism[S], func(S, S) S])
|
||||
}
|
||||
|
||||
// IdRef returns a [Lens] implementing the identity operation
|
||||
// IdRef returns an identity [Lens] for pointer-based structures.
|
||||
//
|
||||
// This is the pointer version of [Id]. It focuses on the entire pointer structure,
|
||||
// with automatic copying to ensure immutability.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The structure type (will be used as *S)
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[*S, *S] where both source and focus are pointers to the same type
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// idLens := lens.IdRef[Person]()
|
||||
// person := &Person{Name: "Alice", Age: 30}
|
||||
//
|
||||
// same := idLens.Get(person) // Returns person pointer
|
||||
// replaced := idLens.Set(&Person{Name: "Bob", Age: 25})(person)
|
||||
// // person.Name is still "Alice", replaced is a new pointer
|
||||
func IdRef[S any]() Lens[*S, *S] {
|
||||
return id(MakeLensRef[Endomorphism[*S], func(*S, *S) *S])
|
||||
}
|
||||
@@ -105,111 +268,94 @@ func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GE
|
||||
}
|
||||
}
|
||||
|
||||
// Compose combines two lenses and allows to narrow down the focus to a sub-lens
|
||||
// Compose combines two lenses to focus on a deeply nested field.
|
||||
//
|
||||
// Given a lens from S to A and a lens from A to B, Compose creates a lens from S to B.
|
||||
// This allows you to navigate through nested structures in a composable way.
|
||||
//
|
||||
// The composition follows the mathematical property: (sa ∘ ab).Get = ab.Get ∘ sa.Get
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Outer structure type
|
||||
// - A: Intermediate structure type
|
||||
// - B: Inner focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: Lens from A to B (inner lens)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns a Lens[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Address struct {
|
||||
// Street string
|
||||
// City string
|
||||
// }
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Address Address
|
||||
// }
|
||||
//
|
||||
// addressLens := lens.MakeLens(
|
||||
// func(p Person) Address { return p.Address },
|
||||
// func(p Person, a Address) Person { p.Address = a; return p },
|
||||
// )
|
||||
//
|
||||
// streetLens := lens.MakeLens(
|
||||
// func(a Address) string { return a.Street },
|
||||
// func(a Address, s string) Address { a.Street = s; return a },
|
||||
// )
|
||||
//
|
||||
// // Compose to access street directly from person
|
||||
// personStreetLens := F.Pipe1(addressLens, lens.Compose[Person](streetLens))
|
||||
//
|
||||
// person := Person{Name: "Alice", Address: Address{Street: "Main St"}}
|
||||
// street := personStreetLens.Get(person) // "Main St"
|
||||
// updated := personStreetLens.Set("Oak Ave")(person)
|
||||
func Compose[S, A, B any](ab Lens[A, B]) func(Lens[S, A]) Lens[S, B] {
|
||||
return compose(MakeLens[func(S) B, func(S, B) S], ab)
|
||||
}
|
||||
|
||||
// ComposeOption combines a `Lens` that returns an optional value with a `Lens` that returns a definite value
|
||||
// the getter returns an `Option[B]` because the container `A` could already be an option
|
||||
// if the setter is invoked with `Some[B]` then the value of `B` will be set, potentially on a default value of `A` if `A` did not exist
|
||||
// if the setter is invoked with `None[B]` then the container `A` is reset to `None[A]` because this is the only way to remove `B`
|
||||
func ComposeOption[S, B, A any](defaultA A) func(ab Lens[A, B]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
defa := F.Constant(defaultA)
|
||||
return func(ab Lens[A, B]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
foldab := O.Fold(O.None[B], F.Flow2(ab.Get, O.Some[B]))
|
||||
return func(sa Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
// set A on S
|
||||
seta := F.Flow2(
|
||||
O.Some[A],
|
||||
sa.Set,
|
||||
)
|
||||
// remove A from S
|
||||
unseta := F.Nullary2(
|
||||
O.None[A],
|
||||
sa.Set,
|
||||
)
|
||||
return MakeLens(
|
||||
F.Flow2(sa.Get, foldab),
|
||||
func(s S, ob O.Option[B]) S {
|
||||
return F.Pipe2(
|
||||
ob,
|
||||
O.Fold(unseta, func(b B) Endomorphism[S] {
|
||||
setbona := F.Flow2(
|
||||
ab.Set(b),
|
||||
seta,
|
||||
)
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.Get,
|
||||
O.Fold(
|
||||
F.Nullary2(
|
||||
defa,
|
||||
setbona,
|
||||
),
|
||||
setbona,
|
||||
),
|
||||
)
|
||||
}),
|
||||
EM.Ap(s),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeOptions combines a `Lens` that returns an optional value with a `Lens` that returns another optional value
|
||||
// the getter returns `None[B]` if either `A` or `B` is `None`
|
||||
// if the setter is called with `Some[B]` and `A` exists, 'A' is updated with `B`
|
||||
// if the setter is called with `Some[B]` and `A` does not exist, the default of 'A' is updated with `B`
|
||||
// if the setter is called with `None[B]` and `A` does not exist this is the identity operation on 'S'
|
||||
// if the setter is called with `None[B]` and `A` does exist, 'B' is removed from 'A'
|
||||
func ComposeOptions[S, B, A any](defaultA A) func(ab Lens[A, O.Option[B]]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
defa := F.Constant(defaultA)
|
||||
noops := EM.Identity[S]
|
||||
noneb := O.None[B]()
|
||||
return func(ab Lens[A, O.Option[B]]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
unsetb := ab.Set(noneb)
|
||||
return func(sa Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
// sets an A onto S
|
||||
seta := F.Flow2(
|
||||
O.Some[A],
|
||||
sa.Set,
|
||||
)
|
||||
return MakeLensCurried(
|
||||
F.Flow2(
|
||||
sa.Get,
|
||||
O.Chain(ab.Get),
|
||||
),
|
||||
func(b O.Option[B]) Endomorphism[S] {
|
||||
return func(s S) S {
|
||||
return O.MonadFold(b, func() Endomorphism[S] {
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.Get,
|
||||
O.Fold(noops, F.Flow2(unsetb, seta)),
|
||||
)
|
||||
}, func(b B) Endomorphism[S] {
|
||||
// sets a B onto an A
|
||||
setb := F.Flow2(
|
||||
ab.Set(O.Some(b)),
|
||||
seta,
|
||||
)
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.Get,
|
||||
O.Fold(F.Nullary2(defa, setb), setb),
|
||||
)
|
||||
})(s)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose combines two lenses and allows to narrow down the focus to a sub-lens
|
||||
// ComposeRef combines two lenses for pointer-based structures.
|
||||
//
|
||||
// This is the pointer version of [Compose], automatically handling copying to ensure immutability.
|
||||
// It allows you to navigate through nested pointer structures in a composable way.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Outer structure type (will be used as *S)
|
||||
// - A: Intermediate structure type
|
||||
// - B: Inner focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: Lens from A to B (inner lens)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[*S, A] and returns a Lens[*S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Address struct {
|
||||
// Street string
|
||||
// }
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Address Address
|
||||
// }
|
||||
//
|
||||
// addressLens := lens.MakeLensRef(
|
||||
// func(p *Person) Address { return p.Address },
|
||||
// func(p *Person, a Address) *Person { p.Address = a; return p },
|
||||
// )
|
||||
//
|
||||
// streetLens := lens.MakeLens(
|
||||
// func(a Address) string { return a.Street },
|
||||
// func(a Address, s string) Address { a.Street = s; return a },
|
||||
// )
|
||||
//
|
||||
// personStreetLens := F.Pipe1(addressLens, lens.ComposeRef[Person](streetLens))
|
||||
func ComposeRef[S, A, B any](ab Lens[A, B]) func(Lens[*S, A]) Lens[*S, B] {
|
||||
return compose(MakeLensRef[func(*S) B, func(*S, B) *S], ab)
|
||||
}
|
||||
@@ -218,101 +364,108 @@ func modify[FCT ~func(A) A, S, A any](f FCT, sa Lens[S, A], s S) S {
|
||||
return sa.Set(f(sa.Get(s)))(s)
|
||||
}
|
||||
|
||||
// Modify changes a property of a [Lens] by invoking a transformation function
|
||||
// if the transformed property has not changes, the method returns the original state
|
||||
// Modify transforms a value through a lens using a transformation F.
|
||||
//
|
||||
// Instead of setting a specific value, Modify applies a function to the current value.
|
||||
// This is useful for updates like incrementing a counter, appending to a string, etc.
|
||||
// If the transformation doesn't change the value, the original structure is returned.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Structure type
|
||||
// - FCT: Transformation function type (A → A)
|
||||
// - A: Focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Transformation function to apply to the focused value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns an Endomorphism[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// counter := Counter{Value: 5}
|
||||
//
|
||||
// // Increment the counter
|
||||
// incremented := F.Pipe2(
|
||||
// valueLens,
|
||||
// lens.Modify[Counter](func(v int) int { return v + 1 }),
|
||||
// F.Ap(counter),
|
||||
// )
|
||||
// // incremented.Value == 6
|
||||
//
|
||||
// // Double the counter
|
||||
// doubled := F.Pipe2(
|
||||
// valueLens,
|
||||
// lens.Modify[Counter](func(v int) int { return v * 2 }),
|
||||
// F.Ap(counter),
|
||||
// )
|
||||
// // doubled.Value == 10
|
||||
func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S] {
|
||||
return function.Curry3(modify[FCT, S, A])(f)
|
||||
return F.Curry3(modify[FCT, S, A])(f)
|
||||
}
|
||||
|
||||
// IMap transforms the focus type of a lens using an isomorphism.
|
||||
//
|
||||
// An isomorphism is a pair of functions (A → B, B → A) that are inverses of each other.
|
||||
// IMap allows you to work with a lens in a different but equivalent type. This is useful
|
||||
// for unit conversions, encoding/decoding, or any bidirectional transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Structure type
|
||||
// - AB: Forward transformation function type (A → B)
|
||||
// - BA: Backward transformation function type (B → A)
|
||||
// - A: Original focus type
|
||||
// - B: Transformed focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: Forward transformation (A → B)
|
||||
// - ba: Backward transformation (B → A)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[E, A] and returns a Lens[E, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Celsius float64
|
||||
// type Fahrenheit float64
|
||||
//
|
||||
// celsiusToFahrenheit := func(c Celsius) Fahrenheit {
|
||||
// return Fahrenheit(c*9/5 + 32)
|
||||
// }
|
||||
//
|
||||
// fahrenheitToCelsius := func(f Fahrenheit) Celsius {
|
||||
// return Celsius((f - 32) * 5 / 9)
|
||||
// }
|
||||
//
|
||||
// type Weather struct {
|
||||
// Temperature Celsius
|
||||
// }
|
||||
//
|
||||
// tempCelsiusLens := lens.MakeLens(
|
||||
// func(w Weather) Celsius { return w.Temperature },
|
||||
// func(w Weather, t Celsius) Weather { w.Temperature = t; return w },
|
||||
// )
|
||||
//
|
||||
// // Create a lens that works with Fahrenheit
|
||||
// tempFahrenheitLens := F.Pipe1(
|
||||
// tempCelsiusLens,
|
||||
// lens.IMap[Weather](celsiusToFahrenheit, fahrenheitToCelsius),
|
||||
// )
|
||||
//
|
||||
// weather := Weather{Temperature: 20} // 20°C
|
||||
// tempF := tempFahrenheitLens.Get(weather) // 68°F
|
||||
// updated := tempFahrenheitLens.Set(86)(weather) // Set to 86°F (30°C)
|
||||
func IMap[E any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Lens[E, A]) Lens[E, B] {
|
||||
return func(ea Lens[E, A]) Lens[E, B] {
|
||||
return Lens[E, B]{Get: F.Flow2(ea.Get, ab), Set: F.Flow2(ba, ea.Set)}
|
||||
}
|
||||
}
|
||||
|
||||
// fromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func fromPredicate[GET ~func(S) O.Option[A], SET ~func(S, O.Option[A]) S, S, A any](creator func(get GET, set SET) Lens[S, O.Option[A]], pred func(A) bool, nilValue A) func(sa Lens[S, A]) Lens[S, O.Option[A]] {
|
||||
fromPred := O.FromPredicate(pred)
|
||||
return func(sa Lens[S, A]) Lens[S, O.Option[A]] {
|
||||
fold := O.Fold(F.Bind1of1(sa.Set)(nilValue), sa.Set)
|
||||
return creator(F.Flow2(sa.Get, fromPred), func(s S, a O.Option[A]) S {
|
||||
return F.Pipe2(
|
||||
a,
|
||||
fold,
|
||||
EM.Ap(s),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func FromPredicate[S, A any](pred func(A) bool, nilValue A) func(sa Lens[S, A]) Lens[S, O.Option[A]] {
|
||||
return fromPredicate(MakeLens[func(S) O.Option[A], func(S, O.Option[A]) S], pred, nilValue)
|
||||
}
|
||||
|
||||
// FromPredicateRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func FromPredicateRef[S, A any](pred func(A) bool, nilValue A) func(sa Lens[*S, A]) Lens[*S, O.Option[A]] {
|
||||
return fromPredicate(MakeLensRef[func(*S) O.Option[A], func(*S, O.Option[A]) *S], pred, nilValue)
|
||||
}
|
||||
|
||||
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the `nil` value will be set instead
|
||||
func FromNillable[S, A any](sa Lens[S, *A]) Lens[S, O.Option[*A]] {
|
||||
return FromPredicate[S](F.IsNonNil[A], nil)(sa)
|
||||
}
|
||||
|
||||
// FromNillableRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the `nil` value will be set instead
|
||||
func FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, O.Option[*A]] {
|
||||
return FromPredicateRef[S](F.IsNonNil[A], nil)(sa)
|
||||
}
|
||||
|
||||
// fromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func fromNullableProp[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], isNullable func(A) O.Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
return func(sa Lens[S, A]) Lens[S, A] {
|
||||
return creator(F.Flow3(
|
||||
sa.Get,
|
||||
isNullable,
|
||||
O.GetOrElse(F.Constant(defaultValue)),
|
||||
), func(s S, a A) S {
|
||||
return sa.Set(a)(s)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func FromNullableProp[S, A any](isNullable func(A) O.Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
return fromNullableProp(MakeLens[func(S) A, func(S, A) S], isNullable, defaultValue)
|
||||
}
|
||||
|
||||
// FromNullablePropRef returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func FromNullablePropRef[S, A any](isNullable func(A) O.Option[A], defaultValue A) func(sa Lens[*S, A]) Lens[*S, A] {
|
||||
return fromNullableProp(MakeLensRef[func(*S) A, func(*S, A) *S], isNullable, defaultValue)
|
||||
}
|
||||
|
||||
// fromFromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func fromOption[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(sa Lens[S, O.Option[A]]) Lens[S, A] {
|
||||
return func(sa Lens[S, O.Option[A]]) Lens[S, A] {
|
||||
return creator(F.Flow2(
|
||||
sa.Get,
|
||||
O.GetOrElse(F.Constant(defaultValue)),
|
||||
), func(s S, a A) S {
|
||||
return sa.Set(O.Some(a))(s)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FromFromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func FromOption[S, A any](defaultValue A) func(sa Lens[S, O.Option[A]]) Lens[S, A] {
|
||||
return fromOption(MakeLens[func(S) A, func(S, A) S], defaultValue)
|
||||
}
|
||||
|
||||
// FromFromOptionRef returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, O.Option[A]]) Lens[*S, A] {
|
||||
return fromOption(MakeLensRef[func(*S) A, func(*S, A) *S], defaultValue)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -172,83 +171,6 @@ func TestPassByValue(t *testing.T) {
|
||||
assert.Equal(t, "value2", s2.name)
|
||||
}
|
||||
|
||||
func TestFromNullableProp(t *testing.T) {
|
||||
// default inner object
|
||||
defaultInner := &Inner{
|
||||
Value: 0,
|
||||
Foo: "foo",
|
||||
}
|
||||
// access to the value
|
||||
value := MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
// access to inner
|
||||
inner := FromNullableProp[Outer](O.FromNillable[Inner], defaultInner)(MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
// compose
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
Compose[Outer](value),
|
||||
)
|
||||
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
|
||||
// the checks
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{}))
|
||||
assert.Equal(t, 0, lens.Get(Outer{}))
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
|
||||
assert.Equal(t, 1, lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
|
||||
assert.Equal(t, outer1, Modify[Outer](F.Identity[int])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestComposeOption(t *testing.T) {
|
||||
// default inner object
|
||||
defaultInner := &Inner{
|
||||
Value: 0,
|
||||
Foo: "foo",
|
||||
}
|
||||
// access to the value
|
||||
value := MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
// access to inner
|
||||
inner := FromNillable(MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
// compose lenses
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
ComposeOption[Outer, int](defaultInner)(value),
|
||||
)
|
||||
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
|
||||
// the checks
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{}))
|
||||
assert.Equal(t, O.None[int](), lens.Get(Outer{}))
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
|
||||
assert.Equal(t, O.Some(1), lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
|
||||
assert.Equal(t, outer1, Modify[Outer](F.Identity[O.Option[int]])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestComposeOptions(t *testing.T) {
|
||||
// default inner object
|
||||
defaultValue1 := 1
|
||||
defaultFoo1 := "foo1"
|
||||
defaultInner := &InnerOpt{
|
||||
Value: &defaultValue1,
|
||||
Foo: &defaultFoo1,
|
||||
}
|
||||
// access to the value
|
||||
value := FromNillable(MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
|
||||
// access to inner
|
||||
inner := FromNillable(MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
|
||||
// compose lenses
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
ComposeOptions[OuterOpt, *int](defaultInner)(value),
|
||||
)
|
||||
// additional settings
|
||||
defaultValue2 := 2
|
||||
defaultFoo2 := "foo2"
|
||||
outer1 := OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}
|
||||
// the checks
|
||||
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{}))
|
||||
assert.Equal(t, O.None[*int](), lens.Get(OuterOpt{}))
|
||||
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo2}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}))
|
||||
assert.Equal(t, O.Some(&defaultValue1), lens.Get(OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}))
|
||||
assert.Equal(t, outer1, Modify[OuterOpt](F.Identity[O.Option[*int]])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestIdRef(t *testing.T) {
|
||||
idLens := IdRef[Street]()
|
||||
street := &Street{num: 1, name: "Main"}
|
||||
@@ -272,93 +194,6 @@ func TestComposeRef(t *testing.T) {
|
||||
assert.Equal(t, sampleStreet.name, sampleAddress.street.name) // Original unchanged
|
||||
}
|
||||
|
||||
func TestFromPredicateRef(t *testing.T) {
|
||||
type Person struct {
|
||||
age int
|
||||
}
|
||||
|
||||
ageLens := MakeLensRef(
|
||||
func(p *Person) int { return p.age },
|
||||
func(p *Person, age int) *Person {
|
||||
p.age = age
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
adultLens := FromPredicateRef[Person](func(age int) bool { return age >= 18 }, 0)(ageLens)
|
||||
|
||||
adult := &Person{age: 25}
|
||||
assert.Equal(t, O.Some(25), adultLens.Get(adult))
|
||||
|
||||
minor := &Person{age: 15}
|
||||
assert.Equal(t, O.None[int](), adultLens.Get(minor))
|
||||
}
|
||||
|
||||
func TestFromNillableRef(t *testing.T) {
|
||||
type Config struct {
|
||||
timeout *int
|
||||
}
|
||||
|
||||
timeoutLens := MakeLensRef(
|
||||
func(c *Config) *int { return c.timeout },
|
||||
func(c *Config, t *int) *Config {
|
||||
c.timeout = t
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
optLens := FromNillableRef(timeoutLens)
|
||||
|
||||
config := &Config{timeout: nil}
|
||||
assert.Equal(t, O.None[*int](), optLens.Get(config))
|
||||
|
||||
timeout := 30
|
||||
configWithTimeout := &Config{timeout: &timeout}
|
||||
assert.True(t, O.IsSome(optLens.Get(configWithTimeout)))
|
||||
}
|
||||
|
||||
func TestFromNullablePropRef(t *testing.T) {
|
||||
type Config struct {
|
||||
timeout *int
|
||||
}
|
||||
|
||||
timeoutLens := MakeLensRef(
|
||||
func(c *Config) *int { return c.timeout },
|
||||
func(c *Config, t *int) *Config {
|
||||
c.timeout = t
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
defaultTimeout := 30
|
||||
safeLens := FromNullablePropRef[Config](O.FromNillable[int], &defaultTimeout)(timeoutLens)
|
||||
|
||||
config := &Config{timeout: nil}
|
||||
assert.Equal(t, &defaultTimeout, safeLens.Get(config))
|
||||
}
|
||||
|
||||
func TestFromOptionRef(t *testing.T) {
|
||||
type Settings struct {
|
||||
retries O.Option[int]
|
||||
}
|
||||
|
||||
retriesLens := MakeLensRef(
|
||||
func(s *Settings) O.Option[int] { return s.retries },
|
||||
func(s *Settings, r O.Option[int]) *Settings {
|
||||
s.retries = r
|
||||
return s
|
||||
},
|
||||
)
|
||||
|
||||
safeLens := FromOptionRef[Settings](3)(retriesLens)
|
||||
|
||||
settings := &Settings{retries: O.None[int]()}
|
||||
assert.Equal(t, 3, safeLens.Get(settings))
|
||||
|
||||
settingsWithRetries := &Settings{retries: O.Some(5)}
|
||||
assert.Equal(t, 5, safeLens.Get(settingsWithRetries))
|
||||
}
|
||||
|
||||
func TestMakeLensCurried(t *testing.T) {
|
||||
nameLens := MakeLensCurried(
|
||||
func(s Street) string { return s.name },
|
||||
|
||||
192
v2/optics/lens/option/compose.go
Normal file
192
v2/optics/lens/option/compose.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Compose composes two lenses that both return optional values.
|
||||
//
|
||||
// This handles the case where both the intermediate structure A and the inner focus B are optional.
|
||||
// The getter returns None[B] if either A or B is None. The setter behavior is:
|
||||
// - Set(Some[B]) when A exists: Updates B in A
|
||||
// - Set(Some[B]) when A doesn't exist: Creates A with defaultA and sets B
|
||||
// - Set(None[B]) when A doesn't exist: Identity operation (no change)
|
||||
// - Set(None[B]) when A exists: Removes B from A (sets it to None)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Outer structure type
|
||||
// - B: Inner focus type (optional)
|
||||
// - A: Intermediate structure type (optional)
|
||||
//
|
||||
// Parameters:
|
||||
// - defaultA: Default value for A when it doesn't exist but B needs to be set
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a LensO[A, B] and returns a function that takes a
|
||||
// LensO[S, A] and returns a LensO[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Settings struct {
|
||||
// MaxRetries *int
|
||||
// }
|
||||
//
|
||||
// type Config struct {
|
||||
// Settings *Settings
|
||||
// }
|
||||
//
|
||||
// settingsLens := lens.FromNillable(lens.MakeLens(
|
||||
// func(c Config) *Settings { return c.Settings },
|
||||
// func(c Config, s *Settings) Config { c.Settings = s; return c },
|
||||
// ))
|
||||
//
|
||||
// retriesLens := lens.FromNillable(lens.MakeLensRef(
|
||||
// func(s *Settings) *int { return s.MaxRetries },
|
||||
// func(s *Settings, r *int) *Settings { s.MaxRetries = r; return s },
|
||||
// ))
|
||||
//
|
||||
// defaultSettings := &Settings{}
|
||||
// configRetriesLens := F.Pipe1(settingsLens,
|
||||
// lens.Compose[Config, *int](defaultSettings)(retriesLens))
|
||||
func Compose[S, B, A any](defaultA A) func(ab LensO[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
noneb := O.None[B]()
|
||||
return func(ab LensO[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
abGet := ab.Get
|
||||
abSetNone := ab.Set(noneb)
|
||||
return func(sa LensO[S, A]) LensO[S, B] {
|
||||
saGet := sa.Get
|
||||
// Pre-compute setter for Some[A]
|
||||
setSomeA := F.Flow2(O.Some[A], sa.Set)
|
||||
return lens.MakeLensCurried(
|
||||
F.Flow2(saGet, O.Chain(abGet)),
|
||||
func(optB Option[B]) Endomorphism[S] {
|
||||
return func(s S) S {
|
||||
optA := saGet(s)
|
||||
return O.MonadFold(
|
||||
optB,
|
||||
// optB is None
|
||||
func() S {
|
||||
return O.MonadFold(
|
||||
optA,
|
||||
// optA is None - no-op
|
||||
F.Constant(s),
|
||||
// optA is Some - unset B in A
|
||||
func(a A) S {
|
||||
return setSomeA(abSetNone(a))(s)
|
||||
},
|
||||
)
|
||||
},
|
||||
// optB is Some
|
||||
func(b B) S {
|
||||
setB := ab.Set(O.Some(b))
|
||||
return O.MonadFold(
|
||||
optA,
|
||||
// optA is None - create with defaultA
|
||||
func() S {
|
||||
return setSomeA(setB(defaultA))(s)
|
||||
},
|
||||
// optA is Some - update B in A
|
||||
func(a A) S {
|
||||
return setSomeA(setB(a))(s)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeOption composes a lens returning an optional value with a lens returning a definite value.
|
||||
//
|
||||
// This is useful when you have an optional intermediate structure and want to focus on a field
|
||||
// within it. The getter returns Option[B] because the container A might not exist. The setter
|
||||
// behavior depends on the input:
|
||||
// - Set(Some[B]): Updates B in A, creating A with defaultA if it doesn't exist
|
||||
// - Set(None[B]): Removes A entirely (sets it to None[A])
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Outer structure type
|
||||
// - B: Inner focus type (definite value)
|
||||
// - A: Intermediate structure type (optional)
|
||||
//
|
||||
// Parameters:
|
||||
// - defaultA: Default value for A when it doesn't exist but B needs to be set
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[A, B] and returns a function that takes a
|
||||
// LensO[S, A] and returns a LensO[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Database struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// type Config struct {
|
||||
// Database *Database
|
||||
// }
|
||||
//
|
||||
// dbLens := lens.FromNillable(lens.MakeLens(
|
||||
// func(c Config) *Database { return c.Database },
|
||||
// func(c Config, db *Database) Config { c.Database = db; return c },
|
||||
// ))
|
||||
//
|
||||
// portLens := lens.MakeLensRef(
|
||||
// func(db *Database) int { return db.Port },
|
||||
// func(db *Database, port int) *Database { db.Port = port; return db },
|
||||
// )
|
||||
//
|
||||
// defaultDB := &Database{Host: "localhost", Port: 5432}
|
||||
// configPortLens := F.Pipe1(dbLens, lens.ComposeOption[Config, int](defaultDB)(portLens))
|
||||
//
|
||||
// config := Config{Database: nil}
|
||||
// port := configPortLens.Get(config) // None[int]
|
||||
// updated := configPortLens.Set(O.Some(3306))(config)
|
||||
// // updated.Database.Port == 3306, Host == "localhost" (from default)
|
||||
func ComposeOption[S, B, A any](defaultA A) func(ab Lens[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
return func(ab Lens[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
abGet := ab.Get
|
||||
abSet := ab.Set
|
||||
return func(sa LensO[S, A]) LensO[S, B] {
|
||||
saGet := sa.Get
|
||||
saSet := sa.Set
|
||||
// Pre-compute setters
|
||||
setNoneA := saSet(O.None[A]())
|
||||
setSomeA := func(a A) Endomorphism[S] {
|
||||
return saSet(O.Some(a))
|
||||
}
|
||||
return lens.MakeLens(
|
||||
func(s S) Option[B] {
|
||||
return O.Map(abGet)(saGet(s))
|
||||
},
|
||||
func(s S, optB Option[B]) S {
|
||||
return O.Fold(
|
||||
// optB is None - remove A entirely
|
||||
F.Constant(setNoneA(s)),
|
||||
// optB is Some - set B
|
||||
func(b B) S {
|
||||
optA := saGet(s)
|
||||
return O.Fold(
|
||||
// optA is None - create with defaultA
|
||||
func() S {
|
||||
return setSomeA(abSet(b)(defaultA))(s)
|
||||
},
|
||||
// optA is Some - update B in A
|
||||
func(a A) S {
|
||||
return setSomeA(abSet(b)(a))(s)
|
||||
},
|
||||
)(optA)
|
||||
},
|
||||
)(optB)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
31
v2/optics/lens/option/coverage.out
Normal file
31
v2/optics/lens/option/coverage.out
Normal file
@@ -0,0 +1,31 @@
|
||||
mode: count
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:55.97,59.60 4 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:59.60,61.43 2 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:61.43,72.39 2 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:72.39,73.25 1 13
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:73.25,74.52 1 13
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:74.52,80.8 1 6
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:80.36,91.8 2 7
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:147.95,149.59 2 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:149.59,151.43 2 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:151.43,164.31 3 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:164.31,167.48 1 12
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:167.48,183.8 2 7
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:12.188,14.41 2 15
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:14.41,16.70 2 15
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:16.70,22.4 1 60
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:28.93,30.2 1 12
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:34.105,36.2 1 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:40.65,42.2 1 10
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:46.70,48.2 1 2
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:51.188,52.40 1 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:52.40,57.23 1 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:57.23,59.4 1 4
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:65.110,67.2 1 2
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:70.115,72.2 1 1
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:75.153,76.41 1 2
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:76.41,80.23 1 2
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:80.23,82.4 1 2
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:88.75,90.2 1 1
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:107.87,109.2 1 1
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/option.go:63.67,65.2 1 1
|
||||
138
v2/optics/lens/option/doc.go
Normal file
138
v2/optics/lens/option/doc.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// 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 option provides utilities for working with lenses that focus on optional values.
|
||||
//
|
||||
// This package extends the lens optics pattern to handle Option types, enabling safe
|
||||
// manipulation of potentially absent values in nested data structures. It provides
|
||||
// functions for creating, composing, and transforming lenses that work with optional
|
||||
// fields.
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// A LensO[S, A] is a Lens[S, Option[A]] - a lens that focuses on an optional value A
|
||||
// within a structure S. This is particularly useful when dealing with nullable pointers,
|
||||
// optional fields, or values that may not always be present.
|
||||
//
|
||||
// # Key Functions
|
||||
//
|
||||
// Creating Lenses from Optional Values:
|
||||
// - FromNillable: Creates a lens from a nullable pointer field
|
||||
// - FromNillableRef: Pointer-based version of FromNillable
|
||||
// - FromPredicate: Creates a lens based on a predicate function
|
||||
// - FromPredicateRef: Pointer-based version of FromPredicate
|
||||
// - FromOption: Converts an optional lens to a definite lens with a default value
|
||||
// - FromOptionRef: Pointer-based version of FromOption
|
||||
// - FromNullableProp: Creates a lens with a default value for nullable properties
|
||||
// - FromNullablePropRef: Pointer-based version of FromNullableProp
|
||||
//
|
||||
// Composing Lenses:
|
||||
// - ComposeOption: Composes a lens returning Option[A] with a lens returning B
|
||||
// - ComposeOptions: Composes two lenses that both return optional values
|
||||
//
|
||||
// Conversions:
|
||||
// - AsTraversal: Converts a lens to a traversal for use with traversal operations
|
||||
//
|
||||
// # Usage Examples
|
||||
//
|
||||
// Working with nullable pointers:
|
||||
//
|
||||
// type Config struct {
|
||||
// Database *DatabaseConfig
|
||||
// }
|
||||
//
|
||||
// type DatabaseConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Create a lens for the optional database config
|
||||
// dbLens := lens.FromNillable(lens.MakeLens(
|
||||
// func(c Config) *DatabaseConfig { return c.Database },
|
||||
// func(c Config, db *DatabaseConfig) Config { c.Database = db; return c },
|
||||
// ))
|
||||
//
|
||||
// // Access the optional value
|
||||
// config := Config{Database: nil}
|
||||
// dbOpt := dbLens.Get(config) // Returns None[*DatabaseConfig]
|
||||
//
|
||||
// // Set a value
|
||||
// newDB := &DatabaseConfig{Host: "localhost", Port: 5432}
|
||||
// updated := dbLens.Set(O.Some(newDB))(config)
|
||||
//
|
||||
// Composing optional lenses:
|
||||
//
|
||||
// // Lens to access port through optional database
|
||||
// portLens := lens.MakeLensRef(
|
||||
// func(db *DatabaseConfig) int { return db.Port },
|
||||
// func(db *DatabaseConfig, port int) *DatabaseConfig { db.Port = port; return db },
|
||||
// )
|
||||
//
|
||||
// defaultDB := &DatabaseConfig{Host: "localhost", Port: 5432}
|
||||
// configPortLens := F.Pipe1(dbLens,
|
||||
// lens.ComposeOption[Config, int](defaultDB)(portLens))
|
||||
//
|
||||
// // Get returns None if database is not set
|
||||
// port := configPortLens.Get(config) // None[int]
|
||||
//
|
||||
// // Set creates the database with default values if needed
|
||||
// withPort := configPortLens.Set(O.Some(3306))(config)
|
||||
// // withPort.Database.Port == 3306, Host == "localhost"
|
||||
//
|
||||
// Working with predicates:
|
||||
//
|
||||
// type Person struct {
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLensRef(
|
||||
// func(p *Person) int { return p.Age },
|
||||
// func(p *Person, age int) *Person { p.Age = age; return p },
|
||||
// )
|
||||
//
|
||||
// // Only consider adults (age >= 18)
|
||||
// adultLens := lens.FromPredicateRef[Person](
|
||||
// func(age int) bool { return age >= 18 },
|
||||
// 0, // nil value for non-adults
|
||||
// )(ageLens)
|
||||
//
|
||||
// adult := &Person{Age: 25}
|
||||
// adultLens.Get(adult) // Some(25)
|
||||
//
|
||||
// minor := &Person{Age: 15}
|
||||
// adultLens.Get(minor) // None[int]
|
||||
//
|
||||
// # Design Patterns
|
||||
//
|
||||
// The package follows functional programming principles:
|
||||
// - Immutability: All operations return new values rather than modifying in place
|
||||
// - Composition: Lenses can be composed to access deeply nested optional values
|
||||
// - Type Safety: The type system ensures correct usage at compile time
|
||||
// - Lawful: All lenses satisfy the lens laws (get-put, put-get, put-put)
|
||||
//
|
||||
// # Performance Considerations
|
||||
//
|
||||
// Lens operations are generally efficient, but composing many lenses can create
|
||||
// function call overhead. For performance-critical code, consider:
|
||||
// - Caching composed lenses rather than recreating them
|
||||
// - Using direct field access for simple cases
|
||||
// - Profiling to identify bottlenecks
|
||||
//
|
||||
// # Related Packages
|
||||
//
|
||||
// - github.com/IBM/fp-go/v2/optics/lens: Core lens functionality
|
||||
// - github.com/IBM/fp-go/v2/option: Option type and operations
|
||||
// - github.com/IBM/fp-go/v2/optics/traversal/option: Traversals for optional values
|
||||
package option
|
||||
109
v2/optics/lens/option/from.go
Normal file
109
v2/optics/lens/option/from.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// fromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func fromPredicate[GET ~func(S) Option[A], SET ~func(S, Option[A]) S, S, A any](creator func(get GET, set SET) LensO[S, A], pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
|
||||
fromPred := O.FromPredicate(pred)
|
||||
return func(sa Lens[S, A]) LensO[S, A] {
|
||||
fold := O.Fold(F.Bind1of1(sa.Set)(nilValue), sa.Set)
|
||||
return creator(F.Flow2(sa.Get, fromPred), func(s S, a Option[A]) S {
|
||||
return F.Pipe2(
|
||||
a,
|
||||
fold,
|
||||
EM.Ap(s),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func FromPredicate[S, A any](pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
|
||||
return fromPredicate(lens.MakeLens[func(S) Option[A], func(S, Option[A]) S], pred, nilValue)
|
||||
}
|
||||
|
||||
// FromPredicateRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func FromPredicateRef[S, A any](pred func(A) bool, nilValue A) func(sa Lens[*S, A]) Lens[*S, Option[A]] {
|
||||
return fromPredicate(lens.MakeLensRef[func(*S) Option[A], func(*S, Option[A]) *S], pred, nilValue)
|
||||
}
|
||||
|
||||
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the `nil` value will be set instead
|
||||
func FromNillable[S, A any](sa Lens[S, *A]) Lens[S, Option[*A]] {
|
||||
return FromPredicate[S](F.IsNonNil[A], nil)(sa)
|
||||
}
|
||||
|
||||
// FromNillableRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the `nil` value will be set instead
|
||||
func FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, Option[*A]] {
|
||||
return FromPredicateRef[S](F.IsNonNil[A], nil)(sa)
|
||||
}
|
||||
|
||||
// fromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func fromNullableProp[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
return func(sa Lens[S, A]) Lens[S, A] {
|
||||
return creator(F.Flow3(
|
||||
sa.Get,
|
||||
isNullable,
|
||||
O.GetOrElse(F.Constant(defaultValue)),
|
||||
), func(s S, a A) S {
|
||||
return sa.Set(a)(s)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func FromNullableProp[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
return fromNullableProp(lens.MakeLens[func(S) A, func(S, A) S], isNullable, defaultValue)
|
||||
}
|
||||
|
||||
// FromNullablePropRef returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func FromNullablePropRef[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[*S, A]) Lens[*S, A] {
|
||||
return fromNullableProp(lens.MakeLensRef[func(*S) A, func(*S, A) *S], isNullable, defaultValue)
|
||||
}
|
||||
|
||||
// fromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func fromOption[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
|
||||
return func(sa LensO[S, A]) Lens[S, A] {
|
||||
return creator(F.Flow2(
|
||||
sa.Get,
|
||||
O.GetOrElse(F.Constant(defaultValue)),
|
||||
), func(s S, a A) S {
|
||||
return sa.Set(O.Some(a))(s)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func FromOption[S, A any](defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
|
||||
return fromOption(lens.MakeLens[func(S) A, func(S, A) S], defaultValue)
|
||||
}
|
||||
|
||||
// FromOptionRef creates a lens from an Option property with a default value for pointer structures.
|
||||
//
|
||||
// This is the pointer version of [FromOption], with automatic copying to ensure immutability.
|
||||
// The getter returns the value inside Some[A], or the defaultValue if it's None[A].
|
||||
// The setter always wraps the value in Some[A].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Structure type (will be used as *S)
|
||||
// - A: Focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - defaultValue: Value to return when the Option is None
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[*S, Option[A]] and returns a Lens[*S, A]
|
||||
func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, Option[A]]) Lens[*S, A] {
|
||||
return fromOption(lens.MakeLensRef[func(*S) A, func(*S, A) *S], defaultValue)
|
||||
}
|
||||
759
v2/optics/lens/option/lens_test.go
Normal file
759
v2/optics/lens/option/lens_test.go
Normal file
@@ -0,0 +1,759 @@
|
||||
// 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 option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
EQT "github.com/IBM/fp-go/v2/eq/testing"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type (
|
||||
Street struct {
|
||||
name string
|
||||
}
|
||||
|
||||
Address struct {
|
||||
street *Street
|
||||
}
|
||||
|
||||
Inner struct {
|
||||
Value int
|
||||
Foo string
|
||||
}
|
||||
|
||||
InnerOpt struct {
|
||||
Value *int
|
||||
Foo *string
|
||||
}
|
||||
|
||||
Outer struct {
|
||||
inner *Inner
|
||||
}
|
||||
|
||||
OuterOpt struct {
|
||||
inner *InnerOpt
|
||||
}
|
||||
)
|
||||
|
||||
func (outer Outer) GetInner() *Inner {
|
||||
return outer.inner
|
||||
}
|
||||
|
||||
func (outer Outer) SetInner(inner *Inner) Outer {
|
||||
outer.inner = inner
|
||||
return outer
|
||||
}
|
||||
|
||||
func (outer OuterOpt) GetInnerOpt() *InnerOpt {
|
||||
return outer.inner
|
||||
}
|
||||
|
||||
func (outer OuterOpt) SetInnerOpt(inner *InnerOpt) OuterOpt {
|
||||
outer.inner = inner
|
||||
return outer
|
||||
}
|
||||
|
||||
func (inner *Inner) GetValue() int {
|
||||
return inner.Value
|
||||
}
|
||||
|
||||
func (inner *Inner) SetValue(value int) *Inner {
|
||||
inner.Value = value
|
||||
return inner
|
||||
}
|
||||
|
||||
func (inner *InnerOpt) GetValue() *int {
|
||||
return inner.Value
|
||||
}
|
||||
|
||||
func (inner *InnerOpt) SetValue(value *int) *InnerOpt {
|
||||
inner.Value = value
|
||||
return inner
|
||||
}
|
||||
|
||||
func (street *Street) GetName() string {
|
||||
return street.name
|
||||
}
|
||||
|
||||
func (street *Street) SetName(name string) *Street {
|
||||
street.name = name
|
||||
return street
|
||||
}
|
||||
|
||||
func (addr *Address) GetStreet() *Street {
|
||||
return addr.street
|
||||
}
|
||||
|
||||
func (addr *Address) SetStreet(s *Street) *Address {
|
||||
addr.street = s
|
||||
return addr
|
||||
}
|
||||
|
||||
var (
|
||||
streetLens = L.MakeLensRef((*Street).GetName, (*Street).SetName)
|
||||
addrLens = L.MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
|
||||
|
||||
sampleStreet = Street{name: "Schönaicherstr"}
|
||||
sampleAddress = Address{street: &sampleStreet}
|
||||
)
|
||||
|
||||
func TestComposeOption(t *testing.T) {
|
||||
// default inner object
|
||||
defaultInner := &Inner{
|
||||
Value: 0,
|
||||
Foo: "foo",
|
||||
}
|
||||
// access to the value
|
||||
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
// access to inner
|
||||
inner := FromNillable(L.MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
// compose lenses
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
ComposeOption[Outer, int](defaultInner)(value),
|
||||
)
|
||||
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
|
||||
// the checks
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{}))
|
||||
assert.Equal(t, O.None[int](), lens.Get(Outer{}))
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
|
||||
assert.Equal(t, O.Some(1), lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
|
||||
assert.Equal(t, outer1, L.Modify[Outer](F.Identity[Option[int]])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestComposeOptions(t *testing.T) {
|
||||
// default inner object
|
||||
defaultValue1 := 1
|
||||
defaultFoo1 := "foo1"
|
||||
defaultInner := &InnerOpt{
|
||||
Value: &defaultValue1,
|
||||
Foo: &defaultFoo1,
|
||||
}
|
||||
// access to the value
|
||||
value := FromNillable(L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
|
||||
// access to inner
|
||||
inner := FromNillable(L.MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
|
||||
// compose lenses
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
Compose[OuterOpt, *int](defaultInner)(value),
|
||||
)
|
||||
// additional settings
|
||||
defaultValue2 := 2
|
||||
defaultFoo2 := "foo2"
|
||||
outer1 := OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}
|
||||
// the checks
|
||||
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{}))
|
||||
assert.Equal(t, O.None[*int](), lens.Get(OuterOpt{}))
|
||||
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo2}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}))
|
||||
assert.Equal(t, O.Some(&defaultValue1), lens.Get(OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}))
|
||||
assert.Equal(t, outer1, L.Modify[OuterOpt](F.Identity[Option[*int]])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestFromNullableProp(t *testing.T) {
|
||||
// default inner object
|
||||
defaultInner := &Inner{
|
||||
Value: 0,
|
||||
Foo: "foo",
|
||||
}
|
||||
// access to the value
|
||||
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
// access to inner
|
||||
inner := FromNullableProp[Outer](O.FromNillable[Inner], defaultInner)(L.MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
// compose
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
L.Compose[Outer](value),
|
||||
)
|
||||
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
|
||||
// the checks
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{}))
|
||||
assert.Equal(t, 0, lens.Get(Outer{}))
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
|
||||
assert.Equal(t, 1, lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
|
||||
assert.Equal(t, outer1, L.Modify[Outer](F.Identity[int])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestFromPredicateRef(t *testing.T) {
|
||||
type Person struct {
|
||||
age int
|
||||
}
|
||||
|
||||
ageLens := L.MakeLensRef(
|
||||
func(p *Person) int { return p.age },
|
||||
func(p *Person, age int) *Person {
|
||||
p.age = age
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
adultLens := FromPredicateRef[Person](func(age int) bool { return age >= 18 }, 0)(ageLens)
|
||||
|
||||
adult := &Person{age: 25}
|
||||
assert.Equal(t, O.Some(25), adultLens.Get(adult))
|
||||
|
||||
minor := &Person{age: 15}
|
||||
assert.Equal(t, O.None[int](), adultLens.Get(minor))
|
||||
}
|
||||
|
||||
func TestFromNillableRef(t *testing.T) {
|
||||
type Config struct {
|
||||
timeout *int
|
||||
}
|
||||
|
||||
timeoutLens := L.MakeLensRef(
|
||||
func(c *Config) *int { return c.timeout },
|
||||
func(c *Config, t *int) *Config {
|
||||
c.timeout = t
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
optLens := FromNillableRef(timeoutLens)
|
||||
|
||||
config := &Config{timeout: nil}
|
||||
assert.Equal(t, O.None[*int](), optLens.Get(config))
|
||||
|
||||
timeout := 30
|
||||
configWithTimeout := &Config{timeout: &timeout}
|
||||
assert.True(t, O.IsSome(optLens.Get(configWithTimeout)))
|
||||
}
|
||||
|
||||
func TestFromNullablePropRef(t *testing.T) {
|
||||
type Config struct {
|
||||
timeout *int
|
||||
}
|
||||
|
||||
timeoutLens := L.MakeLensRef(
|
||||
func(c *Config) *int { return c.timeout },
|
||||
func(c *Config, t *int) *Config {
|
||||
c.timeout = t
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
defaultTimeout := 30
|
||||
safeLens := FromNullablePropRef[Config](O.FromNillable[int], &defaultTimeout)(timeoutLens)
|
||||
|
||||
config := &Config{timeout: nil}
|
||||
assert.Equal(t, &defaultTimeout, safeLens.Get(config))
|
||||
}
|
||||
|
||||
func TestFromOptionRef(t *testing.T) {
|
||||
type Settings struct {
|
||||
retries Option[int]
|
||||
}
|
||||
|
||||
retriesLens := L.MakeLensRef(
|
||||
func(s *Settings) Option[int] { return s.retries },
|
||||
func(s *Settings, r Option[int]) *Settings {
|
||||
s.retries = r
|
||||
return s
|
||||
},
|
||||
)
|
||||
|
||||
safeLens := FromOptionRef[Settings](3)(retriesLens)
|
||||
|
||||
settings := &Settings{retries: O.None[int]()}
|
||||
assert.Equal(t, 3, safeLens.Get(settings))
|
||||
|
||||
settingsWithRetries := &Settings{retries: O.Some(5)}
|
||||
assert.Equal(t, 5, safeLens.Get(settingsWithRetries))
|
||||
}
|
||||
|
||||
func TestFromOption(t *testing.T) {
|
||||
type Config struct {
|
||||
retries Option[int]
|
||||
}
|
||||
|
||||
retriesLens := L.MakeLens(
|
||||
func(c Config) Option[int] { return c.retries },
|
||||
func(c Config, r Option[int]) Config { c.retries = r; return c },
|
||||
)
|
||||
|
||||
defaultRetries := 3
|
||||
safeLens := FromOption[Config](defaultRetries)(retriesLens)
|
||||
|
||||
// Test with None - should return default
|
||||
config := Config{retries: O.None[int]()}
|
||||
assert.Equal(t, defaultRetries, safeLens.Get(config))
|
||||
|
||||
// Test with Some - should return the value
|
||||
configWithRetries := Config{retries: O.Some(5)}
|
||||
assert.Equal(t, 5, safeLens.Get(configWithRetries))
|
||||
|
||||
// Test setter - should always set Some
|
||||
updated := safeLens.Set(10)(config)
|
||||
assert.Equal(t, O.Some(10), updated.retries)
|
||||
|
||||
// Test setter on existing Some - should replace
|
||||
updated2 := safeLens.Set(7)(configWithRetries)
|
||||
assert.Equal(t, O.Some(7), updated2.retries)
|
||||
}
|
||||
|
||||
func TestAsTraversal(t *testing.T) {
|
||||
type Data struct {
|
||||
value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(d Data) int { return d.value },
|
||||
func(d Data, v int) Data { d.value = v; return d },
|
||||
)
|
||||
|
||||
// Convert lens to traversal
|
||||
traversal := AsTraversal[Data, int]()(valueLens)
|
||||
|
||||
// Test that traversal is created (basic smoke test)
|
||||
assert.NotNil(t, traversal)
|
||||
|
||||
// The traversal should work with the data
|
||||
data := Data{value: 42}
|
||||
|
||||
// Verify the traversal can be used (it's a function that takes a functor)
|
||||
// This is a basic smoke test to ensure the conversion works
|
||||
assert.NotNil(t, data)
|
||||
assert.Equal(t, 42, valueLens.Get(data))
|
||||
}
|
||||
|
||||
func TestComposeOptionsEdgeCases(t *testing.T) {
|
||||
// Test setting None when inner doesn't exist
|
||||
defaultValue1 := 1
|
||||
defaultFoo1 := "foo1"
|
||||
defaultInner := &InnerOpt{
|
||||
Value: &defaultValue1,
|
||||
Foo: &defaultFoo1,
|
||||
}
|
||||
|
||||
value := FromNillable(L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
|
||||
inner := FromNillable(L.MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
Compose[OuterOpt, *int](defaultInner)(value),
|
||||
)
|
||||
|
||||
// Setting None when inner doesn't exist should be a no-op
|
||||
emptyOuter := OuterOpt{}
|
||||
result := lens.Set(O.None[*int]())(emptyOuter)
|
||||
assert.Equal(t, O.None[*InnerOpt](), inner.Get(result))
|
||||
|
||||
// Setting None when inner exists should unset the value
|
||||
defaultValue2 := 2
|
||||
defaultFoo2 := "foo2"
|
||||
outerWithInner := OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}
|
||||
result2 := lens.Set(O.None[*int]())(outerWithInner)
|
||||
assert.NotNil(t, result2.inner)
|
||||
assert.Nil(t, result2.inner.Value)
|
||||
assert.Equal(t, &defaultFoo2, result2.inner.Foo)
|
||||
}
|
||||
|
||||
func TestComposeOptionEdgeCases(t *testing.T) {
|
||||
defaultInner := &Inner{
|
||||
Value: 0,
|
||||
Foo: "foo",
|
||||
}
|
||||
|
||||
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
inner := FromNillable(L.MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
ComposeOption[Outer, int](defaultInner)(value),
|
||||
)
|
||||
|
||||
// Setting None should remove the inner entirely
|
||||
outerWithInner := Outer{inner: &Inner{Value: 42, Foo: "bar"}}
|
||||
result := lens.Set(O.None[int]())(outerWithInner)
|
||||
assert.Nil(t, result.inner)
|
||||
|
||||
// Getting from empty should return None
|
||||
emptyOuter := Outer{}
|
||||
assert.Equal(t, O.None[int](), lens.Get(emptyOuter))
|
||||
}
|
||||
|
||||
func TestFromPredicateEdgeCases(t *testing.T) {
|
||||
type Score struct {
|
||||
points int
|
||||
}
|
||||
|
||||
pointsLens := L.MakeLens(
|
||||
func(s Score) int { return s.points },
|
||||
func(s Score, p int) Score { s.points = p; return s },
|
||||
)
|
||||
|
||||
// Only positive scores are valid
|
||||
validLens := FromPredicate[Score](func(p int) bool { return p > 0 }, 0)(pointsLens)
|
||||
|
||||
// Test with valid score
|
||||
validScore := Score{points: 100}
|
||||
assert.Equal(t, O.Some(100), validLens.Get(validScore))
|
||||
|
||||
// Test with invalid score (zero)
|
||||
zeroScore := Score{points: 0}
|
||||
assert.Equal(t, O.None[int](), validLens.Get(zeroScore))
|
||||
|
||||
// Test with invalid score (negative)
|
||||
negativeScore := Score{points: -10}
|
||||
assert.Equal(t, O.None[int](), validLens.Get(negativeScore))
|
||||
|
||||
// Test setting None sets the nil value
|
||||
result := validLens.Set(O.None[int]())(validScore)
|
||||
assert.Equal(t, 0, result.points)
|
||||
|
||||
// Test setting Some sets the value
|
||||
result2 := validLens.Set(O.Some(50))(zeroScore)
|
||||
assert.Equal(t, 50, result2.points)
|
||||
}
|
||||
|
||||
func TestFromNullablePropEdgeCases(t *testing.T) {
|
||||
type Container struct {
|
||||
item *string
|
||||
}
|
||||
|
||||
itemLens := L.MakeLens(
|
||||
func(c Container) *string { return c.item },
|
||||
func(c Container, i *string) Container { c.item = i; return c },
|
||||
)
|
||||
|
||||
defaultItem := "default"
|
||||
safeLens := FromNullableProp[Container](O.FromNillable[string], &defaultItem)(itemLens)
|
||||
|
||||
// Test with nil - should return default
|
||||
emptyContainer := Container{item: nil}
|
||||
assert.Equal(t, &defaultItem, safeLens.Get(emptyContainer))
|
||||
|
||||
// Test with value - should return the value
|
||||
value := "actual"
|
||||
containerWithItem := Container{item: &value}
|
||||
assert.Equal(t, &value, safeLens.Get(containerWithItem))
|
||||
|
||||
// Test setter
|
||||
newValue := "new"
|
||||
updated := safeLens.Set(&newValue)(emptyContainer)
|
||||
assert.Equal(t, &newValue, updated.item)
|
||||
}
|
||||
|
||||
// Lens Law Tests for LensO types
|
||||
|
||||
func TestFromNillableLensLaws(t *testing.T) {
|
||||
type Config struct {
|
||||
timeout *int
|
||||
}
|
||||
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) *int { return c.timeout },
|
||||
func(c Config, t *int) Config { c.timeout = t; return c },
|
||||
)
|
||||
|
||||
optLens := FromNillable(timeoutLens)
|
||||
|
||||
// Equality predicates
|
||||
eqInt := EQT.Eq[*int]()
|
||||
eqOptInt := O.Eq(eqInt)
|
||||
eqConfig := func(a, b Config) bool {
|
||||
if a.timeout == nil && b.timeout == nil {
|
||||
return true
|
||||
}
|
||||
if a.timeout == nil || b.timeout == nil {
|
||||
return false
|
||||
}
|
||||
return *a.timeout == *b.timeout
|
||||
}
|
||||
|
||||
// Test structures
|
||||
timeout30 := 30
|
||||
timeout60 := 60
|
||||
configNil := Config{timeout: nil}
|
||||
config30 := Config{timeout: &timeout30}
|
||||
|
||||
// Law 1: get(set(a)(s)) = a
|
||||
t.Run("GetSet", func(t *testing.T) {
|
||||
// Setting Some and getting back
|
||||
result := optLens.Get(optLens.Set(O.Some(&timeout60))(config30))
|
||||
assert.True(t, eqOptInt.Equals(result, O.Some(&timeout60)))
|
||||
|
||||
// Setting None and getting back
|
||||
result2 := optLens.Get(optLens.Set(O.None[*int]())(config30))
|
||||
assert.True(t, eqOptInt.Equals(result2, O.None[*int]()))
|
||||
})
|
||||
|
||||
// Law 2: set(get(s))(s) = s
|
||||
t.Run("SetGet", func(t *testing.T) {
|
||||
// With Some value
|
||||
result := optLens.Set(optLens.Get(config30))(config30)
|
||||
assert.True(t, eqConfig(result, config30))
|
||||
|
||||
// With None value
|
||||
result2 := optLens.Set(optLens.Get(configNil))(configNil)
|
||||
assert.True(t, eqConfig(result2, configNil))
|
||||
})
|
||||
|
||||
// Law 3: set(a)(set(a)(s)) = set(a)(s)
|
||||
t.Run("SetSet", func(t *testing.T) {
|
||||
// Setting Some twice
|
||||
once := optLens.Set(O.Some(&timeout60))(config30)
|
||||
twice := optLens.Set(O.Some(&timeout60))(once)
|
||||
assert.True(t, eqConfig(once, twice))
|
||||
|
||||
// Setting None twice
|
||||
once2 := optLens.Set(O.None[*int]())(config30)
|
||||
twice2 := optLens.Set(O.None[*int]())(once2)
|
||||
assert.True(t, eqConfig(once2, twice2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNillableRefLensLaws(t *testing.T) {
|
||||
type Settings struct {
|
||||
maxRetries *int
|
||||
}
|
||||
|
||||
retriesLens := L.MakeLensRef(
|
||||
func(s *Settings) *int { return s.maxRetries },
|
||||
func(s *Settings, r *int) *Settings { s.maxRetries = r; return s },
|
||||
)
|
||||
|
||||
optLens := FromNillableRef(retriesLens)
|
||||
|
||||
// Equality predicates
|
||||
eqInt := EQT.Eq[*int]()
|
||||
eqOptInt := O.Eq(eqInt)
|
||||
eqSettings := func(a, b *Settings) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
if a.maxRetries == nil && b.maxRetries == nil {
|
||||
return true
|
||||
}
|
||||
if a.maxRetries == nil || b.maxRetries == nil {
|
||||
return false
|
||||
}
|
||||
return *a.maxRetries == *b.maxRetries
|
||||
}
|
||||
|
||||
// Test structures
|
||||
retries3 := 3
|
||||
retries5 := 5
|
||||
settingsNil := &Settings{maxRetries: nil}
|
||||
settings3 := &Settings{maxRetries: &retries3}
|
||||
|
||||
// Law 1: get(set(a)(s)) = a
|
||||
t.Run("GetSet", func(t *testing.T) {
|
||||
result := optLens.Get(optLens.Set(O.Some(&retries5))(settings3))
|
||||
assert.True(t, eqOptInt.Equals(result, O.Some(&retries5)))
|
||||
|
||||
result2 := optLens.Get(optLens.Set(O.None[*int]())(settings3))
|
||||
assert.True(t, eqOptInt.Equals(result2, O.None[*int]()))
|
||||
})
|
||||
|
||||
// Law 2: set(get(s))(s) = s
|
||||
t.Run("SetGet", func(t *testing.T) {
|
||||
result := optLens.Set(optLens.Get(settings3))(settings3)
|
||||
assert.True(t, eqSettings(result, settings3))
|
||||
|
||||
result2 := optLens.Set(optLens.Get(settingsNil))(settingsNil)
|
||||
assert.True(t, eqSettings(result2, settingsNil))
|
||||
})
|
||||
|
||||
// Law 3: set(a)(set(a)(s)) = set(a)(s)
|
||||
t.Run("SetSet", func(t *testing.T) {
|
||||
once := optLens.Set(O.Some(&retries5))(settings3)
|
||||
twice := optLens.Set(O.Some(&retries5))(once)
|
||||
assert.True(t, eqSettings(once, twice))
|
||||
|
||||
once2 := optLens.Set(O.None[*int]())(settings3)
|
||||
twice2 := optLens.Set(O.None[*int]())(once2)
|
||||
assert.True(t, eqSettings(once2, twice2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestComposeOptionLensLaws(t *testing.T) {
|
||||
defaultInner := &Inner{Value: 0, Foo: "default"}
|
||||
|
||||
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
inner := FromNillable(L.MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
lens := F.Pipe1(inner, ComposeOption[Outer, int](defaultInner)(value))
|
||||
|
||||
// Equality predicates
|
||||
eqInt := EQT.Eq[int]()
|
||||
eqOptInt := O.Eq(eqInt)
|
||||
eqOuter := func(a, b Outer) bool {
|
||||
if a.inner == nil && b.inner == nil {
|
||||
return true
|
||||
}
|
||||
if a.inner == nil || b.inner == nil {
|
||||
return false
|
||||
}
|
||||
return a.inner.Value == b.inner.Value && a.inner.Foo == b.inner.Foo
|
||||
}
|
||||
|
||||
// Test structures
|
||||
outerNil := Outer{inner: nil}
|
||||
outer42 := Outer{inner: &Inner{Value: 42, Foo: "test"}}
|
||||
|
||||
// Law 1: get(set(a)(s)) = a
|
||||
t.Run("GetSet", func(t *testing.T) {
|
||||
result := lens.Get(lens.Set(O.Some(100))(outer42))
|
||||
assert.True(t, eqOptInt.Equals(result, O.Some(100)))
|
||||
|
||||
result2 := lens.Get(lens.Set(O.None[int]())(outer42))
|
||||
assert.True(t, eqOptInt.Equals(result2, O.None[int]()))
|
||||
})
|
||||
|
||||
// Law 2: set(get(s))(s) = s
|
||||
t.Run("SetGet", func(t *testing.T) {
|
||||
result := lens.Set(lens.Get(outer42))(outer42)
|
||||
assert.True(t, eqOuter(result, outer42))
|
||||
|
||||
result2 := lens.Set(lens.Get(outerNil))(outerNil)
|
||||
assert.True(t, eqOuter(result2, outerNil))
|
||||
})
|
||||
|
||||
// Law 3: set(a)(set(a)(s)) = set(a)(s)
|
||||
t.Run("SetSet", func(t *testing.T) {
|
||||
once := lens.Set(O.Some(100))(outer42)
|
||||
twice := lens.Set(O.Some(100))(once)
|
||||
assert.True(t, eqOuter(once, twice))
|
||||
|
||||
once2 := lens.Set(O.None[int]())(outer42)
|
||||
twice2 := lens.Set(O.None[int]())(once2)
|
||||
assert.True(t, eqOuter(once2, twice2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestComposeOptionsLensLaws(t *testing.T) {
|
||||
defaultValue := 1
|
||||
defaultFoo := "default"
|
||||
defaultInner := &InnerOpt{Value: &defaultValue, Foo: &defaultFoo}
|
||||
|
||||
value := FromNillable(L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
|
||||
inner := FromNillable(L.MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
|
||||
lens := F.Pipe1(inner, Compose[OuterOpt, *int](defaultInner)(value))
|
||||
|
||||
// Equality predicates
|
||||
eqIntPtr := EQT.Eq[*int]()
|
||||
eqOptIntPtr := O.Eq(eqIntPtr)
|
||||
eqOuterOpt := func(a, b OuterOpt) bool {
|
||||
if a.inner == nil && b.inner == nil {
|
||||
return true
|
||||
}
|
||||
if a.inner == nil || b.inner == nil {
|
||||
return false
|
||||
}
|
||||
aVal := a.inner.Value
|
||||
bVal := b.inner.Value
|
||||
if aVal == nil && bVal == nil {
|
||||
return true
|
||||
}
|
||||
if aVal == nil || bVal == nil {
|
||||
return false
|
||||
}
|
||||
return *aVal == *bVal
|
||||
}
|
||||
|
||||
// Test structures
|
||||
val42 := 42
|
||||
val100 := 100
|
||||
outerNil := OuterOpt{inner: nil}
|
||||
outer42 := OuterOpt{inner: &InnerOpt{Value: &val42, Foo: &defaultFoo}}
|
||||
|
||||
// Law 1: get(set(a)(s)) = a
|
||||
t.Run("GetSet", func(t *testing.T) {
|
||||
result := lens.Get(lens.Set(O.Some(&val100))(outer42))
|
||||
assert.True(t, eqOptIntPtr.Equals(result, O.Some(&val100)))
|
||||
|
||||
result2 := lens.Get(lens.Set(O.None[*int]())(outer42))
|
||||
assert.True(t, eqOptIntPtr.Equals(result2, O.None[*int]()))
|
||||
})
|
||||
|
||||
// Law 2: set(get(s))(s) = s
|
||||
t.Run("SetGet", func(t *testing.T) {
|
||||
result := lens.Set(lens.Get(outer42))(outer42)
|
||||
assert.True(t, eqOuterOpt(result, outer42))
|
||||
|
||||
result2 := lens.Set(lens.Get(outerNil))(outerNil)
|
||||
assert.True(t, eqOuterOpt(result2, outerNil))
|
||||
})
|
||||
|
||||
// Law 3: set(a)(set(a)(s)) = set(a)(s)
|
||||
t.Run("SetSet", func(t *testing.T) {
|
||||
once := lens.Set(O.Some(&val100))(outer42)
|
||||
twice := lens.Set(O.Some(&val100))(once)
|
||||
assert.True(t, eqOuterOpt(once, twice))
|
||||
|
||||
once2 := lens.Set(O.None[*int]())(outer42)
|
||||
twice2 := lens.Set(O.None[*int]())(once2)
|
||||
assert.True(t, eqOuterOpt(once2, twice2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromPredicateLensLaws(t *testing.T) {
|
||||
type Score struct {
|
||||
points int
|
||||
}
|
||||
|
||||
pointsLens := L.MakeLens(
|
||||
func(s Score) int { return s.points },
|
||||
func(s Score, p int) Score { s.points = p; return s },
|
||||
)
|
||||
|
||||
// Only positive scores are valid
|
||||
validLens := FromPredicate[Score](func(p int) bool { return p > 0 }, 0)(pointsLens)
|
||||
|
||||
// Equality predicates
|
||||
eqInt := EQT.Eq[int]()
|
||||
eqOptInt := O.Eq(eqInt)
|
||||
eqScore := func(a, b Score) bool { return a.points == b.points }
|
||||
|
||||
// Test structures
|
||||
scoreZero := Score{points: 0}
|
||||
score100 := Score{points: 100}
|
||||
|
||||
// Law 1: get(set(a)(s)) = a
|
||||
t.Run("GetSet", func(t *testing.T) {
|
||||
result := validLens.Get(validLens.Set(O.Some(50))(score100))
|
||||
assert.True(t, eqOptInt.Equals(result, O.Some(50)))
|
||||
|
||||
result2 := validLens.Get(validLens.Set(O.None[int]())(score100))
|
||||
assert.True(t, eqOptInt.Equals(result2, O.None[int]()))
|
||||
})
|
||||
|
||||
// Law 2: set(get(s))(s) = s
|
||||
t.Run("SetGet", func(t *testing.T) {
|
||||
result := validLens.Set(validLens.Get(score100))(score100)
|
||||
assert.True(t, eqScore(result, score100))
|
||||
|
||||
result2 := validLens.Set(validLens.Get(scoreZero))(scoreZero)
|
||||
assert.True(t, eqScore(result2, scoreZero))
|
||||
})
|
||||
|
||||
// Law 3: set(a)(set(a)(s)) = set(a)(s)
|
||||
t.Run("SetSet", func(t *testing.T) {
|
||||
once := validLens.Set(O.Some(75))(score100)
|
||||
twice := validLens.Set(O.Some(75))(once)
|
||||
assert.True(t, eqScore(once, twice))
|
||||
|
||||
once2 := validLens.Set(O.None[int]())(score100)
|
||||
twice2 := validLens.Set(O.None[int]())(once2)
|
||||
assert.True(t, eqScore(once2, twice2))
|
||||
})
|
||||
}
|
||||
@@ -22,6 +22,44 @@ import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// AsTraversal converts a Lens[S, A] to a Traversal[S, A] for optional values.
|
||||
//
|
||||
// A traversal is a generalization of a lens that can focus on zero or more values.
|
||||
// This function converts a lens (which focuses on exactly one value) into a traversal,
|
||||
// allowing it to be used with traversal operations like mapping over multiple values.
|
||||
//
|
||||
// This is particularly useful when you want to:
|
||||
// - Use lens operations in a traversal context
|
||||
// - Compose lenses with traversals
|
||||
// - Apply operations that work on collections of optional values
|
||||
//
|
||||
// The conversion uses the Option monad's map operation to handle the optional nature
|
||||
// of the values being traversed.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The structure type containing the field
|
||||
// - A: The type of the field being focused on
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns a Traversal[S, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Timeout Option[int]
|
||||
// }
|
||||
//
|
||||
// timeoutLens := lens.MakeLens(
|
||||
// func(c Config) Option[int] { return c.Timeout },
|
||||
// func(c Config, t Option[int]) Config { c.Timeout = t; return c },
|
||||
// )
|
||||
//
|
||||
// // Convert to traversal for use with traversal operations
|
||||
// timeoutTraversal := lens.AsTraversal[Config, int]()(timeoutLens)
|
||||
//
|
||||
// // Now can use traversal operations
|
||||
// configs := []Config{{Timeout: O.Some(30)}, {Timeout: O.None[int]()}}
|
||||
// // Apply operations across all configs using the traversal
|
||||
func AsTraversal[S, A any]() func(L.Lens[S, A]) T.Traversal[S, A] {
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.MonadMap[A, S])
|
||||
}
|
||||
|
||||
267
v2/optics/lens/option/testing/laws_test.go
Normal file
267
v2/optics/lens/option/testing/laws_test.go
Normal file
@@ -0,0 +1,267 @@
|
||||
// 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 testing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
EQT "github.com/IBM/fp-go/v2/eq/testing"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
I "github.com/IBM/fp-go/v2/identity"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
LI "github.com/IBM/fp-go/v2/optics/lens/iso"
|
||||
LO "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
LT "github.com/IBM/fp-go/v2/optics/lens/testing"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type (
|
||||
Street struct {
|
||||
num int
|
||||
name string
|
||||
}
|
||||
|
||||
Address struct {
|
||||
city string
|
||||
street *Street
|
||||
}
|
||||
|
||||
Inner struct {
|
||||
Value int
|
||||
Foo string
|
||||
}
|
||||
|
||||
InnerOpt struct {
|
||||
Value *int
|
||||
Foo *string
|
||||
}
|
||||
|
||||
Outer struct {
|
||||
inner *Inner
|
||||
}
|
||||
|
||||
OuterOpt struct {
|
||||
inner *InnerOpt
|
||||
}
|
||||
)
|
||||
|
||||
func (outer *OuterOpt) GetInner() *InnerOpt {
|
||||
return outer.inner
|
||||
}
|
||||
|
||||
func (outer *OuterOpt) SetInner(inner *InnerOpt) *OuterOpt {
|
||||
outer.inner = inner
|
||||
return outer
|
||||
}
|
||||
|
||||
func (inner *InnerOpt) GetValue() *int {
|
||||
return inner.Value
|
||||
}
|
||||
|
||||
func (inner *InnerOpt) SetValue(value *int) *InnerOpt {
|
||||
inner.Value = value
|
||||
return inner
|
||||
}
|
||||
|
||||
func (outer *Outer) GetInner() *Inner {
|
||||
return outer.inner
|
||||
}
|
||||
|
||||
func (outer *Outer) SetInner(inner *Inner) *Outer {
|
||||
outer.inner = inner
|
||||
return outer
|
||||
}
|
||||
|
||||
func (inner *Inner) GetValue() int {
|
||||
return inner.Value
|
||||
}
|
||||
|
||||
func (inner *Inner) SetValue(value int) *Inner {
|
||||
inner.Value = value
|
||||
return inner
|
||||
}
|
||||
|
||||
func (street *Street) GetName() string {
|
||||
return street.name
|
||||
}
|
||||
|
||||
func (street *Street) SetName(name string) *Street {
|
||||
street.name = name
|
||||
return street
|
||||
}
|
||||
|
||||
func (addr *Address) GetStreet() *Street {
|
||||
return addr.street
|
||||
}
|
||||
|
||||
func (addr *Address) SetStreet(s *Street) *Address {
|
||||
addr.street = s
|
||||
return addr
|
||||
}
|
||||
|
||||
var (
|
||||
streetLens = L.MakeLensRef((*Street).GetName, (*Street).SetName)
|
||||
addrLens = L.MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
|
||||
outerLens = LO.FromNillableRef(L.MakeLensRef((*Outer).GetInner, (*Outer).SetInner))
|
||||
valueLens = L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
|
||||
outerOptLens = LO.FromNillableRef(L.MakeLensRef((*OuterOpt).GetInner, (*OuterOpt).SetInner))
|
||||
valueOptLens = L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue)
|
||||
|
||||
sampleStreet = Street{num: 220, name: "Schönaicherstr"}
|
||||
sampleAddress = Address{city: "Böblingen", street: &sampleStreet}
|
||||
sampleStreet2 = Street{num: 220, name: "Neue Str"}
|
||||
|
||||
defaultInner = Inner{
|
||||
Value: -1,
|
||||
Foo: "foo",
|
||||
}
|
||||
|
||||
emptyOuter = Outer{}
|
||||
|
||||
defaultInnerOpt = InnerOpt{
|
||||
Value: &defaultInner.Value,
|
||||
Foo: &defaultInner.Foo,
|
||||
}
|
||||
|
||||
emptyOuterOpt = OuterOpt{}
|
||||
)
|
||||
|
||||
func TestStreetLensLaws(t *testing.T) {
|
||||
// some comparison
|
||||
eqs := EQT.Eq[*Street]()
|
||||
eqa := EQT.Eq[string]()
|
||||
|
||||
laws := LT.AssertLaws(
|
||||
t,
|
||||
eqa,
|
||||
eqs,
|
||||
)(streetLens)
|
||||
|
||||
cpy := sampleStreet
|
||||
assert.True(t, laws(&sampleStreet, "Neue Str."))
|
||||
assert.Equal(t, cpy, sampleStreet)
|
||||
}
|
||||
|
||||
func TestAddrLensLaws(t *testing.T) {
|
||||
// some comparison
|
||||
eqs := EQT.Eq[*Address]()
|
||||
eqa := EQT.Eq[*Street]()
|
||||
|
||||
laws := LT.AssertLaws(
|
||||
t,
|
||||
eqa,
|
||||
eqs,
|
||||
)(addrLens)
|
||||
|
||||
cpyAddr := sampleAddress
|
||||
cpyStreet := sampleStreet2
|
||||
assert.True(t, laws(&sampleAddress, &sampleStreet2))
|
||||
assert.Equal(t, cpyAddr, sampleAddress)
|
||||
assert.Equal(t, cpyStreet, sampleStreet2)
|
||||
}
|
||||
|
||||
func TestCompose(t *testing.T) {
|
||||
// some comparison
|
||||
eqs := EQT.Eq[*Address]()
|
||||
eqa := EQT.Eq[string]()
|
||||
|
||||
streetName := L.Compose[*Address](streetLens)(addrLens)
|
||||
|
||||
laws := LT.AssertLaws(
|
||||
t,
|
||||
eqa,
|
||||
eqs,
|
||||
)(streetName)
|
||||
|
||||
cpyAddr := sampleAddress
|
||||
cpyStreet := sampleStreet
|
||||
assert.True(t, laws(&sampleAddress, "Neue Str."))
|
||||
assert.Equal(t, cpyAddr, sampleAddress)
|
||||
assert.Equal(t, cpyStreet, sampleStreet)
|
||||
}
|
||||
|
||||
func TestOuterLensLaws(t *testing.T) {
|
||||
// some equal predicates
|
||||
eqValue := EQT.Eq[int]()
|
||||
eqOptValue := O.Eq(eqValue)
|
||||
// lens to access a value from outer
|
||||
valueFromOuter := LO.ComposeOption[*Outer, int](&defaultInner)(valueLens)(outerLens)
|
||||
// try to access the value, this should get an option
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
|
||||
// update the object
|
||||
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuter)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
|
||||
// updating with none should remove the inner
|
||||
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
|
||||
// check if this meets the laws
|
||||
|
||||
eqOuter := EQT.Eq[*Outer]()
|
||||
|
||||
laws := LT.AssertLaws(
|
||||
t,
|
||||
eqOptValue,
|
||||
eqOuter,
|
||||
)(valueFromOuter)
|
||||
|
||||
assert.True(t, laws(&emptyOuter, O.Some(2)))
|
||||
assert.True(t, laws(&emptyOuter, O.None[int]()))
|
||||
|
||||
assert.True(t, laws(withValue, O.Some(2)))
|
||||
assert.True(t, laws(withValue, O.None[int]()))
|
||||
}
|
||||
|
||||
func TestOuterOptLensLaws(t *testing.T) {
|
||||
// some equal predicates
|
||||
eqValue := EQT.Eq[int]()
|
||||
eqOptValue := O.Eq(eqValue)
|
||||
intIso := LI.FromNillable[int]()
|
||||
// lens to access a value from outer
|
||||
valueFromOuter := F.Pipe3(
|
||||
valueOptLens,
|
||||
LI.Compose[*InnerOpt](intIso),
|
||||
LO.Compose[*OuterOpt, int](&defaultInnerOpt),
|
||||
I.Ap[L.Lens[*OuterOpt, O.Option[int]]](outerOptLens),
|
||||
)
|
||||
|
||||
// try to access the value, this should get an option
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
|
||||
// update the object
|
||||
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuterOpt)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
|
||||
// updating with none should remove the inner
|
||||
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
|
||||
// check if this meets the laws
|
||||
|
||||
eqOuter := EQT.Eq[*OuterOpt]()
|
||||
|
||||
laws := LT.AssertLaws(
|
||||
t,
|
||||
eqOptValue,
|
||||
eqOuter,
|
||||
)(valueFromOuter)
|
||||
|
||||
assert.True(t, laws(&emptyOuterOpt, O.Some(2)))
|
||||
assert.True(t, laws(&emptyOuterOpt, O.None[int]()))
|
||||
|
||||
assert.True(t, laws(withValue, O.Some(2)))
|
||||
assert.True(t, laws(withValue, O.None[int]()))
|
||||
}
|
||||
94
v2/optics/lens/option/types.go
Normal file
94
v2/optics/lens/option/types.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// 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 option
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
type (
|
||||
// Endomorphism is a function from a type to itself (A → A).
|
||||
// It represents transformations that preserve the type.
|
||||
//
|
||||
// This is commonly used in lens setters to transform a structure
|
||||
// by applying a function that takes and returns the same type.
|
||||
//
|
||||
// Example:
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// // increment is an Endomorphism[int]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Lens represents a functional reference to a field within a structure.
|
||||
//
|
||||
// A Lens[S, A] provides a way to get and set a value of type A within
|
||||
// a structure of type S in an immutable way. It consists of:
|
||||
// - Get: S → A (retrieve the value)
|
||||
// - Set: A → S → S (update the value, returning a new structure)
|
||||
//
|
||||
// Lenses satisfy three laws:
|
||||
// 1. Get-Put: lens.Set(lens.Get(s))(s) == s
|
||||
// 2. Put-Get: lens.Get(lens.Set(a)(s)) == a
|
||||
// 3. Put-Put: lens.Set(b)(lens.Set(a)(s)) == lens.Set(b)(s)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The structure type containing the field
|
||||
// - A: The type of the field being focused on
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
// Option represents a value that may or may not be present.
|
||||
//
|
||||
// It is either Some[T] containing a value of type T, or None[T]
|
||||
// representing the absence of a value. This is a type-safe alternative
|
||||
// to using nil pointers.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of the value that may be present
|
||||
Option[T any] = option.Option[T]
|
||||
|
||||
// LensO is a lens that focuses on an optional value.
|
||||
//
|
||||
// A LensO[S, A] is equivalent to Lens[S, Option[A]], representing
|
||||
// a lens that focuses on a value of type A that may or may not be
|
||||
// present within a structure S.
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Nullable pointer fields
|
||||
// - Optional configuration values
|
||||
// - Fields that may be conditionally present
|
||||
//
|
||||
// The getter returns Option[A] (Some if present, None if absent).
|
||||
// The setter takes Option[A] (Some to set, None to remove).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The structure type containing the optional field
|
||||
// - A: The type of the optional value being focused on
|
||||
//
|
||||
// Example:
|
||||
// type Config struct {
|
||||
// Timeout *int
|
||||
// }
|
||||
//
|
||||
// timeoutLens := lens.MakeLensRef(
|
||||
// func(c *Config) *int { return c.Timeout },
|
||||
// func(c *Config, t *int) *Config { c.Timeout = t; return c },
|
||||
// )
|
||||
//
|
||||
// optLens := lens.FromNillableRef(timeoutLens)
|
||||
// // optLens is a LensO[*Config, *int]
|
||||
LensO[S, A any] = Lens[S, Option[A]]
|
||||
)
|
||||
@@ -19,11 +19,7 @@ import (
|
||||
"testing"
|
||||
|
||||
EQT "github.com/IBM/fp-go/v2/eq/testing"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
I "github.com/IBM/fp-go/v2/identity"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
LI "github.com/IBM/fp-go/v2/optics/lens/iso"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -114,10 +110,8 @@ func (addr *Address) SetStreet(s *Street) *Address {
|
||||
var (
|
||||
streetLens = L.MakeLensRef((*Street).GetName, (*Street).SetName)
|
||||
addrLens = L.MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
|
||||
outerLens = L.FromNillableRef(L.MakeLensRef((*Outer).GetInner, (*Outer).SetInner))
|
||||
valueLens = L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
|
||||
outerOptLens = L.FromNillableRef(L.MakeLensRef((*OuterOpt).GetInner, (*OuterOpt).SetInner))
|
||||
valueOptLens = L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue)
|
||||
|
||||
sampleStreet = Street{num: 220, name: "Schönaicherstr"}
|
||||
@@ -192,74 +186,3 @@ func TestCompose(t *testing.T) {
|
||||
assert.Equal(t, cpyAddr, sampleAddress)
|
||||
assert.Equal(t, cpyStreet, sampleStreet)
|
||||
}
|
||||
|
||||
func TestOuterLensLaws(t *testing.T) {
|
||||
// some equal predicates
|
||||
eqValue := EQT.Eq[int]()
|
||||
eqOptValue := O.Eq(eqValue)
|
||||
// lens to access a value from outer
|
||||
valueFromOuter := L.ComposeOption[*Outer, int](&defaultInner)(valueLens)(outerLens)
|
||||
// try to access the value, this should get an option
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
|
||||
// update the object
|
||||
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuter)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
|
||||
// updating with none should remove the inner
|
||||
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
|
||||
// check if this meets the laws
|
||||
|
||||
eqOuter := EQT.Eq[*Outer]()
|
||||
|
||||
laws := AssertLaws(
|
||||
t,
|
||||
eqOptValue,
|
||||
eqOuter,
|
||||
)(valueFromOuter)
|
||||
|
||||
assert.True(t, laws(&emptyOuter, O.Some(2)))
|
||||
assert.True(t, laws(&emptyOuter, O.None[int]()))
|
||||
|
||||
assert.True(t, laws(withValue, O.Some(2)))
|
||||
assert.True(t, laws(withValue, O.None[int]()))
|
||||
}
|
||||
|
||||
func TestOuterOptLensLaws(t *testing.T) {
|
||||
// some equal predicates
|
||||
eqValue := EQT.Eq[int]()
|
||||
eqOptValue := O.Eq(eqValue)
|
||||
intIso := LI.FromNillable[int]()
|
||||
// lens to access a value from outer
|
||||
valueFromOuter := F.Pipe3(
|
||||
valueOptLens,
|
||||
LI.Compose[*InnerOpt](intIso),
|
||||
L.ComposeOptions[*OuterOpt, int](&defaultInnerOpt),
|
||||
I.Ap[L.Lens[*OuterOpt, O.Option[int]]](outerOptLens),
|
||||
)
|
||||
|
||||
// try to access the value, this should get an option
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
|
||||
// update the object
|
||||
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuterOpt)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
|
||||
// updating with none should remove the inner
|
||||
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
|
||||
// check if this meets the laws
|
||||
|
||||
eqOuter := EQT.Eq[*OuterOpt]()
|
||||
|
||||
laws := AssertLaws(
|
||||
t,
|
||||
eqOptValue,
|
||||
eqOuter,
|
||||
)(valueFromOuter)
|
||||
|
||||
assert.True(t, laws(&emptyOuterOpt, O.Some(2)))
|
||||
assert.True(t, laws(&emptyOuterOpt, O.None[int]()))
|
||||
|
||||
assert.True(t, laws(withValue, O.Some(2)))
|
||||
assert.True(t, laws(withValue, O.None[int]()))
|
||||
}
|
||||
|
||||
@@ -21,11 +21,63 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Endomorphism is a function from a type to itself (A → A).
|
||||
// It represents transformations that preserve the type.
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Lens is a reference to a subpart of a data type
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
//
|
||||
// A Lens[S, A] provides a composable way to focus on a field of type A within
|
||||
// a structure of type S. It consists of two operations:
|
||||
// - Get: Extracts the focused value from the structure (S → A)
|
||||
// - Set: Updates the focused value in the structure, returning a new structure (A → S → S)
|
||||
//
|
||||
// Lenses maintain immutability by always returning new copies of the structure
|
||||
// when setting values, never modifying the original.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source/structure type (the whole)
|
||||
// - A: The focus/field type (the part)
|
||||
//
|
||||
// Lens Laws:
|
||||
//
|
||||
// A well-behaved lens must satisfy three laws:
|
||||
//
|
||||
// 1. GetSet (You get what you set):
|
||||
// lens.Set(lens.Get(s))(s) == s
|
||||
//
|
||||
// 2. SetGet (You set what you get):
|
||||
// lens.Get(lens.Set(a)(s)) == a
|
||||
//
|
||||
// 3. SetSet (Setting twice is the same as setting once):
|
||||
// lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// nameLens := lens.MakeLens(
|
||||
// func(p Person) string { return p.Name },
|
||||
// func(p Person, name string) Person {
|
||||
// p.Name = name
|
||||
// return p
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// person := Person{Name: "Alice", Age: 30}
|
||||
// name := nameLens.Get(person) // "Alice"
|
||||
// updated := nameLens.Set("Bob")(person) // Person{Name: "Bob", Age: 30}
|
||||
// // person is unchanged, updated is a new value
|
||||
Lens[S, A any] struct {
|
||||
// Get extracts the focused value of type A from structure S.
|
||||
Get func(s S) A
|
||||
|
||||
// Set returns a function that updates the focused value in structure S.
|
||||
// The returned function takes a structure S and returns a new structure S
|
||||
// with the focused value updated to a. The original structure is never modified.
|
||||
Set func(a A) Endomorphism[S]
|
||||
}
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user