1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-09 23:11:40 +02:00

Compare commits

...

7 Commits

Author SHA1 Message Date
Dr. Carsten Leue
b3bd5e9ad3 fix: bind docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 16:18:15 +01:00
Dr. Carsten Leue
178df09ff7 fix: document ApS
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 16:08:35 +01:00
Dr. Carsten Leue
92eb9715bd fix: implement some useful prisms
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 13:53:02 +01:00
Dr. Carsten Leue
41ebb04ae0 fix: upload coverage to coverall
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 11:45:01 +01:00
Dr. Carsten Leue
b2705e3adf fix: disable to version updates
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 11:39:17 +01:00
renovate[bot]
b232183e47 chore(config): migrate config renovate.json (#143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 11:26:23 +01:00
Dr. Carsten Leue
0f9f89f16d doc: improve documentation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 11:20:50 +01:00
44 changed files with 6559 additions and 277 deletions

View File

@@ -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
View File

@@ -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.
[![Go Reference](https://pkg.go.dev/badge/github.com/IBM/fp-go.svg)](https://pkg.go.dev/github.com/IBM/fp-go)
[![Coverage Status](https://coveralls.io/repos/github/IBM/fp-go/badge.svg?branch=main)](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.
![logo](resources/images/logo.png)
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.

View File

@@ -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
}
]
}

View File

@@ -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.
[![Go Reference](https://pkg.go.dev/badge/github.com/IBM/fp-go/v2.svg)](https://pkg.go.dev/github.com/IBM/fp-go/v2)
[![Coverage Status](https://coveralls.io/repos/github/IBM/fp-go/badge.svg?branch=main&flag=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.

View File

@@ -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,

View File

@@ -403,5 +403,3 @@ func TestSlicePropertyBased(t *testing.T) {
}
})
}
// Made with Bob

View File

@@ -21,14 +21,63 @@ import (
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],
@@ -59,7 +108,43 @@ func BindTo[S1, T any](
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],

View File

@@ -21,14 +21,64 @@ import (
"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 {
// User User
// Config Config
// }
// result := readerioeither.Do(State{})
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)
// })
// }
// },
// ),
// )
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderIOEither[T],
@@ -75,7 +125,47 @@ 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,
// ),
// )
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIOEither[T],

View File

@@ -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] {

View File

@@ -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"))
}

View 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

View 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

View File

@@ -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),

View File

@@ -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")
}

View File

@@ -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 (

View File

@@ -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()))
}

View 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

View File

@@ -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 (
@@ -27,30 +77,88 @@ import (
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]()),
)
// 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,

View File

@@ -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"))
}

View File

@@ -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]
)

View File

@@ -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)

View File

@@ -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))
}

View File

@@ -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,

View File

@@ -21,14 +21,59 @@ import (
"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 {
// 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 +120,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],

View File

@@ -21,14 +21,55 @@ import (
"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 {
// 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],
@@ -75,7 +116,39 @@ 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 {
// 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],

View File

@@ -19,14 +19,56 @@ 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],
@@ -57,7 +99,39 @@ func BindTo[S1, T any](
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],

View File

@@ -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,

View File

@@ -19,14 +19,55 @@ import (
"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],
@@ -57,7 +98,39 @@ func BindTo[S1, T any](
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],

5
v2/logging/coverage.out Normal file
View 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

View File

@@ -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
View 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)
}
}

View File

@@ -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)

View File

@@ -13,7 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Prism is an optic used to select part of a sum type.
package prism
import (
@@ -23,40 +22,131 @@ import (
)
type (
// Prism is an optic used to select part of a sum type.
// Prism is an optic used to select part of a sum type (tagged union).
// It provides two operations:
// - GetOption: Try to extract a value of type A from S (may fail)
// - ReverseGet: Construct an S from an A (always succeeds)
//
// Prisms are useful for working with variant types like Either, Option,
// or custom sum types where you want to focus on a specific variant.
//
// Type Parameters:
// - S: The source type (sum type)
// - A: The focus type (variant within the sum type)
//
// Example:
// type Result interface{ isResult() }
// type Success struct{ Value int }
// type Failure struct{ Error string }
//
// successPrism := MakePrism(
// func(r Result) Option[int] {
// if s, ok := r.(Success); ok {
// return Some(s.Value)
// }
// return None[int]()
// },
// func(v int) Result { return Success{Value: v} },
// )
Prism[S, A any] interface {
GetOption(s S) O.Option[A]
// GetOption attempts to extract a value of type A from S.
// Returns Some(a) if the extraction succeeds, None otherwise.
GetOption(s S) Option[A]
// ReverseGet constructs an S from an A.
// This operation always succeeds.
ReverseGet(a A) S
}
// prismImpl is the internal implementation of the Prism interface.
prismImpl[S, A any] struct {
get func(S) O.Option[A]
get func(S) Option[A]
rev func(A) S
}
)
func (prism prismImpl[S, A]) GetOption(s S) O.Option[A] {
// GetOption implements the Prism interface for prismImpl.
func (prism prismImpl[S, A]) GetOption(s S) Option[A] {
return prism.get(s)
}
// ReverseGet implements the Prism interface for prismImpl.
func (prism prismImpl[S, A]) ReverseGet(a A) S {
return prism.rev(a)
}
func MakePrism[S, A any](get func(S) O.Option[A], rev func(A) S) Prism[S, A] {
// MakePrism constructs a Prism from GetOption and ReverseGet functions.
//
// Parameters:
// - get: Function to extract A from S (returns Option[A])
// - rev: Function to construct S from A
//
// Returns:
// - A Prism[S, A] that uses the provided functions
//
// Example:
//
// prism := MakePrism(
// func(opt Option[int]) Option[int] { return opt },
// func(n int) Option[int] { return Some(n) },
// )
func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
return prismImpl[S, A]{get, rev}
}
// Id returns a prism implementing the identity operation
// Id returns an identity prism that focuses on the entire value.
// GetOption always returns Some(s), and ReverseGet is the identity function.
//
// This is useful as a starting point for prism composition or when you need
// a prism that doesn't actually transform the value.
//
// Example:
//
// idPrism := Id[int]()
// value := idPrism.GetOption(42) // Some(42)
// result := idPrism.ReverseGet(42) // 42
func Id[S any]() Prism[S, S] {
return MakePrism(O.Some[S], F.Identity[S])
}
// FromPredicate creates a prism that matches values satisfying a predicate.
// GetOption returns Some(s) if the predicate is true, None otherwise.
// ReverseGet is the identity function (doesn't validate the predicate).
//
// Parameters:
// - pred: Predicate function to test values
//
// Returns:
// - A Prism[S, S] that filters based on the predicate
//
// Example:
//
// positivePrism := FromPredicate(func(n int) bool { return n > 0 })
// value := positivePrism.GetOption(42) // Some(42)
// value = positivePrism.GetOption(-5) // None[int]
func FromPredicate[S any](pred func(S) bool) Prism[S, S] {
return MakePrism(O.FromPredicate(pred), F.Identity[S])
}
// Compose composes a `Prism` with a `Prism`.
// Compose composes two prisms to create a prism that focuses deeper into a structure.
// The resulting prism first applies the outer prism (S → A), then the inner prism (A → B).
//
// Type Parameters:
// - S: The outermost source type
// - A: The intermediate type
// - B: The innermost focus type
//
// Parameters:
// - ab: The inner prism (A → B)
//
// Returns:
// - A function that takes the outer prism (S → A) and returns the composed prism (S → B)
//
// Example:
//
// outerPrism := MakePrism(...) // Prism[Outer, Inner]
// innerPrism := MakePrism(...) // Prism[Inner, Value]
// composed := Compose[Outer](innerPrism)(outerPrism) // Prism[Outer, Value]
func Compose[S, A, B any](ab Prism[A, B]) func(Prism[S, A]) Prism[S, B] {
return func(sa Prism[S, A]) Prism[S, B] {
return MakePrism(F.Flow2(
@@ -69,7 +159,10 @@ func Compose[S, A, B any](ab Prism[A, B]) func(Prism[S, A]) Prism[S, B] {
}
}
func prismModifyOption[S, A any](f func(A) A, sa Prism[S, A], s S) O.Option[S] {
// prismModifyOption applies a transformation function through a prism,
// returning Some(modified S) if the prism matches, None otherwise.
// This is an internal helper function.
func prismModifyOption[S, A any](f func(A) A, sa Prism[S, A], s S) Option[S] {
return F.Pipe2(
s,
sa.GetOption,
@@ -80,6 +173,10 @@ func prismModifyOption[S, A any](f func(A) A, sa Prism[S, A], s S) O.Option[S] {
)
}
// prismModify applies a transformation function through a prism.
// If the prism matches, it extracts the value, applies the function,
// and reconstructs the result. If the prism doesn't match, returns the original value.
// This is an internal helper function.
func prismModify[S, A any](f func(A) A, sa Prism[S, A], s S) S {
return F.Pipe1(
prismModifyOption(f, sa, s),
@@ -87,23 +184,63 @@ func prismModify[S, A any](f func(A) A, sa Prism[S, A], s S) S {
)
}
// prismSet is an internal helper that creates a setter function.
// Deprecated: Use Set instead.
func prismSet[S, A any](a A) func(Prism[S, A]) EM.Endomorphism[S] {
return EM.Curry3(prismModify[S, A])(F.Constant1[A](a))
return F.Curry3(prismModify[S, A])(F.Constant1[A](a))
}
// Set creates a function that sets a value through a prism.
// If the prism matches, it replaces the focused value with the new value.
// If the prism doesn't match, it returns the original value unchanged.
//
// Parameters:
// - a: The new value to set
//
// Returns:
// - A function that takes a prism and returns an endomorphism (S → S)
//
// Example:
//
// somePrism := MakePrism(...)
// setter := Set[Option[int], int](100)
// result := setter(somePrism)(Some(42)) // Some(100)
// result = setter(somePrism)(None[int]()) // None[int]() (unchanged)
func Set[S, A any](a A) func(Prism[S, A]) EM.Endomorphism[S] {
return EM.Curry3(prismModify[S, A])(F.Constant1[A](a))
return F.Curry3(prismModify[S, A])(F.Constant1[A](a))
}
func prismSome[A any]() Prism[O.Option[A], A] {
return MakePrism(F.Identity[O.Option[A]], O.Some[A])
// prismSome creates a prism that focuses on the Some variant of an Option.
// This is an internal helper used by the Some function.
func prismSome[A any]() Prism[Option[A], A] {
return MakePrism(F.Identity[Option[A]], O.Some[A])
}
// Some returns a `Prism` from a `Prism` focused on the `Some` of a `Option` type.
func Some[S, A any](soa Prism[S, O.Option[A]]) Prism[S, A] {
// Some creates a prism that focuses on the Some variant of an Option within a structure.
// It composes the provided prism (which focuses on an Option[A]) with a prism that
// extracts the value from Some.
//
// Type Parameters:
// - S: The source type
// - A: The value type within the Option
//
// Parameters:
// - soa: A prism that focuses on an Option[A] within S
//
// Returns:
// - A prism that focuses on the A value within Some
//
// Example:
//
// type Config struct { Timeout Option[int] }
// configPrism := MakePrism(...) // Prism[Config, Option[int]]
// timeoutPrism := Some(configPrism) // Prism[Config, int]
// value := timeoutPrism.GetOption(Config{Timeout: Some(30)}) // Some(30)
func Some[S, A any](soa Prism[S, Option[A]]) Prism[S, A] {
return Compose[S](prismSome[A]())(soa)
}
// imap is an internal helper that bidirectionally maps a prism's focus type.
func imap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](sa Prism[S, A], ab AB, ba BA) Prism[S, B] {
return MakePrism(
F.Flow2(sa.GetOption, O.Map(ab)),
@@ -111,6 +248,31 @@ func imap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](sa Prism[S, A], ab AB,
)
}
// IMap bidirectionally maps the focus type of a prism.
// It transforms a Prism[S, A] into a Prism[S, B] using two functions:
// one to map A → B and another to map B → A.
//
// Type Parameters:
// - S: The source type
// - A: The original focus type
// - B: The new focus type
// - AB: Function type A → B
// - BA: Function type B → A
//
// Parameters:
// - ab: Function to map from A to B
// - ba: Function to map from B to A
//
// Returns:
// - A function that transforms Prism[S, A] to Prism[S, B]
//
// Example:
//
// intPrism := MakePrism(...) // Prism[Result, int]
// stringPrism := IMap[Result](
// func(n int) string { return strconv.Itoa(n) },
// func(s string) int { n, _ := strconv.Atoi(s); return n },
// )(intPrism) // Prism[Result, string]
func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Prism[S, A]) Prism[S, B] {
return func(sa Prism[S, A]) Prism[S, B] {
return imap(sa, ab, ba)

File diff suppressed because it is too large Load Diff

312
v2/optics/prism/prisms.go Normal file
View File

@@ -0,0 +1,312 @@
// 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 prism
import (
"encoding/base64"
"net/url"
"time"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/option"
)
// FromEncoding creates a prism for base64 encoding/decoding operations.
// It provides a safe way to work with base64-encoded strings, handling
// encoding and decoding errors gracefully through the Option type.
//
// The prism's GetOption attempts to decode a base64 string into bytes.
// If decoding succeeds, it returns Some([]byte); if it fails (e.g., invalid
// base64 format), it returns None.
//
// The prism's ReverseGet always succeeds, encoding bytes into a base64 string.
//
// Parameters:
// - enc: A base64.Encoding instance (e.g., base64.StdEncoding, base64.URLEncoding)
//
// Returns:
// - A Prism[string, []byte] that safely handles base64 encoding/decoding
//
// Example:
//
// // Create a prism for standard base64 encoding
// b64Prism := FromEncoding(base64.StdEncoding)
//
// // Decode valid base64 string
// data := b64Prism.GetOption("SGVsbG8gV29ybGQ=") // Some([]byte("Hello World"))
//
// // Decode invalid base64 string
// invalid := b64Prism.GetOption("not-valid-base64!!!") // None[[]byte]()
//
// // Encode bytes to base64
// encoded := b64Prism.ReverseGet([]byte("Hello World")) // "SGVsbG8gV29ybGQ="
//
// // Use with Set to update encoded values
// newData := []byte("New Data")
// setter := Set[string, []byte](newData)
// result := setter(b64Prism)("SGVsbG8gV29ybGQ=") // Encodes newData to base64
//
// Common use cases:
// - Safely decoding base64-encoded configuration values
// - Working with base64-encoded API responses
// - Validating and transforming base64 data in pipelines
// - Using different encodings (Standard, URL-safe, RawStd, RawURL)
func FromEncoding(enc *base64.Encoding) Prism[string, []byte] {
return MakePrism(F.Flow2(
either.Eitherize1(enc.DecodeString),
either.Fold(F.Ignore1of1[error](option.None[[]byte]), option.Some),
), enc.EncodeToString)
}
// ParseURL creates a prism for parsing and formatting URLs.
// It provides a safe way to work with URL strings, handling parsing
// errors gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into a *url.URL.
// If parsing succeeds, it returns Some(*url.URL); if it fails (e.g., invalid
// URL format), it returns None.
//
// The prism's ReverseGet always succeeds, converting a *url.URL back to its
// string representation.
//
// Returns:
// - A Prism[string, *url.URL] that safely handles URL parsing/formatting
//
// Example:
//
// // Create a URL parsing prism
// urlPrism := ParseURL()
//
// // Parse valid URL
// parsed := urlPrism.GetOption("https://example.com/path?query=value")
// // Some(*url.URL{Scheme: "https", Host: "example.com", ...})
//
// // Parse invalid URL
// invalid := urlPrism.GetOption("ht!tp://invalid url") // None[*url.URL]()
//
// // Convert URL back to string
// u, _ := url.Parse("https://example.com")
// str := urlPrism.ReverseGet(u) // "https://example.com"
//
// // Use with Set to update URLs
// newURL, _ := url.Parse("https://newsite.com")
// setter := Set[string, *url.URL](newURL)
// result := setter(urlPrism)("https://oldsite.com") // "https://newsite.com"
//
// Common use cases:
// - Validating and parsing URL configuration values
// - Working with API endpoints
// - Transforming URL strings in data pipelines
// - Extracting and modifying URL components safely
func ParseURL() Prism[string, *url.URL] {
return MakePrism(F.Flow2(
either.Eitherize1(url.Parse),
either.Fold(F.Ignore1of1[error](option.None[*url.URL]), option.Some),
), (*url.URL).String)
}
// InstanceOf creates a prism for type assertions on interface{}/any values.
// It provides a safe way to extract values of a specific type from an any value,
// handling type mismatches gracefully through the Option type.
//
// The prism's GetOption attempts to assert that an any value is of type T.
// If the assertion succeeds, it returns Some(T); if it fails, it returns None.
//
// The prism's ReverseGet always succeeds, converting a value of type T back to any.
//
// Type Parameters:
// - T: The target type to extract from any
//
// Returns:
// - A Prism[any, T] that safely handles type assertions
//
// Example:
//
// // Create a prism for extracting int values
// intPrism := InstanceOf[int]()
//
// // Extract int from any
// var value any = 42
// result := intPrism.GetOption(value) // Some(42)
//
// // Type mismatch returns None
// var strValue any = "hello"
// result = intPrism.GetOption(strValue) // None[int]()
//
// // Convert back to any
// anyValue := intPrism.ReverseGet(42) // any(42)
//
// // Use with Set to update typed values
// setter := Set[any, int](100)
// result := setter(intPrism)(any(42)) // any(100)
//
// Common use cases:
// - Safely extracting typed values from interface{} collections
// - Working with heterogeneous data structures
// - Type-safe deserialization and validation
// - Pattern matching on interface{} values
func InstanceOf[T any]() Prism[any, T] {
return MakePrism(option.ToType[T], F.ToAny[T])
}
// ParseDate creates a prism for parsing and formatting dates with a specific layout.
// It provides a safe way to work with date strings, handling parsing errors
// gracefully through the Option type.
//
// The prism's GetOption attempts to parse a string into a time.Time using the
// specified layout. If parsing succeeds, it returns Some(time.Time); if it fails
// (e.g., invalid date format), it returns None.
//
// The prism's ReverseGet always succeeds, formatting a time.Time back to a string
// using the same layout.
//
// Parameters:
// - layout: The time layout string (e.g., "2006-01-02", time.RFC3339)
//
// Returns:
// - A Prism[string, time.Time] that safely handles date parsing/formatting
//
// Example:
//
// // Create a prism for ISO date format
// datePrism := ParseDate("2006-01-02")
//
// // Parse valid date
// parsed := datePrism.GetOption("2024-03-15")
// // Some(time.Time{2024, 3, 15, ...})
//
// // Parse invalid date
// invalid := datePrism.GetOption("not-a-date") // None[time.Time]()
//
// // Format date back to string
// date := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
// str := datePrism.ReverseGet(date) // "2024-03-15"
//
// // Use with Set to update dates
// newDate := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
// setter := Set[string, time.Time](newDate)
// result := setter(datePrism)("2024-03-15") // "2025-01-01"
//
// // Different layouts for different formats
// rfc3339Prism := ParseDate(time.RFC3339)
// parsed = rfc3339Prism.GetOption("2024-03-15T10:30:00Z")
//
// Common use cases:
// - Validating and parsing date configuration values
// - Working with date strings in APIs
// - Converting between date formats
// - Safely handling user-provided date inputs
func ParseDate(layout string) Prism[string, time.Time] {
return MakePrism(F.Flow2(
F.Bind1st(either.Eitherize2(time.Parse), layout),
either.Fold(F.Ignore1of1[error](option.None[time.Time]), option.Some),
), F.Bind2nd(time.Time.Format, layout))
}
// Deref creates a prism for safely dereferencing pointers.
// It provides a safe way to work with nullable pointers, handling nil values
// gracefully through the Option type.
//
// The prism's GetOption attempts to dereference a pointer.
// If the pointer is non-nil, it returns Some(*T); if it's nil, it returns None.
//
// The prism's ReverseGet is the identity function, returning the pointer unchanged.
//
// Type Parameters:
// - T: The type being pointed to
//
// Returns:
// - A Prism[*T, *T] that safely handles pointer dereferencing
//
// Example:
//
// // Create a prism for dereferencing int pointers
// derefPrism := Deref[int]()
//
// // Dereference non-nil pointer
// value := 42
// ptr := &value
// result := derefPrism.GetOption(ptr) // Some(&42)
//
// // Dereference nil pointer
// var nilPtr *int
// result = derefPrism.GetOption(nilPtr) // None[*int]()
//
// // ReverseGet returns the pointer unchanged
// reconstructed := derefPrism.ReverseGet(ptr) // &42
//
// // Use with Set to update non-nil pointers
// newValue := 100
// newPtr := &newValue
// setter := Set[*int, *int](newPtr)
// result := setter(derefPrism)(ptr) // &100
// result = setter(derefPrism)(nilPtr) // nil (unchanged)
//
// Common use cases:
// - Safely working with optional pointer fields
// - Validating non-nil pointers before operations
// - Filtering out nil values in data pipelines
// - Working with database nullable columns
func Deref[T any]() Prism[*T, *T] {
return MakePrism(option.FromNillable[T], F.Identity[*T])
}
// FromEither creates a prism for extracting Right values from Either types.
// It provides a safe way to work with Either values, focusing on the success case
// and handling the error case gracefully through the Option type.
//
// The prism's GetOption attempts to extract the Right value from an Either.
// If the Either is Right(value), it returns Some(value); if it's Left(error), it returns None.
//
// The prism's ReverseGet always succeeds, wrapping a value into a Right.
//
// Type Parameters:
// - E: The error/left type
// - T: The value/right type
//
// Returns:
// - A Prism[Either[E, T], T] that safely extracts Right values
//
// Example:
//
// // Create a prism for extracting successful results
// resultPrism := FromEither[error, int]()
//
// // Extract from Right
// success := either.Right[error](42)
// result := resultPrism.GetOption(success) // Some(42)
//
// // Extract from Left
// failure := either.Left[int](errors.New("failed"))
// result = resultPrism.GetOption(failure) // None[int]()
//
// // Wrap value into Right
// wrapped := resultPrism.ReverseGet(100) // Right(100)
//
// // Use with Set to update successful results
// setter := Set[Either[error, int], int](200)
// result := setter(resultPrism)(success) // Right(200)
// result = setter(resultPrism)(failure) // Left(error) (unchanged)
//
// Common use cases:
// - Extracting successful values from Either results
// - Filtering out errors in data pipelines
// - Working with fallible operations
// - Composing with other prisms for complex error handling
func FromEither[E, T any]() Prism[Either[E, T], T] {
return MakePrism(either.ToOption[E, T], either.Of[E, T])
}

View File

@@ -20,7 +20,43 @@ import (
O "github.com/IBM/fp-go/v2/option"
)
// AsTraversal converts a prism to a traversal
// AsTraversal converts a Prism into a Traversal.
//
// A Traversal is a more general optic that can focus on zero or more values,
// while a Prism focuses on zero or one value. This function lifts a Prism
// into the Traversal abstraction, allowing it to be used in contexts that
// expect traversals.
//
// The conversion works by:
// - If the prism matches (GetOption returns Some), the traversal focuses on that value
// - If the prism doesn't match (GetOption returns None), the traversal focuses on zero values
//
// Type Parameters:
// - R: The traversal function type ~func(func(A) HKTA) func(S) HKTS
// - S: The source type
// - A: The focus type
// - HKTS: Higher-kinded type for S (e.g., functor/applicative context)
// - HKTA: Higher-kinded type for A (e.g., functor/applicative context)
//
// Parameters:
// - fof: Function to lift S into the higher-kinded type HKTS (pure/of operation)
// - fmap: Function to map over HKTA and produce HKTS (functor map operation)
//
// Returns:
// - A function that converts a Prism[S, A] into a Traversal R
//
// Example:
//
// // Convert a prism to a traversal for use with applicative functors
// prism := MakePrism(...)
// traversal := AsTraversal(
// func(s S) HKTS { return pure(s) },
// func(hkta HKTA, f func(A) S) HKTS { return fmap(hkta, f) },
// )(prism)
//
// Note: This function is typically used in advanced scenarios involving
// higher-kinded types and applicative functors. Most users will work
// directly with prisms rather than converting them to traversals.
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
fof func(S) HKTS,
fmap func(HKTA, func(A) S) HKTS,
@@ -32,7 +68,9 @@ func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
s,
sa.GetOption,
O.Fold(
// If prism doesn't match, return the original value lifted into HKTS
F.Nullary2(F.Constant(s), fof),
// If prism matches, apply f to the extracted value and map back
func(a A) HKTS {
return fmap(f(a), func(a A) S {
return prismModify(F.Constant1[A](a), sa, s)

96
v2/optics/prism/types.go Normal file
View File

@@ -0,0 +1,96 @@
// 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 prism
import (
"github.com/IBM/fp-go/v2/either"
O "github.com/IBM/fp-go/v2/option"
)
type (
// Option is a type alias for O.Option[T], representing an optional value.
// It is re-exported here for convenience when working with prisms.
//
// An Option[T] can be either:
// - Some(value): Contains a value of type T
// - None: Represents the absence of a value
//
// This type is commonly used in prism operations, particularly in the
// GetOption method which returns Option[A] to indicate whether a value
// could be extracted from the source type.
//
// Type Parameters:
// - T: The type of the value that may or may not be present
//
// Example:
//
// // A prism's GetOption returns an Option
// prism := MakePrism(...)
// result := prism.GetOption(value) // Returns Option[A]
//
// // Check if the value was extracted successfully
// if O.IsSome(result) {
// // Value was found
// } else {
// // Value was not found (None)
// }
//
// See also:
// - github.com/IBM/fp-go/v2/option for the full Option API
// - Prism.GetOption for the primary use case within this package
Option[T any] = O.Option[T]
// Either is a type alias for either.Either[E, T], representing a value that can be one of two types.
// It is re-exported here for convenience when working with prisms that handle error cases.
//
// An Either[E, T] can be either:
// - Left(error): Contains an error value of type E
// - Right(value): Contains a success value of type T
//
// This type is commonly used in prism operations for error handling, particularly with
// the FromEither prism which extracts Right values and returns None for Left values.
//
// Type Parameters:
// - E: The type of the error/left value
// - T: The type of the success/right value
//
// Example:
//
// // Using FromEither prism to extract success values
// prism := FromEither[error, int]()
//
// // Extract from a Right value
// success := either.Right[error](42)
// result := prism.GetOption(success) // Returns Some(42)
//
// // Extract from a Left value
// failure := either.Left[int](errors.New("failed"))
// result = prism.GetOption(failure) // Returns None
//
// // ReverseGet wraps a value into Right
// wrapped := prism.ReverseGet(100) // Returns Right(100)
//
// Common Use Cases:
// - Error handling in functional pipelines
// - Representing computations that may fail
// - Composing prisms that work with Either types
//
// See also:
// - github.com/IBM/fp-go/v2/either for the full Either API
// - FromEither for creating prisms that work with Either types
// - Prism composition for building complex error-handling pipelines
Either[E, T any] = either.Either[E, T]
)

View File

@@ -19,14 +19,68 @@ import (
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 {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
// result := readereither.Do[Env, error](State{})
func Do[R, E, S any](
empty S,
) ReaderEither[R, E, S] {
return G.Do[ReaderEither[R, E, S], R, E, 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 shared 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
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// result := F.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readereither.ReaderEither[Env, error, User] {
// return readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// },
// ),
// readereither.Bind(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) readereither.ReaderEither[Env, error, Config] {
// // This can access s.User from the previous step
// return readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfigForUser(s.User.ID)
// })
// },
// ),
// )
func Bind[R, E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderEither[R, E, T],
@@ -57,7 +111,47 @@ func BindTo[R, E, S1, T any](
return G.BindTo[ReaderEither[R, E, S1], ReaderEither[R, E, T], R, E, 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 {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// // These operations are independent and can be combined with ApS
// getUser := readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfig()
// })
//
// result := F.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.ApS(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// getUser,
// ),
// readereither.ApS(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// getConfig,
// ),
// )
func ApS[R, E, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderEither[R, E, T],

View File

@@ -22,14 +22,68 @@ 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 {
// Config Config
// User User
// }
// type Env struct {
// ConfigService ConfigService
// UserService UserService
// }
// result := generic.Do[ReaderEither[Env, error, State], Env, error, State](State{})
func Do[GS ~func(R) ET.Either[E, S], R, E, S any](
empty S,
) GS {
return Of[GS, E, R, 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 shared 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 {
// Config Config
// User User
// }
// type Env struct {
// ConfigService ConfigService
// UserService UserService
// }
//
// result := F.Pipe2(
// generic.Do[ReaderEither[Env, error, State], Env, error, State](State{}),
// generic.Bind[ReaderEither[Env, error, State], ReaderEither[Env, error, State], ReaderEither[Env, error, Config], Env, error, State, State, Config](
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) ReaderEither[Env, error, Config] {
// return func(env Env) either.Either[error, Config] {
// return env.ConfigService.Load()
// }
// },
// ),
// generic.Bind[ReaderEither[Env, error, State], ReaderEither[Env, error, State], ReaderEither[Env, error, User], Env, error, State, State, User](
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) ReaderEither[Env, error, User] {
// // This can access s.Config from the previous step
// return func(env Env) either.Either[error, User] {
// return env.UserService.GetUserForConfig(s.Config)
// }
// },
// ),
// )
func Bind[GS1 ~func(R) ET.Either[E, S1], GS2 ~func(R) ET.Either[E, S2], GT ~func(R) ET.Either[E, T], R, E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) GT,
@@ -76,7 +130,47 @@ func BindTo[GS1 ~func(R) ET.Either[E, S1], GT ~func(R) ET.Either[E, T], R, E, S1
)
}
// 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
// User User
// }
// type Env struct {
// ConfigService ConfigService
// UserService UserService
// }
//
// // These operations are independent and can be combined with ApS
// getConfig := func(env Env) either.Either[error, Config] {
// return env.ConfigService.Load()
// }
// getUser := func(env Env) either.Either[error, User] {
// return env.UserService.GetCurrent()
// }
//
// result := F.Pipe2(
// generic.Do[ReaderEither[Env, error, State], Env, error, State](State{}),
// generic.ApS[...](
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// getConfig,
// ),
// generic.ApS[...](
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// getUser,
// ),
// )
func ApS[GS1 ~func(R) ET.Either[E, S1], GS2 ~func(R) ET.Either[E, S2], GT ~func(R) ET.Either[E, T], R, E, S1, S2, T any](
setter func(T) func(S1) S2,
fa GT,

View File

@@ -21,14 +21,68 @@ import (
"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 {
// Host string
// Port int
// }
// type Config struct {
// DefaultHost string
// DefaultPort int
// }
// result := readerio.Do[Config](State{})
func Do[R, S any](
empty S,
) ReaderIO[R, S] {
return Of[R](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 shared 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 {
// Host string
// Port int
// }
// type Config struct {
// DefaultHost string
// DefaultPort int
// }
//
// result := F.Pipe2(
// readerio.Do[Config](State{}),
// readerio.Bind(
// func(host string) func(State) State {
// return func(s State) State { s.Host = host; return s }
// },
// func(s State) readerio.ReaderIO[Config, string] {
// return readerio.Asks(func(c Config) io.IO[string] {
// return io.Of(c.DefaultHost)
// })
// },
// ),
// readerio.Bind(
// func(port int) func(State) State {
// return func(s State) State { s.Port = port; return s }
// },
// func(s State) readerio.ReaderIO[Config, int] {
// // This can access s.Host from the previous step
// return readerio.Asks(func(c Config) io.IO[int] {
// return io.Of(c.DefaultPort)
// })
// },
// ),
// )
func Bind[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderIO[R, T],
@@ -75,7 +129,47 @@ func BindTo[R, 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 {
// Host string
// Port int
// }
// type Config struct {
// DefaultHost string
// DefaultPort int
// }
//
// // These operations are independent and can be combined with ApS
// getHost := readerio.Asks(func(c Config) io.IO[string] {
// return io.Of(c.DefaultHost)
// })
// getPort := readerio.Asks(func(c Config) io.IO[int] {
// return io.Of(c.DefaultPort)
// })
//
// result := F.Pipe2(
// readerio.Do[Config](State{}),
// readerio.ApS(
// func(host string) func(State) State {
// return func(s State) State { s.Host = host; return s }
// },
// getHost,
// ),
// readerio.ApS(
// func(port int) func(State) State {
// return func(s State) State { s.Port = port; return s }
// },
// getPort,
// ),
// )
func ApS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIO[R, T],

View File

@@ -20,14 +20,68 @@ import (
G "github.com/IBM/fp-go/v2/readerioeither/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 {
// User User
// Posts []Post
// }
// type Env struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
// result := readerioeither.Do[Env, error](State{})
func Do[R, E, S any](
empty S,
) ReaderIOEither[R, E, S] {
return G.Do[ReaderIOEither[R, E, S], IOE.IOEither[E, S], R, E, 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 shared 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
// Posts []Post
// }
// type Env struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
//
// result := F.Pipe2(
// readerioeither.Do[Env, error](State{}),
// readerioeither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerioeither.ReaderIOEither[Env, error, User] {
// return readerioeither.Asks(func(env Env) ioeither.IOEither[error, User] {
// return env.UserRepo.FindUser()
// })
// },
// ),
// readerioeither.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) readerioeither.ReaderIOEither[Env, error, []Post] {
// // This can access s.User from the previous step
// return readerioeither.Asks(func(env Env) ioeither.IOEither[error, []Post] {
// return env.PostRepo.FindPostsByUser(s.User.ID)
// })
// },
// ),
// )
func Bind[R, E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderIOEither[R, E, T],
@@ -58,7 +112,47 @@ func BindTo[R, E, S1, T any](
return G.BindTo[ReaderIOEither[R, E, S1], ReaderIOEither[R, E, T], IOE.IOEither[E, S1], IOE.IOEither[E, T], R, E, 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 {
// User User
// Posts []Post
// }
// type Env struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
//
// // These operations are independent and can be combined with ApS
// getUser := readerioeither.Asks(func(env Env) ioeither.IOEither[error, User] {
// return env.UserRepo.FindUser()
// })
// getPosts := readerioeither.Asks(func(env Env) ioeither.IOEither[error, []Post] {
// return env.PostRepo.FindPosts()
// })
//
// result := F.Pipe2(
// readerioeither.Do[Env, error](State{}),
// readerioeither.ApS(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// getUser,
// ),
// readerioeither.ApS(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// getPosts,
// ),
// )
func ApS[R, E, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIOEither[R, E, T],

View File

@@ -22,14 +22,68 @@ 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 {
// User User
// Config Config
// }
// type Env struct {
// UserRepo UserRepository
// ConfigRepo ConfigRepository
// }
// result := generic.Do[ReaderIOEither[Env, error, State], IOEither[error, State], Env, error, State](State{})
func Do[GRS ~func(R) GS, GS ~func() either.Either[E, S], R, E, S any](
empty S,
) GRS {
return Of[GRS, GS, R, E, 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 shared 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
// }
// type Env struct {
// UserRepo UserRepository
// ConfigRepo ConfigRepository
// }
//
// result := F.Pipe2(
// generic.Do[ReaderIOEither[Env, error, State], IOEither[error, State], Env, error, State](State{}),
// generic.Bind[ReaderIOEither[Env, error, State], ReaderIOEither[Env, error, State], ReaderIOEither[Env, error, User], IOEither[error, State], IOEither[error, State], IOEither[error, User], Env, error, State, State, User](
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) ReaderIOEither[Env, error, User] {
// return func(env Env) ioeither.IOEither[error, User] {
// return env.UserRepo.FindUser()
// }
// },
// ),
// generic.Bind[ReaderIOEither[Env, error, State], ReaderIOEither[Env, error, State], ReaderIOEither[Env, error, Config], IOEither[error, State], IOEither[error, State], IOEither[error, Config], Env, error, State, State, Config](
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) ReaderIOEither[Env, error, Config] {
// // This can access s.User from the previous step
// return func(env Env) ioeither.IOEither[error, Config] {
// return env.ConfigRepo.LoadConfigForUser(s.User.ID)
// }
// },
// ),
// )
func Bind[GRS1 ~func(R) GS1, GRS2 ~func(R) GS2, GRT ~func(R) GT, GS1 ~func() either.Either[E, S1], GS2 ~func() either.Either[E, S2], GT ~func() either.Either[E, T], R, E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) GRT,
@@ -76,7 +130,47 @@ func BindTo[GRS1 ~func(R) GS1, GRT ~func(R) GT, GS1 ~func() either.Either[E, S1]
)
}
// 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
// }
// type Env struct {
// UserRepo UserRepository
// ConfigRepo ConfigRepository
// }
//
// // These operations are independent and can be combined with ApS
// getUser := func(env Env) ioeither.IOEither[error, User] {
// return env.UserRepo.FindUser()
// }
// getConfig := func(env Env) ioeither.IOEither[error, Config] {
// return env.ConfigRepo.LoadConfig()
// }
//
// result := F.Pipe2(
// generic.Do[ReaderIOEither[Env, error, State], IOEither[error, State], Env, error, State](State{}),
// generic.ApS[...](
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// getUser,
// ),
// generic.ApS[...](
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// getConfig,
// ),
// )
func ApS[GRTS1 ~func(R) GTS1, GRS1 ~func(R) GS1, GRS2 ~func(R) GS2, GRT ~func(R) GT, GTS1 ~func() either.Either[E, func(T) S2], GS1 ~func() either.Either[E, S1], GS2 ~func() either.Either[E, S2], GT ~func() either.Either[E, T], R, E, S1, S2, T any](
setter func(T) func(S1) S2,
fa GRT,

View File

@@ -20,12 +20,54 @@ import (
G "github.com/IBM/fp-go/v2/record/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 {
// Name string
// Count int
// }
// result := record.Do[string, State]()
func Do[K comparable, S any]() map[K]S {
return G.Do[map[K]S, K, S]()
}
// 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 records, this merges values by key.
//
// 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
// Count int
// }
//
// result := F.Pipe2(
// record.Do[string, State](),
// record.Bind(monoid.Record[string, State]())(
// func(name string) func(State) State {
// return func(s State) State { s.Name = name; return s }
// },
// func(s State) map[string]string {
// return map[string]string{"a": "Alice", "b": "Bob"}
// },
// ),
// record.Bind(monoid.Record[string, State]())(
// func(count int) func(State) State {
// return func(s State) State { s.Count = count; return s }
// },
// func(s State) map[string]int {
// // This can access s.Name from the previous step
// return map[string]int{"a": len(s.Name), "b": len(s.Name) * 2}
// },
// ),
// )
func Bind[S1, T any, K comparable, S2 any](m Mo.Monoid[map[K]S2]) func(setter func(T) func(S1) S2, f func(S1) map[K]T) func(map[K]S1) map[K]S2 {
return G.Bind[map[K]S1, map[K]S2, map[K]T, K, S1, S2, T](m)
}
@@ -51,7 +93,39 @@ func BindTo[S1, T any, K comparable](setter func(T) S1) func(map[K]T) map[K]S1 {
return G.BindTo[map[K]S1, map[K]T, K, 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 {
// Name string
// Count int
// }
//
// // These operations are independent and can be combined with ApS
// names := map[string]string{"a": "Alice", "b": "Bob"}
// counts := map[string]int{"a": 10, "b": 20}
//
// result := F.Pipe2(
// record.Do[string, State](),
// record.ApS(monoid.Record[string, State]())(
// func(name string) func(State) State {
// return func(s State) State { s.Name = name; return s }
// },
// names,
// ),
// record.ApS(monoid.Record[string, State]())(
// func(count int) func(State) State {
// return func(s State) State { s.Count = count; return s }
// },
// counts,
// ),
// ) // map[string]State{"a": {Name: "Alice", Count: 10}, "b": {Name: "Bob", Count: 20}}
func ApS[S1, T any, K comparable, S2 any](m Mo.Monoid[map[K]S2]) func(setter func(T) func(S1) S2, fa map[K]T) func(map[K]S1) map[K]S2 {
return G.ApS[map[K]S1, map[K]S2, map[K]T, K, S1, S2, T](m)
}

View File

@@ -22,12 +22,58 @@ import (
Mo "github.com/IBM/fp-go/v2/monoid"
)
// 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
// Count int
// }
// result := generic.Do[map[string]State, string, State]()
func Do[GS ~map[K]S, K comparable, S any]() GS {
return Empty[GS, K, S]()
}
// 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 records, this merges values by key 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 {
// Name string
// Count int
// }
//
// result := F.Pipe2(
// generic.Do[map[string]State, string, State](),
// generic.Bind[map[string]State, map[string]State, map[string]string, string, State, State, string](
// monoid.Record[string, State](),
// )(
// func(name string) func(State) State {
// return func(s State) State { s.Name = name; return s }
// },
// func(s State) map[string]string {
// return map[string]string{"a": "Alice", "b": "Bob"}
// },
// ),
// generic.Bind[map[string]State, map[string]State, map[string]int, string, State, State, int](
// monoid.Record[string, State](),
// )(
// func(count int) func(State) State {
// return func(s State) State { s.Count = count; return s }
// },
// func(s State) map[string]int {
// // This can access s.Name from the previous step
// return map[string]int{"a": len(s.Name), "b": len(s.Name) * 2}
// },
// ),
// )
func Bind[GS1 ~map[K]S1, GS2 ~map[K]S2, GT ~map[K]T, K comparable, S1, S2, T any](m Mo.Monoid[GS2]) func(setter func(T) func(S1) S2, f func(S1) GT) func(GS1) GS2 {
c := Chain[GS1, GS2, K, S1, S2](m)
return func(setter func(T) func(S1) S2, f func(S1) GT) func(GS1) GS2 {
@@ -72,7 +118,43 @@ func BindTo[GS1 ~map[K]S1, GT ~map[K]T, K comparable, S1, T any](setter func(T)
)
}
// 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 records, this merges values by key.
//
// Example:
//
// type State struct {
// Name string
// Score int
// }
//
// // These operations are independent and can be combined with ApS
// names := map[string]string{"player1": "Alice", "player2": "Bob"}
// scores := map[string]int{"player1": 100, "player2": 200}
//
// result := F.Pipe2(
// generic.Do[map[string]State, string, State](),
// generic.ApS[map[string]State, map[string]State, map[string]string, string, State, State, string](
// monoid.Record[string, State](),
// )(
// func(name string) func(State) State {
// return func(s State) State { s.Name = name; return s }
// },
// names,
// ),
// generic.ApS[map[string]State, map[string]State, map[string]int, string, State, State, int](
// monoid.Record[string, State](),
// )(
// func(score int) func(State) State {
// return func(s State) State { s.Score = score; return s }
// },
// scores,
// ),
// ) // map[string]State{"player1": {Name: "Alice", Score: 100}, "player2": {Name: "Bob", Score: 200}}
func ApS[GS1 ~map[K]S1, GS2 ~map[K]S2, GT ~map[K]T, K comparable, S1, S2, T any](m Mo.Monoid[GS2]) func(setter func(T) func(S1) S2, fa GT) func(GS1) GS2 {
a := Ap[GS2, map[K]func(T) S2, GT, K, S2, T](m)
return func(setter func(T) func(S1) S2, fa GT) func(GS1) GS2 {