mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-24 12:57:26 +02:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4cac1cb3e | ||
|
|
a3fdb03df4 | ||
|
|
47727fd514 | ||
|
|
ece7d088ea | ||
|
|
13d25eca32 | ||
|
|
a68e32308d | ||
|
|
61b948425b | ||
|
|
a276f3acff | ||
|
|
8c656a4297 | ||
|
|
bd9a642e93 | ||
|
|
3b55cae265 | ||
|
|
1472fa5a50 | ||
|
|
49deb57d24 | ||
|
|
abb55ddbd0 | ||
|
|
f6b01dffdc | ||
|
|
43b666edbb | ||
|
|
e42d765852 | ||
|
|
d2da8a32b4 | ||
|
|
7484af664b | ||
|
|
ae38e3f8f4 | ||
|
|
e0f854bda3 | ||
|
|
34786c3cd8 | ||
|
|
a7aa7e3560 | ||
|
|
ff2a4299b2 | ||
|
|
edd66d63e6 | ||
|
|
909aec8eba | ||
|
|
da0344f9bd | ||
|
|
cd79dd56b9 | ||
|
|
df07599a9e | ||
|
|
30ad0e4dd8 | ||
|
|
2374d7f1e4 | ||
|
|
eafc008798 | ||
|
|
46bf065e34 | ||
|
|
b4e303423b | ||
|
|
7afc098f58 | ||
|
|
617e43de19 | ||
|
|
0f7a6c0589 | ||
|
|
e7f78e1a33 | ||
|
|
6505ab1791 | ||
|
|
cfa48985ec | ||
|
|
677523b70f | ||
|
|
8243242cf1 | ||
|
|
9021a8e274 | ||
|
|
f3128e887b | ||
|
|
4583694211 | ||
|
|
b87c20d139 | ||
|
|
9fd5b90138 | ||
|
|
cdc2041d8e | ||
|
|
777fff9a5a | ||
|
|
8acea9043f | ||
|
|
c6445ac021 | ||
|
|
840ffbb51d | ||
|
|
380ba2853c | ||
|
|
c18e5e2107 | ||
|
|
89766bdb26 | ||
|
|
21d116d325 | ||
|
|
7f2e76dd94 | ||
|
|
77965a12ff |
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -24,15 +24,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x']
|
||||
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x']
|
||||
fail-fast: false # Continue with other versions if one fails
|
||||
steps:
|
||||
# full checkout for semantic-release
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true # Enable Go module caching
|
||||
@@ -64,13 +64,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ['1.24.x', '1.25.x']
|
||||
go-version: ['1.24.x', '1.25.x', '1.26.x']
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true # Enable Go module caching
|
||||
@@ -126,17 +126,17 @@ jobs:
|
||||
steps:
|
||||
# full checkout for semantic-release
|
||||
- name: Full checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.LATEST_GO_VERSION }}
|
||||
cache: true # Enable Go module caching
|
||||
|
||||
16
go.sum
16
go.sum
@@ -1,7 +1,3 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -10,20 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
|
||||
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
|
||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"matchDepTypes": [
|
||||
"golang"
|
||||
],
|
||||
"enabled": false
|
||||
"enabled": false,
|
||||
"description": "Disable updates to the go directive in go.mod files - the directive identifies the minimum compatible Go version and should stay as small as possible for maximum compatibility"
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": [
|
||||
|
||||
1
v2/.bobignore
Normal file
1
v2/.bobignore
Normal file
@@ -0,0 +1 @@
|
||||
reflect\reflect.go
|
||||
226
v2/AGENTS.md
Normal file
226
v2/AGENTS.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Agent Guidelines for fp-go/v2
|
||||
|
||||
This document provides guidelines for AI agents working on the fp-go/v2 project.
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Go Doc Comments
|
||||
|
||||
1. **Use Standard Go Doc Format**
|
||||
- Do NOT use markdown-style links like `[text](url)`
|
||||
- Use simple type references: `ReaderResult`, `Validate[I, A]`, `validation.Success`
|
||||
- Go's documentation system will automatically create links
|
||||
|
||||
2. **Structure**
|
||||
```go
|
||||
// FunctionName does something useful.
|
||||
//
|
||||
// Longer description explaining the purpose and behavior.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: Description of type parameter
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - param: Description of parameter
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - ReturnType: Description of return value
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// code example here
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - RelatedFunction: Brief description
|
||||
func FunctionName[T any](param T) ReturnType {
|
||||
```
|
||||
|
||||
3. **Code Examples**
|
||||
- Use idiomatic Go patterns
|
||||
- Prefer `result.Eitherize1(strconv.Atoi)` over manual error handling
|
||||
- Show realistic, runnable examples
|
||||
|
||||
### File Headers
|
||||
|
||||
Always include the Apache 2.0 license header:
|
||||
|
||||
```go
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
```
|
||||
|
||||
## Testing Standards
|
||||
|
||||
### Test Structure
|
||||
|
||||
1. **Organize Tests by Category**
|
||||
```go
|
||||
func TestFunctionName_Success(t *testing.T) {
|
||||
t.Run("specific success case", func(t *testing.T) {
|
||||
// test code
|
||||
})
|
||||
}
|
||||
|
||||
func TestFunctionName_Failure(t *testing.T) {
|
||||
t.Run("specific failure case", func(t *testing.T) {
|
||||
// test code
|
||||
})
|
||||
}
|
||||
|
||||
func TestFunctionName_EdgeCases(t *testing.T) {
|
||||
// edge case tests
|
||||
}
|
||||
|
||||
func TestFunctionName_Integration(t *testing.T) {
|
||||
// integration tests
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use Direct Assertions**
|
||||
- Prefer: `assert.Equal(t, validation.Success(expected), actual)`
|
||||
- Avoid: Verbose `either.MonadFold` patterns unless necessary
|
||||
- Exception: When you need to verify pointer is not nil or extract specific fields
|
||||
|
||||
3. **Use Idiomatic Patterns**
|
||||
- Use `result.Eitherize1` for converting `(T, error)` functions
|
||||
- Use `result.Of` for success values
|
||||
- Use `result.Left` for error values
|
||||
|
||||
### Test Coverage
|
||||
|
||||
Include tests for:
|
||||
- **Success cases**: Normal operation with various input types
|
||||
- **Failure cases**: Error handling and error preservation
|
||||
- **Edge cases**: Nil, empty, zero values, boundary conditions
|
||||
- **Integration**: Composition with other functions
|
||||
- **Type safety**: Verify type parameters work correctly
|
||||
- **Benchmarks**: Performance-critical paths
|
||||
|
||||
### Example Test Pattern
|
||||
|
||||
```go
|
||||
func TestFromReaderResult_Success(t *testing.T) {
|
||||
t.Run("converts successful ReaderResult", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
validator := FromReaderResult[string, int](parseIntRR)
|
||||
|
||||
// Act
|
||||
result := validator("42")(nil)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### Functional Patterns
|
||||
|
||||
1. **Prefer Composition**
|
||||
```go
|
||||
validator := F.Pipe1(
|
||||
FromReaderResult[string, int](parseIntRR),
|
||||
Chain(validatePositive),
|
||||
)
|
||||
```
|
||||
|
||||
2. **Use Type-Safe Helpers**
|
||||
- `result.Eitherize1` for `func(T) (R, error)`
|
||||
- `result.Of` for wrapping success values
|
||||
- `result.Left` for wrapping errors
|
||||
|
||||
3. **Avoid Verbose Patterns**
|
||||
- Don't manually handle `(value, error)` tuples when helpers exist
|
||||
- Don't use `either.MonadFold` in tests unless necessary
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **In Production Code**
|
||||
- Use `validation.Success` for successful validations
|
||||
- Use `validation.FailureWithMessage` for simple failures
|
||||
- Use `validation.FailureWithError` to preserve error causes
|
||||
|
||||
2. **In Tests**
|
||||
- Verify error messages and causes
|
||||
- Check error context is preserved
|
||||
- Test error accumulation when applicable
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Converting Error-Based Functions
|
||||
|
||||
```go
|
||||
// Good: Use Eitherize1
|
||||
parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
|
||||
// Avoid: Manual error handling
|
||||
parseIntRR := func(input string) result.Result[int] {
|
||||
val, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return result.Left[int](err)
|
||||
}
|
||||
return result.Of(val)
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Validation Results
|
||||
|
||||
```go
|
||||
// Good: Direct comparison
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Avoid: Verbose extraction (unless you need to verify specific fields)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
```
|
||||
|
||||
### Documentation Examples
|
||||
|
||||
```go
|
||||
// Good: Concise and idiomatic
|
||||
// parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
// validator := FromReaderResult[string, int](parseIntRR)
|
||||
|
||||
// Avoid: Verbose manual patterns
|
||||
// parseIntRR := func(input string) result.Result[int] {
|
||||
// val, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return result.Left[int](err)
|
||||
// }
|
||||
// return result.Of(val)
|
||||
// }
|
||||
```
|
||||
|
||||
## Checklist for New Code
|
||||
|
||||
- [ ] Apache 2.0 license header included
|
||||
- [ ] Go doc comments use standard format (no markdown links)
|
||||
- [ ] Code examples are idiomatic and concise
|
||||
- [ ] Tests cover success, failure, edge cases, and integration
|
||||
- [ ] Tests use direct assertions where possible
|
||||
- [ ] Benchmarks included for performance-critical code
|
||||
- [ ] All tests pass
|
||||
- [ ] Code uses functional composition patterns
|
||||
- [ ] Error handling preserves context and causes
|
||||
16
v2/DESIGN.md
16
v2/DESIGN.md
@@ -14,6 +14,8 @@ This document explains the key design decisions and principles behind fp-go's AP
|
||||
|
||||
fp-go follows the **"data last"** principle, where the data being operated on is always the last parameter in a function. This design choice enables powerful function composition and partial application patterns.
|
||||
|
||||
This principle is deeply rooted in functional programming tradition, particularly in **Haskell's design philosophy**. Haskell functions are automatically curried and follow the data-last convention, making function composition natural and elegant. For example, Haskell's `map` function has the signature `(a -> b) -> [a] -> [b]`, where the transformation function comes before the list.
|
||||
|
||||
### What is "Data Last"?
|
||||
|
||||
In the "data last" style, functions are structured so that:
|
||||
@@ -31,6 +33,8 @@ The "data last" principle enables:
|
||||
3. **Point-Free Style**: Write transformations without explicitly mentioning the data
|
||||
4. **Reusability**: Create reusable transformation pipelines
|
||||
|
||||
This design aligns with Haskell's approach where all functions are curried by default, enabling elegant composition patterns that have proven effective over decades of functional programming practice.
|
||||
|
||||
### Examples
|
||||
|
||||
#### Basic Transformation
|
||||
@@ -181,8 +185,18 @@ result := O.MonadMap(O.Some("hello"), strings.ToUpper)
|
||||
|
||||
The data-last currying pattern is well-documented in the functional programming community:
|
||||
|
||||
#### Haskell Design Philosophy
|
||||
- [Haskell Wiki - Currying](https://wiki.haskell.org/Currying) - Comprehensive explanation of currying in Haskell
|
||||
- [Learn You a Haskell - Higher Order Functions](http://learnyouahaskell.com/higher-order-functions) - Introduction to currying and partial application
|
||||
- [Haskell's Prelude](https://hackage.haskell.org/package/base/docs/Prelude.html) - Standard library showing data-last convention throughout
|
||||
|
||||
#### General Functional Programming
|
||||
- [Mostly Adequate Guide - Ch. 4: Currying](https://mostly-adequate.gitbook.io/mostly-adequate-guide/ch04) - Excellent introduction with clear examples
|
||||
- [Curry and Function Composition](https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983) by Eric Elliott
|
||||
- [Why Curry Helps](https://hughfdjackson.com/javascript/why-curry-helps/) - Practical benefits of currying
|
||||
|
||||
#### Related Libraries
|
||||
- [fp-ts Documentation](https://gcanti.github.io/fp-ts/) - TypeScript library that inspired fp-go's design
|
||||
- [fp-ts Issue #1238](https://github.com/gcanti/fp-ts/issues/1238) - Real-world examples of data-last refactoring
|
||||
|
||||
## Kleisli and Operator Types
|
||||
@@ -570,5 +584,7 @@ func process(input string) types.Result[types.Option[int]] {
|
||||
|
||||
For more information, see:
|
||||
- [README.md](./README.md) - Overview and quick start
|
||||
- [FUNCTIONAL_IO.md](./FUNCTIONAL_IO.md) - Functional I/O patterns with Context and Reader
|
||||
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison between standard and idiomatic packages
|
||||
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
|
||||
- [Samples](./samples/) - Practical examples
|
||||
829
v2/FUNCTIONAL_IO.md
Normal file
829
v2/FUNCTIONAL_IO.md
Normal file
@@ -0,0 +1,829 @@
|
||||
# Functional I/O in Go: Context, Errors, and the Reader Pattern
|
||||
|
||||
This document explores how functional programming principles apply to I/O operations in Go, comparing traditional imperative approaches with functional patterns using the `context/readerioresult` and `idiomatic/context/readerresult` packages.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Why Context in I/O Operations](#why-context-in-io-operations)
|
||||
- [The Error-Value Tuple Pattern](#the-error-value-tuple-pattern)
|
||||
- [Functional Approach: Reader Pattern](#functional-approach-reader-pattern)
|
||||
- [Benefits of the Functional Approach](#benefits-of-the-functional-approach)
|
||||
- [Side-by-Side Comparison](#side-by-side-comparison)
|
||||
- [Advanced Patterns](#advanced-patterns)
|
||||
- [When to Use Each Approach](#when-to-use-each-approach)
|
||||
|
||||
## Why Context in I/O Operations
|
||||
|
||||
In idiomatic Go, I/O operations conventionally take a `context.Context` as their first parameter:
|
||||
|
||||
```go
|
||||
func QueryDatabase(ctx context.Context, query string) (Result, error)
|
||||
func MakeHTTPRequest(ctx context.Context, url string) (*http.Response, error)
|
||||
func ReadFile(ctx context.Context, path string) ([]byte, error)
|
||||
```
|
||||
|
||||
### The Purpose of Context
|
||||
|
||||
The `context.Context` parameter serves several critical purposes:
|
||||
|
||||
1. **Cancellation Propagation**: Operations can be cancelled when the context is cancelled
|
||||
2. **Deadline Management**: Operations respect timeouts and deadlines
|
||||
3. **Request-Scoped Values**: Carry request metadata (trace IDs, user info, etc.)
|
||||
4. **Resource Cleanup**: Signal to release resources when work is no longer needed
|
||||
|
||||
### Why Context Matters for I/O
|
||||
|
||||
I/O operations are inherently **effectful** - they interact with the outside world:
|
||||
- Reading from disk, network, or database
|
||||
- Writing to external systems
|
||||
- Generating random numbers
|
||||
- Reading the current time
|
||||
|
||||
These operations can:
|
||||
- **Take time**: Network calls may be slow
|
||||
- **Fail**: Connections drop, files don't exist
|
||||
- **Block**: Waiting for external resources
|
||||
- **Need cancellation**: User navigates away, request times out
|
||||
|
||||
Context provides a standard mechanism to control these operations across your entire application.
|
||||
|
||||
## The Error-Value Tuple Pattern
|
||||
|
||||
### Why Operations Must Return Errors
|
||||
|
||||
In Go, I/O operations return `(value, error)` tuples because:
|
||||
|
||||
1. **Context can be cancelled**: Even if the operation would succeed, cancellation must be represented
|
||||
2. **External systems fail**: Networks fail, files are missing, permissions are denied
|
||||
3. **Resources are exhausted**: Out of memory, disk full, connection pool exhausted
|
||||
4. **Timeouts occur**: Operations exceed their deadline
|
||||
|
||||
**There cannot be I/O operations without error handling** because the context itself introduces a failure mode (cancellation) that must be represented in the return type.
|
||||
|
||||
### Traditional Go Pattern
|
||||
|
||||
```go
|
||||
func ProcessUser(ctx context.Context, userID int) (User, error) {
|
||||
// Check context before starting
|
||||
if err := ctx.Err(); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Fetch user from database
|
||||
user, err := db.QueryUser(ctx, userID)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("query user: %w", err)
|
||||
}
|
||||
|
||||
// Validate user
|
||||
if user.Age < 18 {
|
||||
return User{}, errors.New("user too young")
|
||||
}
|
||||
|
||||
// Fetch user's posts
|
||||
posts, err := db.QueryPosts(ctx, user.ID)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("query posts: %w", err)
|
||||
}
|
||||
|
||||
user.Posts = posts
|
||||
return user, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- Explicit error checking at each step
|
||||
- Manual error wrapping and propagation
|
||||
- Context checked manually
|
||||
- Imperative control flow
|
||||
- Error handling mixed with business logic
|
||||
|
||||
## Functional Approach: Reader Pattern
|
||||
|
||||
### The Core Insight
|
||||
|
||||
In functional programming, we separate **what to compute** from **how to execute it**. Instead of functions that perform I/O directly, we create functions that **return descriptions of I/O operations**.
|
||||
|
||||
### Key Type: ReaderIOResult
|
||||
|
||||
```go
|
||||
// A function that takes a context and returns a value or error
|
||||
type ReaderIOResult[A any] = func(context.Context) (A, error)
|
||||
```
|
||||
|
||||
This type represents:
|
||||
- **Reader**: Depends on an environment (context.Context)
|
||||
- **IO**: Performs side effects (I/O operations)
|
||||
- **Result**: Can fail with an error
|
||||
|
||||
### Why This Is Better
|
||||
|
||||
The functional approach **carries the I/O aspect as the return value, not on the input**:
|
||||
|
||||
```go
|
||||
// Traditional: I/O is implicit in the function execution
|
||||
func fetchUser(ctx context.Context, id int) (User, error) {
|
||||
// Performs I/O immediately
|
||||
}
|
||||
|
||||
// Functional: I/O is explicit in the return type
|
||||
func fetchUser(id int) ReaderIOResult[User] {
|
||||
// Returns a description of I/O, doesn't execute yet
|
||||
return func(ctx context.Context) (User, error) {
|
||||
// I/O happens here when the function is called
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key difference**: The functional version is a **curried function** where:
|
||||
1. Business parameters come first: `fetchUser(id)`
|
||||
2. Context comes last: `fetchUser(id)(ctx)`
|
||||
3. The intermediate result is composable: `ReaderIOResult[User]`
|
||||
|
||||
## Benefits of the Functional Approach
|
||||
|
||||
### 1. Separation of Pure and Impure Code
|
||||
|
||||
```go
|
||||
// Pure computation - no I/O, no context needed
|
||||
func validateAge(user User) (User, error) {
|
||||
if user.Age < 18 {
|
||||
return User{}, errors.New("user too young")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Impure I/O operation - needs context
|
||||
func fetchUser(id int) ReaderIOResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
return db.QueryUser(ctx, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Compose them - pure logic lifted into ReaderIOResult
|
||||
pipeline := F.Pipe2(
|
||||
fetchUser(42), // ReaderIOResult[User]
|
||||
readerioresult.ChainEitherK(validateAge), // Lift pure function
|
||||
)
|
||||
|
||||
// Execute when ready
|
||||
user, err := pipeline(ctx)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Pure functions are easier to test (no mocking needed)
|
||||
- Pure functions are easier to reason about (no side effects)
|
||||
- Clear boundary between logic and I/O
|
||||
- Can test business logic independently
|
||||
|
||||
### 2. Composability
|
||||
|
||||
Functions compose naturally without manual error checking:
|
||||
|
||||
```go
|
||||
// Traditional approach - manual error handling
|
||||
func ProcessUserTraditional(ctx context.Context, userID int) (UserWithPosts, error) {
|
||||
user, err := fetchUser(ctx, userID)
|
||||
if err != nil {
|
||||
return UserWithPosts{}, err
|
||||
}
|
||||
|
||||
validated, err := validateUser(user)
|
||||
if err != nil {
|
||||
return UserWithPosts{}, err
|
||||
}
|
||||
|
||||
posts, err := fetchPosts(ctx, validated.ID)
|
||||
if err != nil {
|
||||
return UserWithPosts{}, err
|
||||
}
|
||||
|
||||
return enrichUser(validated, posts), nil
|
||||
}
|
||||
|
||||
// Functional approach - automatic error propagation
|
||||
func ProcessUserFunctional(userID int) ReaderIOResult[UserWithPosts] {
|
||||
return F.Pipe3(
|
||||
fetchUser(userID),
|
||||
readerioresult.ChainEitherK(validateUser),
|
||||
readerioresult.Chain(func(user User) ReaderIOResult[UserWithPosts] {
|
||||
return F.Pipe2(
|
||||
fetchPosts(user.ID),
|
||||
readerioresult.Map(func(posts []Post) UserWithPosts {
|
||||
return enrichUser(user, posts)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- No manual error checking
|
||||
- Automatic short-circuiting on first error
|
||||
- Clear data flow
|
||||
- Easier to refactor and extend
|
||||
|
||||
### 3. Testability
|
||||
|
||||
```go
|
||||
// Mock I/O operations by providing test implementations
|
||||
func TestProcessUser(t *testing.T) {
|
||||
// Create a mock that returns test data
|
||||
mockFetchUser := func(id int) ReaderIOResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
return User{ID: id, Age: 25}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Test with mock - no database needed
|
||||
result, err := mockFetchUser(42)(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 25, result.Age)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Lazy Evaluation
|
||||
|
||||
Operations are not executed until you provide the context:
|
||||
|
||||
```go
|
||||
// Build the pipeline - no I/O happens yet
|
||||
pipeline := F.Pipe3(
|
||||
fetchUser(42),
|
||||
readerioresult.Map(enrichUser),
|
||||
readerioresult.Chain(saveUser),
|
||||
)
|
||||
|
||||
// I/O only happens when we call it with a context
|
||||
user, err := pipeline(ctx)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Build complex operations as pure data structures
|
||||
- Defer execution until needed
|
||||
- Reuse pipelines with different contexts
|
||||
- Test pipelines without executing I/O
|
||||
|
||||
### 5. Context Propagation
|
||||
|
||||
Context is automatically threaded through all operations:
|
||||
|
||||
```go
|
||||
// Traditional - must pass context explicitly everywhere
|
||||
func Process(ctx context.Context) error {
|
||||
user, err := fetchUser(ctx, 42)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
posts, err := fetchPosts(ctx, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return savePosts(ctx, posts)
|
||||
}
|
||||
|
||||
// Functional - context provided once at execution
|
||||
func Process() ReaderIOResult[any] {
|
||||
return F.Pipe2(
|
||||
fetchUser(42),
|
||||
readerioresult.Chain(func(user User) ReaderIOResult[any] {
|
||||
return F.Pipe2(
|
||||
fetchPosts(user.ID),
|
||||
readerioresult.Chain(savePosts),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Context provided once
|
||||
err := readerioresult.Fold(
|
||||
func(err error) error { return err },
|
||||
func(any) error { return nil },
|
||||
)(Process())(ctx)
|
||||
```
|
||||
|
||||
## Side-by-Side Comparison
|
||||
|
||||
### Example: User Service with Database Operations
|
||||
|
||||
#### Traditional Go Style
|
||||
|
||||
```go
|
||||
package traditional
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Fetch user from database
|
||||
func (s *UserService) GetUser(ctx context.Context, id int) (User, error) {
|
||||
var user User
|
||||
|
||||
// Check context
|
||||
if err := ctx.Err(); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Query database
|
||||
row := s.db.QueryRowContext(ctx,
|
||||
"SELECT id, name, email, age FROM users WHERE id = ?", id)
|
||||
|
||||
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("scan user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Validate user
|
||||
func (s *UserService) ValidateUser(ctx context.Context, user User) (User, error) {
|
||||
if user.Age < 18 {
|
||||
return User{}, fmt.Errorf("user %d is too young", user.ID)
|
||||
}
|
||||
if user.Email == "" {
|
||||
return User{}, fmt.Errorf("user %d has no email", user.ID)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Update user email
|
||||
func (s *UserService) UpdateEmail(ctx context.Context, id int, email string) (User, error) {
|
||||
// Check context
|
||||
if err := ctx.Err(); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
// Update database
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
"UPDATE users SET email = ? WHERE id = ?", email, id)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("update email: %w", err)
|
||||
}
|
||||
|
||||
// Fetch updated user
|
||||
return s.GetUser(ctx, id)
|
||||
}
|
||||
|
||||
// Process user: fetch, validate, update email
|
||||
func (s *UserService) ProcessUser(ctx context.Context, id int, newEmail string) (User, error) {
|
||||
// Fetch user
|
||||
user, err := s.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("get user: %w", err)
|
||||
}
|
||||
|
||||
// Validate user
|
||||
validated, err := s.ValidateUser(ctx, user)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("validate user: %w", err)
|
||||
}
|
||||
|
||||
// Update email
|
||||
updated, err := s.UpdateEmail(ctx, validated.ID, newEmail)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("update email: %w", err)
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- ✗ Manual error checking at every step
|
||||
- ✗ Context passed explicitly to every function
|
||||
- ✗ Error wrapping is manual and verbose
|
||||
- ✗ Business logic mixed with error handling
|
||||
- ✗ Hard to test without database
|
||||
- ✗ Difficult to compose operations
|
||||
- ✓ Familiar to Go developers
|
||||
- ✓ Explicit control flow
|
||||
|
||||
#### Functional Go Style (context/readerioresult)
|
||||
|
||||
```go
|
||||
package functional
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Fetch user from database - returns a ReaderIOResult
|
||||
func (s *UserService) GetUser(id int) RIO.ReaderIOResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
var user User
|
||||
row := s.db.QueryRowContext(ctx,
|
||||
"SELECT id, name, email, age FROM users WHERE id = ?", id)
|
||||
|
||||
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("scan user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Validate user - pure function (no I/O, no context)
|
||||
func ValidateUser(user User) (User, error) {
|
||||
if user.Age < 18 {
|
||||
return User{}, fmt.Errorf("user %d is too young", user.ID)
|
||||
}
|
||||
if user.Email == "" {
|
||||
return User{}, fmt.Errorf("user %d has no email", user.ID)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Update user email - returns a ReaderIOResult
|
||||
func (s *UserService) UpdateEmail(id int, email string) RIO.ReaderIOResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
"UPDATE users SET email = ? WHERE id = ?", email, id)
|
||||
if err != nil {
|
||||
return User{}, fmt.Errorf("update email: %w", err)
|
||||
}
|
||||
|
||||
// Chain to GetUser
|
||||
return s.GetUser(id)(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Process user: fetch, validate, update email - composable pipeline
|
||||
func (s *UserService) ProcessUser(id int, newEmail string) RIO.ReaderIOResult[User] {
|
||||
return F.Pipe3(
|
||||
s.GetUser(id), // Fetch user
|
||||
RIO.ChainEitherK(ValidateUser), // Validate (pure function)
|
||||
RIO.Chain(func(user User) RIO.ReaderIOResult[User] {
|
||||
return s.UpdateEmail(user.ID, newEmail) // Update email
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Alternative: Using Do-notation for more complex flows
|
||||
func (s *UserService) ProcessUserDo(id int, newEmail string) RIO.ReaderIOResult[User] {
|
||||
return RIO.Chain(func(user User) RIO.ReaderIOResult[User] {
|
||||
// Validate is pure, lift it into ReaderIOResult
|
||||
validated, err := ValidateUser(user)
|
||||
if err != nil {
|
||||
return RIO.Left[User](err)
|
||||
}
|
||||
// Update with validated user
|
||||
return s.UpdateEmail(validated.ID, newEmail)
|
||||
})(s.GetUser(id))
|
||||
}
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- ✓ Automatic error propagation (no manual checking)
|
||||
- ✓ Context threaded automatically
|
||||
- ✓ Pure functions separated from I/O
|
||||
- ✓ Business logic clear and composable
|
||||
- ✓ Easy to test (mock ReaderIOResult)
|
||||
- ✓ Operations compose naturally
|
||||
- ✓ Lazy evaluation (build pipeline, execute later)
|
||||
- ✗ Requires understanding of functional patterns
|
||||
- ✗ Less familiar to traditional Go developers
|
||||
|
||||
#### Idiomatic Functional Style (idiomatic/context/readerresult)
|
||||
|
||||
For even better performance with the same functional benefits:
|
||||
|
||||
```go
|
||||
package idiomatic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RR "github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// ReaderResult is just: func(context.Context) (A, error)
|
||||
// Same as ReaderIOResult but using native Go tuples
|
||||
|
||||
func (s *UserService) GetUser(id int) RR.ReaderResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
var user User
|
||||
row := s.db.QueryRowContext(ctx,
|
||||
"SELECT id, name, email, age FROM users WHERE id = ?", id)
|
||||
|
||||
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
|
||||
return user, err // Native tuple return
|
||||
}
|
||||
}
|
||||
|
||||
// Pure validation - returns native (User, error) tuple
|
||||
func ValidateUser(user User) (User, error) {
|
||||
if user.Age < 18 {
|
||||
return User{}, fmt.Errorf("user %d is too young", user.ID)
|
||||
}
|
||||
if user.Email == "" {
|
||||
return User{}, fmt.Errorf("user %d has no email", user.ID)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateEmail(id int, email string) RR.ReaderResult[User] {
|
||||
return func(ctx context.Context) (User, error) {
|
||||
_, err := s.db.ExecContext(ctx,
|
||||
"UPDATE users SET email = ? WHERE id = ?", email, id)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
return s.GetUser(id)(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Composable pipeline with native tuples
|
||||
func (s *UserService) ProcessUser(id int, newEmail string) RR.ReaderResult[User] {
|
||||
return F.Pipe3(
|
||||
s.GetUser(id),
|
||||
RR.ChainEitherK(ValidateUser), // Lift pure function
|
||||
RR.Chain(func(user User) RR.ReaderResult[User] {
|
||||
return s.UpdateEmail(user.ID, newEmail)
|
||||
}),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Characteristics:**
|
||||
- ✓ All benefits of functional approach
|
||||
- ✓ **2-10x better performance** (native tuples)
|
||||
- ✓ **Zero allocations** for many operations
|
||||
- ✓ More familiar to Go developers (uses (value, error))
|
||||
- ✓ Seamless integration with existing Go code
|
||||
- ✓ Same composability as ReaderIOResult
|
||||
|
||||
### Usage Comparison
|
||||
|
||||
```go
|
||||
// Traditional
|
||||
func HandleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
service := &UserService{db: db}
|
||||
|
||||
user, err := service.ProcessUser(ctx, 42, "new@email.com")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
// Functional (both styles)
|
||||
func HandleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
service := &UserService{db: db}
|
||||
|
||||
// Build the pipeline (no execution yet)
|
||||
pipeline := service.ProcessUser(42, "new@email.com")
|
||||
|
||||
// Execute with context
|
||||
user, err := pipeline(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
|
||||
// Or using Fold for cleaner error handling
|
||||
func HandleRequestFold(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
service := &UserService{db: db}
|
||||
|
||||
RR.Fold(
|
||||
func(err error) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
},
|
||||
func(user User) {
|
||||
json.NewEncoder(w).Encode(user)
|
||||
},
|
||||
)(service.ProcessUser(42, "new@email.com"))(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Resource Management with Bracket
|
||||
|
||||
```go
|
||||
// Traditional
|
||||
func ProcessFile(ctx context.Context, path string) (string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// Functional - guaranteed cleanup even on panic
|
||||
func ProcessFile(path string) RIO.ReaderIOResult[string] {
|
||||
return RIO.Bracket(
|
||||
// Acquire resource
|
||||
func(ctx context.Context) (*os.File, error) {
|
||||
return os.Open(path)
|
||||
},
|
||||
// Release resource (always called)
|
||||
func(file *os.File, err error) RIO.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) (any, error) {
|
||||
return nil, file.Close()
|
||||
}
|
||||
},
|
||||
// Use resource
|
||||
func(file *os.File) RIO.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) (string, error) {
|
||||
data, err := io.ReadAll(file)
|
||||
return string(data), err
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```go
|
||||
// Traditional - manual goroutines and sync
|
||||
func FetchMultipleUsers(ctx context.Context, ids []int) ([]User, error) {
|
||||
var wg sync.WaitGroup
|
||||
users := make([]User, len(ids))
|
||||
errs := make([]error, len(ids))
|
||||
|
||||
for i, id := range ids {
|
||||
wg.Add(1)
|
||||
go func(i, id int) {
|
||||
defer wg.Done()
|
||||
users[i], errs[i] = fetchUser(ctx, id)
|
||||
}(i, id)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// Functional - automatic parallelization
|
||||
func FetchMultipleUsers(ids []int) RIO.ReaderIOResult[[]User] {
|
||||
operations := A.Map(func(id int) RIO.ReaderIOResult[User] {
|
||||
return fetchUser(id)
|
||||
})(ids)
|
||||
|
||||
return RIO.TraverseArrayPar(F.Identity[RIO.ReaderIOResult[User]])(operations)
|
||||
}
|
||||
```
|
||||
|
||||
### Retry Logic
|
||||
|
||||
```go
|
||||
// Traditional
|
||||
func FetchWithRetry(ctx context.Context, url string, maxRetries int) ([]byte, error) {
|
||||
var lastErr error
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
time.Sleep(time.Second * time.Duration(i+1))
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// Functional
|
||||
func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
|
||||
operation := func(ctx context.Context) ([]byte, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
return RIO.Retry(
|
||||
maxRetries,
|
||||
func(attempt int) time.Duration {
|
||||
return time.Second * time.Duration(attempt)
|
||||
},
|
||||
)(operation)
|
||||
}
|
||||
```
|
||||
|
||||
## When to Use Each Approach
|
||||
|
||||
### Use Traditional Go Style When:
|
||||
|
||||
1. **Team familiarity**: Team is not familiar with functional programming
|
||||
2. **Simple operations**: Single I/O operation with straightforward error handling
|
||||
3. **Existing codebase**: Large codebase already using traditional patterns
|
||||
4. **Learning curve**: Want to minimize onboarding time
|
||||
5. **Explicit control**: Need very explicit control flow
|
||||
|
||||
### Use Functional Style (ReaderIOResult) When:
|
||||
|
||||
1. **Complex pipelines**: Multiple I/O operations that need composition
|
||||
2. **Testability**: Need to test business logic separately from I/O
|
||||
3. **Reusability**: Want to build reusable operation pipelines
|
||||
4. **Error handling**: Want automatic error propagation
|
||||
5. **Resource management**: Need guaranteed cleanup (Bracket)
|
||||
6. **Parallel execution**: Need to parallelize operations easily
|
||||
7. **Type safety**: Want the type system to track I/O effects
|
||||
|
||||
### Use Idiomatic Functional Style (idiomatic/context/readerresult) When:
|
||||
|
||||
1. **All functional benefits**: Want functional patterns with Go idioms
|
||||
2. **Performance critical**: Need 2-10x better performance
|
||||
3. **Zero allocations**: Memory efficiency is important
|
||||
4. **Go integration**: Want seamless integration with existing Go code
|
||||
5. **Production services**: Building high-throughput services
|
||||
6. **Best of both worlds**: Want functional composition with Go's native patterns
|
||||
|
||||
## Summary
|
||||
|
||||
The functional approach to I/O in Go offers significant advantages:
|
||||
|
||||
1. **Separation of Concerns**: Pure logic separated from I/O effects
|
||||
2. **Composability**: Operations compose naturally without manual error checking
|
||||
3. **Testability**: Easy to test without mocking I/O
|
||||
4. **Type Safety**: I/O effects visible in the type system
|
||||
5. **Lazy Evaluation**: Build pipelines, execute when ready
|
||||
6. **Context Propagation**: Automatic threading of context
|
||||
7. **Performance**: Idiomatic version offers 2-10x speedup
|
||||
|
||||
The key insight is that **I/O operations return descriptions of effects** (ReaderIOResult) rather than performing effects immediately. This enables powerful composition patterns while maintaining Go's idiomatic error handling through the `(value, error)` tuple pattern.
|
||||
|
||||
For production Go services, the **idiomatic/context/readerresult** package provides the best balance: full functional programming capabilities with native Go performance and familiar error handling patterns.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [DESIGN.md](./DESIGN.md) - Design principles and patterns
|
||||
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison
|
||||
- [idiomatic/doc.go](./idiomatic/doc.go) - Idiomatic package overview
|
||||
- [context/readerioresult](./context/readerioresult/) - ReaderIOResult package
|
||||
- [idiomatic/context/readerresult](./idiomatic/context/readerresult/) - Idiomatic ReaderResult package
|
||||
@@ -446,6 +446,9 @@ func process() IOResult[string] {
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **[Design Decisions](./DESIGN.md)** - Key design principles and patterns explained
|
||||
- **[Functional I/O in Go](./FUNCTIONAL_IO.md)** - Understanding Context, errors, and the Reader pattern for I/O operations
|
||||
- **[Idiomatic vs Standard Packages](./IDIOMATIC_COMPARISON.md)** - Performance comparison and when to use each approach
|
||||
- **[API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)** - Complete API reference
|
||||
- **[Code Samples](./samples/)** - Practical examples and use cases
|
||||
- **[Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)** - Information about generic type aliases
|
||||
@@ -457,12 +460,15 @@ func process() IOResult[string] {
|
||||
- **Either** - Type-safe error handling with left/right values
|
||||
- **Result** - Simplified Either with error as left type (recommended for error handling)
|
||||
- **IO** - Lazy evaluation and side effect management
|
||||
- **IOOption** - Combine IO with Option for optional values with side effects
|
||||
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
|
||||
- **Reader** - Dependency injection pattern
|
||||
- **ReaderOption** - Combine Reader with Option for optional values with dependency injection
|
||||
- **ReaderIOOption** - Combine Reader, IO, and Option for optional values with dependency injection and side effects
|
||||
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
|
||||
- **Array** - Functional array operations
|
||||
- **Record** - Functional record/map operations
|
||||
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
|
||||
- **[Optics](./optics/README.md)** - Lens, Prism, Optional, and Traversal for immutable updates
|
||||
|
||||
#### Idiomatic Packages (Tuple-based, High Performance)
|
||||
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// From constructs an array from a set of variadic arguments
|
||||
@@ -163,11 +163,11 @@ func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
|
||||
return G.FilterMapWithIndex[[]A, []B](f)
|
||||
}
|
||||
|
||||
// FilterChain maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
|
||||
// ChainOptionK maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
|
||||
//
|
||||
//go:inline
|
||||
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
|
||||
return G.FilterChain[[]A](f)
|
||||
func ChainOptionK[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
|
||||
return G.ChainOptionK[[]A](f)
|
||||
}
|
||||
|
||||
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
|
||||
@@ -190,6 +190,11 @@ func MonadReduce[A, B any](fa []A, f func(B, A) B, initial B) B {
|
||||
return G.MonadReduce(fa, f, initial)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadReduceWithIndex[A, B any](fa []A, f func(int, B, A) B, initial B) B {
|
||||
return G.MonadReduceWithIndex(fa, f, initial)
|
||||
}
|
||||
|
||||
// Reduce folds an array from left to right, applying a function to accumulate a result.
|
||||
//
|
||||
// Example:
|
||||
@@ -234,6 +239,16 @@ func ReduceRef[A, B any](f func(B, *A) B, initial B) func([]A) B {
|
||||
}
|
||||
|
||||
// Append adds an element to the end of an array, returning a new array.
|
||||
// This is a non-curried version that takes both the array and element as parameters.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := []int{1, 2, 3}
|
||||
// result := array.Append(arr, 4)
|
||||
// // result: []int{1, 2, 3, 4}
|
||||
// // arr: []int{1, 2, 3} (unchanged)
|
||||
//
|
||||
// For a curried version, see Push.
|
||||
//
|
||||
//go:inline
|
||||
func Append[A any](as []A, a A) []A {
|
||||
@@ -438,7 +453,7 @@ func Size[A any](as []A) int {
|
||||
// the second contains elements for which it returns true.
|
||||
//
|
||||
//go:inline
|
||||
func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
|
||||
func MonadPartition[A any](as []A, pred func(A) bool) pair.Pair[[]A, []A] {
|
||||
return G.MonadPartition(as, pred)
|
||||
}
|
||||
|
||||
@@ -446,7 +461,7 @@ func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
|
||||
// for which the predicate returns false, the right one those for which the predicate returns true
|
||||
//
|
||||
//go:inline
|
||||
func Partition[A any](pred func(A) bool) func([]A) tuple.Tuple2[[]A, []A] {
|
||||
func Partition[A any](pred func(A) bool) func([]A) pair.Pair[[]A, []A] {
|
||||
return G.Partition[[]A](pred)
|
||||
}
|
||||
|
||||
@@ -514,6 +529,83 @@ func Push[A any](a A) Operator[A, A] {
|
||||
return G.Push[Operator[A, A]](a)
|
||||
}
|
||||
|
||||
// Concat concatenates two arrays, appending the provided array to the end of the input array.
|
||||
// This is a curried function that takes an array to append and returns a function that
|
||||
// takes the base array and returns the concatenated result.
|
||||
//
|
||||
// The function creates a new array containing all elements from the base array followed
|
||||
// by all elements from the appended array. Neither input array is modified.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the arrays
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The array to append to the end of the base array
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a base array and returns a new array with `as` appended to its end
|
||||
//
|
||||
// Behavior:
|
||||
// - Creates a new array with length equal to the sum of both input arrays
|
||||
// - Copies all elements from the base array first
|
||||
// - Appends all elements from the `as` array at the end
|
||||
// - Returns the base array unchanged if `as` is empty
|
||||
// - Returns `as` unchanged if the base array is empty
|
||||
// - Does not modify either input array
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// base := []int{1, 2, 3}
|
||||
// toAppend := []int{4, 5, 6}
|
||||
// result := array.Concat(toAppend)(base)
|
||||
// // result: []int{1, 2, 3, 4, 5, 6}
|
||||
// // base: []int{1, 2, 3} (unchanged)
|
||||
// // toAppend: []int{4, 5, 6} (unchanged)
|
||||
//
|
||||
// Example with empty arrays:
|
||||
//
|
||||
// base := []int{1, 2, 3}
|
||||
// empty := []int{}
|
||||
// result := array.Concat(empty)(base)
|
||||
// // result: []int{1, 2, 3}
|
||||
//
|
||||
// Example with strings:
|
||||
//
|
||||
// words1 := []string{"hello", "world"}
|
||||
// words2 := []string{"foo", "bar"}
|
||||
// result := array.Concat(words2)(words1)
|
||||
// // result: []string{"hello", "world", "foo", "bar"}
|
||||
//
|
||||
// Example with functional composition:
|
||||
//
|
||||
// numbers := []int{1, 2, 3}
|
||||
// result := F.Pipe2(
|
||||
// numbers,
|
||||
// array.Map(N.Mul(2)),
|
||||
// array.Concat([]int{10, 20}),
|
||||
// )
|
||||
// // result: []int{2, 4, 6, 10, 20}
|
||||
//
|
||||
// Use cases:
|
||||
// - Combining multiple arrays into one
|
||||
// - Building arrays incrementally
|
||||
// - Implementing array-based data structures (queues, buffers)
|
||||
// - Merging results from multiple operations
|
||||
// - Creating array pipelines with functional composition
|
||||
//
|
||||
// Performance:
|
||||
// - Time complexity: O(n + m) where n and m are the lengths of the arrays
|
||||
// - Space complexity: O(n + m) for the new array
|
||||
// - Optimized to avoid allocation when one array is empty
|
||||
//
|
||||
// Note: This function is immutable - it creates a new array rather than modifying
|
||||
// the input arrays. For appending a single element, consider using Append or Push.
|
||||
//
|
||||
//go:inline
|
||||
func Concat[A any](as []A) Operator[A, A] {
|
||||
return F.Bind2nd(array.Concat[[]A, A], as)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to an array of functions, producing an array of results.
|
||||
// This is the monadic version that takes both parameters.
|
||||
//
|
||||
@@ -622,3 +714,128 @@ func Prepend[A any](head A) Operator[A, A] {
|
||||
func Reverse[A any](as []A) []A {
|
||||
return G.Reverse(as)
|
||||
}
|
||||
|
||||
// Extend applies a function to every suffix of an array, creating a new array of results.
|
||||
// This is the comonad extend operation for arrays.
|
||||
//
|
||||
// The function f is applied to progressively smaller suffixes of the input array:
|
||||
// - f(as[0:]) for the first element
|
||||
// - f(as[1:]) for the second element
|
||||
// - f(as[2:]) for the third element
|
||||
// - and so on...
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the input array
|
||||
// - B: The type of elements in the output array
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an array suffix and returns a value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms an array of A into an array of B
|
||||
//
|
||||
// Behavior:
|
||||
// - Creates a new array with the same length as the input
|
||||
// - For each position i, applies f to the suffix starting at i
|
||||
// - Returns an empty array if the input is empty
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Sum all elements from current position to end
|
||||
// sumSuffix := array.Extend(func(as []int) int {
|
||||
// return array.Reduce(func(acc, x int) int { return acc + x }, 0)(as)
|
||||
// })
|
||||
// result := sumSuffix([]int{1, 2, 3, 4})
|
||||
// // result: []int{10, 9, 7, 4}
|
||||
// // Explanation: [1+2+3+4, 2+3+4, 3+4, 4]
|
||||
//
|
||||
// Example with length:
|
||||
//
|
||||
// // Get remaining length at each position
|
||||
// lengths := array.Extend(array.Size[int])
|
||||
// result := lengths([]int{10, 20, 30})
|
||||
// // result: []int{3, 2, 1}
|
||||
//
|
||||
// Example with head:
|
||||
//
|
||||
// // Duplicate each element (extract head of each suffix)
|
||||
// duplicate := array.Extend(func(as []int) int {
|
||||
// return F.Pipe1(as, array.Head[int], O.GetOrElse(F.Constant(0)))
|
||||
// })
|
||||
// result := duplicate([]int{1, 2, 3})
|
||||
// // result: []int{1, 2, 3}
|
||||
//
|
||||
// Use cases:
|
||||
// - Computing cumulative or rolling operations
|
||||
// - Implementing sliding window algorithms
|
||||
// - Creating context-aware transformations
|
||||
// - Building comonadic computations
|
||||
//
|
||||
// Comonad laws:
|
||||
// - Left identity: Extend(Extract) == Identity
|
||||
// - Right identity: Extract ∘ Extend(f) == f
|
||||
// - Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))
|
||||
//
|
||||
//go:inline
|
||||
func Extend[A, B any](f func([]A) B) Operator[A, B] {
|
||||
return func(as []A) []B {
|
||||
return G.MakeBy[[]B](len(as), func(i int) B { return f(as[i:]) })
|
||||
}
|
||||
}
|
||||
|
||||
// Extract returns the first element of an array, or a zero value if empty.
|
||||
// This is the comonad extract operation for arrays.
|
||||
//
|
||||
// Extract is the dual of the monadic return/of operation. While Of wraps a value
|
||||
// in a context, Extract unwraps a value from its context.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the array
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input array
|
||||
//
|
||||
// Returns:
|
||||
// - The first element if the array is non-empty, otherwise the zero value of type A
|
||||
//
|
||||
// Behavior:
|
||||
// - Returns as[0] if the array has at least one element
|
||||
// - Returns the zero value of A if the array is empty
|
||||
// - Does not modify the input array
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := array.Extract([]int{1, 2, 3})
|
||||
// // result: 1
|
||||
//
|
||||
// Example with empty array:
|
||||
//
|
||||
// result := array.Extract([]int{})
|
||||
// // result: 0 (zero value for int)
|
||||
//
|
||||
// Example with strings:
|
||||
//
|
||||
// result := array.Extract([]string{"hello", "world"})
|
||||
// // result: "hello"
|
||||
//
|
||||
// Example with empty string array:
|
||||
//
|
||||
// result := array.Extract([]string{})
|
||||
// // result: "" (zero value for string)
|
||||
//
|
||||
// Use cases:
|
||||
// - Extracting the current focus from a comonadic context
|
||||
// - Getting the head element with a default zero value
|
||||
// - Implementing comonad-based computations
|
||||
//
|
||||
// Comonad laws:
|
||||
// - Extract ∘ Of == Identity (extracting from a singleton returns the value)
|
||||
// - Extract ∘ Extend(f) == f (extract after extend equals applying f)
|
||||
//
|
||||
// Note: For a safer alternative that handles empty arrays explicitly,
|
||||
// consider using Head which returns an Option[A].
|
||||
//
|
||||
//go:inline
|
||||
func Extract[A any](as []A) A {
|
||||
return G.Extract(as)
|
||||
}
|
||||
|
||||
@@ -16,308 +16,88 @@
|
||||
package array
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReplicate(t *testing.T) {
|
||||
result := Replicate(3, "a")
|
||||
assert.Equal(t, []string{"a", "a", "a"}, result)
|
||||
|
||||
empty := Replicate(0, 42)
|
||||
assert.Equal(t, []int{}, empty)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
result := MonadMap(src, N.Mul(2))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
func TestMonadMapRef(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
result := MonadMapRef(src, func(x *int) int { return *x * 2 })
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
func TestMapWithIndex(t *testing.T) {
|
||||
src := []string{"a", "b", "c"}
|
||||
mapper := MapWithIndex(func(i int, s string) string {
|
||||
return fmt.Sprintf("%d:%s", i, s)
|
||||
})
|
||||
result := mapper(src)
|
||||
assert.Equal(t, []string{"0:a", "1:b", "2:c"}, result)
|
||||
}
|
||||
|
||||
func TestMapRef(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
mapper := MapRef(func(x *int) int { return *x * 2 })
|
||||
result := mapper(src)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
func TestFilterWithIndex(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
filter := FilterWithIndex(func(i, x int) bool {
|
||||
return i%2 == 0 && x > 2
|
||||
})
|
||||
result := filter(src)
|
||||
assert.Equal(t, []int{3, 5}, result)
|
||||
}
|
||||
|
||||
func TestFilterRef(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
filter := FilterRef(func(x *int) bool { return *x > 2 })
|
||||
result := filter(src)
|
||||
assert.Equal(t, []int{3, 4, 5}, result)
|
||||
}
|
||||
|
||||
func TestMonadFilterMap(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4}
|
||||
result := MonadFilterMap(src, func(x int) O.Option[string] {
|
||||
if x%2 == 0 {
|
||||
return O.Some(fmt.Sprintf("even:%d", x))
|
||||
}
|
||||
return O.None[string]()
|
||||
})
|
||||
assert.Equal(t, []string{"even:2", "even:4"}, result)
|
||||
}
|
||||
|
||||
func TestMonadFilterMapWithIndex(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4}
|
||||
result := MonadFilterMapWithIndex(src, func(i, x int) O.Option[string] {
|
||||
if i%2 == 0 {
|
||||
return O.Some(fmt.Sprintf("%d:%d", i, x))
|
||||
}
|
||||
return O.None[string]()
|
||||
})
|
||||
assert.Equal(t, []string{"0:1", "2:3"}, result)
|
||||
}
|
||||
|
||||
func TestFilterMapWithIndex(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4}
|
||||
filter := FilterMapWithIndex(func(i, x int) O.Option[string] {
|
||||
if i%2 == 0 {
|
||||
return O.Some(fmt.Sprintf("%d:%d", i, x))
|
||||
}
|
||||
return O.None[string]()
|
||||
})
|
||||
result := filter(src)
|
||||
assert.Equal(t, []string{"0:1", "2:3"}, result)
|
||||
}
|
||||
|
||||
func TestFilterMapRef(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
filter := FilterMapRef(
|
||||
func(x *int) bool { return *x > 2 },
|
||||
func(x *int) string { return fmt.Sprintf("val:%d", *x) },
|
||||
)
|
||||
result := filter(src)
|
||||
assert.Equal(t, []string{"val:3", "val:4", "val:5"}, result)
|
||||
}
|
||||
|
||||
func TestReduceWithIndex(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
reducer := ReduceWithIndex(func(i, acc, x int) int {
|
||||
return acc + i + x
|
||||
// TestMonadReduceWithIndex tests the MonadReduceWithIndex function
|
||||
func TestMonadReduceWithIndex(t *testing.T) {
|
||||
// Test with integers - sum with index multiplication
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
result := MonadReduceWithIndex(numbers, func(idx, acc, val int) int {
|
||||
return acc + (val * idx)
|
||||
}, 0)
|
||||
result := reducer(src)
|
||||
assert.Equal(t, 9, result) // 0 + (0+1) + (1+2) + (2+3) = 9
|
||||
}
|
||||
// Expected: 0*1 + 1*2 + 2*3 + 3*4 + 4*5 = 0 + 2 + 6 + 12 + 20 = 40
|
||||
assert.Equal(t, 40, result)
|
||||
|
||||
func TestReduceRightWithIndex(t *testing.T) {
|
||||
src := []string{"a", "b", "c"}
|
||||
reducer := ReduceRightWithIndex(func(i int, x, acc string) string {
|
||||
return fmt.Sprintf("%s%d:%s", acc, i, x)
|
||||
// Test with empty array
|
||||
empty := []int{}
|
||||
result2 := MonadReduceWithIndex(empty, func(idx, acc, val int) int {
|
||||
return acc + val
|
||||
}, 10)
|
||||
assert.Equal(t, 10, result2)
|
||||
|
||||
// Test with strings - concatenate with index
|
||||
words := []string{"a", "b", "c"}
|
||||
result3 := MonadReduceWithIndex(words, func(idx int, acc, val string) string {
|
||||
return acc + val + string(rune('0'+idx))
|
||||
}, "")
|
||||
result := reducer(src)
|
||||
assert.Equal(t, "2:c1:b0:a", result)
|
||||
assert.Equal(t, "a0b1c2", result3)
|
||||
}
|
||||
|
||||
func TestReduceRef(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
reducer := ReduceRef(func(acc int, x *int) int {
|
||||
return acc + *x
|
||||
}, 0)
|
||||
result := reducer(src)
|
||||
assert.Equal(t, 6, result)
|
||||
}
|
||||
|
||||
func TestZero(t *testing.T) {
|
||||
result := Zero[int]()
|
||||
assert.Equal(t, []int{}, result)
|
||||
assert.True(t, IsEmpty(result))
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
result := MonadChain(src, func(x int) []int {
|
||||
return []int{x, x * 10}
|
||||
})
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
chain := Chain(func(x int) []int {
|
||||
return []int{x, x * 10}
|
||||
})
|
||||
result := chain(src)
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fns := []func(int) int{
|
||||
N.Mul(2),
|
||||
N.Add(10),
|
||||
}
|
||||
values := []int{1, 2}
|
||||
result := MonadAp(fns, values)
|
||||
assert.Equal(t, []int{2, 4, 11, 12}, result)
|
||||
}
|
||||
|
||||
func TestMatchLeft(t *testing.T) {
|
||||
matcher := MatchLeft(
|
||||
func() string { return "empty" },
|
||||
func(head int, tail []int) string {
|
||||
return fmt.Sprintf("head:%d,tail:%v", head, tail)
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, "empty", matcher([]int{}))
|
||||
assert.Equal(t, "head:1,tail:[2 3]", matcher([]int{1, 2, 3}))
|
||||
}
|
||||
|
||||
func TestTail(t *testing.T) {
|
||||
assert.Equal(t, O.None[[]int](), Tail([]int{}))
|
||||
assert.Equal(t, O.Some([]int{2, 3}), Tail([]int{1, 2, 3}))
|
||||
assert.Equal(t, O.Some([]int{}), Tail([]int{1}))
|
||||
}
|
||||
|
||||
func TestFirst(t *testing.T) {
|
||||
assert.Equal(t, O.None[int](), First([]int{}))
|
||||
assert.Equal(t, O.Some(1), First([]int{1, 2, 3}))
|
||||
}
|
||||
|
||||
func TestLast(t *testing.T) {
|
||||
assert.Equal(t, O.None[int](), Last([]int{}))
|
||||
assert.Equal(t, O.Some(3), Last([]int{1, 2, 3}))
|
||||
assert.Equal(t, O.Some(1), Last([]int{1}))
|
||||
}
|
||||
|
||||
func TestUpsertAt(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
upsert := UpsertAt(99)
|
||||
|
||||
result1 := upsert(src)
|
||||
assert.Equal(t, []int{1, 2, 3, 99}, result1)
|
||||
}
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
assert.Equal(t, 0, Size([]int{}))
|
||||
assert.Equal(t, 3, Size([]int{1, 2, 3}))
|
||||
}
|
||||
|
||||
func TestMonadPartition(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
result := MonadPartition(src, func(x int) bool { return x > 2 })
|
||||
assert.Equal(t, []int{1, 2}, result.F1)
|
||||
assert.Equal(t, []int{3, 4, 5}, result.F2)
|
||||
}
|
||||
|
||||
func TestIsNil(t *testing.T) {
|
||||
var nilSlice []int
|
||||
assert.True(t, IsNil(nilSlice))
|
||||
assert.False(t, IsNil([]int{}))
|
||||
assert.False(t, IsNil([]int{1}))
|
||||
}
|
||||
|
||||
func TestIsNonNil(t *testing.T) {
|
||||
var nilSlice []int
|
||||
assert.False(t, IsNonNil(nilSlice))
|
||||
assert.True(t, IsNonNil([]int{}))
|
||||
assert.True(t, IsNonNil([]int{1}))
|
||||
}
|
||||
|
||||
func TestConstNil(t *testing.T) {
|
||||
result := ConstNil[int]()
|
||||
assert.True(t, IsNil(result))
|
||||
}
|
||||
|
||||
func TestSliceRight(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
slicer := SliceRight[int](2)
|
||||
result := slicer(src)
|
||||
assert.Equal(t, []int{3, 4, 5}, result)
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
copied := Copy(src)
|
||||
assert.Equal(t, src, copied)
|
||||
// Verify it's a different slice
|
||||
copied[0] = 99
|
||||
assert.Equal(t, 1, src[0])
|
||||
assert.Equal(t, 99, copied[0])
|
||||
}
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
cloner := Clone(N.Mul(2))
|
||||
result := cloner(src)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
func TestFoldMapWithIndex(t *testing.T) {
|
||||
src := []string{"a", "b", "c"}
|
||||
folder := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
|
||||
return fmt.Sprintf("%d:%s", i, s)
|
||||
})
|
||||
result := folder(src)
|
||||
assert.Equal(t, "0:a1:b2:c", result)
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
folder := Fold(N.MonoidSum[int]())
|
||||
result := folder(src)
|
||||
assert.Equal(t, 15, result)
|
||||
}
|
||||
|
||||
func TestPush(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
pusher := Push(4)
|
||||
result := pusher(src)
|
||||
// TestAppend tests the Append function
|
||||
func TestAppend(t *testing.T) {
|
||||
// Test appending to non-empty array
|
||||
arr := []int{1, 2, 3}
|
||||
result := Append(arr, 4)
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
// Verify original array is unchanged
|
||||
assert.Equal(t, []int{1, 2, 3}, arr)
|
||||
|
||||
// Test appending to empty array
|
||||
empty := []int{}
|
||||
result2 := Append(empty, 1)
|
||||
assert.Equal(t, []int{1}, result2)
|
||||
|
||||
// Test appending strings
|
||||
words := []string{"hello", "world"}
|
||||
result3 := Append(words, "!")
|
||||
assert.Equal(t, []string{"hello", "world", "!"}, result3)
|
||||
|
||||
// Test appending to nil array
|
||||
var nilArr []int
|
||||
result4 := Append(nilArr, 42)
|
||||
assert.Equal(t, []int{42}, result4)
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fns := []func(int) string{
|
||||
func(x int) string { return fmt.Sprintf("a%d", x) },
|
||||
func(x int) string { return fmt.Sprintf("b%d", x) },
|
||||
}
|
||||
result := MonadFlap(fns, 5)
|
||||
assert.Equal(t, []string{"a5", "b5"}, result)
|
||||
}
|
||||
// TestStrictEquals tests the StrictEquals function
|
||||
func TestStrictEquals(t *testing.T) {
|
||||
eq := StrictEquals[int]()
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
fns := []func(int) string{
|
||||
func(x int) string { return fmt.Sprintf("a%d", x) },
|
||||
func(x int) string { return fmt.Sprintf("b%d", x) },
|
||||
}
|
||||
flapper := Flap[string](5)
|
||||
result := flapper(fns)
|
||||
assert.Equal(t, []string{"a5", "b5"}, result)
|
||||
}
|
||||
// Test equal arrays
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{1, 2, 3}
|
||||
assert.True(t, eq.Equals(arr1, arr2))
|
||||
|
||||
func TestPrepend(t *testing.T) {
|
||||
src := []int{2, 3, 4}
|
||||
prepender := Prepend(1)
|
||||
result := prepender(src)
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
// Test different arrays
|
||||
arr3 := []int{1, 2, 4}
|
||||
assert.False(t, eq.Equals(arr1, arr3))
|
||||
|
||||
// Test different lengths
|
||||
arr4 := []int{1, 2}
|
||||
assert.False(t, eq.Equals(arr1, arr4))
|
||||
|
||||
// Test empty arrays
|
||||
empty1 := []int{}
|
||||
empty2 := []int{}
|
||||
assert.True(t, eq.Equals(empty1, empty2))
|
||||
|
||||
// Test with strings
|
||||
strEq := StrictEquals[string]()
|
||||
words1 := []string{"hello", "world"}
|
||||
words2 := []string{"hello", "world"}
|
||||
words3 := []string{"hello", "there"}
|
||||
assert.True(t, strEq.Equals(words1, words2))
|
||||
assert.False(t, strEq.Equals(words1, words3))
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -163,11 +163,11 @@ func TestPartition(t *testing.T) {
|
||||
return n > 2
|
||||
}
|
||||
|
||||
assert.Equal(t, T.MakeTuple2(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
|
||||
assert.Equal(t, T.MakeTuple2(From(1), From(3)), Partition(pred)(From(1, 3)))
|
||||
assert.Equal(t, pair.MakePair(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
|
||||
assert.Equal(t, pair.MakePair(From(1), From(3)), Partition(pred)(From(1, 3)))
|
||||
}
|
||||
|
||||
func TestFilterChain(t *testing.T) {
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
src := From(1, 2, 3)
|
||||
|
||||
f := func(i int) O.Option[[]string] {
|
||||
@@ -177,7 +177,7 @@ func TestFilterChain(t *testing.T) {
|
||||
return O.None[[]string]()
|
||||
}
|
||||
|
||||
res := FilterChain(f)(src)
|
||||
res := ChainOptionK(f)(src)
|
||||
|
||||
assert.Equal(t, From("a1", "b1", "a3", "b3"), res)
|
||||
}
|
||||
@@ -474,3 +474,631 @@ func TestReverseProperties(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtract tests the Extract function
|
||||
func TestExtract(t *testing.T) {
|
||||
t.Run("Extract from non-empty array", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 1, result)
|
||||
})
|
||||
|
||||
t.Run("Extract from single element array", func(t *testing.T) {
|
||||
input := []string{"hello"}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("Extract from empty array returns zero value", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("Extract from empty string array returns empty string", func(t *testing.T) {
|
||||
input := []string{}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("Extract does not modify original array", func(t *testing.T) {
|
||||
original := []int{1, 2, 3}
|
||||
originalCopy := []int{1, 2, 3}
|
||||
_ = Extract(original)
|
||||
assert.Equal(t, originalCopy, original)
|
||||
})
|
||||
|
||||
t.Run("Extract with floats", func(t *testing.T) {
|
||||
input := []float64{3.14, 2.71, 1.41}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 3.14, result)
|
||||
})
|
||||
|
||||
t.Run("Extract with structs", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
input := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, Person{"Alice", 30}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtractComonadLaws tests comonad laws for Extract
|
||||
func TestExtractComonadLaws(t *testing.T) {
|
||||
t.Run("Extract ∘ Of == Identity", func(t *testing.T) {
|
||||
value := 42
|
||||
result := Extract(Of(value))
|
||||
assert.Equal(t, value, result)
|
||||
})
|
||||
|
||||
t.Run("Extract ∘ Extend(f) == f", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// Extract(Extend(f)(input)) should equal f(input)
|
||||
extended := Extend(f)(input)
|
||||
result := Extract(extended)
|
||||
expected := f(input)
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtend tests the Extend function
|
||||
func TestExtend(t *testing.T) {
|
||||
t.Run("Extend with sum of suffixes", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
sumSuffix := Extend(func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
})
|
||||
result := sumSuffix(input)
|
||||
expected := []int{10, 9, 7, 4} // [1+2+3+4, 2+3+4, 3+4, 4]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with length of suffixes", func(t *testing.T) {
|
||||
input := []int{10, 20, 30}
|
||||
lengths := Extend(Size[int])
|
||||
result := lengths(input)
|
||||
expected := []int{3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with head extraction", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
duplicate := Extend(func(as []int) int {
|
||||
return F.Pipe2(as, Head[int], O.GetOrElse(F.Constant(0)))
|
||||
})
|
||||
result := duplicate(input)
|
||||
expected := []int{1, 2, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with empty array", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := Extend(Size[int])(input)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with single element", func(t *testing.T) {
|
||||
input := []string{"hello"}
|
||||
result := Extend(func(as []string) int { return len(as) })(input)
|
||||
expected := []int{1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend does not modify original array", func(t *testing.T) {
|
||||
original := []int{1, 2, 3}
|
||||
originalCopy := []int{1, 2, 3}
|
||||
_ = Extend(Size[int])(original)
|
||||
assert.Equal(t, originalCopy, original)
|
||||
})
|
||||
|
||||
t.Run("Extend with string concatenation", func(t *testing.T) {
|
||||
input := []string{"a", "b", "c"}
|
||||
concat := Extend(func(as []string) string {
|
||||
return MonadReduce(as, func(acc, s string) string { return acc + s }, "")
|
||||
})
|
||||
result := concat(input)
|
||||
expected := []string{"abc", "bc", "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with max of suffixes", func(t *testing.T) {
|
||||
input := []int{3, 1, 4, 1, 5}
|
||||
maxSuffix := Extend(func(as []int) int {
|
||||
if len(as) == 0 {
|
||||
return 0
|
||||
}
|
||||
max := as[0]
|
||||
for _, v := range as[1:] {
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
return max
|
||||
})
|
||||
result := maxSuffix(input)
|
||||
expected := []int{5, 5, 5, 5, 5}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendComonadLaws tests comonad laws for Extend
|
||||
func TestExtendComonadLaws(t *testing.T) {
|
||||
t.Run("Left identity: Extend(Extract) == Identity", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := Extend(Extract[int])(input)
|
||||
assert.Equal(t, input, result)
|
||||
})
|
||||
|
||||
t.Run("Right identity: Extract ∘ Extend(f) == f", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// Extract(Extend(f)(input)) should equal f(input)
|
||||
result := F.Pipe2(input, Extend(f), Extract[int])
|
||||
expected := f(input)
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
// f: sum of array
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// g: length of array
|
||||
g := func(as []int) int {
|
||||
return len(as)
|
||||
}
|
||||
|
||||
// Left side: Extend(f) ∘ Extend(g)
|
||||
left := F.Pipe2(input, Extend(g), Extend(f))
|
||||
|
||||
// Right side: Extend(f ∘ Extend(g))
|
||||
right := Extend(func(as []int) int {
|
||||
return f(Extend(g)(as))
|
||||
})(input)
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendComposition tests Extend with other array operations
|
||||
func TestExtendComposition(t *testing.T) {
|
||||
t.Run("Extend after Map", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Map(N.Mul(2)),
|
||||
Extend(func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}),
|
||||
)
|
||||
expected := []int{12, 10, 6} // [2+4+6, 4+6, 6]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Map after Extend", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Extend(Size[int]),
|
||||
Map(N.Mul(10)),
|
||||
)
|
||||
expected := []int{30, 20, 10}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with Filter", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Filter(func(n int) bool { return n%2 == 0 }),
|
||||
Extend(Size[int]),
|
||||
)
|
||||
expected := []int{3, 2, 1} // lengths of [2,4,6], [4,6], [6]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendUseCases demonstrates practical use cases for Extend
|
||||
func TestExtendUseCases(t *testing.T) {
|
||||
t.Run("Running sum (cumulative sum from each position)", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
runningSum := Extend(func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
})
|
||||
result := runningSum(input)
|
||||
expected := []int{15, 14, 12, 9, 5}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Sliding window average", func(t *testing.T) {
|
||||
input := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
|
||||
windowAvg := Extend(func(as []float64) float64 {
|
||||
if len(as) == 0 {
|
||||
return 0
|
||||
}
|
||||
sum := MonadReduce(as, func(acc, x float64) float64 { return acc + x }, 0.0)
|
||||
return sum / float64(len(as))
|
||||
})
|
||||
result := windowAvg(input)
|
||||
expected := []float64{3.0, 3.5, 4.0, 4.5, 5.0}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Check if suffix is sorted", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 2, 1}
|
||||
isSorted := Extend(func(as []int) bool {
|
||||
for i := 1; i < len(as); i++ {
|
||||
if as[i] < as[i-1] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
result := isSorted(input)
|
||||
expected := []bool{false, false, false, false, true}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Count remaining elements", func(t *testing.T) {
|
||||
events := []string{"start", "middle", "end"}
|
||||
remaining := Extend(Size[string])
|
||||
result := remaining(events)
|
||||
expected := []int{3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcat tests the Concat function
|
||||
func TestConcat(t *testing.T) {
|
||||
t.Run("Concat two non-empty arrays", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
toAppend := []int{4, 5, 6}
|
||||
result := Concat(toAppend)(base)
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with empty array to append", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
empty := []int{}
|
||||
result := Concat(empty)(base)
|
||||
assert.Equal(t, base, result)
|
||||
})
|
||||
|
||||
t.Run("Concat to empty base array", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
toAppend := []int{1, 2, 3}
|
||||
result := Concat(toAppend)(empty)
|
||||
assert.Equal(t, toAppend, result)
|
||||
})
|
||||
|
||||
t.Run("Concat two empty arrays", func(t *testing.T) {
|
||||
empty1 := []int{}
|
||||
empty2 := []int{}
|
||||
result := Concat(empty2)(empty1)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("Concat strings", func(t *testing.T) {
|
||||
words1 := []string{"hello", "world"}
|
||||
words2 := []string{"foo", "bar"}
|
||||
result := Concat(words2)(words1)
|
||||
expected := []string{"hello", "world", "foo", "bar"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat single element arrays", func(t *testing.T) {
|
||||
arr1 := []int{1}
|
||||
arr2 := []int{2}
|
||||
result := Concat(arr2)(arr1)
|
||||
expected := []int{1, 2}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Does not modify original arrays", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
toAppend := []int{4, 5, 6}
|
||||
baseCopy := []int{1, 2, 3}
|
||||
toAppendCopy := []int{4, 5, 6}
|
||||
|
||||
_ = Concat(toAppend)(base)
|
||||
|
||||
assert.Equal(t, baseCopy, base)
|
||||
assert.Equal(t, toAppendCopy, toAppend)
|
||||
})
|
||||
|
||||
t.Run("Concat with floats", func(t *testing.T) {
|
||||
arr1 := []float64{1.1, 2.2}
|
||||
arr2 := []float64{3.3, 4.4}
|
||||
result := Concat(arr2)(arr1)
|
||||
expected := []float64{1.1, 2.2, 3.3, 4.4}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with structs", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
arr1 := []Person{{"Alice", 30}, {"Bob", 25}}
|
||||
arr2 := []Person{{"Charlie", 35}}
|
||||
result := Concat(arr2)(arr1)
|
||||
expected := []Person{{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat large arrays", func(t *testing.T) {
|
||||
arr1 := MakeBy(500, F.Identity[int])
|
||||
arr2 := MakeBy(500, func(i int) int { return i + 500 })
|
||||
result := Concat(arr2)(arr1)
|
||||
|
||||
assert.Equal(t, 1000, len(result))
|
||||
assert.Equal(t, 0, result[0])
|
||||
assert.Equal(t, 499, result[499])
|
||||
assert.Equal(t, 500, result[500])
|
||||
assert.Equal(t, 999, result[999])
|
||||
})
|
||||
|
||||
t.Run("Concat multiple times", func(t *testing.T) {
|
||||
arr1 := []int{1}
|
||||
arr2 := []int{2}
|
||||
arr3 := []int{3}
|
||||
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Concat(arr3),
|
||||
)
|
||||
|
||||
expected := []int{1, 2, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatComposition tests Concat with other array operations
|
||||
func TestConcatComposition(t *testing.T) {
|
||||
t.Run("Concat after Map", func(t *testing.T) {
|
||||
numbers := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
numbers,
|
||||
Map(N.Mul(2)),
|
||||
Concat([]int{10, 20}),
|
||||
)
|
||||
expected := []int{2, 4, 6, 10, 20}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Map after Concat", func(t *testing.T) {
|
||||
arr1 := []int{1, 2}
|
||||
arr2 := []int{3, 4}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
expected := []int{2, 4, 6, 8}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Filter", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3, 4}
|
||||
arr2 := []int{5, 6, 7, 8}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Filter(func(n int) bool { return n%2 == 0 }),
|
||||
)
|
||||
expected := []int{2, 4, 6, 8}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Reduce", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Reduce(func(acc, x int) int { return acc + x }, 0),
|
||||
)
|
||||
expected := 21 // 1+2+3+4+5+6
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Reverse", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Reverse[int],
|
||||
)
|
||||
expected := []int{6, 5, 4, 3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Concat with Flatten", func(t *testing.T) {
|
||||
arr1 := [][]int{{1, 2}, {3, 4}}
|
||||
arr2 := [][]int{{5, 6}}
|
||||
result := F.Pipe2(
|
||||
arr1,
|
||||
Concat(arr2),
|
||||
Flatten[int],
|
||||
)
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Multiple Concat operations", func(t *testing.T) {
|
||||
arr1 := []int{1}
|
||||
arr2 := []int{2}
|
||||
arr3 := []int{3}
|
||||
arr4 := []int{4}
|
||||
|
||||
result := Concat(arr4)(Concat(arr3)(Concat(arr2)(arr1)))
|
||||
|
||||
expected := []int{1, 2, 3, 4}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatUseCases demonstrates practical use cases for Concat
|
||||
func TestConcatUseCases(t *testing.T) {
|
||||
t.Run("Building array incrementally", func(t *testing.T) {
|
||||
header := []string{"Name", "Age"}
|
||||
data := []string{"Alice", "30"}
|
||||
footer := []string{"Total: 1"}
|
||||
|
||||
result := F.Pipe2(
|
||||
header,
|
||||
Concat(data),
|
||||
Concat(footer),
|
||||
)
|
||||
|
||||
expected := []string{"Name", "Age", "Alice", "30", "Total: 1"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Merging results from multiple operations", func(t *testing.T) {
|
||||
evens := Filter(func(n int) bool { return n%2 == 0 })([]int{1, 2, 3, 4, 5, 6})
|
||||
odds := Filter(func(n int) bool { return n%2 != 0 })([]int{1, 2, 3, 4, 5, 6})
|
||||
|
||||
result := Concat(odds)(evens)
|
||||
expected := []int{2, 4, 6, 1, 3, 5}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Combining prefix and suffix", func(t *testing.T) {
|
||||
prefix := []string{"Mr.", "Dr."}
|
||||
names := []string{"Smith", "Jones"}
|
||||
|
||||
addPrefix := func(name string) []string {
|
||||
return Map(func(p string) string { return p + " " + name })(prefix)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
names,
|
||||
Chain(addPrefix),
|
||||
F.Identity[[]string],
|
||||
)
|
||||
|
||||
expected := []string{"Mr. Smith", "Dr. Smith", "Mr. Jones", "Dr. Jones"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Queue-like behavior", func(t *testing.T) {
|
||||
queue := []int{1, 2, 3}
|
||||
newItems := []int{4, 5}
|
||||
|
||||
// Add items to end of queue
|
||||
updatedQueue := Concat(newItems)(queue)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, updatedQueue)
|
||||
assert.Equal(t, 1, updatedQueue[0]) // Front of queue
|
||||
assert.Equal(t, 5, updatedQueue[len(updatedQueue)-1]) // Back of queue
|
||||
})
|
||||
|
||||
t.Run("Combining configuration arrays", func(t *testing.T) {
|
||||
defaultConfig := []string{"--verbose", "--color"}
|
||||
userConfig := []string{"--output=file.txt", "--format=json"}
|
||||
|
||||
finalConfig := Concat(userConfig)(defaultConfig)
|
||||
|
||||
expected := []string{"--verbose", "--color", "--output=file.txt", "--format=json"}
|
||||
assert.Equal(t, expected, finalConfig)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatProperties tests mathematical properties of Concat
|
||||
func TestConcatProperties(t *testing.T) {
|
||||
t.Run("Associativity: (a + b) + c == a + (b + c)", func(t *testing.T) {
|
||||
a := []int{1, 2}
|
||||
b := []int{3, 4}
|
||||
c := []int{5, 6}
|
||||
|
||||
// (a + b) + c
|
||||
left := Concat(c)(Concat(b)(a))
|
||||
|
||||
// a + (b + c)
|
||||
right := Concat(Concat(c)(b))(a)
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, left)
|
||||
})
|
||||
|
||||
t.Run("Identity: a + [] == a and [] + a == a", func(t *testing.T) {
|
||||
arr := []int{1, 2, 3}
|
||||
empty := []int{}
|
||||
|
||||
// Right identity
|
||||
rightResult := Concat(empty)(arr)
|
||||
assert.Equal(t, arr, rightResult)
|
||||
|
||||
// Left identity
|
||||
leftResult := Concat(arr)(empty)
|
||||
assert.Equal(t, arr, leftResult)
|
||||
})
|
||||
|
||||
t.Run("Length property: len(a + b) == len(a) + len(b)", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
arr1 []int
|
||||
arr2 []int
|
||||
}{
|
||||
{[]int{1, 2, 3}, []int{4, 5}},
|
||||
{[]int{1}, []int{2, 3, 4, 5}},
|
||||
{[]int{}, []int{1, 2, 3}},
|
||||
{[]int{1, 2, 3}, []int{}},
|
||||
{MakeBy(100, F.Identity[int]), MakeBy(50, F.Identity[int])},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := Concat(tc.arr2)(tc.arr1)
|
||||
expectedLen := len(tc.arr1) + len(tc.arr2)
|
||||
assert.Equal(t, expectedLen, len(result))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Order preservation: elements maintain their relative order", func(t *testing.T) {
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{4, 5, 6}
|
||||
result := Concat(arr2)(arr1)
|
||||
|
||||
// Check arr1 elements are in order
|
||||
assert.Equal(t, 1, result[0])
|
||||
assert.Equal(t, 2, result[1])
|
||||
assert.Equal(t, 3, result[2])
|
||||
|
||||
// Check arr2 elements are in order after arr1
|
||||
assert.Equal(t, 4, result[3])
|
||||
assert.Equal(t, 5, result[4])
|
||||
assert.Equal(t, 6, result[5])
|
||||
})
|
||||
|
||||
t.Run("Immutability: original arrays are not modified", func(t *testing.T) {
|
||||
original1 := []int{1, 2, 3}
|
||||
original2 := []int{4, 5, 6}
|
||||
copy1 := []int{1, 2, 3}
|
||||
copy2 := []int{4, 5, 6}
|
||||
|
||||
_ = Concat(original2)(original1)
|
||||
|
||||
assert.Equal(t, copy1, original1)
|
||||
assert.Equal(t, copy2, original2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,17 +63,26 @@ func Bind[S1, S2, T any](
|
||||
|
||||
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
|
||||
// Unlike Bind, the computation function returns a plain value T rather than []T.
|
||||
// This is useful when you need to compute a derived value from the current context
|
||||
// without introducing additional array elements.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := array.Let(
|
||||
// func(sum int) func(s struct{ X int }) struct{ X, Sum int } {
|
||||
// return func(s struct{ X int }) struct{ X, Sum int } {
|
||||
// return struct{ X, Sum int }{s.X, sum}
|
||||
// }
|
||||
// },
|
||||
// func(s struct{ X int }) int { return s.X * 2 },
|
||||
// type State1 struct{ X int }
|
||||
// type State2 struct{ X, Double int }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// []State1{{X: 5}, {X: 10}},
|
||||
// array.Let(
|
||||
// func(double int) func(s State1) State2 {
|
||||
// return func(s State1) State2 {
|
||||
// return State2{X: s.X, Double: double}
|
||||
// }
|
||||
// },
|
||||
// func(s State1) int { return s.X * 2 },
|
||||
// ),
|
||||
// )
|
||||
// // result: []State2{{X: 5, Double: 10}, {X: 10, Double: 20}}
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
@@ -84,18 +93,25 @@ func Let[S1, S2, T any](
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context S1 to produce a context S2.
|
||||
// This is useful for adding constant values to the context.
|
||||
// This is useful for adding constant values to the context without computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := array.LetTo(
|
||||
// func(name string) func(s struct{ X int }) struct{ X int; Name string } {
|
||||
// return func(s struct{ X int }) struct{ X int; Name string } {
|
||||
// return struct{ X int; Name string }{s.X, name}
|
||||
// }
|
||||
// },
|
||||
// "constant",
|
||||
// type State1 struct{ X int }
|
||||
// type State2 struct{ X int; Name string }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// []State1{{X: 1}, {X: 2}},
|
||||
// array.LetTo(
|
||||
// func(name string) func(s State1) State2 {
|
||||
// return func(s State1) State2 {
|
||||
// return State2{X: s.X, Name: name}
|
||||
// }
|
||||
// },
|
||||
// "constant",
|
||||
// ),
|
||||
// )
|
||||
// // result: []State2{{X: 1, Name: "constant"}, {X: 2, Name: "constant"}}
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
@@ -107,15 +123,19 @@ func LetTo[S1, S2, T any](
|
||||
|
||||
// BindTo initializes a new state S1 from a value T.
|
||||
// This is typically the first operation after Do to start building the context.
|
||||
// It transforms each element of type T into a state of type S1.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct{ X int }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// []int{1, 2, 3},
|
||||
// array.BindTo(func(x int) struct{ X int } {
|
||||
// return struct{ X int }{x}
|
||||
// array.BindTo(func(x int) State {
|
||||
// return State{X: x}
|
||||
// }),
|
||||
// )
|
||||
// // result: []State{{X: 1}, {X: 2}, {X: 3}}
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
|
||||
@@ -22,57 +22,176 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestState1 struct {
|
||||
X int
|
||||
}
|
||||
|
||||
type TestState2 struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
// TestLet tests the Let function
|
||||
func TestLet(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(TestState1{}),
|
||||
type State1 struct {
|
||||
X int
|
||||
}
|
||||
type State2 struct {
|
||||
X int
|
||||
Double int
|
||||
}
|
||||
|
||||
// Test Let with pure computation
|
||||
result := F.Pipe1(
|
||||
[]State1{{X: 5}, {X: 10}},
|
||||
Let(
|
||||
func(y int) func(s TestState1) TestState2 {
|
||||
return func(s TestState1) TestState2 {
|
||||
return TestState2{X: s.X, Y: y}
|
||||
func(double int) func(s State1) State2 {
|
||||
return func(s State1) State2 {
|
||||
return State2{X: s.X, Double: double}
|
||||
}
|
||||
},
|
||||
func(s TestState1) int { return s.X * 2 },
|
||||
func(s State1) int { return s.X * 2 },
|
||||
),
|
||||
Map(func(s TestState2) int { return s.X + s.Y }),
|
||||
)
|
||||
|
||||
assert.Equal(t, []int{0}, result)
|
||||
expected := []State2{{X: 5, Double: 10}, {X: 10, Double: 20}}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// Test Let with empty array
|
||||
empty := []State1{}
|
||||
result2 := F.Pipe1(
|
||||
empty,
|
||||
Let(
|
||||
func(double int) func(s State1) State2 {
|
||||
return func(s State1) State2 {
|
||||
return State2{X: s.X, Double: double}
|
||||
}
|
||||
},
|
||||
func(s State1) int { return s.X * 2 },
|
||||
),
|
||||
)
|
||||
assert.Equal(t, []State2{}, result2)
|
||||
}
|
||||
|
||||
// TestLetTo tests the LetTo function
|
||||
func TestLetTo(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(TestState1{X: 5}),
|
||||
type State1 struct {
|
||||
X int
|
||||
}
|
||||
type State2 struct {
|
||||
X int
|
||||
Name string
|
||||
}
|
||||
|
||||
// Test LetTo with constant value
|
||||
result := F.Pipe1(
|
||||
[]State1{{X: 1}, {X: 2}},
|
||||
LetTo(
|
||||
func(y int) func(s TestState1) TestState2 {
|
||||
return func(s TestState1) TestState2 {
|
||||
return TestState2{X: s.X, Y: y}
|
||||
func(name string) func(s State1) State2 {
|
||||
return func(s State1) State2 {
|
||||
return State2{X: s.X, Name: name}
|
||||
}
|
||||
},
|
||||
42,
|
||||
"constant",
|
||||
),
|
||||
Map(func(s TestState2) int { return s.X + s.Y }),
|
||||
)
|
||||
|
||||
assert.Equal(t, []int{47}, result)
|
||||
expected := []State2{{X: 1, Name: "constant"}, {X: 2, Name: "constant"}}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// Test LetTo with different constant
|
||||
result2 := F.Pipe1(
|
||||
[]State1{{X: 10}},
|
||||
LetTo(
|
||||
func(name string) func(s State1) State2 {
|
||||
return func(s State1) State2 {
|
||||
return State2{X: s.X, Name: name}
|
||||
}
|
||||
},
|
||||
"test",
|
||||
),
|
||||
)
|
||||
|
||||
expected2 := []State2{{X: 10, Name: "test"}}
|
||||
assert.Equal(t, expected2, result2)
|
||||
}
|
||||
|
||||
// TestBindTo tests the BindTo function
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
X int
|
||||
}
|
||||
|
||||
// Test BindTo with integers
|
||||
result := F.Pipe1(
|
||||
[]int{1, 2, 3},
|
||||
BindTo(func(x int) TestState1 {
|
||||
return TestState1{X: x}
|
||||
BindTo(func(x int) State {
|
||||
return State{X: x}
|
||||
}),
|
||||
)
|
||||
|
||||
expected := []TestState1{{X: 1}, {X: 2}, {X: 3}}
|
||||
expected := []State{{X: 1}, {X: 2}, {X: 3}}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// Test BindTo with strings
|
||||
type StringState struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
result2 := F.Pipe1(
|
||||
[]string{"hello", "world"},
|
||||
BindTo(func(s string) StringState {
|
||||
return StringState{Value: s}
|
||||
}),
|
||||
)
|
||||
|
||||
expected2 := []StringState{{Value: "hello"}, {Value: "world"}}
|
||||
assert.Equal(t, expected2, result2)
|
||||
|
||||
// Test BindTo with empty array
|
||||
empty := []int{}
|
||||
result3 := F.Pipe1(
|
||||
empty,
|
||||
BindTo(func(x int) State {
|
||||
return State{X: x}
|
||||
}),
|
||||
)
|
||||
assert.Equal(t, []State{}, result3)
|
||||
}
|
||||
|
||||
// TestDoWithLetAndBindTo tests combining Do, Let, LetTo, and BindTo
|
||||
func TestDoWithLetAndBindTo(t *testing.T) {
|
||||
type State1 struct {
|
||||
X int
|
||||
}
|
||||
type State2 struct {
|
||||
X int
|
||||
Double int
|
||||
}
|
||||
type State3 struct {
|
||||
X int
|
||||
Double int
|
||||
Name string
|
||||
}
|
||||
|
||||
// Test complex pipeline
|
||||
result := F.Pipe3(
|
||||
[]int{5, 10},
|
||||
BindTo(func(x int) State1 {
|
||||
return State1{X: x}
|
||||
}),
|
||||
Let(
|
||||
func(double int) func(s State1) State2 {
|
||||
return func(s State1) State2 {
|
||||
return State2{X: s.X, Double: double}
|
||||
}
|
||||
},
|
||||
func(s State1) int { return s.X * 2 },
|
||||
),
|
||||
LetTo(
|
||||
func(name string) func(s State2) State3 {
|
||||
return func(s State2) State3 {
|
||||
return State3{X: s.X, Double: s.Double, Name: name}
|
||||
}
|
||||
},
|
||||
"result",
|
||||
),
|
||||
)
|
||||
|
||||
expected := []State3{
|
||||
{X: 5, Double: 10, Name: "result"},
|
||||
{X: 10, Double: 20, Name: "result"},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
137
v2/array/coverage.out
Normal file
137
v2/array/coverage.out
Normal file
@@ -0,0 +1,137 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/array/any.go:34.65,36.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/any.go:48.51,50.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:30.33,32.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:37.52,39.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:44.39,46.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:52.50,54.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:58.54,61.23 3 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:61.23,63.3 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:64.2,64.11 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:70.62,72.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:83.48,85.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:89.52,91.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:93.55,96.23 3 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:96.23,98.14 2 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:98.14,100.4 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:102.2,102.15 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:105.75,108.23 3 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:108.23,110.14 2 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:110.14,112.4 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:114.2,114.15 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:120.54,122.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:127.68,129.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:132.58,134.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:140.67,142.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:148.78,150.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:155.65,157.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:162.76,164.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:169.69,171.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:174.80,175.26 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:175.26,177.3 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:180.64,182.25 2 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:182.25,184.3 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:185.2,185.16 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:189.65,191.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:194.79,196.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:206.62,208.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:214.76,216.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:221.67,223.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:229.81,231.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:235.66,236.24 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:236.24,238.3 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:254.37,256.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:261.34,263.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:266.37,268.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:273.25,275.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:280.24,282.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:287.25,289.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:295.56,297.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:308.54,310.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:316.53,318.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:324.50,326.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:331.76,333.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:338.83,340.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:346.38,348.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:354.36,356.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:362.37,364.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:370.36,372.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:375.49,376.26 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:376.26,380.35 4 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:380.35,385.4 4 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:386.3,386.16 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:395.50,397.26 2 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:397.26,398.18 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:398.18,400.4 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:401.3,401.25 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:406.60,407.36 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:407.36,409.3 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:419.36,421.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:424.49,426.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:432.49,434.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:440.42,442.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:447.30,449.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:456.78,458.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:464.75,466.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:469.32,471.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:474.35,476.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:479.28,481.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:486.50,488.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:493.29,495.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:500.47,502.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:507.67,509.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:514.81,516.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:521.45,523.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:528.38,530.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:605.43,607.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:613.52,615.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:621.49,623.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:628.44,630.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:714.33,716.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:780.53,781.26 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:781.26,782.47 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:782.47,782.67 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:839.31,841.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:36.7,38.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:60.20,62.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:91.20,93.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:120.20,122.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:143.19,145.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:166.20,168.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/eq.go:35.37,37.49 2 1
|
||||
github.com/IBM/fp-go/v2/array/eq.go:37.49,39.3 1 1
|
||||
github.com/IBM/fp-go/v2/array/eq.go:43.45,45.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:33.65,35.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:48.79,50.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:68.78,70.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:76.89,78.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:89.64,91.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:97.78,99.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:105.77,107.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:113.88,115.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/magma.go:38.50,40.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/monad.go:39.65,41.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:35.36,37.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:48.42,50.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:52.45,54.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:68.45,73.48 3 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:73.48,75.3 1 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:77.2,77.12 1 1
|
||||
github.com/IBM/fp-go/v2/array/sequence.go:27.19,29.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/sequence.go:69.22,71.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/sequence.go:92.53,98.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/sort.go:35.47,37.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/sort.go:65.68,67.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/sort.go:96.51,98.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:66.34,68.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:83.24,86.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:94.39,96.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:105.29,108.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:110.142,117.46 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:117.46,118.54 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:118.54,125.4 1 1
|
||||
github.com/IBM/fp-go/v2/array/uniq.go:20.43,22.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/uniq.go:49.60,51.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/zip.go:38.73,40.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/zip.go:58.55,60.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/zip.go:81.62,83.2 1 1
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
FC "github.com/IBM/fp-go/v2/internal/functor"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Of constructs a single element array
|
||||
@@ -215,7 +215,7 @@ func Filter[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) AS {
|
||||
return FilterWithIndex[AS](F.Ignore1of2[int](pred))
|
||||
}
|
||||
|
||||
func FilterChain[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
|
||||
func ChainOptionK[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
|
||||
return F.Flow2(
|
||||
FilterMap[GA, []GB](f),
|
||||
Flatten[[]GB],
|
||||
@@ -234,7 +234,7 @@ func FilterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) O.Option[B])
|
||||
return F.Bind2nd(MonadFilterMapWithIndex[GA, GB, A, B], f)
|
||||
}
|
||||
|
||||
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, GA] {
|
||||
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) pair.Pair[GA, GA] {
|
||||
left := Empty[GA]()
|
||||
right := Empty[GA]()
|
||||
array.Reduce(as, func(c bool, a A) bool {
|
||||
@@ -246,10 +246,10 @@ func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, G
|
||||
return c
|
||||
}, true)
|
||||
// returns the partition
|
||||
return tuple.MakeTuple2(left, right)
|
||||
return pair.MakePair(left, right)
|
||||
}
|
||||
|
||||
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) tuple.Tuple2[GA, GA] {
|
||||
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) pair.Pair[GA, GA] {
|
||||
return F.Bind2nd(MonadPartition[GA, A], pred)
|
||||
}
|
||||
|
||||
@@ -375,3 +375,102 @@ func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
|
||||
func Reverse[GT ~[]T, T any](as GT) GT {
|
||||
return array.Reverse(as)
|
||||
}
|
||||
|
||||
// Extract returns the first element of an array, or a zero value if empty.
|
||||
// This is the comonad extract operation for arrays.
|
||||
//
|
||||
// Extract is the dual of the monadic return/of operation. While Of wraps a value
|
||||
// in a context, Extract unwraps a value from its context.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The array type constraint
|
||||
// - A: The type of elements in the array
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input array
|
||||
//
|
||||
// Returns:
|
||||
// - The first element if the array is non-empty, otherwise the zero value of type A
|
||||
//
|
||||
// Behavior:
|
||||
// - Returns as[0] if the array has at least one element
|
||||
// - Returns the zero value of A if the array is empty
|
||||
// - Does not modify the input array
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := Extract([]int{1, 2, 3})
|
||||
// // result: 1
|
||||
//
|
||||
// Example with empty array:
|
||||
//
|
||||
// result := Extract([]int{})
|
||||
// // result: 0 (zero value for int)
|
||||
//
|
||||
// Comonad laws:
|
||||
// - Extract ∘ Of == Identity (extracting from a singleton returns the value)
|
||||
// - Extract ∘ Extend(f) == f (extract after extend equals applying f)
|
||||
//
|
||||
//go:inline
|
||||
func Extract[GA ~[]A, A any](as GA) A {
|
||||
if len(as) > 0 {
|
||||
return as[0]
|
||||
}
|
||||
var zero A
|
||||
return zero
|
||||
}
|
||||
|
||||
// Extend applies a function to every suffix of an array, creating a new array of results.
|
||||
// This is the comonad extend operation for arrays.
|
||||
//
|
||||
// The function f is applied to progressively smaller suffixes of the input array:
|
||||
// - f(as[0:]) for the first element
|
||||
// - f(as[1:]) for the second element
|
||||
// - f(as[2:]) for the third element
|
||||
// - and so on...
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input array type constraint
|
||||
// - GB: The output array type constraint
|
||||
// - A: The type of elements in the input array
|
||||
// - B: The type of elements in the output array
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an array suffix and returns a value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms an array of A into an array of B
|
||||
//
|
||||
// Behavior:
|
||||
// - Creates a new array with the same length as the input
|
||||
// - For each position i, applies f to the suffix starting at i
|
||||
// - Returns an empty array if the input is empty
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Sum all elements from current position to end
|
||||
// sumSuffix := Extend[[]int, []int](func(as []int) int {
|
||||
// return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
// })
|
||||
// result := sumSuffix([]int{1, 2, 3, 4})
|
||||
// // result: []int{10, 9, 7, 4}
|
||||
// // Explanation: [1+2+3+4, 2+3+4, 3+4, 4]
|
||||
//
|
||||
// Example with length:
|
||||
//
|
||||
// // Get remaining length at each position
|
||||
// lengths := Extend[[]int, []int](Size[[]int, int])
|
||||
// result := lengths([]int{10, 20, 30})
|
||||
// // result: []int{3, 2, 1}
|
||||
//
|
||||
// Comonad laws:
|
||||
// - Left identity: Extend(Extract) == Identity
|
||||
// - Right identity: Extract ∘ Extend(f) == f
|
||||
// - Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))
|
||||
//
|
||||
//go:inline
|
||||
func Extend[GA ~[]A, GB ~[]B, A, B any](f func(GA) B) func(GA) GB {
|
||||
return func(as GA) GB {
|
||||
return MakeBy[GB](len(as), func(i int) B { return f(as[i:]) })
|
||||
}
|
||||
}
|
||||
|
||||
298
v2/array/generic/array_test.go
Normal file
298
v2/array/generic/array_test.go
Normal file
@@ -0,0 +1,298 @@
|
||||
// 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 generic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestExtract tests the Extract function
|
||||
func TestExtract(t *testing.T) {
|
||||
t.Run("Extract from non-empty array", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 1, result)
|
||||
})
|
||||
|
||||
t.Run("Extract from single element array", func(t *testing.T) {
|
||||
input := []string{"hello"}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("Extract from empty array returns zero value", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("Extract from empty string array returns empty string", func(t *testing.T) {
|
||||
input := []string{}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("Extract does not modify original array", func(t *testing.T) {
|
||||
original := []int{1, 2, 3}
|
||||
originalCopy := []int{1, 2, 3}
|
||||
_ = Extract(original)
|
||||
assert.Equal(t, originalCopy, original)
|
||||
})
|
||||
|
||||
t.Run("Extract with floats", func(t *testing.T) {
|
||||
input := []float64{3.14, 2.71, 1.41}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 3.14, result)
|
||||
})
|
||||
|
||||
t.Run("Extract with custom slice type", func(t *testing.T) {
|
||||
type IntSlice []int
|
||||
input := IntSlice{10, 20, 30}
|
||||
result := Extract(input)
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtractComonadLaws tests comonad laws for Extract
|
||||
func TestExtractComonadLaws(t *testing.T) {
|
||||
t.Run("Extract ∘ Of == Identity", func(t *testing.T) {
|
||||
value := 42
|
||||
result := Extract(Of[[]int](value))
|
||||
assert.Equal(t, value, result)
|
||||
})
|
||||
|
||||
t.Run("Extract ∘ Extend(f) == f", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// Extract(Extend(f)(input)) should equal f(input)
|
||||
extended := Extend[[]int, []int](f)(input)
|
||||
result := Extract(extended)
|
||||
expected := f(input)
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtend tests the Extend function
|
||||
func TestExtend(t *testing.T) {
|
||||
t.Run("Extend with sum of suffixes", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
sumSuffix := Extend[[]int, []int](func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
})
|
||||
result := sumSuffix(input)
|
||||
expected := []int{10, 9, 7, 4} // [1+2+3+4, 2+3+4, 3+4, 4]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with length of suffixes", func(t *testing.T) {
|
||||
input := []int{10, 20, 30}
|
||||
lengths := Extend[[]int, []int](Size[[]int, int])
|
||||
result := lengths(input)
|
||||
expected := []int{3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with head extraction", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
duplicate := Extend[[]int, []int](Extract[[]int, int])
|
||||
result := duplicate(input)
|
||||
expected := []int{1, 2, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with empty array", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := Extend[[]int, []int](Size[[]int, int])(input)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with single element", func(t *testing.T) {
|
||||
input := []string{"hello"}
|
||||
result := Extend[[]string, []int](func(as []string) int { return len(as) })(input)
|
||||
expected := []int{1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend does not modify original array", func(t *testing.T) {
|
||||
original := []int{1, 2, 3}
|
||||
originalCopy := []int{1, 2, 3}
|
||||
_ = Extend[[]int, []int](Size[[]int, int])(original)
|
||||
assert.Equal(t, originalCopy, original)
|
||||
})
|
||||
|
||||
t.Run("Extend with string concatenation", func(t *testing.T) {
|
||||
input := []string{"a", "b", "c"}
|
||||
concat := Extend[[]string, []string](func(as []string) string {
|
||||
return MonadReduce(as, func(acc, s string) string { return acc + s }, "")
|
||||
})
|
||||
result := concat(input)
|
||||
expected := []string{"abc", "bc", "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with custom slice types", func(t *testing.T) {
|
||||
type IntSlice []int
|
||||
type ResultSlice []int
|
||||
input := IntSlice{1, 2, 3}
|
||||
sumSuffix := Extend[IntSlice, ResultSlice](func(as IntSlice) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
})
|
||||
result := sumSuffix(input)
|
||||
expected := ResultSlice{6, 5, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendComonadLaws tests comonad laws for Extend
|
||||
func TestExtendComonadLaws(t *testing.T) {
|
||||
t.Run("Left identity: Extend(Extract) == Identity", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := Extend[[]int, []int](Extract[[]int, int])(input)
|
||||
assert.Equal(t, input, result)
|
||||
})
|
||||
|
||||
t.Run("Right identity: Extract ∘ Extend(f) == f", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4}
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// Extract(Extend(f)(input)) should equal f(input)
|
||||
result := F.Pipe2(input, Extend[[]int, []int](f), Extract[[]int, int])
|
||||
expected := f(input)
|
||||
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
// f: sum of array
|
||||
f := func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}
|
||||
|
||||
// g: length of array
|
||||
g := func(as []int) int {
|
||||
return len(as)
|
||||
}
|
||||
|
||||
// Left side: Extend(f) ∘ Extend(g)
|
||||
left := F.Pipe2(input, Extend[[]int, []int](g), Extend[[]int, []int](f))
|
||||
|
||||
// Right side: Extend(f ∘ Extend(g))
|
||||
right := Extend[[]int, []int](func(as []int) int {
|
||||
return f(Extend[[]int, []int](g)(as))
|
||||
})(input)
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendComposition tests Extend with other array operations
|
||||
func TestExtendComposition(t *testing.T) {
|
||||
t.Run("Extend after Map", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Map[[]int, []int](func(x int) int { return x * 2 }),
|
||||
Extend[[]int, []int](func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
}),
|
||||
)
|
||||
expected := []int{12, 10, 6} // [2+4+6, 4+6, 6]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Map after Extend", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Extend[[]int, []int](Size[[]int, int]),
|
||||
Map[[]int, []int](func(x int) int { return x * 10 }),
|
||||
)
|
||||
expected := []int{30, 20, 10}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Extend with Filter", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Filter[[]int](func(n int) bool { return n%2 == 0 }),
|
||||
Extend[[]int, []int](Size[[]int, int]),
|
||||
)
|
||||
expected := []int{3, 2, 1} // lengths of [2,4,6], [4,6], [6]
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtendUseCases demonstrates practical use cases for Extend
|
||||
func TestExtendUseCases(t *testing.T) {
|
||||
t.Run("Running sum (cumulative sum from each position)", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
runningSum := Extend[[]int, []int](func(as []int) int {
|
||||
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
|
||||
})
|
||||
result := runningSum(input)
|
||||
expected := []int{15, 14, 12, 9, 5}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Sliding window average", func(t *testing.T) {
|
||||
input := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
|
||||
windowAvg := Extend[[]float64, []float64](func(as []float64) float64 {
|
||||
if len(as) == 0 {
|
||||
return 0
|
||||
}
|
||||
sum := MonadReduce(as, func(acc, x float64) float64 { return acc + x }, 0.0)
|
||||
return sum / float64(len(as))
|
||||
})
|
||||
result := windowAvg(input)
|
||||
expected := []float64{3.0, 3.5, 4.0, 4.5, 5.0}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Check if suffix is sorted", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 2, 1}
|
||||
isSorted := Extend[[]int, []bool](func(as []int) bool {
|
||||
for i := 1; i < len(as); i++ {
|
||||
if as[i] < as[i-1] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
result := isSorted(input)
|
||||
expected := []bool{false, false, false, false, true}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Count remaining elements", func(t *testing.T) {
|
||||
events := []string{"start", "middle", "end"}
|
||||
remaining := Extend[[]string, []int](Size[[]string, string])
|
||||
result := remaining(events)
|
||||
expected := []int{3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
@@ -18,7 +18,7 @@ package generic
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// ZipWith applies a function to pairs of elements at the same index in two arrays, collecting the results in a new array. If one
|
||||
@@ -34,19 +34,19 @@ func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, f
|
||||
|
||||
// Zip takes two arrays and returns an array of corresponding pairs. If one input array is short, excess elements of the
|
||||
// longer array are discarded
|
||||
func Zip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](fb BS) func(AS) CS {
|
||||
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) T.Tuple2[A, B]])(fb, T.MakeTuple2[A, B])
|
||||
func Zip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](fb BS) func(AS) CS {
|
||||
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) pair.Pair[A, B]])(fb, pair.MakePair[A, B])
|
||||
}
|
||||
|
||||
// Unzip is the function is reverse of [Zip]. Takes an array of pairs and return two corresponding arrays
|
||||
func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS, BS] {
|
||||
func Unzip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](cs CS) pair.Pair[AS, BS] {
|
||||
l := len(cs)
|
||||
as := make(AS, l)
|
||||
bs := make(BS, l)
|
||||
for i := range l {
|
||||
t := cs[i]
|
||||
as[i] = t.F1
|
||||
bs[i] = t.F2
|
||||
as[i] = pair.Head(t)
|
||||
bs[i] = pair.Tail(t)
|
||||
}
|
||||
return T.MakeTuple2(as, bs)
|
||||
return pair.MakePair(as, bs)
|
||||
}
|
||||
|
||||
@@ -23,12 +23,45 @@ import (
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// Of constructs a single element array
|
||||
// Of constructs a single element NonEmptyArray.
|
||||
// This is the simplest way to create a NonEmptyArray with exactly one element.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - first: The single element to include in the array
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[A]: A NonEmptyArray containing only the provided element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := Of(42) // NonEmptyArray[int]{42}
|
||||
// str := Of("hello") // NonEmptyArray[string]{"hello"}
|
||||
func Of[A any](first A) NonEmptyArray[A] {
|
||||
return G.Of[NonEmptyArray[A]](first)
|
||||
}
|
||||
|
||||
// From constructs a [NonEmptyArray] from a set of variadic arguments
|
||||
// From constructs a NonEmptyArray from a set of variadic arguments.
|
||||
// The first argument is required to ensure the array is non-empty, and additional
|
||||
// elements can be provided as variadic arguments.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - first: The first element (required to ensure non-emptiness)
|
||||
// - data: Additional elements (optional)
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[A]: A NonEmptyArray containing all provided elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr1 := From(1) // NonEmptyArray[int]{1}
|
||||
// arr2 := From(1, 2, 3) // NonEmptyArray[int]{1, 2, 3}
|
||||
// arr3 := From("a", "b", "c") // NonEmptyArray[string]{"a", "b", "c"}
|
||||
func From[A any](first A, data ...A) NonEmptyArray[A] {
|
||||
count := len(data)
|
||||
if count == 0 {
|
||||
@@ -41,79 +74,358 @@ func From[A any](first A, data ...A) NonEmptyArray[A] {
|
||||
return buffer
|
||||
}
|
||||
|
||||
// IsEmpty always returns false for NonEmptyArray since it's guaranteed to have at least one element.
|
||||
// This function exists for API consistency with regular arrays.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - _: The NonEmptyArray (unused, as the result is always false)
|
||||
//
|
||||
// Returns:
|
||||
// - bool: Always false
|
||||
//
|
||||
//go:inline
|
||||
func IsEmpty[A any](_ NonEmptyArray[A]) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNonEmpty always returns true for NonEmptyArray since it's guaranteed to have at least one element.
|
||||
// This function exists for API consistency with regular arrays.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - _: The NonEmptyArray (unused, as the result is always true)
|
||||
//
|
||||
// Returns:
|
||||
// - bool: Always true
|
||||
//
|
||||
//go:inline
|
||||
func IsNonEmpty[A any](_ NonEmptyArray[A]) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MonadMap applies a function to each element of a NonEmptyArray, returning a new NonEmptyArray with the results.
|
||||
// This is the monadic version of Map that takes the array as the first parameter.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
// - f: The function to apply to each element
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[B]: A new NonEmptyArray with the transformed elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// doubled := MonadMap(arr, func(x int) int { return x * 2 }) // NonEmptyArray[int]{2, 4, 6}
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] {
|
||||
return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f)
|
||||
}
|
||||
|
||||
// Map applies a function to each element of a NonEmptyArray, returning a new NonEmptyArray with the results.
|
||||
// This is the curried version that returns a function.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The function to apply to each element
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := Map(func(x int) int { return x * 2 })
|
||||
// result := double(From(1, 2, 3)) // NonEmptyArray[int]{2, 4, 6}
|
||||
//
|
||||
//go:inline
|
||||
func Map[A, B any](f func(a A) B) Operator[A, B] {
|
||||
return G.Map[NonEmptyArray[A], NonEmptyArray[B]](f)
|
||||
}
|
||||
|
||||
// Reduce applies a function to each element of a NonEmptyArray from left to right,
|
||||
// accumulating a result starting from an initial value.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type of the array
|
||||
// - B: The accumulator type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The reducer function that takes (accumulator, element) and returns a new accumulator
|
||||
// - initial: The initial value for the accumulator
|
||||
//
|
||||
// Returns:
|
||||
// - func(NonEmptyArray[A]) B: A function that reduces the array to a single value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// sum := Reduce(func(acc int, x int) int { return acc + x }, 0)
|
||||
// result := sum(From(1, 2, 3, 4)) // 10
|
||||
//
|
||||
// concat := Reduce(func(acc string, x string) string { return acc + x }, "")
|
||||
// result := concat(From("a", "b", "c")) // "abc"
|
||||
func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B {
|
||||
return func(as NonEmptyArray[A]) B {
|
||||
return array.Reduce(as, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
// ReduceRight applies a function to each element of a NonEmptyArray from right to left,
|
||||
// accumulating a result starting from an initial value.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type of the array
|
||||
// - B: The accumulator type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The reducer function that takes (element, accumulator) and returns a new accumulator
|
||||
// - initial: The initial value for the accumulator
|
||||
//
|
||||
// Returns:
|
||||
// - func(NonEmptyArray[A]) B: A function that reduces the array to a single value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// concat := ReduceRight(func(x string, acc string) string { return acc + x }, "")
|
||||
// result := concat(From("a", "b", "c")) // "cba"
|
||||
func ReduceRight[A, B any](f func(A, B) B, initial B) func(NonEmptyArray[A]) B {
|
||||
return func(as NonEmptyArray[A]) B {
|
||||
return array.ReduceRight(as, f, initial)
|
||||
}
|
||||
}
|
||||
|
||||
// Tail returns all elements of a NonEmptyArray except the first one.
|
||||
// Returns an empty slice if the array has only one element.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - []A: A slice containing all elements except the first (may be empty)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3, 4)
|
||||
// tail := Tail(arr) // []int{2, 3, 4}
|
||||
//
|
||||
// single := From(1)
|
||||
// tail := Tail(single) // []int{}
|
||||
//
|
||||
//go:inline
|
||||
func Tail[A any](as NonEmptyArray[A]) []A {
|
||||
return as[1:]
|
||||
}
|
||||
|
||||
// Head returns the first element of a NonEmptyArray.
|
||||
// This operation is always safe since NonEmptyArray is guaranteed to have at least one element.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - A: The first element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// first := Head(arr) // 1
|
||||
//
|
||||
//go:inline
|
||||
func Head[A any](as NonEmptyArray[A]) A {
|
||||
return as[0]
|
||||
}
|
||||
|
||||
// First returns the first element of a NonEmptyArray.
|
||||
// This is an alias for Head.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - A: The first element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// first := First(arr) // 1
|
||||
//
|
||||
//go:inline
|
||||
func First[A any](as NonEmptyArray[A]) A {
|
||||
return as[0]
|
||||
}
|
||||
|
||||
// Last returns the last element of a NonEmptyArray.
|
||||
// This operation is always safe since NonEmptyArray is guaranteed to have at least one element.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - A: The last element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// last := Last(arr) // 3
|
||||
//
|
||||
//go:inline
|
||||
func Last[A any](as NonEmptyArray[A]) A {
|
||||
return as[len(as)-1]
|
||||
}
|
||||
|
||||
// Size returns the number of elements in a NonEmptyArray.
|
||||
// The result is always at least 1.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - int: The number of elements (always >= 1)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// size := Size(arr) // 3
|
||||
//
|
||||
//go:inline
|
||||
func Size[A any](as NonEmptyArray[A]) int {
|
||||
return G.Size(as)
|
||||
}
|
||||
|
||||
// Flatten flattens a NonEmptyArray of NonEmptyArrays into a single NonEmptyArray.
|
||||
// This operation concatenates all inner arrays into one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - mma: A NonEmptyArray of NonEmptyArrays
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[A]: A flattened NonEmptyArray containing all elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nested := From(From(1, 2), From(3, 4), From(5))
|
||||
// flat := Flatten(nested) // NonEmptyArray[int]{1, 2, 3, 4, 5}
|
||||
func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
|
||||
return G.Flatten(mma)
|
||||
}
|
||||
|
||||
// MonadChain applies a function that returns a NonEmptyArray to each element and flattens the results.
|
||||
// This is the monadic bind operation (flatMap) that takes the array as the first parameter.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The input NonEmptyArray
|
||||
// - f: A function that takes an element and returns a NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[B]: The flattened result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// result := MonadChain(arr, func(x int) NonEmptyArray[int] {
|
||||
// return From(x, x*10)
|
||||
// }) // NonEmptyArray[int]{1, 10, 2, 20, 3, 30}
|
||||
func MonadChain[A, B any](fa NonEmptyArray[A], f Kleisli[A, B]) NonEmptyArray[B] {
|
||||
return G.MonadChain(fa, f)
|
||||
}
|
||||
|
||||
// Chain applies a function that returns a NonEmptyArray to each element and flattens the results.
|
||||
// This is the curried version of MonadChain.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an element and returns a NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// duplicate := Chain(func(x int) NonEmptyArray[int] { return From(x, x) })
|
||||
// result := duplicate(From(1, 2, 3)) // NonEmptyArray[int]{1, 1, 2, 2, 3, 3}
|
||||
func Chain[A, B any](f func(A) NonEmptyArray[B]) Operator[A, B] {
|
||||
return G.Chain[NonEmptyArray[A]](f)
|
||||
}
|
||||
|
||||
// MonadAp applies a NonEmptyArray of functions to a NonEmptyArray of values.
|
||||
// Each function is applied to each value, producing a cartesian product of results.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - B: The output element type
|
||||
// - A: The input element type
|
||||
//
|
||||
// Parameters:
|
||||
// - fab: A NonEmptyArray of functions
|
||||
// - fa: A NonEmptyArray of values
|
||||
//
|
||||
// Returns:
|
||||
// - NonEmptyArray[B]: The result of applying all functions to all values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
|
||||
// vals := From(1, 2)
|
||||
// result := MonadAp(fns, vals) // NonEmptyArray[int]{2, 4, 11, 12}
|
||||
func MonadAp[B, A any](fab NonEmptyArray[func(A) B], fa NonEmptyArray[A]) NonEmptyArray[B] {
|
||||
return G.MonadAp[NonEmptyArray[B]](fab, fa)
|
||||
}
|
||||
|
||||
// Ap applies a NonEmptyArray of functions to a NonEmptyArray of values.
|
||||
// This is the curried version of MonadAp.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - B: The output element type
|
||||
// - A: The input element type
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: A NonEmptyArray of values
|
||||
//
|
||||
// Returns:
|
||||
// - func(NonEmptyArray[func(A) B]) NonEmptyArray[B]: A function that applies functions to the values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// vals := From(1, 2)
|
||||
// applyTo := Ap[int](vals)
|
||||
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
|
||||
// result := applyTo(fns) // NonEmptyArray[int]{2, 4, 11, 12}
|
||||
func Ap[B, A any](fa NonEmptyArray[A]) func(NonEmptyArray[func(A) B]) NonEmptyArray[B] {
|
||||
return G.Ap[NonEmptyArray[B], NonEmptyArray[func(A) B]](fa)
|
||||
}
|
||||
@@ -136,7 +448,23 @@ func Fold[A any](s S.Semigroup[A]) func(NonEmptyArray[A]) A {
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend prepends a single value to an array
|
||||
// Prepend prepends a single value to the beginning of a NonEmptyArray.
|
||||
// Returns a new NonEmptyArray with the value at the front.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - head: The value to prepend
|
||||
//
|
||||
// Returns:
|
||||
// - EM.Endomorphism[NonEmptyArray[A]]: A function that prepends the value to a NonEmptyArray
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(2, 3, 4)
|
||||
// prepend1 := Prepend(1)
|
||||
// result := prepend1(arr) // NonEmptyArray[int]{1, 2, 3, 4}
|
||||
func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] {
|
||||
return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head)
|
||||
}
|
||||
@@ -226,3 +554,59 @@ func ToNonEmptyArray[A any](as []A) Option[NonEmptyArray[A]] {
|
||||
}
|
||||
return option.Some(NonEmptyArray[A](as))
|
||||
}
|
||||
|
||||
// Extract returns the first element of a NonEmptyArray.
|
||||
// This is an alias for Head and is part of the Comonad interface.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input NonEmptyArray
|
||||
//
|
||||
// Returns:
|
||||
// - A: The first element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3)
|
||||
// first := Extract(arr) // 1
|
||||
//
|
||||
//go:inline
|
||||
func Extract[A any](as NonEmptyArray[A]) A {
|
||||
return Head(as)
|
||||
}
|
||||
|
||||
// Extend applies a function to all suffixes of a NonEmptyArray.
|
||||
// For each position i, it applies the function to the subarray starting at position i.
|
||||
// This is part of the Comonad interface.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes a NonEmptyArray and returns a value
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := From(1, 2, 3, 4)
|
||||
// sumSuffix := Extend(func(xs NonEmptyArray[int]) int {
|
||||
// sum := 0
|
||||
// for _, x := range xs {
|
||||
// sum += x
|
||||
// }
|
||||
// return sum
|
||||
// })
|
||||
// result := sumSuffix(arr) // NonEmptyArray[int]{10, 9, 7, 4}
|
||||
// // [1,2,3,4] -> 10, [2,3,4] -> 9, [3,4] -> 7, [4] -> 4
|
||||
//
|
||||
//go:inline
|
||||
func Extend[A, B any](f func(NonEmptyArray[A]) B) Operator[A, B] {
|
||||
return func(as NonEmptyArray[A]) NonEmptyArray[B] {
|
||||
return G.MakeBy[NonEmptyArray[B]](len(as), func(i int) B { return f(as[i:]) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
package nonempty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
STR "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -368,3 +371,522 @@ func TestToNonEmptyArrayUseCases(t *testing.T) {
|
||||
assert.Equal(t, "default", result2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOf tests the Of function
|
||||
func TestOf(t *testing.T) {
|
||||
t.Run("Create single element array with int", func(t *testing.T) {
|
||||
arr := Of(42)
|
||||
assert.Equal(t, 1, Size(arr))
|
||||
assert.Equal(t, 42, Head(arr))
|
||||
})
|
||||
|
||||
t.Run("Create single element array with string", func(t *testing.T) {
|
||||
arr := Of("hello")
|
||||
assert.Equal(t, 1, Size(arr))
|
||||
assert.Equal(t, "hello", Head(arr))
|
||||
})
|
||||
|
||||
t.Run("Create single element array with struct", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
arr := Of(person)
|
||||
assert.Equal(t, 1, Size(arr))
|
||||
assert.Equal(t, "Alice", Head(arr).Name)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFrom tests the From function
|
||||
func TestFrom(t *testing.T) {
|
||||
t.Run("Create array with single element", func(t *testing.T) {
|
||||
arr := From(1)
|
||||
assert.Equal(t, 1, Size(arr))
|
||||
assert.Equal(t, 1, Head(arr))
|
||||
})
|
||||
|
||||
t.Run("Create array with multiple elements", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4, 5)
|
||||
assert.Equal(t, 5, Size(arr))
|
||||
assert.Equal(t, 1, Head(arr))
|
||||
assert.Equal(t, 5, Last(arr))
|
||||
})
|
||||
|
||||
t.Run("Create array with strings", func(t *testing.T) {
|
||||
arr := From("a", "b", "c")
|
||||
assert.Equal(t, 3, Size(arr))
|
||||
assert.Equal(t, "a", Head(arr))
|
||||
assert.Equal(t, "c", Last(arr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestIsEmpty tests the IsEmpty function
|
||||
func TestIsEmpty(t *testing.T) {
|
||||
t.Run("IsEmpty always returns false", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
assert.False(t, IsEmpty(arr))
|
||||
})
|
||||
|
||||
t.Run("IsEmpty returns false for single element", func(t *testing.T) {
|
||||
arr := Of(1)
|
||||
assert.False(t, IsEmpty(arr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestIsNonEmpty tests the IsNonEmpty function
|
||||
func TestIsNonEmpty(t *testing.T) {
|
||||
t.Run("IsNonEmpty always returns true", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
assert.True(t, IsNonEmpty(arr))
|
||||
})
|
||||
|
||||
t.Run("IsNonEmpty returns true for single element", func(t *testing.T) {
|
||||
arr := Of(1)
|
||||
assert.True(t, IsNonEmpty(arr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadMap tests the MonadMap function
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("Map integers to doubles", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4)
|
||||
result := MonadMap(arr, func(x int) int { return x * 2 })
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, 2, Head(result))
|
||||
assert.Equal(t, 8, Last(result))
|
||||
})
|
||||
|
||||
t.Run("Map strings to lengths", func(t *testing.T) {
|
||||
arr := From("a", "bb", "ccc")
|
||||
result := MonadMap(arr, func(s string) int { return len(s) })
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, 1, Head(result))
|
||||
assert.Equal(t, 3, Last(result))
|
||||
})
|
||||
|
||||
t.Run("Map single element", func(t *testing.T) {
|
||||
arr := Of(5)
|
||||
result := MonadMap(arr, func(x int) int { return x * 10 })
|
||||
assert.Equal(t, 1, Size(result))
|
||||
assert.Equal(t, 50, Head(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMap tests the Map function
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("Curried map with integers", func(t *testing.T) {
|
||||
double := Map(func(x int) int { return x * 2 })
|
||||
arr := From(1, 2, 3)
|
||||
result := double(arr)
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, 2, Head(result))
|
||||
assert.Equal(t, 6, Last(result))
|
||||
})
|
||||
|
||||
t.Run("Curried map with strings", func(t *testing.T) {
|
||||
toUpper := Map(func(s string) string { return s + "!" })
|
||||
arr := From("hello", "world")
|
||||
result := toUpper(arr)
|
||||
assert.Equal(t, 2, Size(result))
|
||||
assert.Equal(t, "hello!", Head(result))
|
||||
assert.Equal(t, "world!", Last(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestReduce tests the Reduce function
|
||||
func TestReduce(t *testing.T) {
|
||||
t.Run("Sum integers", func(t *testing.T) {
|
||||
sum := Reduce(func(acc int, x int) int { return acc + x }, 0)
|
||||
arr := From(1, 2, 3, 4, 5)
|
||||
result := sum(arr)
|
||||
assert.Equal(t, 15, result)
|
||||
})
|
||||
|
||||
t.Run("Concatenate strings", func(t *testing.T) {
|
||||
concat := Reduce(func(acc string, x string) string { return acc + x }, "")
|
||||
arr := From("a", "b", "c")
|
||||
result := concat(arr)
|
||||
assert.Equal(t, "abc", result)
|
||||
})
|
||||
|
||||
t.Run("Product of numbers", func(t *testing.T) {
|
||||
product := Reduce(func(acc int, x int) int { return acc * x }, 1)
|
||||
arr := From(2, 3, 4)
|
||||
result := product(arr)
|
||||
assert.Equal(t, 24, result)
|
||||
})
|
||||
|
||||
t.Run("Reduce single element", func(t *testing.T) {
|
||||
sum := Reduce(func(acc int, x int) int { return acc + x }, 10)
|
||||
arr := Of(5)
|
||||
result := sum(arr)
|
||||
assert.Equal(t, 15, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReduceRight tests the ReduceRight function
|
||||
func TestReduceRight(t *testing.T) {
|
||||
t.Run("Concatenate strings right to left", func(t *testing.T) {
|
||||
concat := ReduceRight(func(x string, acc string) string { return acc + x }, "")
|
||||
arr := From("a", "b", "c")
|
||||
result := concat(arr)
|
||||
assert.Equal(t, "cba", result)
|
||||
})
|
||||
|
||||
t.Run("Build list right to left", func(t *testing.T) {
|
||||
buildList := ReduceRight(func(x int, acc []int) []int { return append(acc, x) }, []int{})
|
||||
arr := From(1, 2, 3)
|
||||
result := buildList(arr)
|
||||
assert.Equal(t, []int{3, 2, 1}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTail tests the Tail function
|
||||
func TestTail(t *testing.T) {
|
||||
t.Run("Get tail of multi-element array", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4)
|
||||
tail := Tail(arr)
|
||||
assert.Equal(t, 3, len(tail))
|
||||
assert.Equal(t, []int{2, 3, 4}, tail)
|
||||
})
|
||||
|
||||
t.Run("Get tail of single element array", func(t *testing.T) {
|
||||
arr := Of(1)
|
||||
tail := Tail(arr)
|
||||
assert.Equal(t, 0, len(tail))
|
||||
assert.Equal(t, []int{}, tail)
|
||||
})
|
||||
|
||||
t.Run("Get tail of two element array", func(t *testing.T) {
|
||||
arr := From(1, 2)
|
||||
tail := Tail(arr)
|
||||
assert.Equal(t, 1, len(tail))
|
||||
assert.Equal(t, []int{2}, tail)
|
||||
})
|
||||
}
|
||||
|
||||
// TestHead tests the Head function
|
||||
func TestHead(t *testing.T) {
|
||||
t.Run("Get head of multi-element array", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
head := Head(arr)
|
||||
assert.Equal(t, 1, head)
|
||||
})
|
||||
|
||||
t.Run("Get head of single element array", func(t *testing.T) {
|
||||
arr := Of(42)
|
||||
head := Head(arr)
|
||||
assert.Equal(t, 42, head)
|
||||
})
|
||||
|
||||
t.Run("Get head of string array", func(t *testing.T) {
|
||||
arr := From("first", "second", "third")
|
||||
head := Head(arr)
|
||||
assert.Equal(t, "first", head)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirst tests the First function
|
||||
func TestFirst(t *testing.T) {
|
||||
t.Run("First is alias for Head", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
assert.Equal(t, Head(arr), First(arr))
|
||||
})
|
||||
|
||||
t.Run("Get first element", func(t *testing.T) {
|
||||
arr := From("a", "b", "c")
|
||||
first := First(arr)
|
||||
assert.Equal(t, "a", first)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLast tests the Last function
|
||||
func TestLast(t *testing.T) {
|
||||
t.Run("Get last of multi-element array", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4, 5)
|
||||
last := Last(arr)
|
||||
assert.Equal(t, 5, last)
|
||||
})
|
||||
|
||||
t.Run("Get last of single element array", func(t *testing.T) {
|
||||
arr := Of(42)
|
||||
last := Last(arr)
|
||||
assert.Equal(t, 42, last)
|
||||
})
|
||||
|
||||
t.Run("Get last of string array", func(t *testing.T) {
|
||||
arr := From("first", "second", "third")
|
||||
last := Last(arr)
|
||||
assert.Equal(t, "third", last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSize tests the Size function
|
||||
func TestSize(t *testing.T) {
|
||||
t.Run("Size of multi-element array", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4, 5)
|
||||
size := Size(arr)
|
||||
assert.Equal(t, 5, size)
|
||||
})
|
||||
|
||||
t.Run("Size of single element array", func(t *testing.T) {
|
||||
arr := Of(1)
|
||||
size := Size(arr)
|
||||
assert.Equal(t, 1, size)
|
||||
})
|
||||
|
||||
t.Run("Size of large array", func(t *testing.T) {
|
||||
elements := make([]int, 1000)
|
||||
arr := From(1, elements...)
|
||||
size := Size(arr)
|
||||
assert.Equal(t, 1001, size)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFlatten tests the Flatten function
|
||||
func TestFlatten(t *testing.T) {
|
||||
t.Run("Flatten nested arrays", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5))
|
||||
flat := Flatten(nested)
|
||||
assert.Equal(t, 5, Size(flat))
|
||||
assert.Equal(t, 1, Head(flat))
|
||||
assert.Equal(t, 5, Last(flat))
|
||||
})
|
||||
|
||||
t.Run("Flatten single nested array", func(t *testing.T) {
|
||||
nested := Of(From(1, 2, 3))
|
||||
flat := Flatten(nested)
|
||||
assert.Equal(t, 3, Size(flat))
|
||||
assert.Equal(t, []int{1, 2, 3}, []int(flat))
|
||||
})
|
||||
|
||||
t.Run("Flatten arrays of different sizes", func(t *testing.T) {
|
||||
nested := From(Of(1), From(2, 3, 4), From(5, 6))
|
||||
flat := Flatten(nested)
|
||||
assert.Equal(t, 6, Size(flat))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, []int(flat))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadChain tests the MonadChain function
|
||||
func TestMonadChain(t *testing.T) {
|
||||
t.Run("Chain with duplication", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
assert.Equal(t, 6, Size(result))
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Chain with expansion", func(t *testing.T) {
|
||||
arr := From(1, 2)
|
||||
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
|
||||
return From(x, x+1, x+2)
|
||||
})
|
||||
assert.Equal(t, 6, Size(result))
|
||||
assert.Equal(t, []int{1, 2, 3, 2, 3, 4}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Chain single element", func(t *testing.T) {
|
||||
arr := Of(5)
|
||||
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
|
||||
return From(x, x*2)
|
||||
})
|
||||
assert.Equal(t, 2, Size(result))
|
||||
assert.Equal(t, []int{5, 10}, []int(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestChain tests the Chain function
|
||||
func TestChain(t *testing.T) {
|
||||
t.Run("Curried chain with duplication", func(t *testing.T) {
|
||||
duplicate := Chain(func(x int) NonEmptyArray[int] {
|
||||
return From(x, x)
|
||||
})
|
||||
arr := From(1, 2, 3)
|
||||
result := duplicate(arr)
|
||||
assert.Equal(t, 6, Size(result))
|
||||
assert.Equal(t, []int{1, 1, 2, 2, 3, 3}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Curried chain with transformation", func(t *testing.T) {
|
||||
expand := Chain(func(x int) NonEmptyArray[string] {
|
||||
return Of(fmt.Sprintf("%d", x))
|
||||
})
|
||||
arr := From(1, 2, 3)
|
||||
result := expand(arr)
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, "1", Head(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAp tests the MonadAp function
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("Apply functions to values", func(t *testing.T) {
|
||||
fns := From(
|
||||
func(x int) int { return x * 2 },
|
||||
func(x int) int { return x + 10 },
|
||||
)
|
||||
vals := From(1, 2)
|
||||
result := MonadAp(fns, vals)
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, []int{2, 4, 11, 12}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Apply single function to multiple values", func(t *testing.T) {
|
||||
fns := Of(func(x int) int { return x * 3 })
|
||||
vals := From(1, 2, 3)
|
||||
result := MonadAp(fns, vals)
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, []int{3, 6, 9}, []int(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAp tests the Ap function
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("Curried apply", func(t *testing.T) {
|
||||
vals := From(1, 2)
|
||||
applyTo := Ap[int](vals)
|
||||
fns := From(
|
||||
func(x int) int { return x * 2 },
|
||||
func(x int) int { return x + 10 },
|
||||
)
|
||||
result := applyTo(fns)
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, []int{2, 4, 11, 12}, []int(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFoldMap tests the FoldMap function
|
||||
func TestFoldMap(t *testing.T) {
|
||||
t.Run("FoldMap with sum semigroup", func(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
arr := From(1, 2, 3, 4)
|
||||
result := FoldMap[int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
|
||||
assert.Equal(t, 20, result) // (1*2) + (2*2) + (3*2) + (4*2) = 20
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation", func(t *testing.T) {
|
||||
concatSemigroup := STR.Semigroup
|
||||
arr := From(1, 2, 3)
|
||||
result := FoldMap[int](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
|
||||
assert.Equal(t, "123", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFold tests the Fold function
|
||||
func TestFold(t *testing.T) {
|
||||
t.Run("Fold with sum semigroup", func(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
arr := From(1, 2, 3, 4, 5)
|
||||
result := Fold(sumSemigroup)(arr)
|
||||
assert.Equal(t, 15, result)
|
||||
})
|
||||
|
||||
t.Run("Fold with string concatenation", func(t *testing.T) {
|
||||
concatSemigroup := STR.Semigroup
|
||||
arr := From("a", "b", "c")
|
||||
result := Fold(concatSemigroup)(arr)
|
||||
assert.Equal(t, "abc", result)
|
||||
})
|
||||
|
||||
t.Run("Fold single element", func(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
arr := Of(42)
|
||||
result := Fold(sumSemigroup)(arr)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPrepend tests the Prepend function
|
||||
func TestPrepend(t *testing.T) {
|
||||
t.Run("Prepend to multi-element array", func(t *testing.T) {
|
||||
arr := From(2, 3, 4)
|
||||
prepend1 := Prepend(1)
|
||||
result := prepend1(arr)
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, 1, Head(result))
|
||||
assert.Equal(t, 4, Last(result))
|
||||
})
|
||||
|
||||
t.Run("Prepend to single element array", func(t *testing.T) {
|
||||
arr := Of(2)
|
||||
prepend1 := Prepend(1)
|
||||
result := prepend1(arr)
|
||||
assert.Equal(t, 2, Size(result))
|
||||
assert.Equal(t, []int{1, 2}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Prepend string", func(t *testing.T) {
|
||||
arr := From("world")
|
||||
prependHello := Prepend("hello")
|
||||
result := prependHello(arr)
|
||||
assert.Equal(t, 2, Size(result))
|
||||
assert.Equal(t, "hello", Head(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtract tests the Extract function
|
||||
func TestExtract(t *testing.T) {
|
||||
t.Run("Extract from multi-element array", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
result := Extract(arr)
|
||||
assert.Equal(t, 1, result)
|
||||
})
|
||||
|
||||
t.Run("Extract from single element array", func(t *testing.T) {
|
||||
arr := Of(42)
|
||||
result := Extract(arr)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("Extract is same as Head", func(t *testing.T) {
|
||||
arr := From("a", "b", "c")
|
||||
assert.Equal(t, Head(arr), Extract(arr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestExtend tests the Extend function
|
||||
func TestExtend(t *testing.T) {
|
||||
t.Run("Extend with sum of suffixes", func(t *testing.T) {
|
||||
arr := From(1, 2, 3, 4)
|
||||
sumSuffix := Extend(func(xs NonEmptyArray[int]) int {
|
||||
sum := 0
|
||||
for _, x := range xs {
|
||||
sum += x
|
||||
}
|
||||
return sum
|
||||
})
|
||||
result := sumSuffix(arr)
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, []int{10, 9, 7, 4}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Extend with head of suffixes", func(t *testing.T) {
|
||||
arr := From(1, 2, 3)
|
||||
getHeads := Extend(Head[int])
|
||||
result := getHeads(arr)
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, []int{1, 2, 3}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Extend with size of suffixes", func(t *testing.T) {
|
||||
arr := From("a", "b", "c", "d")
|
||||
getSizes := Extend(Size[string])
|
||||
result := getSizes(arr)
|
||||
assert.Equal(t, 4, Size(result))
|
||||
assert.Equal(t, []int{4, 3, 2, 1}, []int(result))
|
||||
})
|
||||
|
||||
t.Run("Extend single element", func(t *testing.T) {
|
||||
arr := Of(5)
|
||||
double := Extend(func(xs NonEmptyArray[int]) int {
|
||||
return Head(xs) * 2
|
||||
})
|
||||
result := double(arr)
|
||||
assert.Equal(t, 1, Size(result))
|
||||
assert.Equal(t, 10, Head(result))
|
||||
})
|
||||
}
|
||||
|
||||
78
v2/array/sequence_extended_test.go
Normal file
78
v2/array/sequence_extended_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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 array
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestSequenceWithOption tests the generic Sequence function with Option monad
|
||||
func TestSequenceWithOption(t *testing.T) {
|
||||
// Test with Option monad - all Some values
|
||||
opts := From(
|
||||
O.Some(1),
|
||||
O.Some(2),
|
||||
O.Some(3),
|
||||
)
|
||||
|
||||
// Use the Sequence function with Option's applicative monoid
|
||||
monoid := O.ApplicativeMonoid(Monoid[int]())
|
||||
seq := Sequence(O.Map(Of[int]), monoid)
|
||||
result := seq(opts)
|
||||
|
||||
assert.Equal(t, O.Of(From(1, 2, 3)), result)
|
||||
|
||||
// Test with Option monad - contains None
|
||||
optsWithNone := From(
|
||||
O.Some(1),
|
||||
O.None[int](),
|
||||
O.Some(3),
|
||||
)
|
||||
|
||||
result2 := seq(optsWithNone)
|
||||
assert.True(t, O.IsNone(result2))
|
||||
|
||||
// Test with empty array
|
||||
empty := Empty[Option[int]]()
|
||||
result3 := seq(empty)
|
||||
assert.Equal(t, O.Some(Empty[int]()), result3)
|
||||
}
|
||||
|
||||
// TestMonadSequence tests the MonadSequence function
|
||||
func TestMonadSequence(t *testing.T) {
|
||||
// Test with Option monad
|
||||
opts := From(
|
||||
O.Some("hello"),
|
||||
O.Some("world"),
|
||||
)
|
||||
|
||||
monoid := O.ApplicativeMonoid(Monoid[string]())
|
||||
result := MonadSequence(O.Map(Of[string]), monoid, opts)
|
||||
|
||||
assert.Equal(t, O.Of(From("hello", "world")), result)
|
||||
|
||||
// Test with None in the array
|
||||
optsWithNone := From(
|
||||
O.Some("hello"),
|
||||
O.None[string](),
|
||||
)
|
||||
|
||||
result2 := MonadSequence(O.Map(Of[string]), monoid, optsWithNone)
|
||||
assert.Equal(t, O.None[[]string](), result2)
|
||||
}
|
||||
@@ -16,7 +16,11 @@
|
||||
package array
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/internal/traversable"
|
||||
)
|
||||
|
||||
// Traverse maps each element of an array to an effect (HKT), then collects the results
|
||||
@@ -55,9 +59,9 @@ import (
|
||||
//
|
||||
//go:inline
|
||||
func Traverse[A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func([]B) HKTRB,
|
||||
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
fof pointed.OfType[[]B, HKTRB],
|
||||
fmap functor.MapType[[]B, func(B) []B, HKTRB, HKTAB],
|
||||
fap apply.ApType[HKTB, HKTRB, HKTAB],
|
||||
|
||||
f func(A) HKTB) func([]A) HKTRB {
|
||||
return array.Traverse[[]A](fof, fmap, fap, f)
|
||||
@@ -71,7 +75,7 @@ func Traverse[A, B, HKTB, HKTAB, HKTRB any](
|
||||
//
|
||||
//go:inline
|
||||
func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func([]B) HKTRB,
|
||||
fof pointed.OfType[[]B, HKTRB],
|
||||
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
|
||||
@@ -83,7 +87,7 @@ func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
|
||||
|
||||
//go:inline
|
||||
func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func([]B) HKTRB,
|
||||
fof pointed.OfType[[]B, HKTRB],
|
||||
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
|
||||
@@ -93,7 +97,7 @@ func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
|
||||
|
||||
//go:inline
|
||||
func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func([]B) HKTRB,
|
||||
fof pointed.OfType[[]B, HKTRB],
|
||||
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
|
||||
@@ -102,3 +106,22 @@ func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
|
||||
|
||||
return array.MonadTraverseWithIndex(fof, fmap, fap, ta, f)
|
||||
}
|
||||
|
||||
func MakeTraverseType[A, B, HKT_F_B, HKT_F_T_B, HKT_F_B_T_B any]() traversable.TraverseType[A, B, []A, []B, HKT_F_B, HKT_F_T_B, HKT_F_B_T_B] {
|
||||
return func(
|
||||
// ap
|
||||
fof_b pointed.OfType[[]B, HKT_F_T_B],
|
||||
fmap_b functor.MapType[[]B, func(B) []B, HKT_F_T_B, HKT_F_B_T_B],
|
||||
fap_b apply.ApType[HKT_F_B, HKT_F_T_B, HKT_F_B_T_B],
|
||||
|
||||
) func(func(A) HKT_F_B) func([]A) HKT_F_T_B {
|
||||
return func(f func(A) HKT_F_B) func([]A) HKT_F_T_B {
|
||||
return Traverse(
|
||||
fof_b,
|
||||
fmap_b,
|
||||
fap_b,
|
||||
f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
164
v2/array/traverse_extended_test.go
Normal file
164
v2/array/traverse_extended_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// 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 array
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestMonadTraverse tests the MonadTraverse function
|
||||
func TestMonadTraverse(t *testing.T) {
|
||||
// Test converting integers to strings via Option
|
||||
numbers := []int{1, 2, 3}
|
||||
|
||||
result := MonadTraverse(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
numbers,
|
||||
func(n int) O.Option[string] {
|
||||
return O.Some(strconv.Itoa(n))
|
||||
},
|
||||
)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, []string{"1", "2", "3"}, O.GetOrElse(func() []string { return []string{} })(result))
|
||||
|
||||
// Test with a function that can return None
|
||||
result2 := MonadTraverse(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
numbers,
|
||||
func(n int) O.Option[string] {
|
||||
if n == 2 {
|
||||
return O.None[string]()
|
||||
}
|
||||
return O.Some(strconv.Itoa(n))
|
||||
},
|
||||
)
|
||||
|
||||
assert.True(t, O.IsNone(result2))
|
||||
|
||||
// Test with empty array
|
||||
empty := []int{}
|
||||
result3 := MonadTraverse(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
empty,
|
||||
func(n int) O.Option[string] {
|
||||
return O.Some(strconv.Itoa(n))
|
||||
},
|
||||
)
|
||||
|
||||
assert.True(t, O.IsSome(result3))
|
||||
assert.Equal(t, []string{}, O.GetOrElse(func() []string { return nil })(result3))
|
||||
}
|
||||
|
||||
// TestTraverseWithIndex tests the TraverseWithIndex function
|
||||
func TestTraverseWithIndex(t *testing.T) {
|
||||
// Test with index-aware transformation
|
||||
words := []string{"a", "b", "c"}
|
||||
|
||||
traverser := TraverseWithIndex(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
func(idx int, s string) O.Option[string] {
|
||||
return O.Some(s + strconv.Itoa(idx))
|
||||
},
|
||||
)
|
||||
|
||||
result := traverser(words)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, []string{"a0", "b1", "c2"}, O.GetOrElse(func() []string { return []string{} })(result))
|
||||
|
||||
// Test with conditional None based on index
|
||||
traverser2 := TraverseWithIndex(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
func(idx int, s string) O.Option[string] {
|
||||
if idx == 1 {
|
||||
return O.None[string]()
|
||||
}
|
||||
return O.Some(s)
|
||||
},
|
||||
)
|
||||
|
||||
result2 := traverser2(words)
|
||||
assert.True(t, O.IsNone(result2))
|
||||
}
|
||||
|
||||
// TestMonadTraverseWithIndex tests the MonadTraverseWithIndex function
|
||||
func TestMonadTraverseWithIndex(t *testing.T) {
|
||||
// Test with index-aware transformation
|
||||
numbers := []int{10, 20, 30}
|
||||
|
||||
result := MonadTraverseWithIndex(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
numbers,
|
||||
func(idx, n int) O.Option[string] {
|
||||
return O.Some(strconv.Itoa(n * idx))
|
||||
},
|
||||
)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
// Expected: [10*0, 20*1, 30*2] = ["0", "20", "60"]
|
||||
assert.Equal(t, []string{"0", "20", "60"}, O.GetOrElse(func() []string { return []string{} })(result))
|
||||
|
||||
// Test with None at specific index
|
||||
result2 := MonadTraverseWithIndex(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
numbers,
|
||||
func(idx, n int) O.Option[string] {
|
||||
if idx == 2 {
|
||||
return O.None[string]()
|
||||
}
|
||||
return O.Some(strconv.Itoa(n))
|
||||
},
|
||||
)
|
||||
|
||||
assert.True(t, O.IsNone(result2))
|
||||
}
|
||||
|
||||
// TestMakeTraverseType tests the MakeTraverseType function
|
||||
func TestMakeTraverseType(t *testing.T) {
|
||||
// Create a traverse type for Option
|
||||
traverseType := MakeTraverseType[int, string, O.Option[string], O.Option[[]string], O.Option[func(string) []string]]()
|
||||
|
||||
// Use it to traverse an array
|
||||
numbers := []int{1, 2, 3}
|
||||
result := traverseType(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
)(func(n int) O.Option[string] {
|
||||
return O.Some(strconv.Itoa(n * 2))
|
||||
})(numbers)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, []string{"2", "4", "6"}, O.GetOrElse(func() []string { return []string{} })(result))
|
||||
}
|
||||
@@ -17,7 +17,7 @@ package array
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/array/generic"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// ZipWith applies a function to pairs of elements at the same index in two arrays,
|
||||
@@ -55,8 +55,8 @@ func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
|
||||
// // Result: [(a, 1), (b, 2)]
|
||||
//
|
||||
//go:inline
|
||||
func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
|
||||
return G.Zip[[]A, []B, []T.Tuple2[A, B]](fb)
|
||||
func Zip[A, B any](fb []B) func([]A) []pair.Pair[A, B] {
|
||||
return G.Zip[[]A, []B, []pair.Pair[A, B]](fb)
|
||||
}
|
||||
|
||||
// Unzip is the reverse of Zip. It takes an array of pairs (tuples) and returns
|
||||
@@ -78,6 +78,6 @@ func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
|
||||
// ages := result.Tail // [30, 25, 35]
|
||||
//
|
||||
//go:inline
|
||||
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
|
||||
func Unzip[A, B any](cs []pair.Pair[A, B]) pair.Pair[[]A, []B] {
|
||||
return G.Unzip[[]A, []B](cs)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestZip(t *testing.T) {
|
||||
|
||||
res := Zip[string](left)(right)
|
||||
|
||||
assert.Equal(t, From(T.MakeTuple2("a", 1), T.MakeTuple2("b", 2), T.MakeTuple2("c", 3)), res)
|
||||
assert.Equal(t, From(pair.MakePair("a", 1), pair.MakePair("b", 2), pair.MakePair("c", 3)), res)
|
||||
}
|
||||
|
||||
func TestUnzip(t *testing.T) {
|
||||
@@ -51,6 +51,6 @@ func TestUnzip(t *testing.T) {
|
||||
|
||||
unzipped := Unzip(zipped)
|
||||
|
||||
assert.Equal(t, right, unzipped.F1)
|
||||
assert.Equal(t, left, unzipped.F2)
|
||||
assert.Equal(t, right, pair.Head(unzipped))
|
||||
assert.Equal(t, left, pair.Tail(unzipped))
|
||||
}
|
||||
|
||||
@@ -194,6 +194,25 @@ func ArrayNotEmpty[T any](arr []T) Reader {
|
||||
}
|
||||
}
|
||||
|
||||
// ArrayEmpty checks if an array is empty.
|
||||
//
|
||||
// This is the complement of ArrayNotEmpty, asserting that a slice has no elements.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayEmpty(t *testing.T) {
|
||||
// empty := []int{}
|
||||
// assert.ArrayEmpty(empty)(t) // Passes
|
||||
//
|
||||
// numbers := []int{1, 2, 3}
|
||||
// assert.ArrayEmpty(numbers)(t) // Fails
|
||||
// }
|
||||
func ArrayEmpty[T any](arr []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Empty(t, arr)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordNotEmpty checks if a map is not empty.
|
||||
//
|
||||
// Example:
|
||||
@@ -211,6 +230,25 @@ func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
}
|
||||
}
|
||||
|
||||
// RecordEmpty checks if a map is empty.
|
||||
//
|
||||
// This is the complement of RecordNotEmpty, asserting that a map has no key-value pairs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestRecordEmpty(t *testing.T) {
|
||||
// empty := map[string]int{}
|
||||
// assert.RecordEmpty(empty)(t) // Passes
|
||||
//
|
||||
// config := map[string]int{"timeout": 30}
|
||||
// assert.RecordEmpty(config)(t) // Fails
|
||||
// }
|
||||
func RecordEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Empty(t, mp)
|
||||
}
|
||||
}
|
||||
|
||||
// StringNotEmpty checks if a string is not empty.
|
||||
//
|
||||
// Example:
|
||||
@@ -504,21 +542,15 @@ func AllOf(readers []Reader) Reader {
|
||||
//
|
||||
//go:inline
|
||||
func RunAll(testcases map[string]Reader) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
current := true
|
||||
for k, r := range testcases {
|
||||
current = current && t.Run(k, func(t1 *testing.T) {
|
||||
r(t1)
|
||||
})
|
||||
}
|
||||
return current
|
||||
}
|
||||
return SequenceRecord(testcases)
|
||||
}
|
||||
|
||||
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
|
||||
// by providing a function that converts R2 to R1. This allows you to focus a test on a
|
||||
// specific property or subset of a larger data structure.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This is particularly useful when you have an assertion that operates on a specific field
|
||||
// or property, and you want to apply it to a complete object. Instead of extracting the
|
||||
// property and then asserting on it, you can transform the assertion to work directly
|
||||
|
||||
@@ -85,6 +85,33 @@ func TestArrayNotEmpty(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayEmpty(t *testing.T) {
|
||||
t.Run("should pass for empty array", func(t *testing.T) {
|
||||
arr := []int{}
|
||||
result := ArrayEmpty(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayEmpty to pass for empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for non-empty array", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayEmpty(arr)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ArrayEmpty to fail for non-empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with different types", func(t *testing.T) {
|
||||
strArr := []string{}
|
||||
result := ArrayEmpty(strArr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayEmpty to pass for empty string array")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty map", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
@@ -131,6 +158,33 @@ func TestArrayLength(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordEmpty(t *testing.T) {
|
||||
t.Run("should pass for empty map", func(t *testing.T) {
|
||||
mp := map[string]int{}
|
||||
result := RecordEmpty(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordEmpty to pass for empty map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for non-empty map", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := RecordEmpty(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected RecordEmpty to fail for non-empty map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with different key-value types", func(t *testing.T) {
|
||||
mp := map[int]string{}
|
||||
result := RecordEmpty(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordEmpty to pass for empty map with int keys")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordLength(t *testing.T) {
|
||||
t.Run("should pass when map length matches", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
@@ -150,6 +204,33 @@ func TestRecordLength(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty string", func(t *testing.T) {
|
||||
str := "Hello, World!"
|
||||
result := StringNotEmpty(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringNotEmpty to pass for non-empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for empty string", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
str := ""
|
||||
result := StringNotEmpty(str)(mockT)
|
||||
if result {
|
||||
t.Error("Expected StringNotEmpty to fail for empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass for string with whitespace", func(t *testing.T) {
|
||||
str := " "
|
||||
result := StringNotEmpty(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringNotEmpty to pass for string with whitespace")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringLength(t *testing.T) {
|
||||
t.Run("should pass when string length matches", func(t *testing.T) {
|
||||
str := "hello"
|
||||
|
||||
122
v2/assert/from.go
Normal file
122
v2/assert/from.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// FromReaderIOResult converts a ReaderIOResult[Reader] into a Reader.
|
||||
//
|
||||
// This function bridges the gap between context-aware, IO-based computations that may fail
|
||||
// (ReaderIOResult) and the simpler Reader type used for test assertions. It executes the
|
||||
// ReaderIOResult computation using the test's context, handles any potential errors by
|
||||
// converting them to test failures via NoError, and returns the resulting Reader.
|
||||
//
|
||||
// The conversion process:
|
||||
// 1. Executes the ReaderIOResult with the test context (t.Context())
|
||||
// 2. Runs the resulting IO operation ()
|
||||
// 3. Extracts the Result, converting errors to test failures using NoError
|
||||
// 4. Returns a Reader that can be applied to *testing.T
|
||||
//
|
||||
// This is particularly useful when you have test assertions that need to:
|
||||
// - Access context for cancellation or deadlines
|
||||
// - Perform IO operations (file access, network calls, etc.)
|
||||
// - Handle potential errors gracefully in tests
|
||||
//
|
||||
// Parameters:
|
||||
// - ri: A ReaderIOResult that produces a Reader when given a context and executed
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that can be directly applied to *testing.T for assertion
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithContext(t *testing.T) {
|
||||
// // Create a ReaderIOResult that performs an IO operation
|
||||
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
|
||||
// return func() result.Result[assert.Reader] {
|
||||
// // Simulate database check
|
||||
// if err := db.PingContext(ctx); err != nil {
|
||||
// return result.Error[assert.Reader](err)
|
||||
// }
|
||||
// return result.Of[assert.Reader](assert.NoError(nil))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIOResult(checkDatabase)
|
||||
// assertion(t)
|
||||
// }
|
||||
func FromReaderIOResult(ri ReaderIOResult[Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return F.Pipe1(
|
||||
ri(t.Context())(),
|
||||
result.GetOrElse(NoError),
|
||||
)(t)
|
||||
}
|
||||
}
|
||||
|
||||
// FromReaderIO converts a ReaderIO[Reader] into a Reader.
|
||||
//
|
||||
// This function bridges the gap between context-aware, IO-based computations (ReaderIO)
|
||||
// and the simpler Reader type used for test assertions. It executes the ReaderIO
|
||||
// computation using the test's context and returns the resulting Reader.
|
||||
//
|
||||
// Unlike FromReaderIOResult, this function does not handle errors explicitly - it assumes
|
||||
// the IO operation will succeed or that any errors are handled within the ReaderIO itself.
|
||||
//
|
||||
// The conversion process:
|
||||
// 1. Executes the ReaderIO with the test context (t.Context())
|
||||
// 2. Runs the resulting IO operation ()
|
||||
// 3. Returns a Reader that can be applied to *testing.T
|
||||
//
|
||||
// This is particularly useful when you have test assertions that need to:
|
||||
// - Access context for cancellation or deadlines
|
||||
// - Perform IO operations that don't fail (or handle failures internally)
|
||||
// - Integrate with context-aware testing utilities
|
||||
//
|
||||
// Parameters:
|
||||
// - ri: A ReaderIO that produces a Reader when given a context and executed
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that can be directly applied to *testing.T for assertion
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithIO(t *testing.T) {
|
||||
// // Create a ReaderIO that performs an IO operation
|
||||
// logAndCheck := func(ctx context.Context) func() assert.Reader {
|
||||
// return func() assert.Reader {
|
||||
// // Log something using context
|
||||
// logger.InfoContext(ctx, "Running test")
|
||||
// // Return an assertion
|
||||
// return assert.Equal(42)(computeValue())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIO(logAndCheck)
|
||||
// assertion(t)
|
||||
// }
|
||||
func FromReaderIO(ri ReaderIO[Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return ri(t.Context())()(t)
|
||||
}
|
||||
}
|
||||
383
v2/assert/from_test.go
Normal file
383
v2/assert/from_test.go
Normal file
@@ -0,0 +1,383 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package assert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func TestFromReaderIOResult(t *testing.T) {
|
||||
t.Run("should pass when ReaderIOResult returns success with passing assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns a successful Reader
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
// Return a Reader that always passes
|
||||
return result.Of[Reader](func(t *testing.T) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass when ReaderIOResult returns success")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass when ReaderIOResult returns success with Equal assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns a successful Equal assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](Equal(42)(42))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with Equal assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIOResult returns error", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIOResult that returns an error
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Left[Reader](errors.New("test error"))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIOResult to fail when ReaderIOResult returns error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIOResult returns success but assertion fails", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIOResult that returns a failing assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](Equal(42)(43))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIOResult to fail when assertion fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should use test context", func(t *testing.T) {
|
||||
contextUsed := false
|
||||
|
||||
// Create a ReaderIOResult that checks if context is provided
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
if ctx != nil {
|
||||
contextUsed = true
|
||||
}
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](func(t *testing.T) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
reader(t)
|
||||
|
||||
if !contextUsed {
|
||||
t.Error("Expected FromReaderIOResult to use test context")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with NoError assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns NoError assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](NoError(nil))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with NoError assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with complex assertions", func(t *testing.T) {
|
||||
// Create a ReaderIOResult with multiple composed assertions
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
arr := []int{1, 2, 3}
|
||||
assertions := AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](3)(arr),
|
||||
ArrayContains(2)(arr),
|
||||
})
|
||||
return result.Of[Reader](assertions)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with complex assertions")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromReaderIO(t *testing.T) {
|
||||
t.Run("should pass when ReaderIO returns passing assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns a Reader that always passes
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass when ReaderIO returns passing assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass when ReaderIO returns Equal assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns an Equal assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Equal(42)(42)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Equal assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIO returns failing assertion", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIO that returns a failing assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Equal(42)(43)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIO to fail when assertion fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should use test context", func(t *testing.T) {
|
||||
contextUsed := false
|
||||
|
||||
// Create a ReaderIO that checks if context is provided
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
if ctx != nil {
|
||||
contextUsed = true
|
||||
}
|
||||
return func() Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
reader(t)
|
||||
|
||||
if !contextUsed {
|
||||
t.Error("Expected FromReaderIO to use test context")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with NoError assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns NoError assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return NoError(nil)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with NoError assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Error assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns Error assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Error(errors.New("expected error"))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Error assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with complex assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with multiple composed assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
return AllOf([]Reader{
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
ContainsKey[int]("a")(mp),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with complex assertions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with string assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with string assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
str := "hello world"
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(str),
|
||||
StringLength[any, any](11)(str),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with string assertions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Result assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with Result assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
successResult := result.Of[int](42)
|
||||
return Success(successResult)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Success assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Failure assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO with Failure assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
failureResult := result.Left[int](errors.New("test error"))
|
||||
return Failure(failureResult)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Failure assertion")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromReaderIOResultIntegration tests integration scenarios
|
||||
func TestFromReaderIOResultIntegration(t *testing.T) {
|
||||
t.Run("should work in a realistic scenario with context cancellation", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that uses the context
|
||||
ri := func(testCtx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
// Check if context is valid
|
||||
if testCtx == nil {
|
||||
return result.Left[Reader](errors.New("context is nil"))
|
||||
}
|
||||
|
||||
// Return a successful assertion
|
||||
return result.Of[Reader](Equal("test")("test"))
|
||||
}
|
||||
}
|
||||
|
||||
// Use the actual testing.T from the subtest
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected integration test to pass")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromReaderIOIntegration tests integration scenarios
|
||||
func TestFromReaderIOIntegration(t *testing.T) {
|
||||
t.Run("should work in a realistic scenario with logging", func(t *testing.T) {
|
||||
logCalled := false
|
||||
|
||||
// Create a ReaderIO that simulates logging
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
// Simulate logging with context
|
||||
if ctx != nil {
|
||||
logCalled = true
|
||||
}
|
||||
|
||||
// Return an assertion
|
||||
return Equal(100)(100)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
|
||||
if !res {
|
||||
t.Error("Expected integration test to pass")
|
||||
}
|
||||
|
||||
if !logCalled {
|
||||
t.Error("Expected logging to be called")
|
||||
}
|
||||
})
|
||||
}
|
||||
207
v2/assert/logger.go
Normal file
207
v2/assert/logger.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// Logf creates a logging function that outputs formatted test messages using Go's testing.T.Logf.
|
||||
//
|
||||
// This function provides a functional programming approach to test logging, returning a
|
||||
// [ReaderIO] that can be composed with other test operations. It's particularly useful
|
||||
// for debugging tests, tracing execution flow, or documenting test behavior without
|
||||
// affecting test outcomes.
|
||||
//
|
||||
// The function uses a curried design pattern:
|
||||
// 1. First, you provide a format string (prefix) with format verbs (like %v, %d, %s)
|
||||
// 2. This returns a function that takes a value of type T
|
||||
// 3. That function returns a ReaderIO that performs the logging when executed
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - prefix: A format string compatible with fmt.Printf (e.g., "Value: %v", "Count: %d")
|
||||
// The format string should contain exactly one format verb that matches type T
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A function that takes a value of type T and returns a [ReaderIO][*testing.T, Void]
|
||||
// When executed, this ReaderIO logs the formatted message to the test output
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type of value to be logged. Can be any type that can be formatted by fmt
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Debugging test execution by logging intermediate values
|
||||
// - Tracing the flow of complex test scenarios
|
||||
// - Documenting test behavior in the test output
|
||||
// - Logging values in functional pipelines without breaking the chain
|
||||
// - Creating reusable logging operations for specific types
|
||||
//
|
||||
// # Example - Basic Logging
|
||||
//
|
||||
// func TestBasicLogging(t *testing.T) {
|
||||
// // Create a logger for integers
|
||||
// logInt := assert.Logf[int]("Processing value: %d")
|
||||
//
|
||||
// // Use it to log a value
|
||||
// value := 42
|
||||
// logInt(value)(t)() // Outputs: "Processing value: 42"
|
||||
// }
|
||||
//
|
||||
// # Example - Logging in Test Pipeline
|
||||
//
|
||||
// func TestPipelineWithLogging(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Alice", Age: 30}
|
||||
//
|
||||
// // Create a logger for User
|
||||
// logUser := assert.Logf[User]("Testing user: %+v")
|
||||
//
|
||||
// // Log the user being tested
|
||||
// logUser(user)(t)()
|
||||
//
|
||||
// // Continue with assertions
|
||||
// assert.StringNotEmpty(user.Name)(t)
|
||||
// assert.That(func(age int) bool { return age > 0 })(user.Age)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Multiple Loggers for Different Types
|
||||
//
|
||||
// func TestMultipleLoggers(t *testing.T) {
|
||||
// // Create type-specific loggers
|
||||
// logString := assert.Logf[string]("String value: %s")
|
||||
// logInt := assert.Logf[int]("Integer value: %d")
|
||||
// logFloat := assert.Logf[float64]("Float value: %.2f")
|
||||
//
|
||||
// // Use them throughout the test
|
||||
// logString("hello")(t)() // Outputs: "String value: hello"
|
||||
// logInt(42)(t)() // Outputs: "Integer value: 42"
|
||||
// logFloat(3.14159)(t)() // Outputs: "Float value: 3.14"
|
||||
// }
|
||||
//
|
||||
// # Example - Logging Complex Structures
|
||||
//
|
||||
// func TestComplexStructureLogging(t *testing.T) {
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// config := Config{Host: "localhost", Port: 8080, Timeout: 30}
|
||||
//
|
||||
// // Use %+v to include field names
|
||||
// logConfig := assert.Logf[Config]("Configuration: %+v")
|
||||
// logConfig(config)(t)()
|
||||
// // Outputs: "Configuration: {Host:localhost Port:8080 Timeout:30}"
|
||||
//
|
||||
// // Or use %#v for Go-syntax representation
|
||||
// logConfigGo := assert.Logf[Config]("Config (Go syntax): %#v")
|
||||
// logConfigGo(config)(t)()
|
||||
// // Outputs: "Config (Go syntax): assert.Config{Host:"localhost", Port:8080, Timeout:30}"
|
||||
// }
|
||||
//
|
||||
// # Example - Debugging Test Failures
|
||||
//
|
||||
// func TestWithDebugLogging(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// logSlice := assert.Logf[[]int]("Testing slice: %v")
|
||||
//
|
||||
// // Log the input data
|
||||
// logSlice(numbers)(t)()
|
||||
//
|
||||
// // Perform assertions
|
||||
// assert.ArrayNotEmpty(numbers)(t)
|
||||
// assert.ArrayLength[int](5)(numbers)(t)
|
||||
//
|
||||
// // Log intermediate results
|
||||
// sum := 0
|
||||
// for _, n := range numbers {
|
||||
// sum += n
|
||||
// }
|
||||
// logInt := assert.Logf[int]("Sum: %d")
|
||||
// logInt(sum)(t)()
|
||||
//
|
||||
// assert.Equal(15)(sum)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Conditional Logging
|
||||
//
|
||||
// func TestConditionalLogging(t *testing.T) {
|
||||
// logDebug := assert.Logf[string]("DEBUG: %s")
|
||||
//
|
||||
// values := []int{1, 2, 3, 4, 5}
|
||||
// for _, v := range values {
|
||||
// if v%2 == 0 {
|
||||
// logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
|
||||
// }
|
||||
// }
|
||||
// // Outputs:
|
||||
// // DEBUG: Found even number: 2
|
||||
// // DEBUG: Found even number: 4
|
||||
// }
|
||||
//
|
||||
// # Format Verbs
|
||||
//
|
||||
// Common format verbs you can use in the prefix string:
|
||||
// - %v: Default format
|
||||
// - %+v: Default format with field names for structs
|
||||
// - %#v: Go-syntax representation
|
||||
// - %T: Type of the value
|
||||
// - %d: Integer in base 10
|
||||
// - %s: String
|
||||
// - %f: Floating point number
|
||||
// - %t: Boolean (true/false)
|
||||
// - %p: Pointer address
|
||||
//
|
||||
// See the fmt package documentation for a complete list of format verbs.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Logging does not affect test pass/fail status
|
||||
// - Log output appears in test results when running with -v flag or when tests fail
|
||||
// - The function returns Void, indicating it's used for side effects only
|
||||
// - The ReaderIO pattern allows logging to be composed with other operations
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [FromReaderIO]: Converts ReaderIO operations into test assertions
|
||||
// - testing.T.Logf: The underlying Go testing log function
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go testing package: https://pkg.go.dev/testing
|
||||
// - fmt package format verbs: https://pkg.go.dev/fmt
|
||||
// - ReaderIO pattern: Combines Reader (context dependency) with IO (side effects)
|
||||
func Logf[T any](prefix string) func(T) readerio.ReaderIO[*testing.T, Void] {
|
||||
return func(a T) readerio.ReaderIO[*testing.T, Void] {
|
||||
return func(t *testing.T) IO[Void] {
|
||||
return io.FromImpure(func() {
|
||||
t.Logf(prefix, a)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
406
v2/assert/logger_test.go
Normal file
406
v2/assert/logger_test.go
Normal file
@@ -0,0 +1,406 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLogf_BasicInteger tests basic integer logging
|
||||
func TestLogf_BasicInteger(t *testing.T) {
|
||||
logInt := Logf[int]("Processing value: %d")
|
||||
|
||||
// This should not panic and should log the value
|
||||
logInt(42)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicString tests basic string logging
|
||||
func TestLogf_BasicString(t *testing.T) {
|
||||
logString := Logf[string]("String value: %s")
|
||||
|
||||
logString("hello world")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicFloat tests basic float logging
|
||||
func TestLogf_BasicFloat(t *testing.T) {
|
||||
logFloat := Logf[float64]("Float value: %.2f")
|
||||
|
||||
logFloat(3.14159)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicBoolean tests basic boolean logging
|
||||
func TestLogf_BasicBoolean(t *testing.T) {
|
||||
logBool := Logf[bool]("Boolean value: %t")
|
||||
|
||||
logBool(true)(t)()
|
||||
logBool(false)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ComplexStruct tests logging of complex structures
|
||||
func TestLogf_ComplexStruct(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
logUser := Logf[User]("User: %+v")
|
||||
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
logUser(user)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Slice tests logging of slices
|
||||
func TestLogf_Slice(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Slice: %v")
|
||||
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
logSlice(numbers)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Map tests logging of maps
|
||||
func TestLogf_Map(t *testing.T) {
|
||||
logMap := Logf[map[string]int]("Map: %v")
|
||||
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
logMap(data)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Pointer tests logging of pointers
|
||||
func TestLogf_Pointer(t *testing.T) {
|
||||
logPtr := Logf[*int]("Pointer: %p")
|
||||
|
||||
value := 42
|
||||
logPtr(&value)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_NilPointer tests logging of nil pointers
|
||||
func TestLogf_NilPointer(t *testing.T) {
|
||||
logPtr := Logf[*int]("Pointer: %v")
|
||||
|
||||
var nilPtr *int
|
||||
logPtr(nilPtr)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptyString tests logging of empty strings
|
||||
func TestLogf_EmptyString(t *testing.T) {
|
||||
logString := Logf[string]("String: '%s'")
|
||||
|
||||
logString("")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptySlice tests logging of empty slices
|
||||
func TestLogf_EmptySlice(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Slice: %v")
|
||||
|
||||
logSlice([]int{})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptyMap tests logging of empty maps
|
||||
func TestLogf_EmptyMap(t *testing.T) {
|
||||
logMap := Logf[map[string]int]("Map: %v")
|
||||
|
||||
logMap(map[string]int{})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_MultipleTypes tests using multiple loggers for different types
|
||||
func TestLogf_MultipleTypes(t *testing.T) {
|
||||
logString := Logf[string]("String: %s")
|
||||
logInt := Logf[int]("Integer: %d")
|
||||
logFloat := Logf[float64]("Float: %.2f")
|
||||
|
||||
logString("test")(t)()
|
||||
logInt(42)(t)()
|
||||
logFloat(3.14)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_WithinTestPipeline tests logging within a test pipeline
|
||||
func TestLogf_WithinTestPipeline(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
config := Config{Host: "localhost", Port: 8080}
|
||||
|
||||
logConfig := Logf[Config]("Testing config: %+v")
|
||||
logConfig(config)(t)()
|
||||
|
||||
// Continue with assertions
|
||||
StringNotEmpty(config.Host)(t)
|
||||
That(func(port int) bool { return port > 0 })(config.Port)(t)
|
||||
|
||||
// Test passes if no panic occurs and assertions pass
|
||||
}
|
||||
|
||||
// TestLogf_NestedStructures tests logging of nested structures
|
||||
func TestLogf_NestedStructures(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
logPerson := Logf[Person]("Person: %+v")
|
||||
|
||||
person := Person{
|
||||
Name: "Bob",
|
||||
Address: Address{
|
||||
Street: "123 Main St",
|
||||
City: "Springfield",
|
||||
},
|
||||
}
|
||||
|
||||
logPerson(person)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Interface tests logging of interface values
|
||||
func TestLogf_Interface(t *testing.T) {
|
||||
logAny := Logf[any]("Value: %v")
|
||||
|
||||
logAny(42)(t)()
|
||||
logAny("string")(t)()
|
||||
logAny([]int{1, 2, 3})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_GoSyntaxFormat tests logging with Go-syntax format
|
||||
func TestLogf_GoSyntaxFormat(t *testing.T) {
|
||||
type Point struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
logPoint := Logf[Point]("Point: %#v")
|
||||
|
||||
point := Point{X: 10, Y: 20}
|
||||
logPoint(point)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_TypeFormat tests logging with type format
|
||||
func TestLogf_TypeFormat(t *testing.T) {
|
||||
logType := Logf[any]("Type: %T, Value: %v")
|
||||
|
||||
logType(42)(t)()
|
||||
logType("string")(t)()
|
||||
logType(3.14)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_LargeNumbers tests logging of large numbers
|
||||
func TestLogf_LargeNumbers(t *testing.T) {
|
||||
logInt := Logf[int64]("Large number: %d")
|
||||
|
||||
logInt(9223372036854775807)(t)() // Max int64
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_NegativeNumbers tests logging of negative numbers
|
||||
func TestLogf_NegativeNumbers(t *testing.T) {
|
||||
logInt := Logf[int]("Number: %d")
|
||||
|
||||
logInt(-42)(t)()
|
||||
logInt(-100)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_SpecialFloats tests logging of special float values
|
||||
func TestLogf_SpecialFloats(t *testing.T) {
|
||||
logFloat := Logf[float64]("Float: %v")
|
||||
|
||||
logFloat(0.0)(t)()
|
||||
logFloat(-0.0)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_UnicodeStrings tests logging of unicode strings
|
||||
func TestLogf_UnicodeStrings(t *testing.T) {
|
||||
logString := Logf[string]("Unicode: %s")
|
||||
|
||||
logString("Hello, 世界")(t)()
|
||||
logString("Emoji: 🎉🎊")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_MultilineStrings tests logging of multiline strings
|
||||
func TestLogf_MultilineStrings(t *testing.T) {
|
||||
logString := Logf[string]("Multiline:\n%s")
|
||||
|
||||
multiline := `Line 1
|
||||
Line 2
|
||||
Line 3`
|
||||
|
||||
logString(multiline)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ReuseLogger tests reusing the same logger multiple times
|
||||
func TestLogf_ReuseLogger(t *testing.T) {
|
||||
logInt := Logf[int]("Value: %d")
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
logInt(i)(t)()
|
||||
}
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ConditionalLogging tests conditional logging based on values
|
||||
func TestLogf_ConditionalLogging(t *testing.T) {
|
||||
logDebug := Logf[string]("DEBUG: %s")
|
||||
|
||||
values := []int{1, 2, 3, 4, 5}
|
||||
for _, v := range values {
|
||||
if v%2 == 0 {
|
||||
logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
|
||||
}
|
||||
}
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_WithAssertions tests combining logging with assertions
|
||||
func TestLogf_WithAssertions(t *testing.T) {
|
||||
logInt := Logf[int]("Testing value: %d")
|
||||
|
||||
value := 42
|
||||
logInt(value)(t)()
|
||||
|
||||
// Perform assertion after logging
|
||||
Equal(42)(value)(t)
|
||||
|
||||
// Test passes if assertion passes
|
||||
}
|
||||
|
||||
// TestLogf_DebuggingFailures demonstrates using logging to debug test failures
|
||||
func TestLogf_DebuggingFailures(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Input slice: %v")
|
||||
logInt := Logf[int]("Computed sum: %d")
|
||||
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
logSlice(numbers)(t)()
|
||||
|
||||
sum := 0
|
||||
for _, n := range numbers {
|
||||
sum += n
|
||||
}
|
||||
logInt(sum)(t)()
|
||||
|
||||
Equal(15)(sum)(t)
|
||||
|
||||
// Test passes if assertion passes
|
||||
}
|
||||
|
||||
// TestLogf_ComplexDataStructures tests logging of complex nested data
|
||||
func TestLogf_ComplexDataStructures(t *testing.T) {
|
||||
type Metadata struct {
|
||||
Version string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
ID int
|
||||
Title string
|
||||
Metadata Metadata
|
||||
}
|
||||
|
||||
logDoc := Logf[Document]("Document: %+v")
|
||||
|
||||
doc := Document{
|
||||
ID: 1,
|
||||
Title: "Test Document",
|
||||
Metadata: Metadata{
|
||||
Version: "1.0",
|
||||
Tags: []string{"test", "example"},
|
||||
},
|
||||
}
|
||||
|
||||
logDoc(doc)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ArrayTypes tests logging of array types
|
||||
func TestLogf_ArrayTypes(t *testing.T) {
|
||||
logArray := Logf[[5]int]("Array: %v")
|
||||
|
||||
arr := [5]int{1, 2, 3, 4, 5}
|
||||
logArray(arr)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ChannelTypes tests logging of channel types
|
||||
func TestLogf_ChannelTypes(t *testing.T) {
|
||||
logChan := Logf[chan int]("Channel: %v")
|
||||
|
||||
ch := make(chan int, 1)
|
||||
logChan(ch)(t)()
|
||||
close(ch)
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_FunctionTypes tests logging of function types
|
||||
func TestLogf_FunctionTypes(t *testing.T) {
|
||||
logFunc := Logf[func() int]("Function: %v")
|
||||
|
||||
fn := func() int { return 42 }
|
||||
logFunc(fn)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
152
v2/assert/monoid.go
Normal file
152
v2/assert/monoid.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/boolean"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// ApplicativeMonoid returns a [monoid.Monoid] for combining test assertion [Reader]s.
|
||||
//
|
||||
// This monoid combines multiple test assertions using logical AND (conjunction) semantics,
|
||||
// meaning all assertions must pass for the combined assertion to pass. It leverages the
|
||||
// applicative structure of Reader to execute multiple assertions with the same testing.T
|
||||
// context and combines their boolean results using boolean.MonoidAll (logical AND).
|
||||
//
|
||||
// The monoid provides:
|
||||
// - Concat: Combines two assertions such that both must pass (logical AND)
|
||||
// - Empty: Returns an assertion that always passes (identity element)
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Composing multiple test assertions into a single assertion
|
||||
// - Building complex test conditions from simpler ones
|
||||
// - Creating reusable assertion combinators
|
||||
// - Implementing test assertion DSLs
|
||||
//
|
||||
// # Monoid Laws
|
||||
//
|
||||
// The returned monoid satisfies the standard monoid laws:
|
||||
//
|
||||
// 1. Associativity:
|
||||
// Concat(Concat(a1, a2), a3) ≡ Concat(a1, Concat(a2, a3))
|
||||
//
|
||||
// 2. Left Identity:
|
||||
// Concat(Empty(), a) ≡ a
|
||||
//
|
||||
// 3. Right Identity:
|
||||
// Concat(a, Empty()) ≡ a
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [monoid.Monoid][Reader] that combines assertions using logical AND
|
||||
//
|
||||
// # Example - Basic Usage
|
||||
//
|
||||
// func TestUserValidation(t *testing.T) {
|
||||
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
// m := assert.ApplicativeMonoid()
|
||||
//
|
||||
// // Combine multiple assertions
|
||||
// assertion := m.Concat(
|
||||
// assert.Equal("Alice")(user.Name),
|
||||
// m.Concat(
|
||||
// assert.Equal(30)(user.Age),
|
||||
// assert.StringNotEmpty(user.Email),
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// // Execute combined assertion
|
||||
// assertion(t) // All three assertions must pass
|
||||
// }
|
||||
//
|
||||
// # Example - Building Reusable Validators
|
||||
//
|
||||
// func TestWithReusableValidators(t *testing.T) {
|
||||
// m := assert.ApplicativeMonoid()
|
||||
//
|
||||
// // Create a reusable validator
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return m.Concat(
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// m.Concat(
|
||||
// assert.True(u.Age > 0),
|
||||
// assert.StringContains("@")(u.Email),
|
||||
// ),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Bob", Age: 25, Email: "bob@test.com"}
|
||||
// validateUser(user)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Using Empty for Identity
|
||||
//
|
||||
// func TestEmptyIdentity(t *testing.T) {
|
||||
// m := assert.ApplicativeMonoid()
|
||||
// assertion := assert.Equal(42)(42)
|
||||
//
|
||||
// // Empty is the identity - these are equivalent
|
||||
// result1 := m.Concat(m.Empty(), assertion)(t)
|
||||
// result2 := m.Concat(assertion, m.Empty())(t)
|
||||
// result3 := assertion(t)
|
||||
// // All three produce the same result
|
||||
// }
|
||||
//
|
||||
// # Example - Combining with AllOf
|
||||
//
|
||||
// func TestCombiningWithAllOf(t *testing.T) {
|
||||
// // ApplicativeMonoid provides the underlying mechanism for AllOf
|
||||
// arr := []int{1, 2, 3, 4, 5}
|
||||
//
|
||||
// // These are conceptually equivalent:
|
||||
// m := assert.ApplicativeMonoid()
|
||||
// manual := m.Concat(
|
||||
// assert.ArrayNotEmpty(arr),
|
||||
// m.Concat(
|
||||
// assert.ArrayLength[int](5)(arr),
|
||||
// assert.ArrayContains(3)(arr),
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// // AllOf uses ApplicativeMonoid internally
|
||||
// convenient := assert.AllOf([]assert.Reader{
|
||||
// assert.ArrayNotEmpty(arr),
|
||||
// assert.ArrayLength[int](5)(arr),
|
||||
// assert.ArrayContains(3)(arr),
|
||||
// })
|
||||
//
|
||||
// manual(t)
|
||||
// convenient(t)
|
||||
// }
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [AllOf]: Convenient wrapper for combining multiple assertions using this monoid
|
||||
// - [boolean.MonoidAll]: The underlying boolean monoid (logical AND with true as identity)
|
||||
// - [reader.ApplicativeMonoid]: Generic applicative monoid for Reader types
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
|
||||
// - Applicative Functors: https://hackage.haskell.org/package/base/docs/Control-Applicative.html
|
||||
// - Boolean Monoid (All): https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:All
|
||||
func ApplicativeMonoid() monoid.Monoid[Reader] {
|
||||
return reader.ApplicativeMonoid[*testing.T](boolean.MonoidAll)
|
||||
}
|
||||
454
v2/assert/monoid_test.go
Normal file
454
v2/assert/monoid_test.go
Normal file
@@ -0,0 +1,454 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestApplicativeMonoid_Empty tests that Empty returns an assertion that always passes
|
||||
func TestApplicativeMonoid_Empty(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
empty := m.Empty()
|
||||
|
||||
result := empty(t)
|
||||
if !result {
|
||||
t.Error("Expected Empty() to return an assertion that always passes")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_BothPass tests that Concat returns true when both assertions pass
|
||||
func TestApplicativeMonoid_Concat_BothPass(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(42)
|
||||
assertion2 := Equal("hello")("hello")
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected Concat to pass when both assertions pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_FirstFails tests that Concat returns false when first assertion fails
|
||||
func TestApplicativeMonoid_Concat_FirstFails(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(43) // This will fail
|
||||
assertion2 := Equal("hello")("hello")
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when first assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_SecondFails tests that Concat returns false when second assertion fails
|
||||
func TestApplicativeMonoid_Concat_SecondFails(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(42)
|
||||
assertion2 := Equal("hello")("world") // This will fail
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when second assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_BothFail tests that Concat returns false when both assertions fail
|
||||
func TestApplicativeMonoid_Concat_BothFail(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(43) // This will fail
|
||||
assertion2 := Equal("hello")("world") // This will fail
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when both assertions fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_LeftIdentity tests the left identity law: Concat(Empty(), a) = a
|
||||
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Concat(Empty(), assertion) should behave the same as assertion
|
||||
combined := m.Concat(m.Empty(), assertion)
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Left identity law violated: Concat(Empty(), a) should equal a")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RightIdentity tests the right identity law: Concat(a, Empty()) = a
|
||||
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Concat(assertion, Empty()) should behave the same as assertion
|
||||
combined := m.Concat(assertion, m.Empty())
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Right identity law violated: Concat(a, Empty()) should equal a")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Associativity tests the associativity law: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
|
||||
func TestApplicativeMonoid_Associativity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(2)
|
||||
a3 := Equal(3)(3)
|
||||
|
||||
// Concat(Concat(a1, a2), a3)
|
||||
left := m.Concat(m.Concat(a1, a2), a3)
|
||||
|
||||
// Concat(a1, Concat(a2, a3))
|
||||
right := m.Concat(a1, m.Concat(a2, a3))
|
||||
|
||||
result1 := left(t)
|
||||
result2 := right(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Associativity law violated: Concat(Concat(a, b), c) should equal Concat(a, Concat(b, c))")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_AssociativityWithFailure tests associativity when assertions fail
|
||||
func TestApplicativeMonoid_AssociativityWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(3) // This will fail
|
||||
a3 := Equal(3)(3)
|
||||
|
||||
// Concat(Concat(a1, a2), a3)
|
||||
left := m.Concat(m.Concat(a1, a2), a3)
|
||||
|
||||
// Concat(a1, Concat(a2, a3))
|
||||
right := m.Concat(a1, m.Concat(a2, a3))
|
||||
|
||||
result1 := left(mockT)
|
||||
result2 := right(mockT)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Associativity law violated even with failures")
|
||||
}
|
||||
|
||||
if result1 || result2 {
|
||||
t.Error("Expected both to fail when one assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ComplexAssertions tests combining complex assertions
|
||||
func TestApplicativeMonoid_ComplexAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
|
||||
arrayAssertions := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
mapAssertions := m.Concat(
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
)
|
||||
|
||||
combined := m.Concat(arrayAssertions, mapAssertions)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected complex combined assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ComplexAssertionsWithFailure tests complex assertions when one fails
|
||||
func TestApplicativeMonoid_ComplexAssertionsWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
arr := []int{1, 2, 3}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
|
||||
arrayAssertions := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr), // This will fail - array has 3 elements, not 5
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
mapAssertions := m.Concat(
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
)
|
||||
|
||||
combined := m.Concat(arrayAssertions, mapAssertions)
|
||||
|
||||
result := combined(mockT)
|
||||
if result {
|
||||
t.Error("Expected complex combined assertions to fail when one assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_MultipleConcat tests chaining multiple Concat operations
|
||||
func TestApplicativeMonoid_MultipleConcat(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(2)
|
||||
a3 := Equal(3)(3)
|
||||
a4 := Equal(4)(4)
|
||||
|
||||
combined := m.Concat(
|
||||
m.Concat(a1, a2),
|
||||
m.Concat(a3, a4),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected multiple Concat operations to pass when all assertions pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithStringAssertions tests combining string assertions
|
||||
func TestApplicativeMonoid_WithStringAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
str := "hello world"
|
||||
|
||||
combined := m.Concat(
|
||||
StringNotEmpty(str),
|
||||
StringLength[any, any](11)(str),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected string assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithBooleanAssertions tests combining boolean assertions
|
||||
func TestApplicativeMonoid_WithBooleanAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
combined := m.Concat(
|
||||
Equal(true)(true),
|
||||
m.Concat(
|
||||
Equal(false)(false),
|
||||
Equal(true)(true),
|
||||
),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected boolean assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithErrorAssertions tests combining error assertions
|
||||
func TestApplicativeMonoid_WithErrorAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
combined := m.Concat(
|
||||
NoError(nil),
|
||||
Equal("test")("test"),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected error assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_EmptyWithMultipleConcat tests Empty with multiple Concat operations
|
||||
func TestApplicativeMonoid_EmptyWithMultipleConcat(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Multiple Empty values should still act as identity
|
||||
combined := m.Concat(
|
||||
m.Empty(),
|
||||
m.Concat(
|
||||
assertion,
|
||||
m.Empty(),
|
||||
),
|
||||
)
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Multiple Empty values should still act as identity")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_OnlyEmpty tests using only Empty values
|
||||
func TestApplicativeMonoid_OnlyEmpty(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
// Concat of Empty values should still be Empty (identity)
|
||||
combined := m.Concat(m.Empty(), m.Empty())
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected Concat of Empty values to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RealWorldExample tests a realistic use case
|
||||
func TestApplicativeMonoid_RealWorldExample(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
validateUser := func(u User) Reader {
|
||||
return m.Concat(
|
||||
StringNotEmpty(u.Name),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age < 150 })(u.Age),
|
||||
That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(u.Email),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
validUser := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
result := validateUser(validUser)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected valid user to pass all validations")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RealWorldExampleWithFailure tests a realistic use case with failure
|
||||
func TestApplicativeMonoid_RealWorldExampleWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
validateUser := func(u User) Reader {
|
||||
return m.Concat(
|
||||
StringNotEmpty(u.Name),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age < 150 })(u.Age),
|
||||
That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(u.Email),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
invalidUser := User{Name: "Bob", Age: 200, Email: "bob@test.com"} // Age > 150
|
||||
result := validateUser(invalidUser)(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected invalid user to fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_IntegrationWithAllOf demonstrates relationship with AllOf
|
||||
func TestApplicativeMonoid_IntegrationWithAllOf(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
// Using ApplicativeMonoid directly
|
||||
manualCombination := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
// Using AllOf (which uses ApplicativeMonoid internally)
|
||||
allOfCombination := AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
})
|
||||
|
||||
result1 := manualCombination(t)
|
||||
result2 := allOfCombination(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Expected manual combination and AllOf to produce same result")
|
||||
}
|
||||
|
||||
if !result1 || !result2 {
|
||||
t.Error("Expected both combinations to pass")
|
||||
}
|
||||
}
|
||||
650
v2/assert/traverse.go
Normal file
650
v2/assert/traverse.go
Normal file
@@ -0,0 +1,650 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package assert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// TraverseArray transforms an array of values into a test suite by applying a function
|
||||
// that generates named test cases for each element.
|
||||
//
|
||||
// This function enables data-driven testing where you have a collection of test inputs
|
||||
// and want to run a named subtest for each one. It follows the functional programming
|
||||
// pattern of "traverse" - transforming a collection while preserving structure and
|
||||
// accumulating effects (in this case, test execution).
|
||||
//
|
||||
// The function takes each element of the array, applies the provided function to generate
|
||||
// a [Pair] of (test name, test assertion), and runs each as a separate subtest using
|
||||
// Go's t.Run. All subtests must pass for the overall test to pass.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes a value of type T and returns a [Pair] containing:
|
||||
// - Head: The test name (string) for the subtest
|
||||
// - Tail: The test assertion ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Kleisli] function that takes an array of T and returns a [Reader] that:
|
||||
// - Executes each element as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation and reporting via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Data-driven testing with multiple test cases
|
||||
// - Parameterized tests where each parameter gets its own subtest
|
||||
// - Testing collections where each element needs validation
|
||||
// - Property-based testing with generated test data
|
||||
//
|
||||
// # Example - Basic Data-Driven Testing
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Input int
|
||||
// Expected int
|
||||
// }
|
||||
//
|
||||
// testCases := []TestCase{
|
||||
// {Input: 2, Expected: 4},
|
||||
// {Input: 3, Expected: 9},
|
||||
// {Input: 4, Expected: 16},
|
||||
// }
|
||||
//
|
||||
// square := func(n int) int { return n * n }
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
|
||||
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
|
||||
// assertion := assert.Equal(tc.Expected)(square(tc.Input))
|
||||
// return pair.MakePair(name, assertion)
|
||||
// })
|
||||
//
|
||||
// traverse(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - String Validation
|
||||
//
|
||||
// func TestStringValidation(t *testing.T) {
|
||||
// inputs := []string{"hello", "world", "test"}
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(s string) assert.Pair[string, assert.Reader] {
|
||||
// return pair.MakePair(
|
||||
// fmt.Sprintf("validate_%s", s),
|
||||
// assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(s),
|
||||
// assert.That(func(str string) bool { return len(str) > 0 })(s),
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// traverse(inputs)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Complex Object Testing
|
||||
//
|
||||
// func TestUsers(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// users := []User{
|
||||
// {Name: "Alice", Age: 30, Email: "alice@example.com"},
|
||||
// {Name: "Bob", Age: 25, Email: "bob@example.com"},
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(u User) assert.Pair[string, assert.Reader] {
|
||||
// return pair.MakePair(
|
||||
// fmt.Sprintf("user_%s", u.Name),
|
||||
// assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// assert.That(func(age int) bool { return age > 0 })(u.Age),
|
||||
// assert.That(func(email string) bool {
|
||||
// return len(email) > 0 && strings.Contains(email, "@")
|
||||
// })(u.Email),
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// traverse(users)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with RunAll
|
||||
//
|
||||
// TraverseArray and [RunAll] serve similar purposes but differ in their approach:
|
||||
//
|
||||
// - TraverseArray: Generates test cases from an array of data
|
||||
//
|
||||
// - Input: Array of values + function to generate test cases
|
||||
//
|
||||
// - Use when: You have test data and need to generate test cases from it
|
||||
//
|
||||
// - RunAll: Executes pre-defined named test cases
|
||||
//
|
||||
// - Input: Map of test names to assertions
|
||||
//
|
||||
// - Use when: You have already defined test cases with names
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [SequenceSeq2]: Similar but works with Go iterators (Seq2) instead of arrays
|
||||
// - [RunAll]: Executes a map of named test cases
|
||||
// - [AllOf]: Combines multiple assertions without subtests
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
func TraverseArray[T any](f func(T) Pair[string, Reader]) Kleisli[[]T] {
|
||||
return func(ts []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for _, src := range ts {
|
||||
test := f(src)
|
||||
res := t.Run(pair.Head(test), func(t *testing.T) {
|
||||
pair.Tail(test)(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceSeq2 executes a sequence of named test cases provided as a Go iterator.
|
||||
//
|
||||
// This function takes a [Seq2] iterator that yields (name, assertion) pairs and
|
||||
// executes each as a separate subtest using Go's t.Run. It's similar to [TraverseArray]
|
||||
// but works directly with Go's iterator protocol (introduced in Go 1.23) rather than
|
||||
// requiring an array.
|
||||
//
|
||||
// The function iterates through all test cases, running each as a named subtest.
|
||||
// All subtests must pass for the overall test to pass. This provides proper test
|
||||
// isolation and clear reporting of which specific test cases fail.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - s: A [Seq2] iterator that yields pairs of:
|
||||
// - Key: Test name (string) for the subtest
|
||||
// - Value: Test assertion ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Reader] that:
|
||||
// - Executes each test case as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Working with iterator-based test data
|
||||
// - Lazy evaluation of test cases
|
||||
// - Integration with Go 1.23+ iterator patterns
|
||||
// - Memory-efficient testing of large test suites
|
||||
//
|
||||
// # Example - Basic Usage with Iterator
|
||||
//
|
||||
// func TestWithIterator(t *testing.T) {
|
||||
// // Create an iterator of test cases
|
||||
// testCases := func(yield func(string, assert.Reader) bool) {
|
||||
// if !yield("test_addition", assert.Equal(4)(2+2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_subtraction", assert.Equal(1)(3-2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Generated Test Cases
|
||||
//
|
||||
// func TestGeneratedCases(t *testing.T) {
|
||||
// // Generate test cases on the fly
|
||||
// generateTests := func(yield func(string, assert.Reader) bool) {
|
||||
// for i := 1; i <= 5; i++ {
|
||||
// name := fmt.Sprintf("test_%d", i)
|
||||
// assertion := assert.Equal(i*i)(i * i)
|
||||
// if !yield(name, assertion) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(generateTests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Filtering Test Cases
|
||||
//
|
||||
// func TestFilteredCases(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Name string
|
||||
// Input int
|
||||
// Expected int
|
||||
// Skip bool
|
||||
// }
|
||||
//
|
||||
// allCases := []TestCase{
|
||||
// {Name: "test1", Input: 2, Expected: 4, Skip: false},
|
||||
// {Name: "test2", Input: 3, Expected: 9, Skip: true},
|
||||
// {Name: "test3", Input: 4, Expected: 16, Skip: false},
|
||||
// }
|
||||
//
|
||||
// // Create iterator that filters out skipped tests
|
||||
// activeTests := func(yield func(string, assert.Reader) bool) {
|
||||
// for _, tc := range allCases {
|
||||
// if !tc.Skip {
|
||||
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
// if !yield(tc.Name, assertion) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(activeTests)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseArray
|
||||
//
|
||||
// SequenceSeq2 and [TraverseArray] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - SequenceSeq2: Works with iterators (Seq2)
|
||||
//
|
||||
// - Input: Iterator yielding (name, assertion) pairs
|
||||
//
|
||||
// - Use when: Working with Go 1.23+ iterators or lazy evaluation
|
||||
//
|
||||
// - Memory: More efficient for large test suites (lazy evaluation)
|
||||
//
|
||||
// - TraverseArray: Works with arrays
|
||||
//
|
||||
// - Input: Array of values + transformation function
|
||||
//
|
||||
// - Use when: You have an array of test data
|
||||
//
|
||||
// - Memory: All test data must be in memory
|
||||
//
|
||||
// # Comparison with RunAll
|
||||
//
|
||||
// SequenceSeq2 and [RunAll] are very similar:
|
||||
//
|
||||
// - SequenceSeq2: Takes an iterator (Seq2)
|
||||
// - RunAll: Takes a map[string]Reader
|
||||
//
|
||||
// Both execute named test cases as subtests. Choose based on your data structure:
|
||||
// use SequenceSeq2 for iterators, RunAll for maps.
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [TraverseArray]: Similar but works with arrays instead of iterators
|
||||
// - [RunAll]: Executes a map of named test cases
|
||||
// - [AllOf]: Combines multiple assertions without subtests
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go iterators: https://go.dev/blog/range-functions
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
|
||||
func SequenceSeq2[T any](s Seq2[string, Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for name, test := range s {
|
||||
res := t.Run(name, func(t *testing.T) {
|
||||
test(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseRecord transforms a map of values into a test suite by applying a function
|
||||
// that generates test assertions for each map entry.
|
||||
//
|
||||
// This function enables data-driven testing where you have a map of test data and want
|
||||
// to run a named subtest for each entry. The map keys become test names, and the function
|
||||
// transforms each value into a test assertion. It follows the functional programming
|
||||
// pattern of "traverse" - transforming a collection while preserving structure and
|
||||
// accumulating effects (in this case, test execution).
|
||||
//
|
||||
// The function takes each key-value pair from the map, applies the provided function to
|
||||
// generate a [Reader] assertion, and runs each as a separate subtest using Go's t.Run.
|
||||
// All subtests must pass for the overall test to pass.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A [Kleisli] function that takes a value of type T and returns a [Reader] assertion
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Kleisli] function that takes a map[string]T and returns a [Reader] that:
|
||||
// - Executes each map entry as a named subtest (using the key as the test name)
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation and reporting via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Data-driven testing with named test cases in a map
|
||||
// - Testing configuration maps where keys are meaningful names
|
||||
// - Validating collections where natural keys exist
|
||||
// - Property-based testing with named scenarios
|
||||
//
|
||||
// # Example - Basic Configuration Testing
|
||||
//
|
||||
// func TestConfigurations(t *testing.T) {
|
||||
// configs := map[string]int{
|
||||
// "timeout": 30,
|
||||
// "maxRetries": 3,
|
||||
// "bufferSize": 1024,
|
||||
// }
|
||||
//
|
||||
// validatePositive := assert.That(func(n int) bool { return n > 0 })
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validatePositive)
|
||||
// traverse(configs)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - User Validation
|
||||
//
|
||||
// func TestUserMap(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// users := map[string]User{
|
||||
// "alice": {Name: "Alice", Age: 30},
|
||||
// "bob": {Name: "Bob", Age: 25},
|
||||
// "carol": {Name: "Carol", Age: 35},
|
||||
// }
|
||||
//
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// assert.That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validateUser)
|
||||
// traverse(users)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - API Endpoint Testing
|
||||
//
|
||||
// func TestEndpoints(t *testing.T) {
|
||||
// type Endpoint struct {
|
||||
// Path string
|
||||
// Method string
|
||||
// }
|
||||
//
|
||||
// endpoints := map[string]Endpoint{
|
||||
// "get_users": {Path: "/api/users", Method: "GET"},
|
||||
// "create_user": {Path: "/api/users", Method: "POST"},
|
||||
// "delete_user": {Path: "/api/users/:id", Method: "DELETE"},
|
||||
// }
|
||||
//
|
||||
// validateEndpoint := func(e Endpoint) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(e.Path),
|
||||
// assert.That(func(path string) bool {
|
||||
// return strings.HasPrefix(path, "/api/")
|
||||
// })(e.Path),
|
||||
// assert.That(func(method string) bool {
|
||||
// return method == "GET" || method == "POST" ||
|
||||
// method == "PUT" || method == "DELETE"
|
||||
// })(e.Method),
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validateEndpoint)
|
||||
// traverse(endpoints)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseArray
|
||||
//
|
||||
// TraverseRecord and [TraverseArray] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - TraverseRecord: Works with maps (records)
|
||||
//
|
||||
// - Input: Map with string keys + transformation function
|
||||
//
|
||||
// - Use when: You have named test data in a map
|
||||
//
|
||||
// - Test names: Derived from map keys
|
||||
//
|
||||
// - TraverseArray: Works with arrays
|
||||
//
|
||||
// - Input: Array of values + function that generates names and assertions
|
||||
//
|
||||
// - Use when: You have sequential test data
|
||||
//
|
||||
// - Test names: Generated by the transformation function
|
||||
//
|
||||
// # Comparison with SequenceRecord
|
||||
//
|
||||
// TraverseRecord and [SequenceRecord] are closely related:
|
||||
//
|
||||
// - TraverseRecord: Transforms values into assertions
|
||||
//
|
||||
// - Input: map[string]T + function T -> Reader
|
||||
//
|
||||
// - Use when: You need to transform data before asserting
|
||||
//
|
||||
// - SequenceRecord: Executes pre-defined assertions
|
||||
//
|
||||
// - Input: map[string]Reader
|
||||
//
|
||||
// - Use when: Assertions are already defined
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [SequenceRecord]: Similar but takes pre-defined assertions
|
||||
// - [TraverseArray]: Similar but works with arrays
|
||||
// - [RunAll]: Alias for SequenceRecord
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
func TraverseRecord[T any](f Kleisli[T]) Kleisli[map[string]T] {
|
||||
return func(m map[string]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for name, src := range m {
|
||||
res := t.Run(name, func(t *testing.T) {
|
||||
f(src)(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceRecord executes a map of named test cases as subtests.
|
||||
//
|
||||
// This function takes a map where keys are test names and values are test assertions
|
||||
// ([Reader]), and executes each as a separate subtest using Go's t.Run. It's the
|
||||
// record (map) equivalent of [SequenceSeq2] and is actually aliased as [RunAll] for
|
||||
// convenience.
|
||||
//
|
||||
// The function iterates through all map entries, running each as a named subtest.
|
||||
// All subtests must pass for the overall test to pass. This provides proper test
|
||||
// isolation and clear reporting of which specific test cases fail.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: A map[string]Reader where:
|
||||
// - Keys: Test names (strings) for the subtests
|
||||
// - Values: Test assertions ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Reader] that:
|
||||
// - Executes each map entry as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Executing a collection of pre-defined named test cases
|
||||
// - Organizing related tests in a map structure
|
||||
// - Running multiple assertions with descriptive names
|
||||
// - Building test suites programmatically
|
||||
//
|
||||
// # Example - Basic Named Tests
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// tests := map[string]assert.Reader{
|
||||
// "addition": assert.Equal(4)(2 + 2),
|
||||
// "subtraction": assert.Equal(1)(3 - 2),
|
||||
// "multiplication": assert.Equal(6)(2 * 3),
|
||||
// "division": assert.Equal(2)(6 / 3),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - String Validation Suite
|
||||
//
|
||||
// func TestStringValidations(t *testing.T) {
|
||||
// testString := "hello world"
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "not_empty": assert.StringNotEmpty(testString),
|
||||
// "correct_length": assert.StringLength[any, any](11)(testString),
|
||||
// "has_space": assert.That(func(s string) bool {
|
||||
// return strings.Contains(s, " ")
|
||||
// })(testString),
|
||||
// "lowercase": assert.That(func(s string) bool {
|
||||
// return s == strings.ToLower(s)
|
||||
// })(testString),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Complex Object Validation
|
||||
//
|
||||
// func TestUserValidation(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "name_not_empty": assert.StringNotEmpty(user.Name),
|
||||
// "age_positive": assert.That(func(age int) bool { return age > 0 })(user.Age),
|
||||
// "age_reasonable": assert.That(func(age int) bool { return age < 150 })(user.Age),
|
||||
// "email_valid": assert.That(func(email string) bool {
|
||||
// return strings.Contains(email, "@") && strings.Contains(email, ".")
|
||||
// })(user.Email),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Array Validation Suite
|
||||
//
|
||||
// func TestArrayValidations(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "not_empty": assert.ArrayNotEmpty(numbers),
|
||||
// "correct_length": assert.ArrayLength[int](5)(numbers),
|
||||
// "contains_three": assert.ArrayContains(3)(numbers),
|
||||
// "all_positive": assert.That(func(arr []int) bool {
|
||||
// for _, n := range arr {
|
||||
// if n <= 0 {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// })(numbers),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseRecord
|
||||
//
|
||||
// SequenceRecord and [TraverseRecord] are closely related:
|
||||
//
|
||||
// - SequenceRecord: Executes pre-defined assertions
|
||||
//
|
||||
// - Input: map[string]Reader (assertions already created)
|
||||
//
|
||||
// - Use when: You have already defined test cases with assertions
|
||||
//
|
||||
// - TraverseRecord: Transforms values into assertions
|
||||
//
|
||||
// - Input: map[string]T + function T -> Reader
|
||||
//
|
||||
// - Use when: You need to transform data before asserting
|
||||
//
|
||||
// # Comparison with SequenceSeq2
|
||||
//
|
||||
// SequenceRecord and [SequenceSeq2] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - SequenceRecord: Works with maps
|
||||
//
|
||||
// - Input: map[string]Reader
|
||||
//
|
||||
// - Use when: You have named test cases in a map
|
||||
//
|
||||
// - Iteration order: Non-deterministic (map iteration)
|
||||
//
|
||||
// - SequenceSeq2: Works with iterators
|
||||
//
|
||||
// - Input: Seq2[string, Reader]
|
||||
//
|
||||
// - Use when: You have test cases in an iterator
|
||||
//
|
||||
// - Iteration order: Deterministic (iterator order)
|
||||
//
|
||||
// # Note on Map Iteration Order
|
||||
//
|
||||
// Go maps have non-deterministic iteration order. If test execution order matters,
|
||||
// consider using [SequenceSeq2] with an iterator that provides deterministic ordering,
|
||||
// or use [TraverseArray] with a slice of test cases.
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [RunAll]: Alias for SequenceRecord
|
||||
// - [TraverseRecord]: Similar but transforms values into assertions
|
||||
// - [SequenceSeq2]: Similar but works with iterators
|
||||
// - [TraverseArray]: Similar but works with arrays
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
|
||||
func SequenceRecord(m map[string]Reader) Reader {
|
||||
return TraverseRecord(reader.Ask[Reader]())(m)
|
||||
}
|
||||
960
v2/assert/traverse_test.go
Normal file
960
v2/assert/traverse_test.go
Normal file
@@ -0,0 +1,960 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// TestTraverseArray_EmptyArray tests that TraverseArray handles empty arrays correctly
|
||||
func TestTraverseArray_EmptyArray(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(n)(n),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with empty array")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_SingleElement tests TraverseArray with a single element
|
||||
func TestTraverseArray_SingleElement(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(n*2)(n*2),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with single element")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_MultipleElements tests TraverseArray with multiple passing elements
|
||||
func TestTraverseArray_MultipleElements(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("square_%d", n),
|
||||
Equal(n*n)(n*n),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{1, 2, 3, 4, 5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with all passing elements")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_WithFailure tests that TraverseArray fails when one element fails
|
||||
func TestTraverseArray_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(10)(n), // Will fail for all except 10
|
||||
)
|
||||
})
|
||||
|
||||
// Run in a subtest - we expect the subtests to fail, so t.Run returns false
|
||||
result := traverse([]int{1, 2, 3})(t)
|
||||
|
||||
// The traverse should return false because assertions fail
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when elements don't match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_MixedResults tests TraverseArray with some passing and some failing
|
||||
func TestTraverseArray_MixedResults(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("is_even_%d", n),
|
||||
Equal(0)(n%2), // Only passes for even numbers
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{2, 3, 4})(t) // 3 is odd, should fail
|
||||
|
||||
// The traverse should return false because one assertion fails
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when some elements fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_StringData tests TraverseArray with string data
|
||||
func TestTraverseArray_StringData(t *testing.T) {
|
||||
words := []string{"hello", "world", "test"}
|
||||
|
||||
traverse := TraverseArray(func(s string) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("validate_%s", s),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(s),
|
||||
That(func(str string) bool { return len(str) > 0 })(s),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(words)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with valid strings")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_ComplexObjects tests TraverseArray with complex objects
|
||||
func TestTraverseArray_ComplexObjects(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "Bob", Age: 25},
|
||||
{Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseArray(func(u User) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("user_%s", u.Name),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with valid users")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_ComplexObjectsWithFailure tests TraverseArray with invalid complex objects
|
||||
func TestTraverseArray_ComplexObjectsWithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "", Age: 25}, // Invalid: empty name
|
||||
{Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseArray(func(u User) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("user_%s", u.Name),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
|
||||
// The traverse should return false because one user is invalid
|
||||
if result {
|
||||
t.Error("Expected traverse to return false with invalid user")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_DataDrivenTesting demonstrates data-driven testing pattern
|
||||
func TestTraverseArray_DataDrivenTesting(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testCases := []TestCase{
|
||||
{Input: 2, Expected: 4},
|
||||
{Input: 3, Expected: 9},
|
||||
{Input: 4, Expected: 16},
|
||||
{Input: 5, Expected: 25},
|
||||
}
|
||||
|
||||
square := func(n int) int { return n * n }
|
||||
|
||||
traverse := TraverseArray(func(tc TestCase) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected),
|
||||
Equal(tc.Expected)(square(tc.Input)),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(testCases)(t)
|
||||
if !result {
|
||||
t.Error("Expected all test cases to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_EmptySequence tests that SequenceSeq2 handles empty sequences correctly
|
||||
func TestSequenceSeq2_EmptySequence(t *testing.T) {
|
||||
emptySeq := func(yield func(string, Reader) bool) {
|
||||
// Empty - yields nothing
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](emptySeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with empty sequence")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_SingleTest tests SequenceSeq2 with a single test
|
||||
func TestSequenceSeq2_SingleTest(t *testing.T) {
|
||||
singleSeq := func(yield func(string, Reader) bool) {
|
||||
yield("test_one", Equal(42)(42))
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](singleSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with single test")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_MultipleTests tests SequenceSeq2 with multiple passing tests
|
||||
func TestSequenceSeq2_MultipleTests(t *testing.T) {
|
||||
multiSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_addition", Equal(4)(2+2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_subtraction", Equal(1)(3-2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_multiplication", Equal(6)(2*3)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](multiSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with all passing tests")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_WithFailure tests that SequenceSeq2 fails when one test fails
|
||||
func TestSequenceSeq2_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
failSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_pass", Equal(4)(2+2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_fail", Equal(5)(2+2)) { // This will fail
|
||||
return
|
||||
}
|
||||
if !yield("test_pass2", Equal(6)(2*3)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](failSeq)(t)
|
||||
|
||||
// The sequence should return false because one test fails
|
||||
if result {
|
||||
t.Error("Expected sequence to return false when one test fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_GeneratedTests tests SequenceSeq2 with generated test cases
|
||||
func TestSequenceSeq2_GeneratedTests(t *testing.T) {
|
||||
generateTests := func(yield func(string, Reader) bool) {
|
||||
for i := 1; i <= 5; i++ {
|
||||
name := fmt.Sprintf("test_%d", i)
|
||||
assertion := Equal(i * i)(i * i)
|
||||
if !yield(name, assertion) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](generateTests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all generated tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_StringTests tests SequenceSeq2 with string assertions
|
||||
func TestSequenceSeq2_StringTests(t *testing.T) {
|
||||
stringSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_hello", StringNotEmpty("hello")) {
|
||||
return
|
||||
}
|
||||
if !yield("test_world", StringNotEmpty("world")) {
|
||||
return
|
||||
}
|
||||
if !yield("test_length", StringLength[any, any](5)("hello")) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](stringSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all string tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_ArrayTests tests SequenceSeq2 with array assertions
|
||||
func TestSequenceSeq2_ArrayTests(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
arraySeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_not_empty", ArrayNotEmpty(arr)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_length", ArrayLength[int](5)(arr)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_contains", ArrayContains(3)(arr)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](arraySeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all array tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_ComplexAssertions tests SequenceSeq2 with complex combined assertions
|
||||
func TestSequenceSeq2_ComplexAssertions(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
|
||||
userSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_name", StringNotEmpty(user.Name)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_age", That(func(age int) bool { return age > 0 && age < 150 })(user.Age)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_email", That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(user.Email)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](userSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all user validation tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_EarlyTermination tests that SequenceSeq2 respects early termination
|
||||
func TestSequenceSeq2_EarlyTermination(t *testing.T) {
|
||||
executionCount := 0
|
||||
|
||||
earlyTermSeq := func(yield func(string, Reader) bool) {
|
||||
executionCount++
|
||||
if !yield("test_1", Equal(1)(1)) {
|
||||
return
|
||||
}
|
||||
executionCount++
|
||||
if !yield("test_2", Equal(2)(2)) {
|
||||
return
|
||||
}
|
||||
executionCount++
|
||||
// This should execute even though we don't check the return
|
||||
yield("test_3", Equal(3)(3))
|
||||
executionCount++
|
||||
}
|
||||
|
||||
SequenceSeq2[Reader](earlyTermSeq)(t)
|
||||
|
||||
// All iterations should execute since we're not terminating early
|
||||
if executionCount != 4 {
|
||||
t.Errorf("Expected 4 executions, got %d", executionCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_WithMapConversion demonstrates converting a map to Seq2
|
||||
func TestSequenceSeq2_WithMapConversion(t *testing.T) {
|
||||
testMap := map[string]Reader{
|
||||
"test_addition": Equal(4)(2 + 2),
|
||||
"test_multiplication": Equal(6)(2 * 3),
|
||||
"test_subtraction": Equal(1)(3 - 2),
|
||||
}
|
||||
|
||||
// Convert map to Seq2
|
||||
mapSeq := func(yield func(string, Reader) bool) {
|
||||
for name, assertion := range testMap {
|
||||
if !yield(name, assertion) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](mapSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all map-based tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_vs_SequenceSeq2 demonstrates the relationship between the two functions
|
||||
func TestTraverseArray_vs_SequenceSeq2(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Name string
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testCases := []TestCase{
|
||||
{Name: "test_1", Input: 2, Expected: 4},
|
||||
{Name: "test_2", Input: 3, Expected: 9},
|
||||
{Name: "test_3", Input: 4, Expected: 16},
|
||||
}
|
||||
|
||||
// Using TraverseArray
|
||||
traverseResult := TraverseArray(func(tc TestCase) Pair[string, Reader] {
|
||||
return pair.MakePair(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input))
|
||||
})(testCases)(t)
|
||||
|
||||
// Using SequenceSeq2
|
||||
seqResult := SequenceSeq2[Reader](func(yield func(string, Reader) bool) {
|
||||
for _, tc := range testCases {
|
||||
if !yield(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})(t)
|
||||
|
||||
if traverseResult != seqResult {
|
||||
t.Error("Expected TraverseArray and SequenceSeq2 to produce same result")
|
||||
}
|
||||
|
||||
if !traverseResult || !seqResult {
|
||||
t.Error("Expected both approaches to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_EmptyMap tests that TraverseRecord handles empty maps correctly
|
||||
func TestTraverseRecord_EmptyMap(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n)(n)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with empty map")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_SingleEntry tests TraverseRecord with a single map entry
|
||||
func TestTraverseRecord_SingleEntry(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n * 2)(n * 2)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{"test_5": 5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with single entry")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_MultipleEntries tests TraverseRecord with multiple passing entries
|
||||
func TestTraverseRecord_MultipleEntries(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n * n)(n * n)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"square_1": 1,
|
||||
"square_2": 2,
|
||||
"square_3": 3,
|
||||
"square_4": 4,
|
||||
"square_5": 5,
|
||||
})(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with all passing entries")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_WithFailure tests that TraverseRecord fails when one entry fails
|
||||
func TestTraverseRecord_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(10)(n) // Will fail for all except 10
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"test_1": 1,
|
||||
"test_2": 2,
|
||||
"test_3": 3,
|
||||
})(t)
|
||||
|
||||
// The traverse should return false because entries don't match
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when entries don't match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_MixedResults tests TraverseRecord with some passing and some failing
|
||||
func TestTraverseRecord_MixedResults(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(0)(n % 2) // Only passes for even numbers
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"even_2": 2,
|
||||
"odd_3": 3,
|
||||
"even_4": 4,
|
||||
})(t)
|
||||
|
||||
// The traverse should return false because some entries fail
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when some entries fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_StringData tests TraverseRecord with string data
|
||||
func TestTraverseRecord_StringData(t *testing.T) {
|
||||
words := map[string]string{
|
||||
"greeting": "hello",
|
||||
"world": "world",
|
||||
"test": "test",
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(s string) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(s),
|
||||
That(func(str string) bool { return len(str) > 0 })(s),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(words)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with valid strings")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ComplexObjects tests TraverseRecord with complex objects
|
||||
func TestTraverseRecord_ComplexObjects(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := map[string]User{
|
||||
"alice": {Name: "Alice", Age: 30},
|
||||
"bob": {Name: "Bob", Age: 25},
|
||||
"charlie": {Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(u User) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with valid users")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ComplexObjectsWithFailure tests TraverseRecord with invalid complex objects
|
||||
func TestTraverseRecord_ComplexObjectsWithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := map[string]User{
|
||||
"alice": {Name: "Alice", Age: 30},
|
||||
"invalid": {Name: "", Age: 25}, // Invalid: empty name
|
||||
"charlie": {Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(u User) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
|
||||
// The traverse should return false because one user is invalid
|
||||
if result {
|
||||
t.Error("Expected traverse to return false with invalid user")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ConfigurationTesting demonstrates configuration testing pattern
|
||||
func TestTraverseRecord_ConfigurationTesting(t *testing.T) {
|
||||
configs := map[string]int{
|
||||
"timeout": 30,
|
||||
"maxRetries": 3,
|
||||
"bufferSize": 1024,
|
||||
}
|
||||
|
||||
validatePositive := That(func(n int) bool { return n > 0 })
|
||||
|
||||
traverse := TraverseRecord(validatePositive)
|
||||
result := traverse(configs)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all configuration values to be positive")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_APIEndpointTesting demonstrates API endpoint testing pattern
|
||||
func TestTraverseRecord_APIEndpointTesting(t *testing.T) {
|
||||
type Endpoint struct {
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
|
||||
endpoints := map[string]Endpoint{
|
||||
"get_users": {Path: "/api/users", Method: "GET"},
|
||||
"create_user": {Path: "/api/users", Method: "POST"},
|
||||
"delete_user": {Path: "/api/users/:id", Method: "DELETE"},
|
||||
}
|
||||
|
||||
validateEndpoint := func(e Endpoint) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(e.Path),
|
||||
That(func(path string) bool {
|
||||
return len(path) > 0 && path[0] == '/'
|
||||
})(e.Path),
|
||||
That(func(method string) bool {
|
||||
return method == "GET" || method == "POST" ||
|
||||
method == "PUT" || method == "DELETE"
|
||||
})(e.Method),
|
||||
})
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(validateEndpoint)
|
||||
result := traverse(endpoints)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all endpoints to be valid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_EmptyMap tests that SequenceRecord handles empty maps correctly
|
||||
func TestSequenceRecord_EmptyMap(t *testing.T) {
|
||||
result := SequenceRecord(map[string]Reader{})(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with empty map")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_SingleTest tests SequenceRecord with a single test
|
||||
func TestSequenceRecord_SingleTest(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"test_one": Equal(42)(42),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with single test")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_MultipleTests tests SequenceRecord with multiple passing tests
|
||||
func TestSequenceRecord_MultipleTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"test_addition": Equal(4)(2 + 2),
|
||||
"test_subtraction": Equal(1)(3 - 2),
|
||||
"test_multiplication": Equal(6)(2 * 3),
|
||||
"test_division": Equal(2)(6 / 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with all passing tests")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_WithFailure tests that SequenceRecord fails when one test fails
|
||||
func TestSequenceRecord_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
tests := map[string]Reader{
|
||||
"test_pass": Equal(4)(2 + 2),
|
||||
"test_fail": Equal(5)(2 + 2), // This will fail
|
||||
"test_pass2": Equal(6)(2 * 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
|
||||
// The sequence should return false because one test fails
|
||||
if result {
|
||||
t.Error("Expected sequence to return false when one test fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_StringTests tests SequenceRecord with string assertions
|
||||
func TestSequenceRecord_StringTests(t *testing.T) {
|
||||
testString := "hello world"
|
||||
|
||||
tests := map[string]Reader{
|
||||
"not_empty": StringNotEmpty(testString),
|
||||
"correct_length": StringLength[any, any](11)(testString),
|
||||
"has_space": That(func(s string) bool {
|
||||
for _, ch := range s {
|
||||
if ch == ' ' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(testString),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all string tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ArrayTests tests SequenceRecord with array assertions
|
||||
func TestSequenceRecord_ArrayTests(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"not_empty": ArrayNotEmpty(arr),
|
||||
"correct_length": ArrayLength[int](5)(arr),
|
||||
"contains_three": ArrayContains(3)(arr),
|
||||
"all_positive": That(func(arr []int) bool {
|
||||
for _, n := range arr {
|
||||
if n <= 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})(arr),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all array tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ComplexAssertions tests SequenceRecord with complex combined assertions
|
||||
func TestSequenceRecord_ComplexAssertions(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"name_not_empty": StringNotEmpty(user.Name),
|
||||
"age_positive": That(func(age int) bool { return age > 0 })(user.Age),
|
||||
"age_reasonable": That(func(age int) bool { return age < 150 })(user.Age),
|
||||
"email_valid": That(func(email string) bool {
|
||||
hasAt := false
|
||||
hasDot := false
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
hasAt = true
|
||||
}
|
||||
if ch == '.' {
|
||||
hasDot = true
|
||||
}
|
||||
}
|
||||
return hasAt && hasDot
|
||||
})(user.Email),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all user validation tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_MathOperations demonstrates basic math operations testing
|
||||
func TestSequenceRecord_MathOperations(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"addition": Equal(4)(2 + 2),
|
||||
"subtraction": Equal(1)(3 - 2),
|
||||
"multiplication": Equal(6)(2 * 3),
|
||||
"division": Equal(2)(6 / 3),
|
||||
"modulo": Equal(1)(7 % 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all math operations to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_BooleanTests tests SequenceRecord with boolean assertions
|
||||
func TestSequenceRecord_BooleanTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"true_is_true": Equal(true)(true),
|
||||
"false_is_false": Equal(false)(false),
|
||||
"not_true": Equal(false)(!true),
|
||||
"not_false": Equal(true)(!false),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all boolean tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ErrorTests tests SequenceRecord with error assertions
|
||||
func TestSequenceRecord_ErrorTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"no_error": NoError(nil),
|
||||
"equal_value": Equal("test")("test"),
|
||||
"not_empty": StringNotEmpty("hello"),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all error tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_vs_SequenceRecord demonstrates the relationship between the two functions
|
||||
func TestTraverseRecord_vs_SequenceRecord(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testData := map[string]TestCase{
|
||||
"test_1": {Input: 2, Expected: 4},
|
||||
"test_2": {Input: 3, Expected: 9},
|
||||
"test_3": {Input: 4, Expected: 16},
|
||||
}
|
||||
|
||||
// Using TraverseRecord
|
||||
traverseResult := TraverseRecord(func(tc TestCase) Reader {
|
||||
return Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
})(testData)(t)
|
||||
|
||||
// Using SequenceRecord (manually creating the map)
|
||||
tests := make(map[string]Reader)
|
||||
for name, tc := range testData {
|
||||
tests[name] = Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
}
|
||||
seqResult := SequenceRecord(tests)(t)
|
||||
|
||||
if traverseResult != seqResult {
|
||||
t.Error("Expected TraverseRecord and SequenceRecord to produce same result")
|
||||
}
|
||||
|
||||
if !traverseResult || !seqResult {
|
||||
t.Error("Expected both approaches to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_WithAllOf demonstrates combining SequenceRecord with AllOf
|
||||
func TestSequenceRecord_WithAllOf(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"array_validations": AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
}),
|
||||
"element_checks": AllOf([]Reader{
|
||||
That(func(a []int) bool { return a[0] == 1 })(arr),
|
||||
That(func(a []int) bool { return a[4] == 5 })(arr),
|
||||
}),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected combined assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ConfigValidation demonstrates real-world configuration validation
|
||||
func TestTraverseRecord_ConfigValidation(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
Min int
|
||||
Max int
|
||||
}
|
||||
|
||||
configs := map[string]Config{
|
||||
"timeout": {Value: 30, Min: 1, Max: 60},
|
||||
"maxRetries": {Value: 3, Min: 1, Max: 10},
|
||||
"bufferSize": {Value: 1024, Min: 512, Max: 4096},
|
||||
}
|
||||
|
||||
validateConfig := func(c Config) Reader {
|
||||
return AllOf([]Reader{
|
||||
That(func(val int) bool { return val >= c.Min })(c.Value),
|
||||
That(func(val int) bool { return val <= c.Max })(c.Value),
|
||||
})
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(validateConfig)
|
||||
result := traverse(configs)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all configurations to be within valid ranges")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_RealWorldExample demonstrates a realistic use case
|
||||
func TestSequenceRecord_RealWorldExample(t *testing.T) {
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
response := Response{StatusCode: 200, Body: `{"status":"ok"}`}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"status_ok": Equal(200)(response.StatusCode),
|
||||
"body_not_empty": StringNotEmpty(response.Body),
|
||||
"body_is_json": That(func(s string) bool {
|
||||
return len(s) > 0 && s[0] == '{' && s[len(s)-1] == '}'
|
||||
})(response.Body),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected response validation to pass")
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,32 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package assert
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerio"
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -13,23 +34,506 @@ import (
|
||||
|
||||
type (
|
||||
// Result represents a computation that may fail with an error.
|
||||
//
|
||||
// This is an alias for [result.Result][T], which encapsulates either a successful
|
||||
// value of type T or an error. It's commonly used in test assertions to represent
|
||||
// operations that might fail, allowing for functional error handling without exceptions.
|
||||
//
|
||||
// A Result can be in one of two states:
|
||||
// - Success: Contains a value of type T
|
||||
// - Failure: Contains an error
|
||||
//
|
||||
// This type is particularly useful in testing scenarios where you need to:
|
||||
// - Test functions that return results
|
||||
// - Chain operations that might fail
|
||||
// - Handle errors functionally
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestResultHandling(t *testing.T) {
|
||||
// successResult := result.Of[int](42)
|
||||
// assert.Success(successResult)(t) // Passes
|
||||
//
|
||||
// failureResult := result.Error[int](errors.New("failed"))
|
||||
// assert.Failure(failureResult)(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Success]: Asserts a Result is successful
|
||||
// - [Failure]: Asserts a Result contains an error
|
||||
// - [result.Result]: The underlying Result type
|
||||
Result[T any] = result.Result[T]
|
||||
|
||||
// Reader represents a test assertion that depends on a testing.T context and returns a boolean.
|
||||
// Reader represents a test assertion that depends on a [testing.T] context and returns a boolean.
|
||||
//
|
||||
// This is the core type for all assertions in this package. It's an alias for
|
||||
// [reader.Reader][*testing.T, bool], which is a function that takes a testing context
|
||||
// and produces a boolean result indicating whether the assertion passed.
|
||||
//
|
||||
// The Reader pattern enables:
|
||||
// - Composable assertions that can be combined using functional operators
|
||||
// - Deferred execution - assertions are defined but not executed until applied to a test
|
||||
// - Reusable assertion logic that can be applied to multiple tests
|
||||
// - Functional composition of complex test conditions
|
||||
//
|
||||
// All assertion functions in this package return a Reader, which must be applied
|
||||
// to a *testing.T to execute the assertion:
|
||||
//
|
||||
// assertion := assert.Equal(42)(result) // Creates a Reader
|
||||
// assertion(t) // Executes the assertion
|
||||
//
|
||||
// Readers can be composed using functions like [AllOf], [ApplicativeMonoid], or
|
||||
// functional operators from the reader package.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderComposition(t *testing.T) {
|
||||
// // Create individual assertions
|
||||
// assertion1 := assert.Equal(42)(42)
|
||||
// assertion2 := assert.StringNotEmpty("hello")
|
||||
//
|
||||
// // Combine them
|
||||
// combined := assert.AllOf([]assert.Reader{assertion1, assertion2})
|
||||
//
|
||||
// // Execute the combined assertion
|
||||
// combined(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Kleisli]: Function that produces a Reader from a value
|
||||
// - [AllOf]: Combines multiple Readers
|
||||
// - [ApplicativeMonoid]: Monoid for combining Readers
|
||||
Reader = reader.Reader[*testing.T, bool]
|
||||
|
||||
// Kleisli represents a function that produces a test assertion Reader from a value of type T.
|
||||
// Kleisli represents a function that produces a test assertion [Reader] from a value of type T.
|
||||
//
|
||||
// This is an alias for [reader.Reader][T, Reader], which is a function that takes a value
|
||||
// of type T and returns a Reader (test assertion). This pattern is fundamental to the
|
||||
// "data last" principle used throughout this package.
|
||||
//
|
||||
// Kleisli functions enable:
|
||||
// - Partial application of assertions - configure the expected value first, apply actual value later
|
||||
// - Reusable assertion builders that can be applied to different values
|
||||
// - Functional composition of assertion pipelines
|
||||
// - Point-free style programming with assertions
|
||||
//
|
||||
// Most assertion functions in this package return a Kleisli, which must be applied
|
||||
// to the actual value being tested, and then to a *testing.T:
|
||||
//
|
||||
// kleisli := assert.Equal(42) // Kleisli[int] - expects an int
|
||||
// reader := kleisli(result) // Reader - assertion ready to execute
|
||||
// reader(t) // Execute the assertion
|
||||
//
|
||||
// Or more concisely:
|
||||
//
|
||||
// assert.Equal(42)(result)(t)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestKleisliPattern(t *testing.T) {
|
||||
// // Create a reusable assertion for positive numbers
|
||||
// isPositive := assert.That(func(n int) bool { return n > 0 })
|
||||
//
|
||||
// // Apply it to different values
|
||||
// isPositive(42)(t) // Passes
|
||||
// isPositive(100)(t) // Passes
|
||||
// // isPositive(-5)(t) would fail
|
||||
//
|
||||
// // Can be used with Local for property testing
|
||||
// type User struct { Age int }
|
||||
// checkAge := assert.Local(func(u User) int { return u.Age })(isPositive)
|
||||
// checkAge(User{Age: 25})(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Reader]: The assertion type produced by Kleisli
|
||||
// - [Local]: Focuses a Kleisli on a property of a larger structure
|
||||
Kleisli[T any] = reader.Reader[T, Reader]
|
||||
|
||||
// Predicate represents a function that tests a value of type T and returns a boolean.
|
||||
//
|
||||
// This is an alias for [predicate.Predicate][T], which is a simple function that
|
||||
// takes a value and returns true or false based on some condition. Predicates are
|
||||
// used with the [That] function to create custom assertions.
|
||||
//
|
||||
// Predicates enable:
|
||||
// - Custom validation logic for any type
|
||||
// - Reusable test conditions
|
||||
// - Composition of complex validation rules
|
||||
// - Integration with functional programming patterns
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPredicates(t *testing.T) {
|
||||
// // Simple predicate
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// assert.That(isEven)(42)(t) // Passes
|
||||
//
|
||||
// // String predicate
|
||||
// hasPrefix := func(s string) bool { return strings.HasPrefix(s, "test") }
|
||||
// assert.That(hasPrefix)("test_file.go")(t) // Passes
|
||||
//
|
||||
// // Complex predicate
|
||||
// isValidEmail := func(s string) bool {
|
||||
// return strings.Contains(s, "@") && strings.Contains(s, ".")
|
||||
// }
|
||||
// assert.That(isValidEmail)("user@example.com")(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [That]: Creates an assertion from a Predicate
|
||||
// - [predicate.Predicate]: The underlying predicate type
|
||||
Predicate[T any] = predicate.Predicate[T]
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
//
|
||||
// This is an alias for [lens.Lens][S, T], which provides a composable way to focus
|
||||
// on a specific field within a larger structure. Lenses enable getting and setting
|
||||
// values in nested data structures in a functional, immutable way.
|
||||
//
|
||||
// In the context of testing, lenses are used with [LocalL] to focus assertions
|
||||
// on specific properties of complex objects without manually extracting those properties.
|
||||
//
|
||||
// A Lens[S, T] focuses on a value of type T within a structure of type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestLensUsage(t *testing.T) {
|
||||
// type Address struct { City string }
|
||||
// type User struct { Name string; Address Address }
|
||||
//
|
||||
// // Define lenses (typically generated)
|
||||
// addressLens := lens.Lens[User, Address]{...}
|
||||
// cityLens := lens.Lens[Address, string]{...}
|
||||
//
|
||||
// // Compose lenses to focus on nested field
|
||||
// userCityLens := lens.Compose(addressLens, cityLens)
|
||||
//
|
||||
// // Use with LocalL to assert on nested property
|
||||
// user := User{Name: "Alice", Address: Address{City: "NYC"}}
|
||||
// assert.LocalL(userCityLens)(assert.Equal("NYC"))(user)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [LocalL]: Uses a Lens to focus assertions on a property
|
||||
// - [lens.Lens]: The underlying lens type
|
||||
// - [Optional]: Similar but for values that may not exist
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Optional is an optic that focuses on a value that may or may not be present.
|
||||
//
|
||||
// This is an alias for [optional.Optional][S, T], which is similar to a [Lens] but
|
||||
// handles cases where the focused value might not exist. Optionals are useful for
|
||||
// working with nullable fields, optional properties, or values that might be absent.
|
||||
//
|
||||
// In testing, Optionals are used with [FromOptional] to create assertions that
|
||||
// verify whether an optional value is present and, if so, whether it satisfies
|
||||
// certain conditions.
|
||||
//
|
||||
// An Optional[S, T] focuses on an optional value of type T within a structure of type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestOptionalUsage(t *testing.T) {
|
||||
// type Config struct { Timeout *int }
|
||||
//
|
||||
// // Define optional (typically generated)
|
||||
// timeoutOptional := optional.Optional[Config, int]{...}
|
||||
//
|
||||
// // Test when value is present
|
||||
// config1 := Config{Timeout: ptr(30)}
|
||||
// assert.FromOptional(timeoutOptional)(
|
||||
// assert.Equal(30),
|
||||
// )(config1)(t) // Passes
|
||||
//
|
||||
// // Test when value is absent
|
||||
// config2 := Config{Timeout: nil}
|
||||
// // FromOptional would fail because value is not present
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromOptional]: Creates assertions for optional values
|
||||
// - [optional.Optional]: The underlying optional type
|
||||
// - [Lens]: Similar but for values that always exist
|
||||
Optional[S, T any] = optional.Optional[S, T]
|
||||
|
||||
// Prism is an optic that focuses on a case of a sum type.
|
||||
//
|
||||
// This is an alias for [prism.Prism][S, T], which provides a way to focus on one
|
||||
// variant of a sum type (like Result, Option, Either, etc.). Prisms enable pattern
|
||||
// matching and extraction of values from sum types in a functional way.
|
||||
//
|
||||
// In testing, Prisms are used with [FromPrism] to create assertions that verify
|
||||
// whether a value matches a specific case and, if so, whether the contained value
|
||||
// satisfies certain conditions.
|
||||
//
|
||||
// A Prism[S, T] focuses on a value of type T that may be contained within a sum type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPrismUsage(t *testing.T) {
|
||||
// // Prism for extracting success value from Result
|
||||
// successPrism := prism.Success[int]()
|
||||
//
|
||||
// // Test successful result
|
||||
// successResult := result.Of[int](42)
|
||||
// assert.FromPrism(successPrism)(
|
||||
// assert.Equal(42),
|
||||
// )(successResult)(t) // Passes
|
||||
//
|
||||
// // Prism for extracting error from Result
|
||||
// failurePrism := prism.Failure[int]()
|
||||
//
|
||||
// // Test failed result
|
||||
// failureResult := result.Error[int](errors.New("failed"))
|
||||
// assert.FromPrism(failurePrism)(
|
||||
// assert.Error,
|
||||
// )(failureResult)(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromPrism]: Creates assertions for prism-focused values
|
||||
// - [prism.Prism]: The underlying prism type
|
||||
// - [Optional]: Similar but for optional values
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
|
||||
// ReaderIOResult represents a context-aware, IO-based computation that may fail.
|
||||
//
|
||||
// This is an alias for [readerioresult.ReaderIOResult][A], which combines three
|
||||
// computational effects:
|
||||
// - Reader: Depends on a context (like context.Context)
|
||||
// - IO: Performs side effects (like file I/O, network calls)
|
||||
// - Result: May fail with an error
|
||||
//
|
||||
// In testing, ReaderIOResult is used with [FromReaderIOResult] to convert
|
||||
// context-aware, effectful computations into test assertions. This is useful
|
||||
// when your test assertions need to:
|
||||
// - Access a context for cancellation or deadlines
|
||||
// - Perform IO operations (database queries, API calls, file access)
|
||||
// - Handle potential errors gracefully
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderIOResult(t *testing.T) {
|
||||
// // Create a ReaderIOResult that performs IO and may fail
|
||||
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
|
||||
// return func() result.Result[assert.Reader] {
|
||||
// // Perform database check with context
|
||||
// if err := db.PingContext(ctx); err != nil {
|
||||
// return result.Error[assert.Reader](err)
|
||||
// }
|
||||
// return result.Of[assert.Reader](assert.NoError(nil))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIOResult(checkDatabase)
|
||||
// assertion(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromReaderIOResult]: Converts ReaderIOResult to Reader
|
||||
// - [ReaderIO]: Similar but without error handling
|
||||
// - [readerioresult.ReaderIOResult]: The underlying type
|
||||
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
|
||||
|
||||
// ReaderIO represents a context-aware, IO-based computation.
|
||||
//
|
||||
// This is an alias for [readerio.ReaderIO][A], which combines two computational effects:
|
||||
// - Reader: Depends on a context (like context.Context)
|
||||
// - IO: Performs side effects (like logging, metrics)
|
||||
//
|
||||
// In testing, ReaderIO is used with [FromReaderIO] to convert context-aware,
|
||||
// effectful computations into test assertions. This is useful when your test
|
||||
// assertions need to:
|
||||
// - Access a context for cancellation or deadlines
|
||||
// - Perform IO operations that don't fail (or handle failures internally)
|
||||
// - Integrate with context-aware utilities
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderIO(t *testing.T) {
|
||||
// // Create a ReaderIO that performs IO
|
||||
// logAndCheck := func(ctx context.Context) func() assert.Reader {
|
||||
// return func() assert.Reader {
|
||||
// // Log with context
|
||||
// logger.InfoContext(ctx, "Running test")
|
||||
// // Return assertion
|
||||
// return assert.Equal(42)(computeValue())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIO(logAndCheck)
|
||||
// assertion(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromReaderIO]: Converts ReaderIO to Reader
|
||||
// - [ReaderIOResult]: Similar but with error handling
|
||||
// - [readerio.ReaderIO]: The underlying type
|
||||
ReaderIO[A any] = readerio.ReaderIO[A]
|
||||
|
||||
// Seq2 represents a Go iterator that yields key-value pairs.
|
||||
//
|
||||
// This is an alias for [iter.Seq2][K, A], which is Go's standard iterator type
|
||||
// introduced in Go 1.23. It represents a sequence of key-value pairs that can be
|
||||
// iterated over using a for-range loop.
|
||||
//
|
||||
// In testing, Seq2 is used with [SequenceSeq2] to execute a sequence of named
|
||||
// test cases provided as an iterator. This enables:
|
||||
// - Lazy evaluation of test cases
|
||||
// - Memory-efficient testing of large test suites
|
||||
// - Integration with Go's iterator patterns
|
||||
// - Dynamic generation of test cases
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestSeq2Usage(t *testing.T) {
|
||||
// // Create an iterator of test cases
|
||||
// testCases := func(yield func(string, assert.Reader) bool) {
|
||||
// if !yield("test_addition", assert.Equal(4)(2+2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute all test cases
|
||||
// assert.SequenceSeq2[assert.Reader](testCases)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [SequenceSeq2]: Executes a Seq2 of test cases
|
||||
// - [TraverseArray]: Similar but for arrays
|
||||
// - [iter.Seq2]: The underlying iterator type
|
||||
Seq2[K, A any] = iter.Seq2[K, A]
|
||||
|
||||
// Pair represents a tuple of two values with potentially different types.
|
||||
//
|
||||
// This is an alias for [pair.Pair][L, R], which holds two values: a "head" (or "left")
|
||||
// of type L and a "tail" (or "right") of type R. Pairs are useful for grouping
|
||||
// related values together without defining a custom struct.
|
||||
//
|
||||
// In testing, Pairs are used with [TraverseArray] to associate test names with
|
||||
// their corresponding assertions. Each element in the array is transformed into
|
||||
// a Pair[string, Reader] where the string is the test name and the Reader is
|
||||
// the assertion to execute.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPairUsage(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Input int
|
||||
// Expected int
|
||||
// }
|
||||
//
|
||||
// testCases := []TestCase{
|
||||
// {Input: 2, Expected: 4},
|
||||
// {Input: 3, Expected: 9},
|
||||
// }
|
||||
//
|
||||
// // Transform each test case into a named assertion
|
||||
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
|
||||
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
|
||||
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
// return pair.MakePair(name, assertion)
|
||||
// })
|
||||
//
|
||||
// traverse(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [TraverseArray]: Uses Pairs to create named test cases
|
||||
// - [pair.Pair]: The underlying pair type
|
||||
// - [pair.MakePair]: Creates a Pair
|
||||
// - [pair.Head]: Extracts the first value
|
||||
// - [pair.Tail]: Extracts the second value
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
|
||||
// Void represents the absence of a meaningful value, similar to unit type in functional programming.
|
||||
//
|
||||
// This is an alias for [function.Void], which is used to represent operations that don't
|
||||
// return a meaningful value but may perform side effects. In the context of testing, Void
|
||||
// is used with IO operations that perform actions without producing a result.
|
||||
//
|
||||
// Void is conceptually similar to:
|
||||
// - Unit type in functional languages (Haskell's (), Scala's Unit)
|
||||
// - void in languages like C/Java (but as a value, not just a type)
|
||||
// - Empty struct{} in Go (but with clearer semantic meaning)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithSideEffect(t *testing.T) {
|
||||
// // An IO operation that logs but returns Void
|
||||
// logOperation := func() function.Void {
|
||||
// log.Println("Test executed")
|
||||
// return function.Void{}
|
||||
// }
|
||||
//
|
||||
// // Execute the operation
|
||||
// logOperation()
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [IO]: Wraps side-effecting operations
|
||||
// - [function.Void]: The underlying void type
|
||||
Void = function.Void
|
||||
|
||||
// IO represents a side-effecting computation that produces a value of type A.
|
||||
//
|
||||
// This is an alias for [io.IO][A], which encapsulates operations that perform side effects
|
||||
// (like I/O operations, logging, or state mutations) and return a value. IO is a lazy
|
||||
// computation - it describes an effect but doesn't execute it until explicitly run.
|
||||
//
|
||||
// In testing, IO is used to:
|
||||
// - Defer execution of side effects until needed
|
||||
// - Compose multiple side-effecting operations
|
||||
// - Maintain referential transparency in test setup
|
||||
// - Separate effect description from effect execution
|
||||
//
|
||||
// An IO[A] is essentially a function `func() A` that:
|
||||
// - Encapsulates a side effect
|
||||
// - Returns a value of type A when executed
|
||||
// - Can be composed with other IO operations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestIOOperation(t *testing.T) {
|
||||
// // Define an IO operation that reads a file
|
||||
// readConfig := func() io.IO[string] {
|
||||
// return func() string {
|
||||
// data, _ := os.ReadFile("config.txt")
|
||||
// return string(data)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // The IO is not executed yet - it's just a description
|
||||
// configIO := readConfig()
|
||||
//
|
||||
// // Execute the IO to get the result
|
||||
// config := configIO()
|
||||
// assert.StringNotEmpty(config)(t)
|
||||
// }
|
||||
//
|
||||
// Example with composition:
|
||||
//
|
||||
// func TestIOComposition(t *testing.T) {
|
||||
// // Chain multiple IO operations
|
||||
// pipeline := io.Map(
|
||||
// func(s string) int { return len(s) },
|
||||
// )(readFileIO)
|
||||
//
|
||||
// // Execute the composed operation
|
||||
// length := pipeline()
|
||||
// assert.That(func(n int) bool { return n > 0 })(length)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [ReaderIO]: Combines Reader and IO effects
|
||||
// - [ReaderIOResult]: Adds error handling to ReaderIO
|
||||
// - [io.IO]: The underlying IO type
|
||||
// - [Void]: Represents operations without meaningful return values
|
||||
IO[A any] = io.IO[A]
|
||||
)
|
||||
|
||||
@@ -1,7 +1,81 @@
|
||||
// Copyright (c) 2024 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 builder provides a generic Builder pattern interface for constructing
|
||||
// complex objects with validation.
|
||||
//
|
||||
// The Builder pattern is useful when:
|
||||
// - Object construction requires multiple steps
|
||||
// - Construction may fail with validation errors
|
||||
// - You want to separate construction logic from the object itself
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// type PersonBuilder struct {
|
||||
// name string
|
||||
// age int
|
||||
// }
|
||||
//
|
||||
// func (b PersonBuilder) Build() result.Result[Person] {
|
||||
// if b.name == "" {
|
||||
// return result.Error[Person](errors.New("name is required"))
|
||||
// }
|
||||
// if b.age < 0 {
|
||||
// return result.Error[Person](errors.New("age must be non-negative"))
|
||||
// }
|
||||
// return result.Of(Person{Name: b.name, Age: b.age})
|
||||
// }
|
||||
package builder
|
||||
|
||||
type (
|
||||
// Builder is a generic interface for the Builder pattern that constructs
|
||||
// objects of type T with validation.
|
||||
//
|
||||
// The Build method returns a Result[T] which can be either:
|
||||
// - Success: containing the constructed object of type T
|
||||
// - Error: containing an error if validation or construction fails
|
||||
//
|
||||
// This allows builders to perform validation and return meaningful errors
|
||||
// during the construction process, making it explicit that object creation
|
||||
// may fail.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of object being built
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type ConfigBuilder struct {
|
||||
// host string
|
||||
// port int
|
||||
// }
|
||||
//
|
||||
// func (b ConfigBuilder) Build() result.Result[Config] {
|
||||
// if b.host == "" {
|
||||
// return result.Error[Config](errors.New("host is required"))
|
||||
// }
|
||||
// if b.port <= 0 || b.port > 65535 {
|
||||
// return result.Error[Config](errors.New("invalid port"))
|
||||
// }
|
||||
// return result.Of(Config{Host: b.host, Port: b.port})
|
||||
// }
|
||||
Builder[T any] interface {
|
||||
// Build constructs and validates an object of type T.
|
||||
//
|
||||
// Returns:
|
||||
// - Result[T]: A Result containing either the successfully built object
|
||||
// or an error if validation or construction fails.
|
||||
Build() Result[T]
|
||||
}
|
||||
)
|
||||
|
||||
374
v2/builder/builder_test.go
Normal file
374
v2/builder/builder_test.go
Normal file
@@ -0,0 +1,374 @@
|
||||
// Copyright (c) 2024 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 builder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for demonstration
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
type PersonBuilder struct {
|
||||
name string
|
||||
age int
|
||||
}
|
||||
|
||||
func (b PersonBuilder) WithName(name string) PersonBuilder {
|
||||
b.name = name
|
||||
return b
|
||||
}
|
||||
|
||||
func (b PersonBuilder) WithAge(age int) PersonBuilder {
|
||||
b.age = age
|
||||
return b
|
||||
}
|
||||
|
||||
func (b PersonBuilder) Build() Result[Person] {
|
||||
if b.name == "" {
|
||||
return result.Left[Person](errors.New("name is required"))
|
||||
}
|
||||
if b.age < 0 {
|
||||
return result.Left[Person](errors.New("age must be non-negative"))
|
||||
}
|
||||
if b.age > 150 {
|
||||
return result.Left[Person](errors.New("age must be realistic"))
|
||||
}
|
||||
return result.Of(Person{Name: b.name, Age: b.age})
|
||||
}
|
||||
|
||||
func NewPersonBuilder(p Person) PersonBuilder {
|
||||
return PersonBuilder{name: p.Name, age: p.Age}
|
||||
}
|
||||
|
||||
// Config example for additional test coverage
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type ConfigBuilder struct {
|
||||
host string
|
||||
port int
|
||||
}
|
||||
|
||||
func (b ConfigBuilder) WithHost(host string) ConfigBuilder {
|
||||
b.host = host
|
||||
return b
|
||||
}
|
||||
|
||||
func (b ConfigBuilder) WithPort(port int) ConfigBuilder {
|
||||
b.port = port
|
||||
return b
|
||||
}
|
||||
|
||||
func (b ConfigBuilder) Build() Result[Config] {
|
||||
if b.host == "" {
|
||||
return result.Left[Config](errors.New("host is required"))
|
||||
}
|
||||
if b.port <= 0 || b.port > 65535 {
|
||||
return result.Left[Config](errors.New("port must be between 1 and 65535"))
|
||||
}
|
||||
return result.Of(Config{Host: b.host, Port: b.port})
|
||||
}
|
||||
|
||||
func NewConfigBuilder(c Config) ConfigBuilder {
|
||||
return ConfigBuilder{host: c.Host, port: c.Port}
|
||||
}
|
||||
|
||||
// Tests for Builder interface
|
||||
|
||||
func TestBuilder_SuccessfulBuild(t *testing.T) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsRight(res), "Build should succeed")
|
||||
person := result.ToOption(res)
|
||||
assert.True(t, O.IsSome(person), "Result should contain a person")
|
||||
|
||||
p := O.GetOrElse(func() Person { return Person{} })(person)
|
||||
assert.Equal(t, "Alice", p.Name)
|
||||
assert.Equal(t, 30, p.Age)
|
||||
}
|
||||
|
||||
func TestBuilder_ValidationFailure_MissingName(t *testing.T) {
|
||||
builder := PersonBuilder{}.WithAge(30)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when name is missing")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Person) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "name is required", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ValidationFailure_NegativeAge(t *testing.T) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Bob").
|
||||
WithAge(-5)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when age is negative")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Person) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "age must be non-negative", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ValidationFailure_UnrealisticAge(t *testing.T) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Charlie").
|
||||
WithAge(200)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when age is unrealistic")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Person) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "age must be realistic", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ConfigSuccessfulBuild(t *testing.T) {
|
||||
builder := ConfigBuilder{}.
|
||||
WithHost("localhost").
|
||||
WithPort(8080)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsRight(res), "Build should succeed")
|
||||
config := result.ToOption(res)
|
||||
assert.True(t, O.IsSome(config), "Result should contain a config")
|
||||
|
||||
c := O.GetOrElse(func() Config { return Config{} })(config)
|
||||
assert.Equal(t, "localhost", c.Host)
|
||||
assert.Equal(t, 8080, c.Port)
|
||||
}
|
||||
|
||||
func TestBuilder_ConfigValidationFailure_MissingHost(t *testing.T) {
|
||||
builder := ConfigBuilder{}.WithPort(8080)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail when host is missing")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Config) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "host is required", err.Error())
|
||||
}
|
||||
|
||||
func TestBuilder_ConfigValidationFailure_InvalidPort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
port int
|
||||
}{
|
||||
{"zero port", 0},
|
||||
{"negative port", -1},
|
||||
{"port too large", 70000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
builder := ConfigBuilder{}.
|
||||
WithHost("localhost").
|
||||
WithPort(tt.port)
|
||||
|
||||
res := builder.Build()
|
||||
|
||||
assert.True(t, result.IsLeft(res), "Build should fail for invalid port")
|
||||
err := result.Fold(
|
||||
func(e error) error { return e },
|
||||
func(Config) error { return errors.New("unexpected success") },
|
||||
)(res)
|
||||
assert.Equal(t, "port must be between 1 and 65535", err.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for BuilderPrism
|
||||
|
||||
func TestBuilderPrism_GetOption_ValidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
personOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsSome(personOpt), "GetOption should return Some for valid builder")
|
||||
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
|
||||
assert.Equal(t, "Alice", person.Name)
|
||||
assert.Equal(t, 30, person.Age)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_GetOption_InvalidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
builder := PersonBuilder{}.WithAge(30) // Missing name
|
||||
|
||||
personOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsNone(personOpt), "GetOption should return None for invalid builder")
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ReverseGet(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
person := Person{Name: "Bob", Age: 25}
|
||||
|
||||
builder := prism.ReverseGet(person)
|
||||
|
||||
assert.Equal(t, "Bob", builder.name)
|
||||
assert.Equal(t, 25, builder.age)
|
||||
|
||||
// Verify the builder can build the same person
|
||||
res := builder.Build()
|
||||
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
|
||||
|
||||
rebuilt := O.GetOrElse(func() Person { return Person{} })(result.ToOption(res))
|
||||
assert.Equal(t, person, rebuilt)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_RoundTrip_ValidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
|
||||
originalBuilder := PersonBuilder{}.
|
||||
WithName("Charlie").
|
||||
WithAge(35)
|
||||
|
||||
// Extract person from builder
|
||||
personOpt := prism.GetOption(originalBuilder)
|
||||
assert.True(t, O.IsSome(personOpt), "Should extract person from valid builder")
|
||||
|
||||
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
|
||||
|
||||
// Reconstruct builder from person
|
||||
rebuiltBuilder := prism.ReverseGet(person)
|
||||
|
||||
// Verify the rebuilt builder produces the same person
|
||||
rebuiltRes := rebuiltBuilder.Build()
|
||||
assert.True(t, result.IsRight(rebuiltRes), "Rebuilt builder should be valid")
|
||||
|
||||
rebuiltPerson := O.GetOrElse(func() Person { return Person{} })(result.ToOption(rebuiltRes))
|
||||
assert.Equal(t, person, rebuiltPerson)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ConfigPrism(t *testing.T) {
|
||||
prism := BuilderPrism(NewConfigBuilder)
|
||||
|
||||
builder := ConfigBuilder{}.
|
||||
WithHost("example.com").
|
||||
WithPort(443)
|
||||
|
||||
configOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsSome(configOpt), "GetOption should return Some for valid config builder")
|
||||
config := O.GetOrElse(func() Config { return Config{} })(configOpt)
|
||||
assert.Equal(t, "example.com", config.Host)
|
||||
assert.Equal(t, 443, config.Port)
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ConfigPrism_InvalidBuilder(t *testing.T) {
|
||||
prism := BuilderPrism(NewConfigBuilder)
|
||||
|
||||
builder := ConfigBuilder{}.WithPort(8080) // Missing host
|
||||
|
||||
configOpt := prism.GetOption(builder)
|
||||
|
||||
assert.True(t, O.IsNone(configOpt), "GetOption should return None for invalid config builder")
|
||||
}
|
||||
|
||||
func TestBuilderPrism_ConfigPrism_ReverseGet(t *testing.T) {
|
||||
prism := BuilderPrism(NewConfigBuilder)
|
||||
|
||||
config := Config{Host: "api.example.com", Port: 9000}
|
||||
|
||||
builder := prism.ReverseGet(config)
|
||||
|
||||
assert.Equal(t, "api.example.com", builder.host)
|
||||
assert.Equal(t, 9000, builder.port)
|
||||
|
||||
// Verify the builder can build the same config
|
||||
res := builder.Build()
|
||||
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
|
||||
|
||||
rebuilt := O.GetOrElse(func() Config { return Config{} })(result.ToOption(res))
|
||||
assert.Equal(t, config, rebuilt)
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
|
||||
func BenchmarkBuilder_SuccessfulBuild(b *testing.B) {
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = builder.Build()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuilder_FailedBuild(b *testing.B) {
|
||||
builder := PersonBuilder{}.WithAge(30) // Missing name
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = builder.Build()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuilderPrism_GetOption(b *testing.B) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
builder := PersonBuilder{}.
|
||||
WithName("Alice").
|
||||
WithAge(30)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = prism.GetOption(builder)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuilderPrism_ReverseGet(b *testing.B) {
|
||||
prism := BuilderPrism(NewPersonBuilder)
|
||||
person := Person{Name: "Bob", Age: 25}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = prism.ReverseGet(person)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
// Copyright (c) 2024 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 builder
|
||||
|
||||
import (
|
||||
@@ -6,7 +21,61 @@ import (
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// BuilderPrism createa a [Prism] that converts between a builder and its type
|
||||
// BuilderPrism creates a [Prism] that converts between a builder and its built type.
|
||||
//
|
||||
// A Prism is an optic that focuses on a case of a sum type, providing bidirectional
|
||||
// conversion with the possibility of failure. This function creates a prism that:
|
||||
// - Extracts: Attempts to build the object from the builder (may fail)
|
||||
// - Constructs: Creates a builder from a valid object (always succeeds)
|
||||
//
|
||||
// The extraction direction (builder -> object) uses the Build method and converts
|
||||
// the Result to an Option, where errors become None. The construction direction
|
||||
// (object -> builder) uses the provided creator function.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of the object being built
|
||||
// - B: The builder type that implements Builder[T]
|
||||
//
|
||||
// Parameters:
|
||||
// - creator: A function that creates a builder from a valid object of type T.
|
||||
// This function should initialize the builder with all fields from the object.
|
||||
//
|
||||
// Returns:
|
||||
// - Prism[B, T]: A prism that can convert between the builder and the built type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// type PersonBuilder struct {
|
||||
// name string
|
||||
// age int
|
||||
// }
|
||||
//
|
||||
// func (b PersonBuilder) Build() result.Result[Person] {
|
||||
// if b.name == "" {
|
||||
// return result.Error[Person](errors.New("name required"))
|
||||
// }
|
||||
// return result.Of(Person{Name: b.name, Age: b.age})
|
||||
// }
|
||||
//
|
||||
// func NewPersonBuilder(p Person) PersonBuilder {
|
||||
// return PersonBuilder{name: p.Name, age: p.Age}
|
||||
// }
|
||||
//
|
||||
// // Create a prism for PersonBuilder
|
||||
// prism := BuilderPrism(NewPersonBuilder)
|
||||
//
|
||||
// // Use the prism to extract a Person from a valid builder
|
||||
// builder := PersonBuilder{name: "Alice", age: 30}
|
||||
// person := prism.GetOption(builder) // Some(Person{Name: "Alice", Age: 30})
|
||||
//
|
||||
// // Use the prism to create a builder from a Person
|
||||
// p := Person{Name: "Bob", Age: 25}
|
||||
// b := prism.ReverseGet(p) // PersonBuilder{name: "Bob", age: 25}
|
||||
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
|
||||
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/identity"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
@@ -14,6 +13,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
)
|
||||
|
||||
@@ -241,125 +241,155 @@ func isResetTimeExceeded(ct time.Time) option.Kleisli[openState, openState] {
|
||||
})
|
||||
}
|
||||
|
||||
// handleSuccessOnClosed handles a successful request when the circuit breaker is in closed state.
|
||||
// It updates the closed state by recording the success and returns an IO operation that
|
||||
// modifies the breaker state.
|
||||
// handleSuccessOnClosed creates a Reader that handles successful requests when the circuit is closed.
|
||||
// This function is used to update the circuit breaker state after a successful operation completes
|
||||
// while the circuit is in the closed state.
|
||||
//
|
||||
// This function is part of the circuit breaker's state management for the closed state.
|
||||
// When a request succeeds in closed state:
|
||||
// 1. The current time is obtained
|
||||
// 2. The addSuccess function is called with the current time to update the ClosedState
|
||||
// 3. The updated ClosedState is wrapped in a Right (closed) BreakerState
|
||||
// 4. The breaker state is modified with the new state
|
||||
// The function takes a Reader that adds a success record to the ClosedState and lifts it to work
|
||||
// with BreakerState by mapping over the Right (closed) side of the Either type. This ensures that
|
||||
// success tracking only affects the closed state and leaves any open state unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - currentTime: An IO operation that provides the current time
|
||||
// - addSuccess: A Reader that takes a time and returns an endomorphism for ClosedState,
|
||||
// typically resetting failure counters or history
|
||||
// - addSuccess: A Reader that takes the current time and returns an Endomorphism that updates
|
||||
// the ClosedState by recording a successful operation. This typically increments a success
|
||||
// counter or updates a success history.
|
||||
//
|
||||
// Returns:
|
||||
// - An io.Kleisli that takes another io.Kleisli and chains them together.
|
||||
// The outer Kleisli takes an Endomorphism[BreakerState] and returns BreakerState.
|
||||
// This allows composing the success handling with other state modifications.
|
||||
// - A Reader[time.Time, Endomorphism[BreakerState]] that, when given the current time, produces
|
||||
// an endomorphism that updates the BreakerState by applying the success update to the closed
|
||||
// state (if closed) or leaving the state unchanged (if open).
|
||||
//
|
||||
// Thread Safety: This function creates IO operations that will atomically modify the
|
||||
// IORef[BreakerState] when executed. The state modifications are thread-safe.
|
||||
//
|
||||
// Type signature:
|
||||
//
|
||||
// io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState]
|
||||
// Thread Safety: This is a pure function that creates new state instances. The returned
|
||||
// endomorphism is safe for concurrent use as it does not mutate its input.
|
||||
//
|
||||
// Usage Context:
|
||||
// - Called when a request succeeds while the circuit is closed
|
||||
// - Resets failure tracking (counter or history) in the ClosedState
|
||||
// - Keeps the circuit in closed state
|
||||
// - Called after a successful request completes while the circuit is closed
|
||||
// - Updates success metrics/counters in the ClosedState
|
||||
// - Does not affect the circuit state if it's already open
|
||||
// - Part of the normal operation flow when the circuit breaker is functioning properly
|
||||
func handleSuccessOnClosed(
|
||||
currentTime IO[time.Time],
|
||||
addSuccess Reader[time.Time, Endomorphism[ClosedState]],
|
||||
) io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState] {
|
||||
) Reader[time.Time, Endomorphism[BreakerState]] {
|
||||
return F.Flow2(
|
||||
io.Chain,
|
||||
identity.Flap[IO[BreakerState]](F.Pipe1(
|
||||
currentTime,
|
||||
io.Map(F.Flow2(
|
||||
addSuccess,
|
||||
either.Map[openState],
|
||||
)))),
|
||||
addSuccess,
|
||||
either.Map[openState],
|
||||
)
|
||||
}
|
||||
|
||||
// handleFailureOnClosed handles a failed request when the circuit breaker is in closed state.
|
||||
// It updates the closed state by recording the failure and checks if the circuit should open.
|
||||
// handleFailureOnClosed creates a Reader that handles failed requests when the circuit is closed.
|
||||
// This function manages the critical logic for determining whether a failure should cause the
|
||||
// circuit breaker to open (transition from closed to open state).
|
||||
//
|
||||
// This function is part of the circuit breaker's state management for the closed state.
|
||||
// When a request fails in closed state:
|
||||
// 1. The current time is obtained
|
||||
// 2. The addError function is called to record the failure in the ClosedState
|
||||
// 3. The checkClosedState function is called to determine if the failure threshold is exceeded
|
||||
// 4. If the threshold is exceeded (Check returns None):
|
||||
// - The circuit transitions to open state using openCircuit
|
||||
// - A new openState is created with resetAt time calculated from the retry policy
|
||||
// 5. If the threshold is not exceeded (Check returns Some):
|
||||
// - The circuit remains closed with the updated failure tracking
|
||||
// The function orchestrates three key operations:
|
||||
// 1. Records the failure in the ClosedState using addError
|
||||
// 2. Checks if the failure threshold has been exceeded using checkClosedState
|
||||
// 3. If threshold exceeded, opens the circuit; otherwise, keeps it closed with updated error count
|
||||
//
|
||||
// The decision flow is:
|
||||
// - Add the error to the closed state's error tracking
|
||||
// - Check if the updated closed state exceeds the failure threshold
|
||||
// - If threshold exceeded (checkClosedState returns None):
|
||||
// - Create a new openState with calculated reset time based on retry policy
|
||||
// - Transition the circuit to open state (Left side of Either)
|
||||
// - If threshold not exceeded (checkClosedState returns Some):
|
||||
// - Keep the circuit closed with the updated error count
|
||||
// - Continue allowing requests through
|
||||
//
|
||||
// Parameters:
|
||||
// - currentTime: An IO operation that provides the current time
|
||||
// - addError: A Reader that takes a time and returns an endomorphism for ClosedState,
|
||||
// recording a failure (incrementing counter or adding to history)
|
||||
// - checkClosedState: A Reader that takes a time and returns an option.Kleisli that checks
|
||||
// if the ClosedState should remain closed. Returns Some if circuit stays closed, None if it should open.
|
||||
// - openCircuit: A Reader that takes a time and returns an openState with calculated resetAt time
|
||||
// - addError: A Reader that takes the current time and returns an Endomorphism that updates
|
||||
// the ClosedState by recording a failed operation. This typically increments an error
|
||||
// counter or adds to an error history.
|
||||
// - checkClosedState: A Reader that takes the current time and returns an option.Kleisli that
|
||||
// validates whether the ClosedState is still within acceptable failure thresholds.
|
||||
// Returns Some(ClosedState) if threshold not exceeded, None if threshold exceeded.
|
||||
// - openCircuit: A Reader that takes the current time and creates a new openState with
|
||||
// appropriate reset time calculated from the retry policy. Used when transitioning to open.
|
||||
//
|
||||
// Returns:
|
||||
// - An io.Kleisli that takes another io.Kleisli and chains them together.
|
||||
// The outer Kleisli takes an Endomorphism[BreakerState] and returns BreakerState.
|
||||
// This allows composing the failure handling with other state modifications.
|
||||
// - A Reader[time.Time, Endomorphism[BreakerState]] that, when given the current time, produces
|
||||
// an endomorphism that either:
|
||||
// - Keeps the circuit closed with updated error tracking (if threshold not exceeded)
|
||||
// - Opens the circuit with calculated reset time (if threshold exceeded)
|
||||
//
|
||||
// Thread Safety: This function creates IO operations that will atomically modify the
|
||||
// IORef[BreakerState] when executed. The state modifications are thread-safe.
|
||||
//
|
||||
// Type signature:
|
||||
//
|
||||
// io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState]
|
||||
//
|
||||
// State Transitions:
|
||||
// - Closed -> Closed: When failure threshold is not exceeded (Some from checkClosedState)
|
||||
// - Closed -> Open: When failure threshold is exceeded (None from checkClosedState)
|
||||
// Thread Safety: This is a pure function that creates new state instances. The returned
|
||||
// endomorphism is safe for concurrent use as it does not mutate its input.
|
||||
//
|
||||
// Usage Context:
|
||||
// - Called when a request fails while the circuit is closed
|
||||
// - Records the failure in the ClosedState (counter or history)
|
||||
// - May trigger transition to open state if threshold is exceeded
|
||||
// - Called after a failed request completes while the circuit is closed
|
||||
// - Implements the core circuit breaker logic for opening the circuit
|
||||
// - Determines when to stop allowing requests through to protect the failing service
|
||||
// - Critical for preventing cascading failures in distributed systems
|
||||
//
|
||||
// State Transition:
|
||||
// - Closed (under threshold) -> Closed (with incremented error count)
|
||||
// - Closed (at/over threshold) -> Open (with reset time for recovery attempt)
|
||||
func handleFailureOnClosed(
|
||||
currentTime IO[time.Time],
|
||||
addError Reader[time.Time, Endomorphism[ClosedState]],
|
||||
checkClosedState Reader[time.Time, option.Kleisli[ClosedState, ClosedState]],
|
||||
openCircuit Reader[time.Time, openState],
|
||||
) io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState] {
|
||||
|
||||
return F.Flow2(
|
||||
io.Chain,
|
||||
identity.Flap[IO[BreakerState]](F.Pipe1(
|
||||
currentTime,
|
||||
io.Map(func(ct time.Time) either.Operator[openState, ClosedState, ClosedState] {
|
||||
return either.Chain(F.Flow3(
|
||||
addError(ct),
|
||||
checkClosedState(ct),
|
||||
option.Fold(
|
||||
F.Pipe2(
|
||||
ct,
|
||||
lazy.Of,
|
||||
lazy.Map(F.Flow2(
|
||||
openCircuit,
|
||||
createOpenCircuit,
|
||||
)),
|
||||
),
|
||||
createClosedCircuit,
|
||||
),
|
||||
))
|
||||
}))),
|
||||
) Reader[time.Time, Endomorphism[BreakerState]] {
|
||||
return F.Pipe2(
|
||||
F.Pipe1(
|
||||
addError,
|
||||
reader.ApS(reader.Map[ClosedState], checkClosedState),
|
||||
),
|
||||
reader.Chain(F.Flow2(
|
||||
reader.Map[ClosedState](option.Fold(
|
||||
F.Pipe2(
|
||||
openCircuit,
|
||||
reader.Map[time.Time](createOpenCircuit),
|
||||
lazy.Of,
|
||||
),
|
||||
F.Flow2(
|
||||
createClosedCircuit,
|
||||
reader.Of[time.Time],
|
||||
),
|
||||
)),
|
||||
reader.Sequence,
|
||||
)),
|
||||
reader.Map[time.Time](either.Chain[openState, ClosedState, ClosedState]),
|
||||
)
|
||||
}
|
||||
|
||||
func handleErrorOnClosed2[E any](
|
||||
checkError option.Kleisli[E, E],
|
||||
onSuccess Reader[time.Time, Endomorphism[BreakerState]],
|
||||
onFailure Reader[time.Time, Endomorphism[BreakerState]],
|
||||
) reader.Kleisli[time.Time, E, Endomorphism[BreakerState]] {
|
||||
return F.Flow3(
|
||||
checkError,
|
||||
option.MapTo[E](onFailure),
|
||||
option.GetOrElse(lazy.Of(onSuccess)),
|
||||
)
|
||||
}
|
||||
|
||||
func stateModifier(
|
||||
modify io.Kleisli[Endomorphism[BreakerState], BreakerState],
|
||||
) reader.Operator[time.Time, Endomorphism[BreakerState], IO[BreakerState]] {
|
||||
return reader.Map[time.Time](modify)
|
||||
}
|
||||
|
||||
func reportOnClose2(
|
||||
onClosed ReaderIO[time.Time, Void],
|
||||
onOpened ReaderIO[time.Time, Void],
|
||||
) readerio.Operator[time.Time, BreakerState, Void] {
|
||||
return readerio.Chain(either.Fold(
|
||||
reader.Of[openState](onOpened),
|
||||
reader.Of[ClosedState](onClosed),
|
||||
))
|
||||
}
|
||||
|
||||
func applyAndReportClose2(
|
||||
currentTime IO[time.Time],
|
||||
metrics readerio.Operator[time.Time, BreakerState, Void],
|
||||
) func(io.Kleisli[Endomorphism[BreakerState], BreakerState]) func(Reader[time.Time, Endomorphism[BreakerState]]) IO[Void] {
|
||||
return func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) func(Reader[time.Time, Endomorphism[BreakerState]]) IO[Void] {
|
||||
return F.Flow3(
|
||||
reader.Map[time.Time](modify),
|
||||
metrics,
|
||||
readerio.ReadIO[Void](currentTime),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MakeCircuitBreaker creates a circuit breaker implementation for a higher-kinded type.
|
||||
@@ -402,6 +432,8 @@ func MakeCircuitBreaker[E, T, HKTT, HKTOP, HKTHKTT any](
|
||||
chainFirstIOK func(io.Kleisli[T, BreakerState]) func(HKTT) HKTT,
|
||||
chainFirstLeftIOK func(io.Kleisli[E, BreakerState]) func(HKTT) HKTT,
|
||||
|
||||
chainFirstIOK2 func(io.Kleisli[Either[E, T], Void]) func(HKTT) HKTT,
|
||||
|
||||
fromIO func(IO[func(HKTT) HKTT]) HKTOP,
|
||||
flap func(HKTT) func(HKTOP) HKTHKTT,
|
||||
flatten func(HKTHKTT) HKTT,
|
||||
@@ -437,47 +469,22 @@ func MakeCircuitBreaker[E, T, HKTT, HKTOP, HKTHKTT any](
|
||||
reader.Of[HKTT],
|
||||
)
|
||||
|
||||
handleSuccess := handleSuccessOnClosed(currentTime, addSuccess)
|
||||
handleFailure := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
|
||||
handleSuccess2 := handleSuccessOnClosed(addSuccess)
|
||||
handleFailure2 := handleFailureOnClosed(addError, checkClosedState, openCircuit)
|
||||
|
||||
handleError2 := handleErrorOnClosed2(checkError, handleSuccess2, handleFailure2)
|
||||
|
||||
metricsClose2 := reportOnClose2(metrics.Accept, metrics.Open)
|
||||
apply2 := applyAndReportClose2(currentTime, metricsClose2)
|
||||
|
||||
onClosed := func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) Operator {
|
||||
|
||||
return F.Flow2(
|
||||
// error case
|
||||
chainFirstLeftIOK(F.Flow3(
|
||||
checkError,
|
||||
option.Fold(
|
||||
// the error is not applicable, handle as success
|
||||
F.Pipe2(
|
||||
modify,
|
||||
handleSuccess,
|
||||
lazy.Of,
|
||||
),
|
||||
// the error is relevant, record it
|
||||
F.Pipe2(
|
||||
modify,
|
||||
handleFailure,
|
||||
reader.Of[E],
|
||||
),
|
||||
),
|
||||
// metering
|
||||
io.ChainFirst(either.Fold(
|
||||
F.Flow2(
|
||||
openedAtLens.Get,
|
||||
metrics.Open,
|
||||
),
|
||||
func(c ClosedState) IO[Void] {
|
||||
return io.Of(function.VOID)
|
||||
},
|
||||
)),
|
||||
)),
|
||||
// good case
|
||||
chainFirstIOK(F.Pipe2(
|
||||
modify,
|
||||
handleSuccess,
|
||||
reader.Of[T],
|
||||
)),
|
||||
)
|
||||
return chainFirstIOK2(F.Flow2(
|
||||
either.Fold(
|
||||
handleError2,
|
||||
reader.Of[T](handleSuccess2),
|
||||
),
|
||||
apply2(modify),
|
||||
))
|
||||
}
|
||||
|
||||
onCanary := func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) Operator {
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioref"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -452,43 +452,128 @@ func TestIsResetTimeExceeded(t *testing.T) {
|
||||
|
||||
// TestHandleSuccessOnClosed tests the handleSuccessOnClosed function
|
||||
func TestHandleSuccessOnClosed(t *testing.T) {
|
||||
t.Run("resets failure count on success", func(t *testing.T) {
|
||||
t.Run("updates closed state with success when circuit is closed", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
currentTime := vt.Now
|
||||
addSuccess := reader.From1(ClosedState.AddSuccess)
|
||||
currentTime := vt.Now()
|
||||
|
||||
// Create initial state with some failures
|
||||
now := vt.Now()
|
||||
// Create a simple addSuccess reader that increments a counter
|
||||
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddSuccess(ct)
|
||||
}
|
||||
}
|
||||
|
||||
// Create initial closed state
|
||||
initialClosed := MakeClosedStateCounter(3)
|
||||
initialClosed = initialClosed.AddError(now)
|
||||
initialClosed = initialClosed.AddError(now)
|
||||
initialState := createClosedCircuit(initialClosed)
|
||||
|
||||
ref := io.Run(ioref.MakeIORef(initialState))
|
||||
modify := modifyV(ref)
|
||||
// Apply handleSuccessOnClosed
|
||||
handler := handleSuccessOnClosed(addSuccess)
|
||||
endomorphism := handler(currentTime)
|
||||
result := endomorphism(initialState)
|
||||
|
||||
handler := handleSuccessOnClosed(currentTime, addSuccess)
|
||||
// Verify the state is still closed
|
||||
assert.True(t, IsClosed(result), "state should remain closed after success")
|
||||
|
||||
// Apply the handler
|
||||
result := io.Run(handler(modify))
|
||||
|
||||
// Verify state is still closed and failures are reset
|
||||
assert.True(t, IsClosed(result), "circuit should remain closed after success")
|
||||
// Verify the closed state was updated
|
||||
closedState := either.Fold(
|
||||
func(openState) ClosedState { return initialClosed },
|
||||
F.Identity[ClosedState],
|
||||
)(result)
|
||||
// The success should have been recorded (implementation-specific verification)
|
||||
assert.NotNil(t, closedState, "closed state should be present")
|
||||
})
|
||||
|
||||
t.Run("keeps circuit closed", func(t *testing.T) {
|
||||
t.Run("does not affect open state", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
currentTime := vt.Now
|
||||
addSuccess := reader.From1(ClosedState.AddSuccess)
|
||||
currentTime := vt.Now()
|
||||
|
||||
initialState := createClosedCircuit(MakeClosedStateCounter(3))
|
||||
ref := io.Run(ioref.MakeIORef(initialState))
|
||||
modify := modifyV(ref)
|
||||
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddSuccess(ct)
|
||||
}
|
||||
}
|
||||
|
||||
handler := handleSuccessOnClosed(currentTime, addSuccess)
|
||||
result := io.Run(handler(modify))
|
||||
// Create initial open state
|
||||
initialOpen := openState{
|
||||
openedAt: currentTime.Add(-1 * time.Minute),
|
||||
resetAt: currentTime.Add(1 * time.Minute),
|
||||
retryStatus: retry.DefaultRetryStatus,
|
||||
canaryRequest: false,
|
||||
}
|
||||
initialState := createOpenCircuit(initialOpen)
|
||||
|
||||
assert.True(t, IsClosed(result), "circuit should remain closed")
|
||||
// Apply handleSuccessOnClosed
|
||||
handler := handleSuccessOnClosed(addSuccess)
|
||||
endomorphism := handler(currentTime)
|
||||
result := endomorphism(initialState)
|
||||
|
||||
// Verify the state remains open and unchanged
|
||||
assert.True(t, IsOpen(result), "state should remain open")
|
||||
|
||||
// Extract and verify the open state is unchanged
|
||||
openResult := either.Fold(
|
||||
func(os openState) openState { return os },
|
||||
func(ClosedState) openState { return initialOpen },
|
||||
)(result)
|
||||
assert.Equal(t, initialOpen.openedAt, openResult.openedAt, "openedAt should be unchanged")
|
||||
assert.Equal(t, initialOpen.resetAt, openResult.resetAt, "resetAt should be unchanged")
|
||||
assert.Equal(t, initialOpen.canaryRequest, openResult.canaryRequest, "canaryRequest should be unchanged")
|
||||
})
|
||||
|
||||
t.Run("preserves time parameter through reader", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
time1 := vt.Now()
|
||||
vt.Advance(1 * time.Hour)
|
||||
time2 := vt.Now()
|
||||
|
||||
var capturedTime time.Time
|
||||
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
capturedTime = ct
|
||||
return F.Identity[ClosedState]
|
||||
}
|
||||
|
||||
initialClosed := MakeClosedStateCounter(3)
|
||||
initialState := createClosedCircuit(initialClosed)
|
||||
|
||||
handler := handleSuccessOnClosed(addSuccess)
|
||||
|
||||
// Apply with time1
|
||||
endomorphism1 := handler(time1)
|
||||
endomorphism1(initialState)
|
||||
assert.Equal(t, time1, capturedTime, "should pass time1 to addSuccess")
|
||||
|
||||
// Apply with time2
|
||||
endomorphism2 := handler(time2)
|
||||
endomorphism2(initialState)
|
||||
assert.Equal(t, time2, capturedTime, "should pass time2 to addSuccess")
|
||||
})
|
||||
|
||||
t.Run("composes correctly with multiple successes", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
currentTime := vt.Now()
|
||||
|
||||
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddSuccess(ct)
|
||||
}
|
||||
}
|
||||
|
||||
initialClosed := MakeClosedStateCounter(3)
|
||||
initialState := createClosedCircuit(initialClosed)
|
||||
|
||||
handler := handleSuccessOnClosed(addSuccess)
|
||||
endomorphism := handler(currentTime)
|
||||
|
||||
// Apply multiple times
|
||||
result1 := endomorphism(initialState)
|
||||
result2 := endomorphism(result1)
|
||||
result3 := endomorphism(result2)
|
||||
|
||||
// All should remain closed
|
||||
assert.True(t, IsClosed(result1), "state should remain closed after first success")
|
||||
assert.True(t, IsClosed(result2), "state should remain closed after second success")
|
||||
assert.True(t, IsClosed(result3), "state should remain closed after third success")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -496,9 +581,26 @@ func TestHandleSuccessOnClosed(t *testing.T) {
|
||||
func TestHandleFailureOnClosed(t *testing.T) {
|
||||
t.Run("keeps circuit closed when threshold not exceeded", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
currentTime := vt.Now
|
||||
addError := reader.From1(ClosedState.AddError)
|
||||
checkClosedState := reader.From1(ClosedState.Check)
|
||||
currentTime := vt.Now()
|
||||
|
||||
// Create a closed state that allows 3 errors
|
||||
initialClosed := MakeClosedStateCounter(3)
|
||||
|
||||
// addError increments error count
|
||||
addError := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddError(ct)
|
||||
}
|
||||
}
|
||||
|
||||
// checkClosedState returns Some if under threshold
|
||||
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
|
||||
return func(cs ClosedState) Option[ClosedState] {
|
||||
return cs.Check(ct)
|
||||
}
|
||||
}
|
||||
|
||||
// openCircuit creates an open state (shouldn't be called in this test)
|
||||
openCircuit := func(ct time.Time) openState {
|
||||
return openState{
|
||||
openedAt: ct,
|
||||
@@ -508,26 +610,39 @@ func TestHandleFailureOnClosed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Create initial state with room for more failures
|
||||
now := vt.Now()
|
||||
initialClosed := MakeClosedStateCounter(5) // threshold is 5
|
||||
initialClosed = initialClosed.AddError(now)
|
||||
initialState := createClosedCircuit(initialClosed)
|
||||
|
||||
ref := io.Run(ioref.MakeIORef(initialState))
|
||||
modify := modifyV(ref)
|
||||
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
|
||||
endomorphism := handler(currentTime)
|
||||
|
||||
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
|
||||
result := io.Run(handler(modify))
|
||||
// First error - should stay closed
|
||||
result1 := endomorphism(initialState)
|
||||
assert.True(t, IsClosed(result1), "circuit should remain closed after first error")
|
||||
|
||||
assert.True(t, IsClosed(result), "circuit should remain closed when threshold not exceeded")
|
||||
// Second error - should stay closed
|
||||
result2 := endomorphism(result1)
|
||||
assert.True(t, IsClosed(result2), "circuit should remain closed after second error")
|
||||
})
|
||||
|
||||
t.Run("opens circuit when threshold exceeded", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
currentTime := vt.Now
|
||||
addError := reader.From1(ClosedState.AddError)
|
||||
checkClosedState := reader.From1(ClosedState.Check)
|
||||
currentTime := vt.Now()
|
||||
|
||||
// Create a closed state that allows only 2 errors (opens at 2nd error)
|
||||
initialClosed := MakeClosedStateCounter(2)
|
||||
|
||||
addError := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddError(ct)
|
||||
}
|
||||
}
|
||||
|
||||
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
|
||||
return func(cs ClosedState) Option[ClosedState] {
|
||||
return cs.Check(ct)
|
||||
}
|
||||
}
|
||||
|
||||
openCircuit := func(ct time.Time) openState {
|
||||
return openState{
|
||||
openedAt: ct,
|
||||
@@ -537,26 +652,85 @@ func TestHandleFailureOnClosed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Create initial state at threshold
|
||||
now := vt.Now()
|
||||
initialClosed := MakeClosedStateCounter(2) // threshold is 2
|
||||
initialClosed = initialClosed.AddError(now)
|
||||
initialState := createClosedCircuit(initialClosed)
|
||||
|
||||
ref := io.Run(ioref.MakeIORef(initialState))
|
||||
modify := modifyV(ref)
|
||||
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
|
||||
endomorphism := handler(currentTime)
|
||||
|
||||
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
|
||||
result := io.Run(handler(modify))
|
||||
// First error - should stay closed (count=1, threshold=2)
|
||||
result1 := endomorphism(initialState)
|
||||
assert.True(t, IsClosed(result1), "circuit should remain closed after first error")
|
||||
|
||||
assert.True(t, IsOpen(result), "circuit should open when threshold exceeded")
|
||||
// Second error - should open (count=2, threshold=2)
|
||||
result2 := endomorphism(result1)
|
||||
assert.True(t, IsOpen(result2), "circuit should open when threshold reached")
|
||||
})
|
||||
|
||||
t.Run("records failure in closed state", func(t *testing.T) {
|
||||
t.Run("creates open state with correct reset time", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
currentTime := vt.Now
|
||||
addError := reader.From1(ClosedState.AddError)
|
||||
checkClosedState := reader.From1(ClosedState.Check)
|
||||
currentTime := vt.Now()
|
||||
expectedResetTime := currentTime.Add(5 * time.Minute)
|
||||
|
||||
initialClosed := MakeClosedStateCounter(1) // Opens at 1st error
|
||||
|
||||
addError := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddError(ct)
|
||||
}
|
||||
}
|
||||
|
||||
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
|
||||
return func(cs ClosedState) Option[ClosedState] {
|
||||
return cs.Check(ct)
|
||||
}
|
||||
}
|
||||
|
||||
openCircuit := func(ct time.Time) openState {
|
||||
return openState{
|
||||
openedAt: ct,
|
||||
resetAt: expectedResetTime,
|
||||
retryStatus: retry.DefaultRetryStatus,
|
||||
canaryRequest: false,
|
||||
}
|
||||
}
|
||||
|
||||
initialState := createClosedCircuit(initialClosed)
|
||||
|
||||
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
|
||||
endomorphism := handler(currentTime)
|
||||
|
||||
// First error - should open immediately (threshold=1)
|
||||
result1 := endomorphism(initialState)
|
||||
assert.True(t, IsOpen(result1), "circuit should open after first error")
|
||||
|
||||
// Verify the open state has correct reset time
|
||||
resultOpen := either.Fold(
|
||||
func(os openState) openState { return os },
|
||||
func(ClosedState) openState { return openState{} },
|
||||
)(result1)
|
||||
assert.Equal(t, expectedResetTime, resultOpen.resetAt, "reset time should match expected")
|
||||
assert.Equal(t, currentTime, resultOpen.openedAt, "opened time should be current time")
|
||||
})
|
||||
|
||||
t.Run("edge case: zero error threshold", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
currentTime := vt.Now()
|
||||
|
||||
// Create a closed state that allows 0 errors (opens immediately)
|
||||
initialClosed := MakeClosedStateCounter(0)
|
||||
|
||||
addError := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddError(ct)
|
||||
}
|
||||
}
|
||||
|
||||
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
|
||||
return func(cs ClosedState) Option[ClosedState] {
|
||||
return cs.Check(ct)
|
||||
}
|
||||
}
|
||||
|
||||
openCircuit := func(ct time.Time) openState {
|
||||
return openState{
|
||||
openedAt: ct,
|
||||
@@ -566,14 +740,212 @@ func TestHandleFailureOnClosed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
initialState := createClosedCircuit(MakeClosedStateCounter(10))
|
||||
ref := io.Run(ioref.MakeIORef(initialState))
|
||||
modify := modifyV(ref)
|
||||
initialState := createClosedCircuit(initialClosed)
|
||||
|
||||
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
|
||||
result := io.Run(handler(modify))
|
||||
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
|
||||
endomorphism := handler(currentTime)
|
||||
|
||||
// Should still be closed but with failure recorded
|
||||
assert.True(t, IsClosed(result), "circuit should remain closed")
|
||||
// First error should immediately open the circuit
|
||||
result := endomorphism(initialState)
|
||||
assert.True(t, IsOpen(result), "circuit should open immediately with zero threshold")
|
||||
})
|
||||
|
||||
t.Run("edge case: very high error threshold", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
currentTime := vt.Now()
|
||||
|
||||
// Create a closed state that allows 1000 errors
|
||||
initialClosed := MakeClosedStateCounter(1000)
|
||||
|
||||
addError := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddError(ct)
|
||||
}
|
||||
}
|
||||
|
||||
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
|
||||
return func(cs ClosedState) Option[ClosedState] {
|
||||
return cs.Check(ct)
|
||||
}
|
||||
}
|
||||
|
||||
openCircuit := func(ct time.Time) openState {
|
||||
return openState{
|
||||
openedAt: ct,
|
||||
resetAt: ct.Add(1 * time.Minute),
|
||||
retryStatus: retry.DefaultRetryStatus,
|
||||
canaryRequest: false,
|
||||
}
|
||||
}
|
||||
|
||||
initialState := createClosedCircuit(initialClosed)
|
||||
|
||||
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
|
||||
endomorphism := handler(currentTime)
|
||||
|
||||
// Apply many errors
|
||||
result := initialState
|
||||
for i := 0; i < 100; i++ {
|
||||
result = endomorphism(result)
|
||||
}
|
||||
|
||||
// Should still be closed after 100 errors
|
||||
assert.True(t, IsClosed(result), "circuit should remain closed with high threshold")
|
||||
})
|
||||
|
||||
t.Run("preserves time parameter through reader chain", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
time1 := vt.Now()
|
||||
vt.Advance(2 * time.Hour)
|
||||
time2 := vt.Now()
|
||||
|
||||
var capturedAddErrorTime, capturedCheckTime, capturedOpenTime time.Time
|
||||
|
||||
initialClosed := MakeClosedStateCounter(2) // Need 2 errors to open
|
||||
|
||||
addError := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
capturedAddErrorTime = ct
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddError(ct)
|
||||
}
|
||||
}
|
||||
|
||||
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
|
||||
capturedCheckTime = ct
|
||||
return func(cs ClosedState) Option[ClosedState] {
|
||||
return cs.Check(ct)
|
||||
}
|
||||
}
|
||||
|
||||
openCircuit := func(ct time.Time) openState {
|
||||
capturedOpenTime = ct
|
||||
return openState{
|
||||
openedAt: ct,
|
||||
resetAt: ct.Add(1 * time.Minute),
|
||||
retryStatus: retry.DefaultRetryStatus,
|
||||
canaryRequest: false,
|
||||
}
|
||||
}
|
||||
|
||||
initialState := createClosedCircuit(initialClosed)
|
||||
|
||||
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
|
||||
|
||||
// Apply with time1 - first error, stays closed
|
||||
endomorphism1 := handler(time1)
|
||||
result1 := endomorphism1(initialState)
|
||||
assert.Equal(t, time1, capturedAddErrorTime, "addError should receive time1")
|
||||
assert.Equal(t, time1, capturedCheckTime, "checkClosedState should receive time1")
|
||||
|
||||
// Apply with time2 - second error, should trigger open
|
||||
endomorphism2 := handler(time2)
|
||||
endomorphism2(result1)
|
||||
assert.Equal(t, time2, capturedAddErrorTime, "addError should receive time2")
|
||||
assert.Equal(t, time2, capturedCheckTime, "checkClosedState should receive time2")
|
||||
assert.Equal(t, time2, capturedOpenTime, "openCircuit should receive time2")
|
||||
})
|
||||
|
||||
t.Run("handles transition from closed to open correctly", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
currentTime := vt.Now()
|
||||
|
||||
initialClosed := MakeClosedStateCounter(2) // Opens at 2nd error
|
||||
|
||||
addError := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddError(ct)
|
||||
}
|
||||
}
|
||||
|
||||
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
|
||||
return func(cs ClosedState) Option[ClosedState] {
|
||||
return cs.Check(ct)
|
||||
}
|
||||
}
|
||||
|
||||
openCircuit := func(ct time.Time) openState {
|
||||
return openState{
|
||||
openedAt: ct,
|
||||
resetAt: ct.Add(1 * time.Minute),
|
||||
retryStatus: retry.DefaultRetryStatus,
|
||||
canaryRequest: false,
|
||||
}
|
||||
}
|
||||
|
||||
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
|
||||
endomorphism := handler(currentTime)
|
||||
|
||||
// Start with closed state
|
||||
state := createClosedCircuit(initialClosed)
|
||||
assert.True(t, IsClosed(state), "initial state should be closed")
|
||||
|
||||
// First error - should stay closed (count=1, threshold=2)
|
||||
state = endomorphism(state)
|
||||
assert.True(t, IsClosed(state), "should remain closed after first error")
|
||||
|
||||
// Second error - should open (count=2, threshold=2)
|
||||
state = endomorphism(state)
|
||||
assert.True(t, IsOpen(state), "should open after second error")
|
||||
|
||||
// Verify it's truly open with correct properties
|
||||
resultOpen := either.Fold(
|
||||
func(os openState) openState { return os },
|
||||
func(ClosedState) openState { return openState{} },
|
||||
)(state)
|
||||
assert.False(t, resultOpen.canaryRequest, "canaryRequest should be false initially")
|
||||
assert.Equal(t, currentTime, resultOpen.openedAt, "openedAt should be current time")
|
||||
})
|
||||
|
||||
t.Run("does not affect already open state", func(t *testing.T) {
|
||||
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
|
||||
currentTime := vt.Now()
|
||||
|
||||
addError := func(ct time.Time) Endomorphism[ClosedState] {
|
||||
return func(cs ClosedState) ClosedState {
|
||||
return cs.AddError(ct)
|
||||
}
|
||||
}
|
||||
|
||||
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
|
||||
return func(cs ClosedState) Option[ClosedState] {
|
||||
return cs.Check(ct)
|
||||
}
|
||||
}
|
||||
|
||||
openCircuit := func(ct time.Time) openState {
|
||||
return openState{
|
||||
openedAt: ct,
|
||||
resetAt: ct.Add(1 * time.Minute),
|
||||
retryStatus: retry.DefaultRetryStatus,
|
||||
canaryRequest: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Start with an already open state
|
||||
existingOpen := openState{
|
||||
openedAt: currentTime.Add(-5 * time.Minute),
|
||||
resetAt: currentTime.Add(5 * time.Minute),
|
||||
retryStatus: retry.DefaultRetryStatus,
|
||||
canaryRequest: true,
|
||||
}
|
||||
initialState := createOpenCircuit(existingOpen)
|
||||
|
||||
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
|
||||
endomorphism := handler(currentTime)
|
||||
|
||||
// Apply to open state - should not change it
|
||||
result := endomorphism(initialState)
|
||||
|
||||
assert.True(t, IsOpen(result), "state should remain open")
|
||||
|
||||
// The open state should be unchanged since handleFailureOnClosed
|
||||
// only operates on the Right (closed) side of the Either
|
||||
openResult := either.Fold(
|
||||
func(os openState) openState { return os },
|
||||
func(ClosedState) openState { return openState{} },
|
||||
)(result)
|
||||
assert.Equal(t, existingOpen.openedAt, openResult.openedAt, "openedAt should be unchanged")
|
||||
assert.Equal(t, existingOpen.resetAt, openResult.resetAt, "resetAt should be unchanged")
|
||||
assert.Equal(t, existingOpen.canaryRequest, openResult.canaryRequest, "canaryRequest should be unchanged")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ import (
|
||||
//
|
||||
// Thread Safety: This type is immutable and safe for concurrent use.
|
||||
type CircuitBreakerError struct {
|
||||
Name string
|
||||
// Name: The name identifying this circuit breaker instance
|
||||
Name string
|
||||
|
||||
// ResetAt: The time at which the circuit breaker will transition from open to half-open state
|
||||
ResetAt time.Time
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -110,6 +111,25 @@ type (
|
||||
name string
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// voidMetrics is a no-op implementation of the Metrics interface that does nothing.
|
||||
// All methods return the same pre-allocated IO[Void] operation that immediately returns
|
||||
// without performing any action.
|
||||
//
|
||||
// This implementation is useful for:
|
||||
// - Testing scenarios where metrics collection is not needed
|
||||
// - Production environments where metrics overhead should be eliminated
|
||||
// - Benchmarking circuit breaker logic without metrics interference
|
||||
// - Default initialization when no metrics implementation is provided
|
||||
//
|
||||
// Thread Safety: This implementation is safe for concurrent use. The noop IO operation
|
||||
// is immutable and can be safely shared across goroutines.
|
||||
//
|
||||
// Performance: This is the most efficient Metrics implementation as it performs no
|
||||
// operations and has minimal memory overhead (single shared IO[Void] instance).
|
||||
voidMetrics struct {
|
||||
noop IO[Void]
|
||||
}
|
||||
)
|
||||
|
||||
// doLog is a helper method that creates an IO operation for logging a circuit breaker event.
|
||||
@@ -206,3 +226,79 @@ func (m *loggingMetrics) Canary(ct time.Time) IO[Void] {
|
||||
func MakeMetricsFromLogger(name string, logger *log.Logger) Metrics {
|
||||
return &loggingMetrics{name: name, logger: logger}
|
||||
}
|
||||
|
||||
// Open implements the Metrics interface for voidMetrics.
|
||||
// Returns a no-op IO operation that does nothing.
|
||||
//
|
||||
// Thread Safety: Safe for concurrent use.
|
||||
func (m *voidMetrics) Open(_ time.Time) IO[Void] {
|
||||
return m.noop
|
||||
}
|
||||
|
||||
// Accept implements the Metrics interface for voidMetrics.
|
||||
// Returns a no-op IO operation that does nothing.
|
||||
//
|
||||
// Thread Safety: Safe for concurrent use.
|
||||
func (m *voidMetrics) Accept(_ time.Time) IO[Void] {
|
||||
return m.noop
|
||||
}
|
||||
|
||||
// Canary implements the Metrics interface for voidMetrics.
|
||||
// Returns a no-op IO operation that does nothing.
|
||||
//
|
||||
// Thread Safety: Safe for concurrent use.
|
||||
func (m *voidMetrics) Canary(_ time.Time) IO[Void] {
|
||||
return m.noop
|
||||
}
|
||||
|
||||
// Close implements the Metrics interface for voidMetrics.
|
||||
// Returns a no-op IO operation that does nothing.
|
||||
//
|
||||
// Thread Safety: Safe for concurrent use.
|
||||
func (m *voidMetrics) Close(_ time.Time) IO[Void] {
|
||||
return m.noop
|
||||
}
|
||||
|
||||
// Reject implements the Metrics interface for voidMetrics.
|
||||
// Returns a no-op IO operation that does nothing.
|
||||
//
|
||||
// Thread Safety: Safe for concurrent use.
|
||||
func (m *voidMetrics) Reject(_ time.Time) IO[Void] {
|
||||
return m.noop
|
||||
}
|
||||
|
||||
// MakeVoidMetrics creates a no-op Metrics implementation that performs no operations.
|
||||
// All methods return the same pre-allocated IO[Void] operation that does nothing when executed.
|
||||
//
|
||||
// This is useful for:
|
||||
// - Testing scenarios where metrics collection is not needed
|
||||
// - Production environments where metrics overhead should be eliminated
|
||||
// - Benchmarking circuit breaker logic without metrics interference
|
||||
// - Default initialization when no metrics implementation is provided
|
||||
//
|
||||
// Returns:
|
||||
// - Metrics: A thread-safe no-op Metrics implementation
|
||||
//
|
||||
// Thread Safety: The returned Metrics implementation is safe for concurrent use.
|
||||
// All methods return the same immutable IO[Void] operation.
|
||||
//
|
||||
// Performance: This is the most efficient Metrics implementation with minimal overhead.
|
||||
// The IO[Void] operation is pre-allocated once and reused for all method calls.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// metrics := MakeVoidMetrics()
|
||||
//
|
||||
// // All operations do nothing
|
||||
// io.Run(metrics.Open(time.Now())) // No-op
|
||||
// io.Run(metrics.Accept(time.Now())) // No-op
|
||||
// io.Run(metrics.Reject(time.Now())) // No-op
|
||||
//
|
||||
// // Useful for testing
|
||||
// breaker := MakeCircuitBreaker(
|
||||
// // ... other parameters ...
|
||||
// MakeVoidMetrics(), // No metrics overhead
|
||||
// )
|
||||
func MakeVoidMetrics() Metrics {
|
||||
return &voidMetrics{io.Of(function.VOID)}
|
||||
}
|
||||
|
||||
@@ -504,3 +504,443 @@ func TestMetricsIOOperations(t *testing.T) {
|
||||
assert.Len(t, lines, 3, "should execute multiple times")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMakeVoidMetrics tests the MakeVoidMetrics constructor
|
||||
func TestMakeVoidMetrics(t *testing.T) {
|
||||
t.Run("creates valid Metrics implementation", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
|
||||
assert.NotNil(t, metrics, "MakeVoidMetrics should return non-nil Metrics")
|
||||
})
|
||||
|
||||
t.Run("returns voidMetrics type", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
|
||||
_, ok := metrics.(*voidMetrics)
|
||||
assert.True(t, ok, "should return *voidMetrics type")
|
||||
})
|
||||
|
||||
t.Run("initializes noop IO operation", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics().(*voidMetrics)
|
||||
|
||||
assert.NotNil(t, metrics.noop, "noop IO operation should be initialized")
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMetricsAccept tests the Accept method of voidMetrics
|
||||
func TestVoidMetricsAccept(t *testing.T) {
|
||||
t.Run("returns non-nil IO operation", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Accept(timestamp)
|
||||
|
||||
assert.NotNil(t, ioOp, "should return non-nil IO operation")
|
||||
})
|
||||
|
||||
t.Run("IO operation executes without side effects", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Accept(timestamp)
|
||||
result := io.Run(ioOp)
|
||||
|
||||
assert.NotNil(t, result, "IO operation should execute successfully")
|
||||
})
|
||||
|
||||
t.Run("returns same IO operation instance", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics().(*voidMetrics)
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp1 := metrics.Accept(timestamp)
|
||||
ioOp2 := metrics.Accept(timestamp)
|
||||
|
||||
// Both should be non-nil (we can't compare functions directly in Go)
|
||||
assert.NotNil(t, ioOp1, "should return non-nil IO operation")
|
||||
assert.NotNil(t, ioOp2, "should return non-nil IO operation")
|
||||
|
||||
// Verify they execute without error
|
||||
io.Run(ioOp1)
|
||||
io.Run(ioOp2)
|
||||
})
|
||||
|
||||
t.Run("ignores timestamp parameter", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
time1 := time.Date(2026, 1, 9, 15, 30, 0, 0, time.UTC)
|
||||
time2 := time.Date(2026, 1, 9, 16, 30, 0, 0, time.UTC)
|
||||
|
||||
ioOp1 := metrics.Accept(time1)
|
||||
ioOp2 := metrics.Accept(time2)
|
||||
|
||||
// Should return same operation regardless of timestamp
|
||||
io.Run(ioOp1)
|
||||
io.Run(ioOp2)
|
||||
// No assertions needed - just verify it doesn't panic
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMetricsReject tests the Reject method of voidMetrics
|
||||
func TestVoidMetricsReject(t *testing.T) {
|
||||
t.Run("returns non-nil IO operation", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Reject(timestamp)
|
||||
|
||||
assert.NotNil(t, ioOp, "should return non-nil IO operation")
|
||||
})
|
||||
|
||||
t.Run("IO operation executes without side effects", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Reject(timestamp)
|
||||
result := io.Run(ioOp)
|
||||
|
||||
assert.NotNil(t, result, "IO operation should execute successfully")
|
||||
})
|
||||
|
||||
t.Run("returns same IO operation instance", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Reject(timestamp)
|
||||
|
||||
assert.NotNil(t, ioOp, "should return non-nil IO operation")
|
||||
io.Run(ioOp) // Verify it executes without error
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMetricsOpen tests the Open method of voidMetrics
|
||||
func TestVoidMetricsOpen(t *testing.T) {
|
||||
t.Run("returns non-nil IO operation", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Open(timestamp)
|
||||
|
||||
assert.NotNil(t, ioOp, "should return non-nil IO operation")
|
||||
})
|
||||
|
||||
t.Run("IO operation executes without side effects", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Open(timestamp)
|
||||
result := io.Run(ioOp)
|
||||
|
||||
assert.NotNil(t, result, "IO operation should execute successfully")
|
||||
})
|
||||
|
||||
t.Run("returns same IO operation instance", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Open(timestamp)
|
||||
|
||||
assert.NotNil(t, ioOp, "should return non-nil IO operation")
|
||||
io.Run(ioOp) // Verify it executes without error
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMetricsClose tests the Close method of voidMetrics
|
||||
func TestVoidMetricsClose(t *testing.T) {
|
||||
t.Run("returns non-nil IO operation", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Close(timestamp)
|
||||
|
||||
assert.NotNil(t, ioOp, "should return non-nil IO operation")
|
||||
})
|
||||
|
||||
t.Run("IO operation executes without side effects", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Close(timestamp)
|
||||
result := io.Run(ioOp)
|
||||
|
||||
assert.NotNil(t, result, "IO operation should execute successfully")
|
||||
})
|
||||
|
||||
t.Run("returns same IO operation instance", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Close(timestamp)
|
||||
|
||||
assert.NotNil(t, ioOp, "should return non-nil IO operation")
|
||||
io.Run(ioOp) // Verify it executes without error
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMetricsCanary tests the Canary method of voidMetrics
|
||||
func TestVoidMetricsCanary(t *testing.T) {
|
||||
t.Run("returns non-nil IO operation", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Canary(timestamp)
|
||||
|
||||
assert.NotNil(t, ioOp, "should return non-nil IO operation")
|
||||
})
|
||||
|
||||
t.Run("IO operation executes without side effects", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Canary(timestamp)
|
||||
result := io.Run(ioOp)
|
||||
|
||||
assert.NotNil(t, result, "IO operation should execute successfully")
|
||||
})
|
||||
|
||||
t.Run("returns same IO operation instance", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Canary(timestamp)
|
||||
|
||||
assert.NotNil(t, ioOp, "should return non-nil IO operation")
|
||||
io.Run(ioOp) // Verify it executes without error
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMetricsThreadSafety tests concurrent access to voidMetrics
|
||||
func TestVoidMetricsThreadSafety(t *testing.T) {
|
||||
t.Run("handles concurrent metric calls", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 100
|
||||
wg.Add(numGoroutines * 5) // 5 methods
|
||||
|
||||
timestamp := time.Now()
|
||||
|
||||
// Launch multiple goroutines calling all methods concurrently
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Run(metrics.Accept(timestamp))
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Run(metrics.Reject(timestamp))
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Run(metrics.Open(timestamp))
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Run(metrics.Close(timestamp))
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Run(metrics.Canary(timestamp))
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
// Test passes if no panic occurs
|
||||
})
|
||||
|
||||
t.Run("all methods return valid IO operations concurrently", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 50
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
timestamp := time.Now()
|
||||
results := make([]IO[Void], numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
// Each goroutine calls a different method
|
||||
switch idx % 5 {
|
||||
case 0:
|
||||
results[idx] = metrics.Accept(timestamp)
|
||||
case 1:
|
||||
results[idx] = metrics.Reject(timestamp)
|
||||
case 2:
|
||||
results[idx] = metrics.Open(timestamp)
|
||||
case 3:
|
||||
results[idx] = metrics.Close(timestamp)
|
||||
case 4:
|
||||
results[idx] = metrics.Canary(timestamp)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// All results should be non-nil and executable
|
||||
for i, result := range results {
|
||||
assert.NotNil(t, result, "result %d should be non-nil", i)
|
||||
io.Run(result) // Verify it executes without error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMetricsPerformance tests performance characteristics
|
||||
func TestVoidMetricsPerformance(t *testing.T) {
|
||||
t.Run("has minimal overhead", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
// Execute many operations quickly
|
||||
iterations := 10000
|
||||
for i := 0; i < iterations; i++ {
|
||||
io.Run(metrics.Accept(timestamp))
|
||||
io.Run(metrics.Reject(timestamp))
|
||||
io.Run(metrics.Open(timestamp))
|
||||
io.Run(metrics.Close(timestamp))
|
||||
io.Run(metrics.Canary(timestamp))
|
||||
}
|
||||
// Test passes if it completes quickly without issues
|
||||
})
|
||||
|
||||
t.Run("all methods return valid IO operations", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
// All methods should return non-nil IO operations
|
||||
accept := metrics.Accept(timestamp)
|
||||
reject := metrics.Reject(timestamp)
|
||||
open := metrics.Open(timestamp)
|
||||
close := metrics.Close(timestamp)
|
||||
canary := metrics.Canary(timestamp)
|
||||
|
||||
assert.NotNil(t, accept, "Accept should return non-nil")
|
||||
assert.NotNil(t, reject, "Reject should return non-nil")
|
||||
assert.NotNil(t, open, "Open should return non-nil")
|
||||
assert.NotNil(t, close, "Close should return non-nil")
|
||||
assert.NotNil(t, canary, "Canary should return non-nil")
|
||||
|
||||
// All should execute without error
|
||||
io.Run(accept)
|
||||
io.Run(reject)
|
||||
io.Run(open)
|
||||
io.Run(close)
|
||||
io.Run(canary)
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMetricsIntegration tests integration scenarios
|
||||
func TestVoidMetricsIntegration(t *testing.T) {
|
||||
t.Run("can be used as drop-in replacement for loggingMetrics", func(t *testing.T) {
|
||||
// Create both types of metrics
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
loggingMetrics := MakeMetricsFromLogger("TestCircuit", logger)
|
||||
voidMetrics := MakeVoidMetrics()
|
||||
|
||||
timestamp := time.Now()
|
||||
|
||||
// Both should implement the same interface
|
||||
var m1 Metrics = loggingMetrics
|
||||
var m2 Metrics = voidMetrics
|
||||
|
||||
// Both should be callable
|
||||
io.Run(m1.Accept(timestamp))
|
||||
io.Run(m2.Accept(timestamp))
|
||||
|
||||
// Logging metrics should have output
|
||||
assert.NotEmpty(t, buf.String(), "logging metrics should produce output")
|
||||
|
||||
// Void metrics should have no observable side effects
|
||||
// (we can't directly test this, but the test passes if no panic occurs)
|
||||
})
|
||||
|
||||
t.Run("simulates complete circuit breaker lifecycle without side effects", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
baseTime := time.Date(2026, 1, 9, 15, 30, 0, 0, time.UTC)
|
||||
|
||||
// Simulate circuit breaker lifecycle - all should be no-ops
|
||||
io.Run(metrics.Accept(baseTime))
|
||||
io.Run(metrics.Accept(baseTime.Add(1 * time.Second)))
|
||||
io.Run(metrics.Open(baseTime.Add(2 * time.Second)))
|
||||
io.Run(metrics.Reject(baseTime.Add(3 * time.Second)))
|
||||
io.Run(metrics.Canary(baseTime.Add(30 * time.Second)))
|
||||
io.Run(metrics.Close(baseTime.Add(31 * time.Second)))
|
||||
|
||||
// Test passes if no panic occurs and completes quickly
|
||||
})
|
||||
}
|
||||
|
||||
// TestVoidMetricsEdgeCases tests edge cases
|
||||
func TestVoidMetricsEdgeCases(t *testing.T) {
|
||||
t.Run("handles zero time", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
zeroTime := time.Time{}
|
||||
|
||||
io.Run(metrics.Accept(zeroTime))
|
||||
io.Run(metrics.Reject(zeroTime))
|
||||
io.Run(metrics.Open(zeroTime))
|
||||
io.Run(metrics.Close(zeroTime))
|
||||
io.Run(metrics.Canary(zeroTime))
|
||||
|
||||
// Test passes if no panic occurs
|
||||
})
|
||||
|
||||
t.Run("handles far future time", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
futureTime := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
|
||||
|
||||
io.Run(metrics.Accept(futureTime))
|
||||
io.Run(metrics.Reject(futureTime))
|
||||
io.Run(metrics.Open(futureTime))
|
||||
io.Run(metrics.Close(futureTime))
|
||||
io.Run(metrics.Canary(futureTime))
|
||||
|
||||
// Test passes if no panic occurs
|
||||
})
|
||||
|
||||
t.Run("IO operations are idempotent", func(t *testing.T) {
|
||||
metrics := MakeVoidMetrics()
|
||||
timestamp := time.Now()
|
||||
|
||||
ioOp := metrics.Accept(timestamp)
|
||||
|
||||
// Execute same operation multiple times
|
||||
io.Run(ioOp)
|
||||
io.Run(ioOp)
|
||||
io.Run(ioOp)
|
||||
|
||||
// Test passes if no panic occurs
|
||||
})
|
||||
}
|
||||
|
||||
// TestMetricsComparison compares loggingMetrics and voidMetrics
|
||||
func TestMetricsComparison(t *testing.T) {
|
||||
t.Run("both implement Metrics interface", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
|
||||
var m1 Metrics = MakeMetricsFromLogger("Test", logger)
|
||||
var m2 Metrics = MakeVoidMetrics()
|
||||
|
||||
assert.NotNil(t, m1)
|
||||
assert.NotNil(t, m2)
|
||||
})
|
||||
|
||||
t.Run("voidMetrics has no observable side effects unlike loggingMetrics", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
loggingMetrics := MakeMetricsFromLogger("Test", logger)
|
||||
voidMetrics := MakeVoidMetrics()
|
||||
|
||||
timestamp := time.Now()
|
||||
|
||||
// Logging metrics produces output
|
||||
io.Run(loggingMetrics.Accept(timestamp))
|
||||
assert.NotEmpty(t, buf.String(), "logging metrics should produce output")
|
||||
|
||||
// Void metrics has no observable output
|
||||
// (we can only verify it doesn't panic)
|
||||
io.Run(voidMetrics.Accept(timestamp))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
"github.com/IBM/fp-go/v2/state"
|
||||
)
|
||||
@@ -79,10 +80,13 @@ type (
|
||||
// and produces a value of type A. Used for dependency injection and configuration.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
|
||||
// openState represents the internal state when the circuit breaker is open.
|
||||
// In the open state, requests are blocked to give the failing service time to recover.
|
||||
// The circuit breaker will transition to a half-open state (canary request) after resetAt.
|
||||
openState struct {
|
||||
// openedAt is the time when the circuit breaker opened the circuit
|
||||
openedAt time.Time
|
||||
|
||||
// resetAt is the time when the circuit breaker should attempt a canary request
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateTraverseTuple(f *os.File, i int) {
|
||||
@@ -422,10 +423,10 @@ func ApplyCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateApplyHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func createCombinations(n int, all, prev []int) [][]int {
|
||||
@@ -284,10 +285,10 @@ func BindCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateBindHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func Commands() []*C.Command {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"strings"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// Deprecated:
|
||||
@@ -261,10 +262,10 @@ func ContextReaderIOEitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateContextReaderIOEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateMakeProvider(f *os.File, i int) {
|
||||
@@ -221,10 +222,10 @@ func DICommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateDIHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func eitherHKT(typeE string) func(typeA string) string {
|
||||
@@ -190,10 +191,10 @@ func EitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func identityHKT(typeA string) string {
|
||||
@@ -93,10 +94,10 @@ func IdentityCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIdentityHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func nonGenericIO(param string) string {
|
||||
@@ -102,10 +103,10 @@ func IOCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIOHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// [GA ~func() ET.Either[E, A], GB ~func() ET.Either[E, B], GTAB ~func() ET.Either[E, T.Tuple2[A, B]], E, A, B any](a GA, b GB) GTAB {
|
||||
@@ -273,10 +274,10 @@ func IOEitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIOEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func nonGenericIOOption(param string) string {
|
||||
@@ -107,10 +108,10 @@ func IOOptionCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateIOOptionHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
106
v2/cli/lens.go
106
v2/cli/lens.go
@@ -17,6 +17,7 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
@@ -28,7 +29,7 @@ import (
|
||||
"text/template"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -86,7 +87,9 @@ type templateData struct {
|
||||
}
|
||||
|
||||
const lensStructTemplate = `
|
||||
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
|
||||
// {{.Name}}Lenses provides [lenses] for accessing fields of [{{.Name}}]
|
||||
//
|
||||
// [lenses]: __lens.Lens
|
||||
type {{.Name}}Lenses{{.TypeParams}} struct {
|
||||
// mandatory fields
|
||||
{{- range .Fields}}
|
||||
@@ -100,7 +103,10 @@ type {{.Name}}Lenses{{.TypeParams}} struct {
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
|
||||
// {{.Name}}RefLenses provides [lenses] for accessing fields of [{{.Name}}] via a reference to [{{.Name}}]
|
||||
//
|
||||
//
|
||||
// [lenses]: __lens.Lens
|
||||
type {{.Name}}RefLenses{{.TypeParams}} struct {
|
||||
// mandatory fields
|
||||
{{- range .Fields}}
|
||||
@@ -111,23 +117,32 @@ type {{.Name}}RefLenses{{.TypeParams}} struct {
|
||||
{{- if .IsComparable}}
|
||||
{{.Name}}O __lens_option.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
// prisms
|
||||
{{- range .Fields}}
|
||||
{{.Name}}P __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}Prisms provides prisms for accessing fields of {{.Name}}
|
||||
// {{.Name}}Prisms provides [prisms] for accessing fields of [{{.Name}}]
|
||||
//
|
||||
// [prisms]: __prism.Prism
|
||||
type {{.Name}}Prisms{{.TypeParams}} struct {
|
||||
{{- range .Fields}}
|
||||
{{.Name}} __prism.Prism[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}RefPrisms provides [prisms] for accessing fields of [{{.Name}}] via a reference to [{{.Name}}]
|
||||
//
|
||||
// [prisms]: __prism.Prism
|
||||
type {{.Name}}RefPrisms{{.TypeParams}} struct {
|
||||
{{- range .Fields}}
|
||||
{{.Name}} __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
}
|
||||
`
|
||||
|
||||
const lensConstructorTemplate = `
|
||||
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
|
||||
// Make{{.Name}}Lenses creates a new [{{.Name}}Lenses] with [lenses] for all fields
|
||||
//
|
||||
// [lenses]:__lens.Lens
|
||||
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
@@ -157,7 +172,9 @@ func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
|
||||
}
|
||||
}
|
||||
|
||||
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
|
||||
// Make{{.Name}}RefLenses creates a new [{{.Name}}RefLenses] with [lenses] for all fields
|
||||
//
|
||||
// [lenses]:__lens.Lens
|
||||
func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames}} {
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
@@ -195,7 +212,9 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
|
||||
}
|
||||
}
|
||||
|
||||
// Make{{.Name}}Prisms creates a new {{.Name}}Prisms with prisms for all fields
|
||||
// Make{{.Name}}Prisms creates a new [{{.Name}}Prisms] with [prisms] for all fields
|
||||
//
|
||||
// [prisms]:__prism.Prism
|
||||
func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
@@ -235,6 +254,49 @@ func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
|
||||
// Make{{.Name}}RefPrisms creates a new [{{.Name}}RefPrisms] with [prisms] for all fields
|
||||
//
|
||||
// [prisms]:__prism.Prism
|
||||
func Make{{.Name}}RefPrisms{{.TypeParams}}() {{.Name}}RefPrisms{{.TypeParamNames}} {
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
_fromNonZero{{.Name}} := __option.FromNonZero[{{.TypeName}}]()
|
||||
_prism{{.Name}} := __prism.MakePrismWithName(
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return _fromNonZero{{.Name}}(s.{{.Name}}) },
|
||||
func(v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} {
|
||||
{{- if .IsEmbedded}}
|
||||
var result {{$.Name}}{{$.TypeParamNames}}
|
||||
result.{{.Name}} = v
|
||||
return &result
|
||||
{{- else}}
|
||||
return &{{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
|
||||
{{- end}}
|
||||
},
|
||||
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||
)
|
||||
{{- else}}
|
||||
_prism{{.Name}} := __prism.MakePrismWithName(
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return __option.Some(s.{{.Name}}) },
|
||||
func(v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} {
|
||||
{{- if .IsEmbedded}}
|
||||
var result {{$.Name}}{{$.TypeParamNames}}
|
||||
result.{{.Name}} = v
|
||||
return &result
|
||||
{{- else}}
|
||||
return &{{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
|
||||
{{- end}}
|
||||
},
|
||||
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||
)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}RefPrisms{{.TypeParamNames}} {
|
||||
{{- range .Fields}}
|
||||
{{.Name}}: _prism{{.Name}},
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var (
|
||||
@@ -535,9 +597,9 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
|
||||
}
|
||||
|
||||
for _, name := range field.Names {
|
||||
// Only export lenses for exported fields
|
||||
if name.IsExported() {
|
||||
fieldTypeName := getTypeName(field.Type)
|
||||
// Generate lenses for both exported and unexported fields
|
||||
fieldTypeName := getTypeName(field.Type)
|
||||
if true { // Keep the block structure for minimal changes
|
||||
isOptional := false
|
||||
baseType := fieldTypeName
|
||||
|
||||
@@ -697,9 +759,9 @@ func parseFile(filename string) ([]structInfo, string, error) {
|
||||
continue
|
||||
}
|
||||
for _, name := range field.Names {
|
||||
// Only export lenses for exported fields
|
||||
if name.IsExported() {
|
||||
typeName := getTypeName(field.Type)
|
||||
// Generate lenses for both exported and unexported fields
|
||||
typeName := getTypeName(field.Type)
|
||||
if true { // Keep the block structure for minimal changes
|
||||
isOptional := false
|
||||
baseType := typeName
|
||||
isComparable := false
|
||||
@@ -934,12 +996,12 @@ func LensCommand() *C.Command {
|
||||
flagVerbose,
|
||||
flagIncludeTestFiles,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateLensHelpers(
|
||||
ctx.String(keyLensDir),
|
||||
ctx.String(keyFilename),
|
||||
ctx.Bool(keyVerbose),
|
||||
ctx.Bool(keyIncludeTestFile),
|
||||
cmd.String(keyLensDir),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Bool(keyVerbose),
|
||||
cmd.Bool(keyIncludeTestFile),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1086,3 +1086,255 @@ type ComparableBox[T comparable] struct {
|
||||
// Verify that MakeLensRef is NOT used (since both fields are comparable)
|
||||
assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
|
||||
}
|
||||
|
||||
func TestParseFileWithUnexportedFields(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type Config struct {
|
||||
PublicName string
|
||||
privateName string
|
||||
PublicValue int
|
||||
privateValue *int
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 1)
|
||||
|
||||
// Check Config struct
|
||||
config := structs[0]
|
||||
assert.Equal(t, "Config", config.Name)
|
||||
assert.Len(t, config.Fields, 4, "Should include both exported and unexported fields")
|
||||
|
||||
// Check exported field
|
||||
assert.Equal(t, "PublicName", config.Fields[0].Name)
|
||||
assert.Equal(t, "string", config.Fields[0].TypeName)
|
||||
assert.False(t, config.Fields[0].IsOptional)
|
||||
|
||||
// Check unexported field
|
||||
assert.Equal(t, "privateName", config.Fields[1].Name)
|
||||
assert.Equal(t, "string", config.Fields[1].TypeName)
|
||||
assert.False(t, config.Fields[1].IsOptional)
|
||||
|
||||
// Check exported int field
|
||||
assert.Equal(t, "PublicValue", config.Fields[2].Name)
|
||||
assert.Equal(t, "int", config.Fields[2].TypeName)
|
||||
assert.False(t, config.Fields[2].IsOptional)
|
||||
|
||||
// Check unexported pointer field
|
||||
assert.Equal(t, "privateValue", config.Fields[3].Name)
|
||||
assert.Equal(t, "*int", config.Fields[3].TypeName)
|
||||
assert.True(t, config.Fields[3].IsOptional)
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersWithUnexportedFields(t *testing.T) {
|
||||
// Create a temporary directory with test files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type MixedStruct struct {
|
||||
PublicField string
|
||||
privateField int
|
||||
OptionalPrivate *string
|
||||
}
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen_lens.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
genPath := filepath.Join(tmpDir, outputFile)
|
||||
_, err = os.Stat(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read and verify the generated content
|
||||
content, err := os.ReadFile(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// Check for expected content
|
||||
assert.Contains(t, contentStr, "package testpkg")
|
||||
assert.Contains(t, contentStr, "MixedStructLenses")
|
||||
assert.Contains(t, contentStr, "MakeMixedStructLenses")
|
||||
|
||||
// Check that lenses are generated for all fields (exported and unexported)
|
||||
assert.Contains(t, contentStr, "PublicField __lens.Lens[MixedStruct, string]")
|
||||
assert.Contains(t, contentStr, "privateField __lens.Lens[MixedStruct, int]")
|
||||
assert.Contains(t, contentStr, "OptionalPrivate __lens.Lens[MixedStruct, *string]")
|
||||
|
||||
// Check lens constructors
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct) string { return s.PublicField }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct) int { return s.privateField }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct) *string { return s.OptionalPrivate }")
|
||||
|
||||
// Check setters
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct, v string) MixedStruct { s.PublicField = v; return s }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct, v int) MixedStruct { s.privateField = v; return s }")
|
||||
assert.Contains(t, contentStr, "func(s MixedStruct, v *string) MixedStruct { s.OptionalPrivate = v; return s }")
|
||||
}
|
||||
|
||||
func TestParseFileWithOnlyUnexportedFields(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type PrivateConfig struct {
|
||||
name string
|
||||
value int
|
||||
enabled bool
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 1)
|
||||
|
||||
// Check PrivateConfig struct
|
||||
config := structs[0]
|
||||
assert.Equal(t, "PrivateConfig", config.Name)
|
||||
assert.Len(t, config.Fields, 3, "Should include all unexported fields")
|
||||
|
||||
// Check all fields are unexported
|
||||
assert.Equal(t, "name", config.Fields[0].Name)
|
||||
assert.Equal(t, "value", config.Fields[1].Name)
|
||||
assert.Equal(t, "enabled", config.Fields[2].Name)
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersWithUnexportedEmbeddedFields(t *testing.T) {
|
||||
// Create a temporary directory with test files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
type BaseConfig struct {
|
||||
publicBase string
|
||||
privateBase int
|
||||
}
|
||||
|
||||
// fp-go:Lens
|
||||
type ExtendedConfig struct {
|
||||
BaseConfig
|
||||
PublicField string
|
||||
privateField bool
|
||||
}
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen_lens.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
genPath := filepath.Join(tmpDir, outputFile)
|
||||
_, err = os.Stat(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read and verify the generated content
|
||||
content, err := os.ReadFile(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// Check for expected content
|
||||
assert.Contains(t, contentStr, "package testpkg")
|
||||
assert.Contains(t, contentStr, "ExtendedConfigLenses")
|
||||
|
||||
// Check that lenses are generated for embedded unexported fields
|
||||
assert.Contains(t, contentStr, "publicBase __lens.Lens[ExtendedConfig, string]")
|
||||
assert.Contains(t, contentStr, "privateBase __lens.Lens[ExtendedConfig, int]")
|
||||
|
||||
// Check that lenses are generated for direct fields (both exported and unexported)
|
||||
assert.Contains(t, contentStr, "PublicField __lens.Lens[ExtendedConfig, string]")
|
||||
assert.Contains(t, contentStr, "privateField __lens.Lens[ExtendedConfig, bool]")
|
||||
}
|
||||
|
||||
func TestParseFileWithMixedFieldVisibility(t *testing.T) {
|
||||
// Create a temporary test file with various field visibility patterns
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type ComplexStruct struct {
|
||||
// Exported fields
|
||||
Name string
|
||||
Age int
|
||||
Email *string
|
||||
|
||||
// Unexported fields
|
||||
password string
|
||||
secretKey []byte
|
||||
internalID *int
|
||||
|
||||
// Mixed with tags
|
||||
PublicWithTag string ` + "`json:\"public,omitempty\"`" + `
|
||||
privateWithTag int ` + "`json:\"private,omitempty\"`" + `
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 1)
|
||||
|
||||
// Check ComplexStruct
|
||||
complex := structs[0]
|
||||
assert.Equal(t, "ComplexStruct", complex.Name)
|
||||
assert.Len(t, complex.Fields, 8, "Should include all fields regardless of visibility")
|
||||
|
||||
// Verify field names and types
|
||||
fieldNames := []string{"Name", "Age", "Email", "password", "secretKey", "internalID", "PublicWithTag", "privateWithTag"}
|
||||
for i, expectedName := range fieldNames {
|
||||
assert.Equal(t, expectedName, complex.Fields[i].Name, "Field %d should be %s", i, expectedName)
|
||||
}
|
||||
|
||||
// Check optional fields
|
||||
assert.False(t, complex.Fields[0].IsOptional, "Name should not be optional")
|
||||
assert.True(t, complex.Fields[2].IsOptional, "Email (pointer) should be optional")
|
||||
assert.True(t, complex.Fields[5].IsOptional, "internalID (pointer) should be optional")
|
||||
assert.True(t, complex.Fields[6].IsOptional, "PublicWithTag (with omitempty) should be optional")
|
||||
assert.True(t, complex.Fields[7].IsOptional, "privateWithTag (with omitempty) should be optional")
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func optionHKT(typeA string) string {
|
||||
@@ -200,10 +201,10 @@ func OptionCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateOptionHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateUnsliced(f *os.File, i int) {
|
||||
@@ -423,10 +424,10 @@ func PipeCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generatePipeHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateReaderFrom(f, fg *os.File, i int) {
|
||||
@@ -154,10 +155,10 @@ func ReaderCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateReaderHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func generateReaderIOEitherFrom(f, fg *os.File, i int) {
|
||||
@@ -284,10 +285,10 @@ func ReaderIOEitherCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateReaderIOEitherHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
C "github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func writeTupleType(f *os.File, symbol string, i int) {
|
||||
@@ -615,10 +616,10 @@ func TupleCommand() *C.Command {
|
||||
flagCount,
|
||||
flagFilename,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
Action: func(ctx context.Context, cmd *C.Command) error {
|
||||
return generateTupleHelpers(
|
||||
ctx.String(keyFilename),
|
||||
ctx.Int(keyCount),
|
||||
cmd.String(keyFilename),
|
||||
cmd.Int(keyCount),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@ package consumer
|
||||
// This is the contravariant map operation for Consumers, analogous to reader.Local
|
||||
// but operating on the input side rather than the output side.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Given a Consumer[R1] that consumes values of type R1, and a function f that
|
||||
// converts R2 to R1, Local creates a new Consumer[R2] that:
|
||||
// 1. Takes a value of type R2
|
||||
// 2. Applies f to convert it to R1
|
||||
// 3. Passes the result to the original Consumer[R1]
|
||||
// 1. Takes a value of type R2
|
||||
// 2. Applies f to convert it to R1
|
||||
// 3. Passes the result to the original Consumer[R1]
|
||||
//
|
||||
// This is particularly useful for adapting consumers to work with different input types,
|
||||
// similar to how reader.Local adapts readers to work with different environment types.
|
||||
@@ -168,10 +170,262 @@ package consumer
|
||||
// - reader.Local transforms the environment before reading
|
||||
// - consumer.Local transforms the input before consuming
|
||||
// - Both are contravariant functors on their input type
|
||||
func Local[R2, R1 any](f func(R2) R1) Operator[R1, R2] {
|
||||
func Local[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return func(c Consumer[R1]) Consumer[R2] {
|
||||
return func(r2 R2) {
|
||||
c(f(r2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose is an alias for Local that emphasizes the composition aspect of consumer transformation.
|
||||
// It composes a preprocessing function with a consumer, creating a new consumer that applies
|
||||
// the function before consuming the value.
|
||||
//
|
||||
// This function is semantically identical to Local but uses terminology that may be more familiar
|
||||
// to developers coming from functional programming backgrounds where "compose" is a common operation.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// The name "Compose" highlights that we're composing two operations:
|
||||
// 1. The transformation function f: R2 -> R1
|
||||
// 2. The consumer c: R1 -> ()
|
||||
//
|
||||
// Result: A composed consumer: R2 -> ()
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The input type of the original Consumer (what it expects)
|
||||
// - R2: The input type of the new Consumer (what you have)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that converts R2 to R1 (preprocessing function)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Consumer[R1] into Consumer[R2]
|
||||
//
|
||||
// Example - Basic composition:
|
||||
//
|
||||
// // Consumer that logs integers
|
||||
// logInt := func(x int) {
|
||||
// fmt.Printf("Value: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Compose with a string-to-int parser
|
||||
// parseToInt := func(s string) int {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// return n
|
||||
// }
|
||||
//
|
||||
// logString := consumer.Compose(parseToInt)(logInt)
|
||||
// logString("42") // Logs: "Value: 42"
|
||||
//
|
||||
// Example - Composing multiple transformations:
|
||||
//
|
||||
// type Data struct {
|
||||
// Value string
|
||||
// }
|
||||
//
|
||||
// type Wrapper struct {
|
||||
// Data Data
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs strings
|
||||
// logString := func(s string) {
|
||||
// fmt.Println(s)
|
||||
// }
|
||||
//
|
||||
// // Compose transformations step by step
|
||||
// extractData := func(w Wrapper) Data { return w.Data }
|
||||
// extractValue := func(d Data) string { return d.Value }
|
||||
//
|
||||
// logData := consumer.Compose(extractValue)(logString)
|
||||
// logWrapper := consumer.Compose(extractData)(logData)
|
||||
//
|
||||
// logWrapper(Wrapper{Data: Data{Value: "Hello"}}) // Logs: "Hello"
|
||||
//
|
||||
// Example - Function composition style:
|
||||
//
|
||||
// // Compose is particularly useful when thinking in terms of function composition
|
||||
// type Request struct {
|
||||
// Body []byte
|
||||
// }
|
||||
//
|
||||
// // Consumer that processes strings
|
||||
// processString := func(s string) {
|
||||
// fmt.Printf("Processing: %s\n", s)
|
||||
// }
|
||||
//
|
||||
// // Compose byte-to-string conversion with processing
|
||||
// bytesToString := func(b []byte) string {
|
||||
// return string(b)
|
||||
// }
|
||||
// extractBody := func(r Request) []byte {
|
||||
// return r.Body
|
||||
// }
|
||||
//
|
||||
// // Chain compositions
|
||||
// processBytes := consumer.Compose(bytesToString)(processString)
|
||||
// processRequest := consumer.Compose(extractBody)(processBytes)
|
||||
//
|
||||
// processRequest(Request{Body: []byte("test")}) // Logs: "Processing: test"
|
||||
//
|
||||
// Relationship to Local:
|
||||
// - Compose and Local are identical in implementation
|
||||
// - Compose emphasizes the functional composition aspect
|
||||
// - Local emphasizes the environment/context transformation aspect
|
||||
// - Use Compose when thinking about function composition
|
||||
// - Use Local when thinking about adapting to different contexts
|
||||
//
|
||||
// Use Cases:
|
||||
// - Building processing pipelines with clear composition semantics
|
||||
// - Adapting consumers in a functional programming style
|
||||
// - Creating reusable consumer transformations
|
||||
// - Chaining multiple preprocessing steps
|
||||
func Compose[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return Local(f)
|
||||
}
|
||||
|
||||
// Contramap is the categorical name for the contravariant functor operation on Consumers.
|
||||
// It transforms a Consumer by preprocessing its input, making it the dual of the covariant
|
||||
// functor's map operation.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#contravariant
|
||||
//
|
||||
// In category theory, a contravariant functor reverses the direction of morphisms.
|
||||
// While a covariant functor maps f: A -> B to map(f): F[A] -> F[B],
|
||||
// a contravariant functor maps f: A -> B to contramap(f): F[B] -> F[A].
|
||||
//
|
||||
// For Consumers:
|
||||
// - Consumer[A] is contravariant in A
|
||||
// - Given f: R2 -> R1, contramap(f) transforms Consumer[R1] to Consumer[R2]
|
||||
// - The direction is reversed: we go from Consumer[R1] to Consumer[R2]
|
||||
//
|
||||
// This is semantically identical to Local and Compose, but uses the standard
|
||||
// categorical terminology that emphasizes the contravariant nature of the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R1: The input type of the original Consumer (what it expects)
|
||||
// - R2: The input type of the new Consumer (what you have)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that converts R2 to R1 (preprocessing function)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Consumer[R1] into Consumer[R2]
|
||||
//
|
||||
// Example - Basic contravariant mapping:
|
||||
//
|
||||
// // Consumer that logs integers
|
||||
// logInt := func(x int) {
|
||||
// fmt.Printf("Value: %d\n", x)
|
||||
// }
|
||||
//
|
||||
// // Contramap with a string-to-int parser
|
||||
// parseToInt := func(s string) int {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// return n
|
||||
// }
|
||||
//
|
||||
// logString := consumer.Contramap(parseToInt)(logInt)
|
||||
// logString("42") // Logs: "Value: 42"
|
||||
//
|
||||
// Example - Demonstrating contravariance:
|
||||
//
|
||||
// // In covariant functors (like Option, Array), map goes "forward":
|
||||
// // map: (A -> B) -> F[A] -> F[B]
|
||||
// //
|
||||
// // In contravariant functors (like Consumer), contramap goes "backward":
|
||||
// // contramap: (B -> A) -> F[A] -> F[B]
|
||||
//
|
||||
// type Animal struct{ Name string }
|
||||
// type Dog struct{ Animal Animal; Breed string }
|
||||
//
|
||||
// // Consumer for animals
|
||||
// consumeAnimal := func(a Animal) {
|
||||
// fmt.Printf("Animal: %s\n", a.Name)
|
||||
// }
|
||||
//
|
||||
// // Function from Dog to Animal (B -> A)
|
||||
// dogToAnimal := func(d Dog) Animal {
|
||||
// return d.Animal
|
||||
// }
|
||||
//
|
||||
// // Contramap creates Consumer[Dog] from Consumer[Animal]
|
||||
// // Direction is reversed: Consumer[Animal] -> Consumer[Dog]
|
||||
// consumeDog := consumer.Contramap(dogToAnimal)(consumeAnimal)
|
||||
//
|
||||
// consumeDog(Dog{
|
||||
// Animal: Animal{Name: "Buddy"},
|
||||
// Breed: "Golden Retriever",
|
||||
// }) // Logs: "Animal: Buddy"
|
||||
//
|
||||
// Example - Contravariant functor laws:
|
||||
//
|
||||
// // Law 1: Identity
|
||||
// // contramap(identity) = identity
|
||||
// identity := func(x int) int { return x }
|
||||
// consumer1 := consumer.Contramap(identity)(consumeInt)
|
||||
// // consumer1 behaves identically to consumeInt
|
||||
//
|
||||
// // Law 2: Composition
|
||||
// // contramap(f . g) = contramap(g) . contramap(f)
|
||||
// // Note: composition order is reversed compared to covariant map
|
||||
// f := func(s string) int { n, _ := strconv.Atoi(s); return n }
|
||||
// g := func(b bool) string { if b { return "1" } else { return "0" } }
|
||||
//
|
||||
// // These two are equivalent:
|
||||
// consumer2 := consumer.Contramap(func(b bool) int { return f(g(b)) })(consumeInt)
|
||||
// consumer3 := consumer.Contramap(g)(consumer.Contramap(f)(consumeInt))
|
||||
//
|
||||
// Example - Practical use with type hierarchies:
|
||||
//
|
||||
// type Logger interface {
|
||||
// Log(string)
|
||||
// }
|
||||
//
|
||||
// type Message struct {
|
||||
// Text string
|
||||
// Timestamp time.Time
|
||||
// }
|
||||
//
|
||||
// // Consumer that logs strings
|
||||
// logString := func(s string) {
|
||||
// fmt.Println(s)
|
||||
// }
|
||||
//
|
||||
// // Contramap to handle Message types
|
||||
// extractText := func(m Message) string {
|
||||
// return fmt.Sprintf("[%s] %s", m.Timestamp.Format(time.RFC3339), m.Text)
|
||||
// }
|
||||
//
|
||||
// logMessage := consumer.Contramap(extractText)(logString)
|
||||
// logMessage(Message{
|
||||
// Text: "Hello",
|
||||
// Timestamp: time.Now(),
|
||||
// }) // Logs: "[2024-01-20T10:00:00Z] Hello"
|
||||
//
|
||||
// Relationship to Local and Compose:
|
||||
// - Contramap, Local, and Compose are identical in implementation
|
||||
// - Contramap emphasizes the categorical/theoretical aspect
|
||||
// - Local emphasizes the context transformation aspect
|
||||
// - Compose emphasizes the function composition aspect
|
||||
// - Use Contramap when working with category theory concepts
|
||||
// - Use Local when adapting to different contexts
|
||||
// - Use Compose when building functional pipelines
|
||||
//
|
||||
// Category Theory Background:
|
||||
// - Consumer[A] forms a contravariant functor
|
||||
// - The contravariant functor laws must hold:
|
||||
// 1. contramap(id) = id
|
||||
// 2. contramap(f ∘ g) = contramap(g) ∘ contramap(f)
|
||||
// - This is dual to the covariant functor (map) operation
|
||||
// - Consumers are contravariant because they consume rather than produce values
|
||||
//
|
||||
// Use Cases:
|
||||
// - Working with contravariant functors in a categorical style
|
||||
// - Adapting consumers to work with more specific types
|
||||
// - Building type-safe consumer transformations
|
||||
// - Implementing profunctor patterns (Consumer is a profunctor)
|
||||
func Contramap[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
|
||||
return Local(f)
|
||||
}
|
||||
|
||||
@@ -381,3 +381,513 @@ func TestLocal(t *testing.T) {
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContramap(t *testing.T) {
|
||||
t.Run("basic contravariant mapping", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
parseToInt := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
consumeString := Contramap(parseToInt)(consumeInt)
|
||||
consumeString("42")
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("contravariant identity law", func(t *testing.T) {
|
||||
// contramap(identity) = identity
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
identity := function.Identity[int]
|
||||
consumeIdentity := Contramap(identity)(consumeInt)
|
||||
|
||||
consumeIdentity(42)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
// Should behave identically to original consumer
|
||||
consumeInt(100)
|
||||
capturedDirect := captured
|
||||
consumeIdentity(100)
|
||||
capturedMapped := captured
|
||||
|
||||
assert.Equal(t, capturedDirect, capturedMapped)
|
||||
})
|
||||
|
||||
t.Run("contravariant composition law", func(t *testing.T) {
|
||||
// contramap(f . g) = contramap(g) . contramap(f)
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
f := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
g := func(b bool) string {
|
||||
if b {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
// Compose f and g manually
|
||||
fg := func(b bool) int {
|
||||
return f(g(b))
|
||||
}
|
||||
|
||||
// Method 1: contramap(f . g)
|
||||
consumer1 := Contramap(fg)(consumeInt)
|
||||
consumer1(true)
|
||||
result1 := captured
|
||||
|
||||
// Method 2: contramap(g) . contramap(f)
|
||||
consumer2 := Contramap(g)(Contramap(f)(consumeInt))
|
||||
consumer2(true)
|
||||
result2 := captured
|
||||
|
||||
assert.Equal(t, result1, result2)
|
||||
assert.Equal(t, 1, result1)
|
||||
})
|
||||
|
||||
t.Run("type hierarchy adaptation", func(t *testing.T) {
|
||||
type Animal struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type Dog struct {
|
||||
Animal Animal
|
||||
Breed string
|
||||
}
|
||||
|
||||
var capturedName string
|
||||
consumeAnimal := func(a Animal) {
|
||||
capturedName = a.Name
|
||||
}
|
||||
|
||||
dogToAnimal := func(d Dog) Animal {
|
||||
return d.Animal
|
||||
}
|
||||
|
||||
consumeDog := Contramap(dogToAnimal)(consumeAnimal)
|
||||
consumeDog(Dog{
|
||||
Animal: Animal{Name: "Buddy"},
|
||||
Breed: "Golden Retriever",
|
||||
})
|
||||
|
||||
assert.Equal(t, "Buddy", capturedName)
|
||||
})
|
||||
|
||||
t.Run("field extraction with contramap", func(t *testing.T) {
|
||||
type Message struct {
|
||||
Text string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
var capturedText string
|
||||
consumeString := func(s string) {
|
||||
capturedText = s
|
||||
}
|
||||
|
||||
extractText := func(m Message) string {
|
||||
return m.Text
|
||||
}
|
||||
|
||||
consumeMessage := Contramap(extractText)(consumeString)
|
||||
consumeMessage(Message{
|
||||
Text: "Hello",
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
assert.Equal(t, "Hello", capturedText)
|
||||
})
|
||||
|
||||
t.Run("multiple contramap applications", func(t *testing.T) {
|
||||
type Level3 struct{ Value int }
|
||||
type Level2 struct{ L3 Level3 }
|
||||
type Level1 struct{ L2 Level2 }
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
extract3 := func(l3 Level3) int { return l3.Value }
|
||||
extract2 := func(l2 Level2) Level3 { return l2.L3 }
|
||||
extract1 := func(l1 Level1) Level2 { return l1.L2 }
|
||||
|
||||
// Chain contramap operations
|
||||
consumeLevel3 := Contramap(extract3)(consumeInt)
|
||||
consumeLevel2 := Contramap(extract2)(consumeLevel3)
|
||||
consumeLevel1 := Contramap(extract1)(consumeLevel2)
|
||||
|
||||
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("contramap with calculation", func(t *testing.T) {
|
||||
type Rectangle struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
var capturedArea int
|
||||
consumeArea := func(area int) {
|
||||
capturedArea = area
|
||||
}
|
||||
|
||||
calculateArea := func(r Rectangle) int {
|
||||
return r.Width * r.Height
|
||||
}
|
||||
|
||||
consumeRectangle := Contramap(calculateArea)(consumeArea)
|
||||
consumeRectangle(Rectangle{Width: 5, Height: 10})
|
||||
|
||||
assert.Equal(t, 50, capturedArea)
|
||||
})
|
||||
|
||||
t.Run("contramap preserves side effects", func(t *testing.T) {
|
||||
callCount := 0
|
||||
consumer := func(x int) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
contramappedConsumer := Contramap(transform)(consumer)
|
||||
|
||||
contramappedConsumer("1")
|
||||
contramappedConsumer("2")
|
||||
contramappedConsumer("3")
|
||||
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("contramap with pointer types", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
dereference := func(p *int) int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
consumePointer := Contramap(dereference)(consumeInt)
|
||||
|
||||
value := 42
|
||||
consumePointer(&value)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumePointer(nil)
|
||||
assert.Equal(t, 0, captured)
|
||||
})
|
||||
|
||||
t.Run("contramap equivalence with Local", func(t *testing.T) {
|
||||
var capturedLocal, capturedContramap int
|
||||
|
||||
consumeIntLocal := func(x int) {
|
||||
capturedLocal = x
|
||||
}
|
||||
|
||||
consumeIntContramap := func(x int) {
|
||||
capturedContramap = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// Both should produce identical results
|
||||
consumerLocal := Local(transform)(consumeIntLocal)
|
||||
consumerContramap := Contramap(transform)(consumeIntContramap)
|
||||
|
||||
consumerLocal("42")
|
||||
consumerContramap("42")
|
||||
|
||||
assert.Equal(t, capturedLocal, capturedContramap)
|
||||
assert.Equal(t, 42, capturedLocal)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose(t *testing.T) {
|
||||
t.Run("basic composition", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
parseToInt := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
consumeString := Compose(parseToInt)(consumeInt)
|
||||
consumeString("42")
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("composing multiple transformations", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
type Wrapper struct {
|
||||
Data Data
|
||||
}
|
||||
|
||||
var captured string
|
||||
consumeString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
extractData := func(w Wrapper) Data { return w.Data }
|
||||
extractValue := func(d Data) string { return d.Value }
|
||||
|
||||
// Compose step by step
|
||||
consumeData := Compose(extractValue)(consumeString)
|
||||
consumeWrapper := Compose(extractData)(consumeData)
|
||||
|
||||
consumeWrapper(Wrapper{Data: Data{Value: "Hello"}})
|
||||
|
||||
assert.Equal(t, "Hello", captured)
|
||||
})
|
||||
|
||||
t.Run("function composition style", func(t *testing.T) {
|
||||
type Request struct {
|
||||
Body []byte
|
||||
}
|
||||
|
||||
var captured string
|
||||
processString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
bytesToString := func(b []byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
extractBody := func(r Request) []byte {
|
||||
return r.Body
|
||||
}
|
||||
|
||||
// Chain compositions
|
||||
processBytes := Compose(bytesToString)(processString)
|
||||
processRequest := Compose(extractBody)(processBytes)
|
||||
|
||||
processRequest(Request{Body: []byte("test")})
|
||||
|
||||
assert.Equal(t, "test", captured)
|
||||
})
|
||||
|
||||
t.Run("compose with identity", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
identity := function.Identity[int]
|
||||
composedConsumer := Compose(identity)(consumeInt)
|
||||
|
||||
composedConsumer(42)
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("compose with field extraction", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
var capturedName string
|
||||
consumeName := func(name string) {
|
||||
capturedName = name
|
||||
}
|
||||
|
||||
extractName := func(u User) string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
consumeUser := Compose(extractName)(consumeName)
|
||||
consumeUser(User{Name: "Alice", Email: "alice@example.com", Age: 30})
|
||||
|
||||
assert.Equal(t, "Alice", capturedName)
|
||||
})
|
||||
|
||||
t.Run("compose with calculation", func(t *testing.T) {
|
||||
type Circle struct {
|
||||
Radius float64
|
||||
}
|
||||
|
||||
var capturedArea float64
|
||||
consumeArea := func(area float64) {
|
||||
capturedArea = area
|
||||
}
|
||||
|
||||
calculateArea := func(c Circle) float64 {
|
||||
return 3.14159 * c.Radius * c.Radius
|
||||
}
|
||||
|
||||
consumeCircle := Compose(calculateArea)(consumeArea)
|
||||
consumeCircle(Circle{Radius: 5.0})
|
||||
|
||||
assert.InDelta(t, 78.53975, capturedArea, 0.00001)
|
||||
})
|
||||
|
||||
t.Run("compose with slice operations", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeLength := func(n int) {
|
||||
captured = n
|
||||
}
|
||||
|
||||
getLength := func(s []string) int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
consumeSlice := Compose(getLength)(consumeLength)
|
||||
consumeSlice([]string{"a", "b", "c", "d"})
|
||||
|
||||
assert.Equal(t, 4, captured)
|
||||
})
|
||||
|
||||
t.Run("compose with map operations", func(t *testing.T) {
|
||||
var captured bool
|
||||
consumeHasKey := func(has bool) {
|
||||
captured = has
|
||||
}
|
||||
|
||||
hasKey := func(m map[string]int) bool {
|
||||
_, exists := m["key"]
|
||||
return exists
|
||||
}
|
||||
|
||||
consumeMap := Compose(hasKey)(consumeHasKey)
|
||||
|
||||
consumeMap(map[string]int{"key": 42})
|
||||
assert.True(t, captured)
|
||||
|
||||
consumeMap(map[string]int{"other": 42})
|
||||
assert.False(t, captured)
|
||||
})
|
||||
|
||||
t.Run("compose preserves consumer behavior", func(t *testing.T) {
|
||||
callCount := 0
|
||||
consumer := func(x int) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
composedConsumer := Compose(transform)(consumer)
|
||||
|
||||
composedConsumer("1")
|
||||
composedConsumer("2")
|
||||
composedConsumer("3")
|
||||
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("compose with error handling", func(t *testing.T) {
|
||||
type Result struct {
|
||||
Value int
|
||||
Error error
|
||||
}
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
extractValue := func(r Result) int {
|
||||
if r.Error != nil {
|
||||
return -1
|
||||
}
|
||||
return r.Value
|
||||
}
|
||||
|
||||
consumeResult := Compose(extractValue)(consumeInt)
|
||||
|
||||
consumeResult(Result{Value: 42, Error: nil})
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumeResult(Result{Value: 100, Error: assert.AnError})
|
||||
assert.Equal(t, -1, captured)
|
||||
})
|
||||
|
||||
t.Run("compose equivalence with Local", func(t *testing.T) {
|
||||
var capturedLocal, capturedCompose int
|
||||
|
||||
consumeIntLocal := func(x int) {
|
||||
capturedLocal = x
|
||||
}
|
||||
|
||||
consumeIntCompose := func(x int) {
|
||||
capturedCompose = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// Both should produce identical results
|
||||
consumerLocal := Local(transform)(consumeIntLocal)
|
||||
consumerCompose := Compose(transform)(consumeIntCompose)
|
||||
|
||||
consumerLocal("42")
|
||||
consumerCompose("42")
|
||||
|
||||
assert.Equal(t, capturedLocal, capturedCompose)
|
||||
assert.Equal(t, 42, capturedLocal)
|
||||
})
|
||||
|
||||
t.Run("compose equivalence with Contramap", func(t *testing.T) {
|
||||
var capturedCompose, capturedContramap int
|
||||
|
||||
consumeIntCompose := func(x int) {
|
||||
capturedCompose = x
|
||||
}
|
||||
|
||||
consumeIntContramap := func(x int) {
|
||||
capturedContramap = x
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
// All three should produce identical results
|
||||
consumerCompose := Compose(transform)(consumeIntCompose)
|
||||
consumerContramap := Contramap(transform)(consumeIntContramap)
|
||||
|
||||
consumerCompose("42")
|
||||
consumerContramap("42")
|
||||
|
||||
assert.Equal(t, capturedCompose, capturedContramap)
|
||||
assert.Equal(t, 42, capturedCompose)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import (
|
||||
// return result.Of("done")
|
||||
// }
|
||||
//
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel() // Cancel immediately
|
||||
//
|
||||
// wrapped := WithContext(ctx, computation)
|
||||
|
||||
@@ -61,7 +61,7 @@ import (
|
||||
//
|
||||
// // Safely read file with automatic cleanup
|
||||
// safeRead := Bracket(acquireFile, readFile, closeFile)
|
||||
// result := safeRead(context.Background())()
|
||||
// result := safeRead(t.Context())()
|
||||
//
|
||||
//go:inline
|
||||
func Bracket[
|
||||
|
||||
@@ -50,7 +50,7 @@ import (
|
||||
// // Sequence it to apply Config first
|
||||
// sequenced := SequenceReader[Config, int](getMultiplier)
|
||||
// cfg := Config{Timeout: 30}
|
||||
// result := sequenced(cfg)(context.Background())() // Returns 60
|
||||
// result := sequenced(cfg)(t.Context())() // Returns 60
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
|
||||
@@ -107,7 +107,7 @@ func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]]
|
||||
//
|
||||
// // Provide Config to get final result
|
||||
// cfg := Config{Multiplier: 5}
|
||||
// finalResult := result(cfg)(context.Background())() // Returns 50
|
||||
// finalResult := result(cfg)(t.Context())() // Returns 50
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
|
||||
@@ -81,7 +81,7 @@ func SLogWithCallback[A any](
|
||||
// Chain(SLog[string]("Extracted name")),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // Logs: "Extracted name" value="Alice"
|
||||
//
|
||||
|
||||
74
v2/context/readerio/profunctor.go
Normal file
74
v2/context/readerio/profunctor.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIO.
|
||||
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Modify the context before passing it to the ReaderIO (via f)
|
||||
// - Transform the result value after the IO effect completes (via g)
|
||||
//
|
||||
// The function f returns both a new context and a CancelFunc that should be called to release resources.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The original result type produced by the ReaderIO
|
||||
// - B: The new output result type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - g: Function to transform the output value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
)
|
||||
}
|
||||
|
||||
// Contramap changes the context during the execution of a ReaderIO.
|
||||
// This is the contravariant functor operation that transforms the input context.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is an alias for Local and is useful for adapting a ReaderIO to work with
|
||||
// a modified context by providing a function that transforms the context.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type (unchanged)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
97
v2/context/readerio/profunctor_test.go
Normal file
97
v2/context/readerio/profunctor_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both context and output", func(t *testing.T) {
|
||||
// ReaderIO that reads a value from context
|
||||
getValue := func(ctx context.Context) IO[int] {
|
||||
return func() int {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return v.(int)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Transform context and result
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "key", 42)
|
||||
return newCtx, func() {}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addKey, toString)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, "42", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("context transformation", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IO[int] {
|
||||
return func() int {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return v.(int)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "key", 100)
|
||||
return newCtx, func() {}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalBasic tests basic Local functionality
|
||||
func TestLocalBasic(t *testing.T) {
|
||||
t.Run("adds timeout to context", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IO[bool] {
|
||||
return func() bool {
|
||||
_, hasDeadline := ctx.Deadline()
|
||||
return hasDeadline
|
||||
}
|
||||
}
|
||||
|
||||
addTimeout := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, time.Second)
|
||||
}
|
||||
|
||||
adapted := Local[bool](addTimeout)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.True(t, result)
|
||||
})
|
||||
}
|
||||
@@ -560,6 +560,63 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
|
||||
return RIO.Read[A](r)
|
||||
}
|
||||
|
||||
// ReadIO executes a ReaderIO computation by providing a context wrapped in an IO effect.
|
||||
// This is useful when the context itself needs to be computed or retrieved through side effects.
|
||||
//
|
||||
// The function takes an IO[context.Context] (an effectful computation that produces a context) and returns
|
||||
// a function that can execute a ReaderIO[A] to produce an IO[A].
|
||||
//
|
||||
// This is particularly useful in scenarios where:
|
||||
// - The context needs to be created with side effects (e.g., loading configuration)
|
||||
// - The context requires initialization or setup
|
||||
// - You want to compose context creation with the computation that uses it
|
||||
//
|
||||
// The execution flow is:
|
||||
// 1. Execute the IO[context.Context] to get the context
|
||||
// 2. Pass the context to the ReaderIO[A] to get an IO[A]
|
||||
// 3. Execute the resulting IO[A] to get the final result A
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type of the ReaderIO computation
|
||||
//
|
||||
// Parameters:
|
||||
// - r: An IO effect that produces a context.Context
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIO[A] and returns an IO[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "context"
|
||||
// G "github.com/IBM/fp-go/v2/io"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// )
|
||||
//
|
||||
// // Create context with side effects (e.g., loading config)
|
||||
// createContext := G.Of(context.WithValue(t.Context(), "key", "value"))
|
||||
//
|
||||
// // A computation that uses the context
|
||||
// getValue := readerio.FromReader(func(ctx context.Context) string {
|
||||
// if val := ctx.Value("key"); val != nil {
|
||||
// return val.(string)
|
||||
// }
|
||||
// return "default"
|
||||
// })
|
||||
//
|
||||
// // Compose them together
|
||||
// result := readerio.ReadIO[string](createContext)(getValue)
|
||||
// value := result() // Executes both effects and returns "value"
|
||||
//
|
||||
// Comparison with Read:
|
||||
// - [Read]: Takes a pure context.Context value and executes the ReaderIO immediately
|
||||
// - [ReadIO]: Takes an IO[context.Context] and chains the effects together
|
||||
//
|
||||
//go:inline
|
||||
func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
return RIO.ReadIO[A](r)
|
||||
}
|
||||
|
||||
// Local transforms the context.Context environment before passing it to a ReaderIO computation.
|
||||
//
|
||||
// This is the Reader's local operation, which allows you to modify the environment
|
||||
@@ -607,7 +664,7 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
|
||||
// getUser,
|
||||
// addUser,
|
||||
// )
|
||||
// user := result(context.Background())() // Returns "Alice"
|
||||
// user := result(t.Context())() // Returns "Alice"
|
||||
//
|
||||
// Timeout Example:
|
||||
//
|
||||
@@ -674,7 +731,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// fetchData,
|
||||
// readerio.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// data := result(context.Background())() // Returns Data{} after 5s timeout
|
||||
// data := result(t.Context())() // Returns Data{} after 5s timeout
|
||||
//
|
||||
// Successful Example:
|
||||
//
|
||||
@@ -683,7 +740,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// quickFetch,
|
||||
// readerio.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// data := result(context.Background())() // Returns Data{Value: "quick"}
|
||||
// data := result(t.Context())() // Returns Data{Value: "quick"}
|
||||
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
@@ -734,12 +791,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
// fetchData,
|
||||
// readerio.WithDeadline[Data](deadline),
|
||||
// )
|
||||
// data := result(context.Background())() // Returns Data{} if past deadline
|
||||
// data := result(t.Context())() // Returns Data{} if past deadline
|
||||
//
|
||||
// Combining with Parent Context:
|
||||
//
|
||||
// // If parent context already has a deadline, the earlier one takes precedence
|
||||
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
|
||||
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
|
||||
// defer cancel()
|
||||
//
|
||||
// laterDeadline := time.Now().Add(2 * time.Hour)
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestMonadMap(t *testing.T) {
|
||||
rio := Of(5)
|
||||
doubled := MonadMap(rio, N.Mul(2))
|
||||
|
||||
result := doubled(context.Background())()
|
||||
result := doubled(t.Context())()
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
@@ -41,14 +41,14 @@ func TestMap(t *testing.T) {
|
||||
Map(utils.Double),
|
||||
)
|
||||
|
||||
assert.Equal(t, 2, g(context.Background())())
|
||||
assert.Equal(t, 2, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
rio := Of(42)
|
||||
replaced := MonadMapTo(rio, "constant")
|
||||
|
||||
result := replaced(context.Background())()
|
||||
result := replaced(t.Context())()
|
||||
assert.Equal(t, "constant", result)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestMapTo(t *testing.T) {
|
||||
MapTo[int]("constant"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "constant", result(context.Background())())
|
||||
assert.Equal(t, "constant", result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
@@ -67,7 +67,7 @@ func TestMonadChain(t *testing.T) {
|
||||
return Of(n * 3)
|
||||
})
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
@@ -78,7 +78,7 @@ func TestChain(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
@@ -89,7 +89,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
return Of("side effect")
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func TestChainFirst(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func TestMonadTap(t *testing.T) {
|
||||
return Of(func() {})
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -132,14 +132,14 @@ func TestTap(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
rio := Of(100)
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
|
||||
assert.Equal(t, 100, result)
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func TestMonadAp(t *testing.T) {
|
||||
faIO := Of(5)
|
||||
result := MonadAp(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
assert.Equal(t, 10, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
@@ -158,7 +158,7 @@ func TestAp(t *testing.T) {
|
||||
Ap[int](Of(1)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 2, g(context.Background())())
|
||||
assert.Equal(t, 2, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
@@ -166,7 +166,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
faIO := Of(5)
|
||||
result := MonadApSeq(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestApSeq(t *testing.T) {
|
||||
@@ -175,7 +175,7 @@ func TestApSeq(t *testing.T) {
|
||||
ApSeq[int](Of(5)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, g(context.Background())())
|
||||
assert.Equal(t, 15, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadApPar(t *testing.T) {
|
||||
@@ -183,7 +183,7 @@ func TestMonadApPar(t *testing.T) {
|
||||
faIO := Of(5)
|
||||
result := MonadApPar(fabIO, faIO)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
@@ -192,12 +192,12 @@ func TestApPar(t *testing.T) {
|
||||
ApPar[int](Of(5)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, g(context.Background())())
|
||||
assert.Equal(t, 15, g(t.Context())())
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
rio := Ask()
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
ctx := context.WithValue(t.Context(), "key", "value")
|
||||
result := rio(ctx)()
|
||||
|
||||
assert.Equal(t, ctx, result)
|
||||
@@ -207,7 +207,7 @@ func TestFromIO(t *testing.T) {
|
||||
ioAction := G.Of(42)
|
||||
rio := FromIO(ioAction)
|
||||
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func TestFromReader(t *testing.T) {
|
||||
}
|
||||
|
||||
rio := FromReader(rdr)
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func TestFromLazy(t *testing.T) {
|
||||
lazy := func() int { return 42 }
|
||||
rio := FromLazy(lazy)
|
||||
|
||||
result := rio(context.Background())()
|
||||
result := rio(t.Context())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ func TestMonadChainIOK(t *testing.T) {
|
||||
return G.Of(n * 4)
|
||||
})
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())())
|
||||
assert.Equal(t, 20, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainIOK(t *testing.T) {
|
||||
@@ -247,7 +247,7 @@ func TestChainIOK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())())
|
||||
assert.Equal(t, 20, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
@@ -258,7 +258,7 @@ func TestMonadChainFirstIOK(t *testing.T) {
|
||||
return G.Of("side effect")
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -273,7 +273,7 @@ func TestChainFirstIOK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -286,7 +286,7 @@ func TestMonadTapIOK(t *testing.T) {
|
||||
return G.Of(func() {})
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -301,7 +301,7 @@ func TestTapIOK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -313,8 +313,8 @@ func TestDefer(t *testing.T) {
|
||||
return Of(counter)
|
||||
})
|
||||
|
||||
result1 := rio(context.Background())()
|
||||
result2 := rio(context.Background())()
|
||||
result1 := rio(t.Context())()
|
||||
result2 := rio(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, result1)
|
||||
assert.Equal(t, 2, result2)
|
||||
@@ -328,8 +328,8 @@ func TestMemoize(t *testing.T) {
|
||||
return counter
|
||||
}))
|
||||
|
||||
result1 := memoized(context.Background())()
|
||||
result2 := memoized(context.Background())()
|
||||
result1 := memoized(t.Context())()
|
||||
result2 := memoized(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, result1)
|
||||
assert.Equal(t, 1, result2) // Same value, memoized
|
||||
@@ -339,7 +339,7 @@ func TestFlatten(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
flattened := Flatten(nested)
|
||||
|
||||
result := flattened(context.Background())()
|
||||
result := flattened(t.Context())()
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ func TestMonadFlap(t *testing.T) {
|
||||
fabIO := Of(N.Mul(3))
|
||||
result := MonadFlap(fabIO, 7)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
assert.Equal(t, 21, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
@@ -356,7 +356,7 @@ func TestFlap(t *testing.T) {
|
||||
Flap[int](7),
|
||||
)
|
||||
|
||||
assert.Equal(t, 21, result(context.Background())())
|
||||
assert.Equal(t, 21, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainReaderK(t *testing.T) {
|
||||
@@ -365,7 +365,7 @@ func TestMonadChainReaderK(t *testing.T) {
|
||||
return func(ctx context.Context) int { return n * 2 }
|
||||
})
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
assert.Equal(t, 10, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainReaderK(t *testing.T) {
|
||||
@@ -376,7 +376,7 @@ func TestChainReaderK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 10, result(context.Background())())
|
||||
assert.Equal(t, 10, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstReaderK(t *testing.T) {
|
||||
@@ -389,7 +389,7 @@ func TestMonadChainFirstReaderK(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -406,7 +406,7 @@ func TestChainFirstReaderK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -421,7 +421,7 @@ func TestMonadTapReaderK(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
@@ -438,14 +438,14 @@ func TestTapReaderK(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, 42, sideEffect)
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
rio := Of(42)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
ioAction := Read[int](ctx)(rio)
|
||||
result := ioAction()
|
||||
|
||||
@@ -463,7 +463,7 @@ func TestComplexPipeline(t *testing.T) {
|
||||
Map(N.Add(10)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 20, result(context.Background())()) // (5 * 2) + 10 = 20
|
||||
assert.Equal(t, 20, result(t.Context())()) // (5 * 2) + 10 = 20
|
||||
}
|
||||
|
||||
func TestFromIOWithChain(t *testing.T) {
|
||||
@@ -476,7 +476,7 @@ func TestFromIOWithChain(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, 15, result(context.Background())())
|
||||
assert.Equal(t, 15, result(t.Context())())
|
||||
}
|
||||
|
||||
func TestTapWithLogging(t *testing.T) {
|
||||
@@ -496,7 +496,192 @@ func TestTapWithLogging(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
value := result(context.Background())()
|
||||
value := result(t.Context())()
|
||||
assert.Equal(t, 84, value)
|
||||
assert.Equal(t, []int{42, 84}, logged)
|
||||
}
|
||||
|
||||
func TestReadIO(t *testing.T) {
|
||||
// Test basic ReadIO functionality
|
||||
contextIO := G.Of(context.WithValue(t.Context(), "testKey", "testValue"))
|
||||
rio := FromReader(func(ctx context.Context) string {
|
||||
if val := ctx.Value("testKey"); val != nil {
|
||||
return val.(string)
|
||||
}
|
||||
return "default"
|
||||
})
|
||||
|
||||
ioAction := ReadIO[string](contextIO)(rio)
|
||||
result := ioAction()
|
||||
|
||||
assert.Equal(t, "testValue", result)
|
||||
}
|
||||
|
||||
func TestReadIOWithBackground(t *testing.T) {
|
||||
// Test ReadIO with plain background context
|
||||
contextIO := G.Of(t.Context())
|
||||
rio := Of(42)
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(rio)
|
||||
result := ioAction()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestReadIOWithChain(t *testing.T) {
|
||||
// Test ReadIO with chained operations
|
||||
contextIO := G.Of(context.WithValue(t.Context(), "multiplier", 3))
|
||||
|
||||
result := F.Pipe1(
|
||||
FromReader(func(ctx context.Context) int {
|
||||
if val := ctx.Value("multiplier"); val != nil {
|
||||
return val.(int)
|
||||
}
|
||||
return 1
|
||||
}),
|
||||
Chain(func(n int) ReaderIO[int] {
|
||||
return Of(n * 10)
|
||||
}),
|
||||
)
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(result)
|
||||
value := ioAction()
|
||||
|
||||
assert.Equal(t, 30, value) // 3 * 10
|
||||
}
|
||||
|
||||
func TestReadIOWithMap(t *testing.T) {
|
||||
// Test ReadIO with Map operations
|
||||
contextIO := G.Of(t.Context())
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(5),
|
||||
Map(N.Mul(2)),
|
||||
Map(N.Add(10)),
|
||||
)
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(result)
|
||||
value := ioAction()
|
||||
|
||||
assert.Equal(t, 20, value) // (5 * 2) + 10
|
||||
}
|
||||
|
||||
func TestReadIOWithSideEffects(t *testing.T) {
|
||||
// Test ReadIO with side effects in context creation
|
||||
counter := 0
|
||||
contextIO := func() context.Context {
|
||||
counter++
|
||||
return context.WithValue(t.Context(), "counter", counter)
|
||||
}
|
||||
|
||||
rio := FromReader(func(ctx context.Context) int {
|
||||
if val := ctx.Value("counter"); val != nil {
|
||||
return val.(int)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(rio)
|
||||
result := ioAction()
|
||||
|
||||
assert.Equal(t, 1, result)
|
||||
assert.Equal(t, 1, counter)
|
||||
}
|
||||
|
||||
func TestReadIOMultipleExecutions(t *testing.T) {
|
||||
// Test that ReadIO creates fresh effects on each execution
|
||||
counter := 0
|
||||
contextIO := func() context.Context {
|
||||
counter++
|
||||
return t.Context()
|
||||
}
|
||||
|
||||
rio := Of(42)
|
||||
ioAction := ReadIO[int](contextIO)(rio)
|
||||
|
||||
result1 := ioAction()
|
||||
result2 := ioAction()
|
||||
|
||||
assert.Equal(t, 42, result1)
|
||||
assert.Equal(t, 42, result2)
|
||||
assert.Equal(t, 2, counter) // Context IO executed twice
|
||||
}
|
||||
|
||||
func TestReadIOComparisonWithRead(t *testing.T) {
|
||||
// Compare ReadIO with Read to show the difference
|
||||
ctx := context.WithValue(t.Context(), "key", "value")
|
||||
|
||||
rio := FromReader(func(ctx context.Context) string {
|
||||
if val := ctx.Value("key"); val != nil {
|
||||
return val.(string)
|
||||
}
|
||||
return "default"
|
||||
})
|
||||
|
||||
// Using Read (direct context)
|
||||
ioAction1 := Read[string](ctx)(rio)
|
||||
result1 := ioAction1()
|
||||
|
||||
// Using ReadIO (context wrapped in IO)
|
||||
contextIO := G.Of(ctx)
|
||||
ioAction2 := ReadIO[string](contextIO)(rio)
|
||||
result2 := ioAction2()
|
||||
|
||||
assert.Equal(t, result1, result2)
|
||||
assert.Equal(t, "value", result1)
|
||||
assert.Equal(t, "value", result2)
|
||||
}
|
||||
|
||||
func TestReadIOWithComplexContext(t *testing.T) {
|
||||
// Test ReadIO with complex context manipulation
|
||||
type contextKey string
|
||||
const (
|
||||
userKey contextKey = "user"
|
||||
tokenKey contextKey = "token"
|
||||
)
|
||||
|
||||
contextIO := G.Of(
|
||||
context.WithValue(
|
||||
context.WithValue(t.Context(), userKey, "Alice"),
|
||||
tokenKey,
|
||||
"secret123",
|
||||
),
|
||||
)
|
||||
|
||||
rio := FromReader(func(ctx context.Context) map[string]string {
|
||||
result := make(map[string]string)
|
||||
if user := ctx.Value(userKey); user != nil {
|
||||
result["user"] = user.(string)
|
||||
}
|
||||
if token := ctx.Value(tokenKey); token != nil {
|
||||
result["token"] = token.(string)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
ioAction := ReadIO[map[string]string](contextIO)(rio)
|
||||
result := ioAction()
|
||||
|
||||
assert.Equal(t, "Alice", result["user"])
|
||||
assert.Equal(t, "secret123", result["token"])
|
||||
}
|
||||
|
||||
func TestReadIOWithAsk(t *testing.T) {
|
||||
// Test ReadIO combined with Ask
|
||||
contextIO := G.Of(context.WithValue(t.Context(), "data", 100))
|
||||
|
||||
result := F.Pipe1(
|
||||
Ask(),
|
||||
Map(func(ctx context.Context) int {
|
||||
if val := ctx.Value("data"); val != nil {
|
||||
return val.(int)
|
||||
}
|
||||
return 0
|
||||
}),
|
||||
)
|
||||
|
||||
ioAction := ReadIO[int](contextIO)(result)
|
||||
value := ioAction()
|
||||
|
||||
assert.Equal(t, 100, value)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ import (
|
||||
// }
|
||||
//
|
||||
// countdown := TailRec(countdownStep)
|
||||
// result := countdown(10)(context.Background())() // Returns "Done!"
|
||||
// result := countdown(10)(t.Context())() // Returns "Done!"
|
||||
//
|
||||
// Example - Sum with context:
|
||||
//
|
||||
@@ -77,7 +77,7 @@ import (
|
||||
// }
|
||||
//
|
||||
// sum := TailRec(sumStep)
|
||||
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(context.Background())()
|
||||
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(t.Context())()
|
||||
// // Returns 15, safe even for very large slices
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -80,7 +80,7 @@ import (
|
||||
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
|
||||
//
|
||||
// // Execute
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := retryingFetch(ctx)() // Returns "success" after 3 attempts
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -56,7 +56,7 @@ This creates several problems:
|
||||
|
||||
```go
|
||||
computation := getComputation()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
cfg := Config{Value: 42}
|
||||
|
||||
// Must apply in this specific order
|
||||
@@ -176,7 +176,7 @@ db := Database{ConnectionString: "localhost:5432"}
|
||||
query := queryWithDB(db) // ✅ Database injected
|
||||
|
||||
// Use query with any context
|
||||
result := query(context.Background())()
|
||||
result := query(t.Context())()
|
||||
```
|
||||
|
||||
### 3. Point-Free Composition
|
||||
@@ -289,7 +289,7 @@ withConfig := traversed(getValue)
|
||||
|
||||
// Now we can provide Config to get the final result
|
||||
cfg := Config{Multiplier: 5, Prefix: "Result"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
result := withConfig(cfg)(ctx)() // Returns Right("Result: 50")
|
||||
```
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
// safeFetch := WithContextK(fetchUser)
|
||||
//
|
||||
// // If context is cancelled, returns immediately without executing fetchUser
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
// cancel() // Cancel immediately
|
||||
// result := safeFetch(123)(ctx)() // Returns context.Canceled error
|
||||
//
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/circuitbreaker"
|
||||
"github.com/IBM/fp-go/v2/context/readerio"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
)
|
||||
@@ -27,6 +28,9 @@ func MakeCircuitBreaker[T any](
|
||||
Left,
|
||||
ChainFirstIOK,
|
||||
ChainFirstLeftIOK,
|
||||
|
||||
readerio.ChainFirstIOK,
|
||||
|
||||
FromIO,
|
||||
Flap,
|
||||
Flatten,
|
||||
|
||||
@@ -187,7 +187,7 @@ func main() {
|
||||
result := cb(env)
|
||||
|
||||
// Execute the protected operation
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
protectedOp := pair.Tail(result)
|
||||
outcome := protectedOp(ctx)()
|
||||
}
|
||||
|
||||
@@ -608,7 +608,7 @@ func TestCircuitBreaker_ErrorMessageFormat(t *testing.T) {
|
||||
protectedOp := pair.Tail(resultEnv)
|
||||
outcome := protectedOp(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft[string](outcome))
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
|
||||
// Error message should indicate circuit breaker is open
|
||||
_, err := result.Unwrap(outcome)
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
// }
|
||||
//
|
||||
// // Execute the computation
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := fetchUser("123")(ctx)()
|
||||
// // result is Either[error, User]
|
||||
//
|
||||
@@ -161,7 +161,7 @@
|
||||
// All operations respect context cancellation. When a context is cancelled, operations
|
||||
// will return an error containing the cancellation cause:
|
||||
//
|
||||
// ctx, cancel := context.WithCancelCause(context.Background())
|
||||
// ctx, cancel := context.WithCancelCause(t.Context())
|
||||
// cancel(errors.New("operation cancelled"))
|
||||
// result := computation(ctx)() // Returns Left with cancellation error
|
||||
//
|
||||
|
||||
@@ -37,7 +37,7 @@ import (
|
||||
// return either.Eq(eq.FromEquals(func(x, y int) bool { return x == y }))(a, b)
|
||||
// })
|
||||
// eqRIE := Eq(eqInt)
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -30,7 +29,7 @@ func TestWithTempFile(t *testing.T) {
|
||||
|
||||
res := WithTempFile(onWriteAll[*os.File]([]byte("Carsten")))
|
||||
|
||||
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(context.Background())())
|
||||
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(t.Context())())
|
||||
}
|
||||
|
||||
func TestWithTempFileOnClosedFile(t *testing.T) {
|
||||
@@ -43,5 +42,5 @@ func TestWithTempFileOnClosedFile(t *testing.T) {
|
||||
)
|
||||
})
|
||||
|
||||
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(context.Background())())
|
||||
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(t.Context())())
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import (
|
||||
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
|
||||
//
|
||||
// filter := readerioresult.FilterOrElse(isPositive, onNegative)
|
||||
// result := filter(readerioresult.Right(42))(context.Background())()
|
||||
// result := filter(readerioresult.Right(42))(t.Context())()
|
||||
//
|
||||
//go:inline
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
|
||||
@@ -71,7 +71,7 @@ import (
|
||||
//
|
||||
// // Now we can partially apply the Config
|
||||
// cfg := Config{Timeout: 30}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := sequenced(ctx)(cfg)() // Returns Right(60)
|
||||
//
|
||||
// This is especially useful in point-free style when building computation pipelines:
|
||||
@@ -133,7 +133,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
|
||||
//
|
||||
// // Partially apply the Database
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := sequenced(ctx)(db)() // Executes IO and returns Right("Query result...")
|
||||
//
|
||||
// In point-free style, this enables clean composition:
|
||||
@@ -195,7 +195,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R
|
||||
//
|
||||
// // Partially apply the Config
|
||||
// cfg := Config{MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := sequenced(ctx)(cfg)() // Returns Right(3)
|
||||
//
|
||||
// // With invalid config
|
||||
@@ -276,7 +276,7 @@ func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kl
|
||||
//
|
||||
// // Now we can provide the Config to get the final result
|
||||
// cfg := Config{Multiplier: 5}
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// finalResult := result(cfg)(ctx)() // Returns Right(50)
|
||||
//
|
||||
// In point-free style, this enables clean composition:
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Example_sequenceReader_basicUsage demonstrates the basic usage of SequenceReader
|
||||
@@ -233,7 +234,7 @@ func Example_sequenceReaderResult_errorHandling() {
|
||||
ctx := context.Background()
|
||||
pipeline := F.Pipe2(
|
||||
sequenced(ctx),
|
||||
RIOE.Map(func(x int) int { return x * 2 }),
|
||||
RIOE.Map(N.Mul(2)),
|
||||
RIOE.Chain(func(x int) RIOE.ReaderIOResult[string] {
|
||||
return RIOE.Of(fmt.Sprintf("Result: %d", x))
|
||||
}),
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
// The Reader environment (string) is now the first parameter
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original
|
||||
result1 := original(ctx)()
|
||||
@@ -75,7 +75,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
|
||||
db := Database{ConnectionString: "localhost:5432"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
expected := "Query on localhost:5432"
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
@@ -132,7 +132,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Sequence
|
||||
sequenced := SequenceReader(original)
|
||||
@@ -158,7 +158,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Test with zero values
|
||||
@@ -184,7 +184,7 @@ func TestSequenceReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReader(original)
|
||||
@@ -217,14 +217,14 @@ func TestSequenceReader(t *testing.T) {
|
||||
withConfig := sequenced(cfg)
|
||||
|
||||
// Now we have a ReaderIOResult[int] that can be used in different contexts
|
||||
ctx1 := context.Background()
|
||||
ctx1 := t.Context()
|
||||
result1 := withConfig(ctx1)()
|
||||
assert.True(t, either.IsRight(result1))
|
||||
value1, _ := either.Unwrap(result1)
|
||||
assert.Equal(t, 50, value1)
|
||||
|
||||
// Can reuse with different context
|
||||
ctx2 := context.Background()
|
||||
ctx2 := t.Context()
|
||||
result2 := withConfig(ctx2)()
|
||||
assert.True(t, either.IsRight(result2))
|
||||
value2, _ := either.Unwrap(result2)
|
||||
@@ -246,7 +246,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReaderIO(original)
|
||||
|
||||
// Test original
|
||||
@@ -273,7 +273,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
@@ -303,7 +303,7 @@ func TestSequenceReaderIO(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReaderIO(original)
|
||||
@@ -327,7 +327,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReaderResult(original)
|
||||
|
||||
// Test original
|
||||
@@ -356,7 +356,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with error
|
||||
result1 := original(ctx)()
|
||||
@@ -384,7 +384,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test original with inner error
|
||||
result1 := original(ctx)()
|
||||
@@ -421,7 +421,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test outer error
|
||||
sequenced1 := SequenceReaderResult(makeOriginal(-20))
|
||||
@@ -460,7 +460,7 @@ func TestSequenceReaderResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
sequenced := SequenceReaderResult(original)
|
||||
@@ -484,7 +484,7 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
empty := Empty{}
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
@@ -514,7 +514,7 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
data := &Data{Value: 100}
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
@@ -544,7 +544,7 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
sequenced := SequenceReader(original)
|
||||
|
||||
// Call multiple times with same inputs
|
||||
@@ -583,7 +583,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 5}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -614,7 +614,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 5}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsLeft(finalResult))
|
||||
@@ -643,7 +643,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Database and execute
|
||||
db := Database{Prefix: "ID"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(db)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -673,7 +673,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Settings and execute
|
||||
settings := Settings{Prefix: "[", Suffix: "]"}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(settings)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -705,14 +705,14 @@ func TestTraverseReader(t *testing.T) {
|
||||
withConfig := result(cfg)
|
||||
|
||||
// Can now use with different contexts
|
||||
ctx1 := context.Background()
|
||||
ctx1 := t.Context()
|
||||
finalResult1 := withConfig(ctx1)()
|
||||
assert.True(t, either.IsRight(finalResult1))
|
||||
value1, _ := either.Unwrap(finalResult1)
|
||||
assert.Equal(t, 30, value1)
|
||||
|
||||
// Reuse with different context
|
||||
ctx2 := context.Background()
|
||||
ctx2 := t.Context()
|
||||
finalResult2 := withConfig(ctx2)()
|
||||
assert.True(t, either.IsRight(finalResult2))
|
||||
value2, _ := either.Unwrap(finalResult2)
|
||||
@@ -746,7 +746,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
result := traversed(original)
|
||||
|
||||
// Use canceled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
cfg := Config{Value: 5}
|
||||
@@ -778,7 +778,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config with zero offset
|
||||
cfg := Config{Offset: 0}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -807,7 +807,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Provide Config and execute
|
||||
cfg := Config{Multiplier: 4}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult := result(cfg)(ctx)()
|
||||
|
||||
assert.True(t, either.IsRight(finalResult))
|
||||
@@ -843,7 +843,7 @@ func TestTraverseReader(t *testing.T) {
|
||||
|
||||
// Test with value within range
|
||||
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
finalResult1 := result(rules1)(ctx)()
|
||||
assert.True(t, either.IsRight(finalResult1))
|
||||
value1, _ := either.Unwrap(finalResult1)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
// )
|
||||
//
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
// result := requester(t.Context())()
|
||||
package builder
|
||||
|
||||
import (
|
||||
@@ -103,7 +103,7 @@ import (
|
||||
// B.WithJSONBody(map[string]string{"name": "John"}),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
// result := requester(t.Context())()
|
||||
//
|
||||
// Example without body:
|
||||
//
|
||||
@@ -113,7 +113,7 @@ import (
|
||||
// B.WithMethod("GET"),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
// result := requester(t.Context())()
|
||||
func Requester(builder *R.Builder) RIOEH.Requester {
|
||||
|
||||
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOResult[*http.Request] {
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestBuilderWithQuery(t *testing.T) {
|
||||
RIOE.Map(func(r *http.Request) *url.URL {
|
||||
return r.URL
|
||||
}),
|
||||
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[any] {
|
||||
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[Void] {
|
||||
return IO.FromImpure(func() {
|
||||
q := u.Query()
|
||||
assert.Equal(t, "10", q.Get("limit"))
|
||||
@@ -55,7 +55,7 @@ func TestBuilderWithQuery(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.True(t, E.IsRight(req(context.Background())()))
|
||||
assert.True(t, E.IsRight(req(t.Context())()))
|
||||
}
|
||||
|
||||
// TestBuilderWithoutBody tests creating a request without a body
|
||||
@@ -67,7 +67,7 @@ func TestBuilderWithoutBody(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestBuilderWithBody(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestBuilderWithHeaders(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestBuilderWithInvalidURL(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func TestBuilderWithEmptyMethod(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
// Empty method should still work (defaults to GET in http.NewRequest)
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
@@ -161,7 +161,7 @@ func TestBuilderWithMultipleHeaders(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -185,7 +185,7 @@ func TestBuilderWithBodyAndHeaders(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -207,7 +207,7 @@ func TestBuilderContextCancellation(t *testing.T) {
|
||||
requester := Requester(builder)
|
||||
|
||||
// Create a cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
result := requester(ctx)()
|
||||
@@ -233,7 +233,7 @@ func TestBuilderWithDifferentMethods(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result for method %s", method)
|
||||
|
||||
@@ -256,7 +256,7 @@ func TestBuilderWithJSON(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
@@ -277,7 +277,7 @@ func TestBuilderWithBearer(t *testing.T) {
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
result := requester(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
|
||||
7
v2/context/readerioresult/http/builder/types.go
Normal file
7
v2/context/readerioresult/http/builder/types.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package builder
|
||||
|
||||
import "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
type (
|
||||
Void = function.Void
|
||||
)
|
||||
@@ -28,7 +28,7 @@
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// result := ReadJSON[MyType](client)(request)
|
||||
// response := result(context.Background())()
|
||||
// response := result(t.Context())()
|
||||
package http
|
||||
|
||||
import (
|
||||
@@ -157,8 +157,8 @@ func MakeClient(httpClient *http.Client) Client {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// fullResp := ReadFullResponse(client)(request)
|
||||
// result := fullResp(context.Background())()
|
||||
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
|
||||
// result := fullResp(t.Context())()
|
||||
func ReadFullResponse(client Client) RIOE.Operator[*http.Request, H.FullResponse] {
|
||||
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
|
||||
return F.Flow3(
|
||||
client.Do(req),
|
||||
@@ -194,8 +194,8 @@ func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// readBytes := ReadAll(client)
|
||||
// result := readBytes(request)(context.Background())()
|
||||
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
// result := readBytes(request)(t.Context())()
|
||||
func ReadAll(client Client) RIOE.Operator[*http.Request, []byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
RIOE.Map(H.Body),
|
||||
@@ -218,8 +218,8 @@ func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/text")
|
||||
// readText := ReadText(client)
|
||||
// result := readText(request)(context.Background())()
|
||||
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
|
||||
// result := readText(request)(t.Context())()
|
||||
func ReadText(client Client) RIOE.Operator[*http.Request, string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
RIOE.Map(B.ToString),
|
||||
@@ -231,7 +231,7 @@ func ReadText(client Client) RIOE.Kleisli[Requester, string] {
|
||||
// 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) RIOE.Kleisli[Requester, A] {
|
||||
func ReadJson[A any](client Client) RIOE.Operator[*http.Request, A] {
|
||||
return ReadJSON[A](client)
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
|
||||
// 3. Reads the response body as bytes
|
||||
//
|
||||
// This function is used internally by ReadJSON to ensure proper JSON response handling.
|
||||
func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
func readJSON(client Client) RIOE.Operator[*http.Request, []byte] {
|
||||
return F.Flow3(
|
||||
ReadFullResponse(client),
|
||||
RIOE.ChainFirstEitherK(F.Flow2(
|
||||
@@ -277,8 +277,8 @@ func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
// 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) RIOE.Kleisli[Requester, A] {
|
||||
// result := readUser(request)(t.Context())()
|
||||
func ReadJSON[A any](client Client) RIOE.Operator[*http.Request, A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
RIOE.ChainEitherK(J.Unmarshal[A]),
|
||||
|
||||
@@ -65,7 +65,7 @@ var (
|
||||
// This function assumes the context contains logging information; it will panic if not present.
|
||||
getLoggingContext = F.Flow3(
|
||||
loggingContextValue,
|
||||
option.ToType[loggingContext],
|
||||
option.InstanceOf[loggingContext],
|
||||
option.GetOrElse(getDefaultLoggingContext),
|
||||
)
|
||||
)
|
||||
@@ -429,7 +429,7 @@ func LogEntryExitWithCallback[A any](
|
||||
// loggedFetch := LogEntryExit[User]("fetchUser")(fetchUser(123))
|
||||
//
|
||||
// // Execute
|
||||
// result := loggedFetch(context.Background())()
|
||||
// result := loggedFetch(t.Context())()
|
||||
// // Logs:
|
||||
// // [entering 1] fetchUser
|
||||
// // [exiting 1] fetchUser [0.1s]
|
||||
@@ -441,7 +441,7 @@ func LogEntryExitWithCallback[A any](
|
||||
// }
|
||||
//
|
||||
// logged := LogEntryExit[string]("failingOp")(failingOp())
|
||||
// result := logged(context.Background())()
|
||||
// result := logged(t.Context())()
|
||||
// // Logs:
|
||||
// // [entering 2] failingOp
|
||||
// // [throwing 2] failingOp [0.0s]: connection timeout
|
||||
@@ -461,7 +461,7 @@ func LogEntryExitWithCallback[A any](
|
||||
// LogEntryExit[[]Order]("fetchOrders"),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // Logs:
|
||||
// // [entering 3] fetchUser
|
||||
// // [exiting 3] fetchUser [0.1s]
|
||||
@@ -474,8 +474,8 @@ func LogEntryExitWithCallback[A any](
|
||||
// op1 := LogEntryExit[Data]("operation1")(fetchData(1))
|
||||
// op2 := LogEntryExit[Data]("operation2")(fetchData(2))
|
||||
//
|
||||
// go op1(context.Background())()
|
||||
// go op2(context.Background())()
|
||||
// go op1(t.Context())()
|
||||
// go op2(t.Context())()
|
||||
// // Logs (order may vary):
|
||||
// // [entering 5] operation1
|
||||
// // [entering 6] operation2
|
||||
@@ -615,7 +615,7 @@ func SLogWithCallback[A any](
|
||||
// Map(func(u User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // If error, logs: "Fetched user" error="user not found"
|
||||
//
|
||||
@@ -679,7 +679,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
// Map(func(u User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// result := pipeline(context.Background())()
|
||||
// result := pipeline(t.Context())()
|
||||
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
|
||||
// // Returns: result.Of("Alice")
|
||||
//
|
||||
@@ -694,7 +694,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
// TapSLog[Payment]("Payment processed"),
|
||||
// )
|
||||
//
|
||||
// result := processOrder(context.Background())()
|
||||
// result := processOrder(t.Context())()
|
||||
// // Logs each successful step with the intermediate values
|
||||
// // If any step fails, subsequent TapSLog calls don't log
|
||||
//
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestLoggingContext(t *testing.T) {
|
||||
LogEntryExit[string]("TestLoggingContext2"),
|
||||
)
|
||||
|
||||
assert.Equal(t, result.Of("Sample"), data(context.Background())())
|
||||
assert.Equal(t, result.Of("Sample"), data(t.Context())())
|
||||
}
|
||||
|
||||
// TestLogEntryExitSuccess tests successful operation logging
|
||||
@@ -43,7 +43,7 @@ func TestLogEntryExitSuccess(t *testing.T) {
|
||||
LogEntryExit[string]("TestOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("success value"), res)
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestLogEntryExitError(t *testing.T) {
|
||||
LogEntryExit[string]("FailingOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestLogEntryExitNested(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
|
||||
res := outerOp(context.Background())()
|
||||
res := outerOp(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
@@ -137,7 +137,7 @@ func TestLogEntryExitWithCallback(t *testing.T) {
|
||||
LogEntryExitWithCallback[int](slog.LevelDebug, customCallback, "DebugOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
|
||||
@@ -163,7 +163,7 @@ func TestLogEntryExitDisabled(t *testing.T) {
|
||||
LogEntryExit[string]("DisabledOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
@@ -197,7 +197,7 @@ func TestLogEntryExitF(t *testing.T) {
|
||||
LogEntryExitF(onEntry, onExit),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
|
||||
@@ -234,7 +234,7 @@ func TestLogEntryExitFWithError(t *testing.T) {
|
||||
LogEntryExitF(onEntry, onExit),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
|
||||
@@ -257,7 +257,7 @@ func TestLoggingIDUniqueness(t *testing.T) {
|
||||
Of(i),
|
||||
LogEntryExit[int]("Operation"),
|
||||
)
|
||||
op(context.Background())()
|
||||
op(t.Context())()
|
||||
}
|
||||
|
||||
logOutput := buf.String()
|
||||
@@ -287,7 +287,7 @@ func TestLogEntryExitWithContextLogger(t *testing.T) {
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
ctx := logging.WithLogger(contextLogger)(t.Context())
|
||||
|
||||
operation := F.Pipe1(
|
||||
Of("context value"),
|
||||
@@ -326,7 +326,7 @@ func TestLogEntryExitTiming(t *testing.T) {
|
||||
LogEntryExit[string]("SlowOperation"),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
@@ -379,7 +379,7 @@ func TestLogEntryExitChainedOperations(t *testing.T) {
|
||||
)),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("2"), res)
|
||||
|
||||
@@ -408,7 +408,7 @@ func TestTapSLog(t *testing.T) {
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(84), res)
|
||||
|
||||
@@ -443,7 +443,7 @@ func TestTapSLogInPipeline(t *testing.T) {
|
||||
TapSLog[int]("Step 3: Final length"),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(11), res)
|
||||
|
||||
@@ -472,7 +472,7 @@ func TestTapSLogWithError(t *testing.T) {
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := pipeline(context.Background())()
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
|
||||
@@ -504,7 +504,7 @@ func TestTapSLogWithStruct(t *testing.T) {
|
||||
Map(func(u User) string { return u.Name }),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of("Alice"), res)
|
||||
|
||||
@@ -530,7 +530,7 @@ func TestTapSLogDisabled(t *testing.T) {
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := operation(context.Background())()
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.Equal(t, result.Of(84), res)
|
||||
|
||||
@@ -546,7 +546,7 @@ func TestTapSLogWithContextLogger(t *testing.T) {
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
ctx := logging.WithLogger(contextLogger)(t.Context())
|
||||
|
||||
operation := F.Pipe2(
|
||||
Of("test value"),
|
||||
@@ -572,7 +572,7 @@ func TestSLogLogsSuccessValue(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create a Result and log it
|
||||
res1 := result.Of(42)
|
||||
@@ -594,7 +594,7 @@ func TestSLogLogsErrorValue(t *testing.T) {
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
// Create an error Result and log it
|
||||
@@ -620,7 +620,7 @@ func TestSLogWithCallbackCustomLevel(t *testing.T) {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Create a Result and log it with custom callback
|
||||
res1 := result.Of(42)
|
||||
@@ -645,7 +645,7 @@ func TestSLogWithCallbackLogsError(t *testing.T) {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
testErr := errors.New("warning error")
|
||||
|
||||
// Create an error Result and log it with custom callback
|
||||
|
||||
75
v2/context/readerioresult/profunctor.go
Normal file
75
v2/context/readerioresult/profunctor.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIOResult.
|
||||
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Modify the context before passing it to the ReaderIOResult (via f)
|
||||
// - Transform the success value after the IO effect completes (via g)
|
||||
//
|
||||
// The function f returns both a new context and a CancelFunc that should be called to release resources.
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The original success type produced by the ReaderIOResult
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
)
|
||||
}
|
||||
|
||||
// Contramap changes the context during the execution of a ReaderIOResult.
|
||||
// This is the contravariant functor operation that transforms the input context.
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Contramap is an alias for Local and is useful for adapting a ReaderIOResult to work with
|
||||
// a modified context by providing a function that transforms the context.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
98
v2/context/readerioresult/profunctor_test.go
Normal file
98
v2/context/readerioresult/profunctor_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both context and output", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return R.Of(v.(int))
|
||||
}
|
||||
return R.Of(0)
|
||||
}
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "key", 42)
|
||||
return newCtx, func() {}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(addKey, toString)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of("42"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("context transformation", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return R.Of(v.(int))
|
||||
}
|
||||
return R.Of(0)
|
||||
}
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "key", 100)
|
||||
return newCtx, func() {}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of(100), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalBasic tests basic Local functionality
|
||||
func TestLocalBasic(t *testing.T) {
|
||||
t.Run("adds value to context", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
if v := ctx.Value("user"); v != nil {
|
||||
return R.Of(v.(string))
|
||||
}
|
||||
return R.Of("unknown")
|
||||
}
|
||||
}
|
||||
|
||||
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
newCtx := context.WithValue(ctx, "user", "Alice")
|
||||
return newCtx, func() {}
|
||||
}
|
||||
|
||||
adapted := Local[string](addUser)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of("Alice"), result)
|
||||
})
|
||||
}
|
||||
@@ -222,7 +222,7 @@ func withCancelCauseFunc[A any](cancel context.CancelCauseFunc, ma IOResult[A])
|
||||
return function.Pipe3(
|
||||
ma,
|
||||
ioresult.Swap[A],
|
||||
ioeither.ChainFirstIOK[A](func(err error) func() any {
|
||||
ioeither.ChainFirstIOK[A](func(err error) func() Void {
|
||||
return io.FromImpure(func() { cancel(err) })
|
||||
}),
|
||||
ioeither.Swap[A],
|
||||
@@ -452,7 +452,7 @@ func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
|
||||
// Returns a function that chains Option-returning functions into ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -800,7 +800,7 @@ func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
|
||||
func FromReaderOption[A any](onNone Lazy[error]) Kleisli[ReaderOption[context.Context, A], A] {
|
||||
return RIOR.FromReaderOption[context.Context, A](onNone)
|
||||
}
|
||||
|
||||
@@ -895,17 +895,17 @@ func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
func ChainReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
func ChainFirstReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
func TapReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -914,6 +914,21 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
|
||||
return RIOR.Read[A](r)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ReadIO[A any](r IO[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
|
||||
return RIOR.ReadIO[A](r)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ReadIOEither[A any](r IOResult[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
|
||||
return RIOR.ReadIOEither[A](r)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ReadIOResult[A any](r IOResult[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
|
||||
return RIOR.ReadIOResult[A](r)
|
||||
}
|
||||
|
||||
// MonadChainLeft chains a computation on the left (error) side of a [ReaderIOResult].
|
||||
// If the input is a Left value, it applies the function f to transform the error and potentially
|
||||
// change the error type. If the input is a Right value, it passes through unchanged.
|
||||
@@ -1026,7 +1041,7 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
// getUser,
|
||||
// addUser,
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns ("Alice", nil)
|
||||
// value, err := result(t.Context())() // Returns ("Alice", nil)
|
||||
//
|
||||
// Timeout Example:
|
||||
//
|
||||
@@ -1097,7 +1112,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// fetchData,
|
||||
// readerioresult.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) after 5s
|
||||
// value, err := result(t.Context())() // Returns (Data{}, context.DeadlineExceeded) after 5s
|
||||
//
|
||||
// Successful Example:
|
||||
//
|
||||
@@ -1106,7 +1121,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// quickFetch,
|
||||
// readerioresult.WithTimeout[Data](5*time.Second),
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns (Data{Value: "quick"}, nil)
|
||||
// value, err := result(t.Context())() // Returns (Data{Value: "quick"}, nil)
|
||||
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
@@ -1158,12 +1173,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
// fetchData,
|
||||
// readerioresult.WithDeadline[Data](deadline),
|
||||
// )
|
||||
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
|
||||
// value, err := result(t.Context())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
|
||||
//
|
||||
// Combining with Parent Context:
|
||||
//
|
||||
// // If parent context already has a deadline, the earlier one takes precedence
|
||||
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
|
||||
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
|
||||
// defer cancel()
|
||||
//
|
||||
// laterDeadline := time.Now().Add(2 * time.Hour)
|
||||
|
||||
@@ -36,56 +36,56 @@ func TestFromEither(t *testing.T) {
|
||||
t.Run("Right value", func(t *testing.T) {
|
||||
either := E.Right[error]("success")
|
||||
result := FromEither(either)
|
||||
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("success"), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Left value", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
either := E.Left[string](err)
|
||||
result := FromEither(either)
|
||||
assert.Equal(t, E.Left[string](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[string](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromResult(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
result := FromResult(E.Right[error](42))
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := FromResult(E.Left[int](err))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLeft(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := Left[string](err)
|
||||
assert.Equal(t, E.Left[string](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[string](err), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestRight(t *testing.T) {
|
||||
result := Right("success")
|
||||
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("success"), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
result := Of(42)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("Map over Right", func(t *testing.T) {
|
||||
result := MonadMap(Of(5), N.Mul(2))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Map over Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := MonadMap(Left[int](err), N.Mul(2))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,34 +93,34 @@ func TestMap(t *testing.T) {
|
||||
t.Run("Map with success", func(t *testing.T) {
|
||||
mapper := Map(N.Mul(2))
|
||||
result := mapper(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Map with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
mapper := Map(N.Mul(2))
|
||||
result := mapper(Left[int](err))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
t.Run("MapTo with success", func(t *testing.T) {
|
||||
result := MonadMapTo(Of("original"), 42)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("MapTo with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
result := MonadMapTo(Left[string](err), 42)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
mapper := MapTo[string](42)
|
||||
result := mapper(Of("original"))
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
@@ -128,7 +128,7 @@ func TestMonadChain(t *testing.T) {
|
||||
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
|
||||
return Of(x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Chain with error in first", func(t *testing.T) {
|
||||
@@ -136,7 +136,7 @@ func TestMonadChain(t *testing.T) {
|
||||
result := MonadChain(Left[int](err), func(x int) ReaderIOResult[int] {
|
||||
return Of(x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Chain with error in second", func(t *testing.T) {
|
||||
@@ -144,7 +144,7 @@ func TestMonadChain(t *testing.T) {
|
||||
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
|
||||
return Left[int](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestChain(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
@@ -161,7 +161,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
|
||||
return Of("ignored")
|
||||
})
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainFirst propagates error from second", func(t *testing.T) {
|
||||
@@ -169,7 +169,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
|
||||
return Left[string](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ func TestChainFirst(t *testing.T) {
|
||||
return Of("ignored")
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
@@ -186,7 +186,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
fab := Of(N.Mul(2))
|
||||
fa := Of(5)
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ApSeq with error in function", func(t *testing.T) {
|
||||
@@ -194,7 +194,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
fab := Left[func(int) int](err)
|
||||
fa := Of(5)
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ApSeq with error in value", func(t *testing.T) {
|
||||
@@ -202,7 +202,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
fab := Of(N.Mul(2))
|
||||
fa := Left[int](err)
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func TestApSeq(t *testing.T) {
|
||||
fa := Of(5)
|
||||
fab := Of(N.Mul(2))
|
||||
result := MonadApSeq(fab, fa)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
@@ -218,11 +218,11 @@ func TestApPar(t *testing.T) {
|
||||
fa := Of(5)
|
||||
fab := Of(N.Mul(2))
|
||||
result := MonadApPar(fab, fa)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ApPar with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
fa := Of(5)
|
||||
fab := Of(N.Mul(2))
|
||||
@@ -239,7 +239,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
|
||||
)
|
||||
result := pred(5)
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Predicate false", func(t *testing.T) {
|
||||
@@ -248,7 +248,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
|
||||
)
|
||||
result := pred(-5)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsLeft(res))
|
||||
})
|
||||
}
|
||||
@@ -259,7 +259,7 @@ func TestOrElse(t *testing.T) {
|
||||
return Of(42)
|
||||
})
|
||||
result := fallback(Of(10))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("OrElse with error", func(t *testing.T) {
|
||||
@@ -268,13 +268,13 @@ func TestOrElse(t *testing.T) {
|
||||
return Of(42)
|
||||
})
|
||||
result := fallback(Left[int](err))
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
result := Ask()
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
res := result(ctx)()
|
||||
assert.True(t, E.IsRight(res))
|
||||
ctxResult := E.ToOption(res)
|
||||
@@ -286,7 +286,7 @@ func TestMonadChainEitherK(t *testing.T) {
|
||||
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
|
||||
return E.Right[error](x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainEitherK with error", func(t *testing.T) {
|
||||
@@ -294,7 +294,7 @@ func TestMonadChainEitherK(t *testing.T) {
|
||||
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
|
||||
return E.Left[int](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ func TestChainEitherK(t *testing.T) {
|
||||
return E.Right[error](x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
@@ -311,7 +311,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
|
||||
return E.Right[error]("ignored")
|
||||
})
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainFirstEitherK propagates error", func(t *testing.T) {
|
||||
@@ -319,7 +319,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
|
||||
return E.Left[string](err)
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ func TestChainFirstEitherK(t *testing.T) {
|
||||
return E.Right[error]("ignored")
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
@@ -339,7 +339,7 @@ func TestChainOptionK(t *testing.T) {
|
||||
return O.Some(x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainOptionK with None", func(t *testing.T) {
|
||||
@@ -349,7 +349,7 @@ func TestChainOptionK(t *testing.T) {
|
||||
return O.None[int]()
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsLeft(res))
|
||||
})
|
||||
}
|
||||
@@ -358,44 +358,44 @@ func TestFromIOEither(t *testing.T) {
|
||||
t.Run("FromIOEither with success", func(t *testing.T) {
|
||||
ioe := IOE.Of[error](42)
|
||||
result := FromIOEither(ioe)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("FromIOEither with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
ioe := IOE.Left[int](err)
|
||||
result := FromIOEither(ioe)
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIOResult(t *testing.T) {
|
||||
ioe := IOE.Of[error](42)
|
||||
result := FromIOResult(ioe)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
io := IOG.Of(42)
|
||||
result := FromIO(io)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
reader := R.Of[context.Context](42)
|
||||
result := FromReader(reader)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFromLazy(t *testing.T) {
|
||||
lazy := func() int { return 42 }
|
||||
result := FromLazy(lazy)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestNever(t *testing.T) {
|
||||
t.Run("Never with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
result := Never[int]()
|
||||
|
||||
// Cancel immediately
|
||||
@@ -406,7 +406,7 @@ func TestNever(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Never with timeout", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
result := Never[int]()
|
||||
@@ -419,7 +419,7 @@ func TestMonadChainIOK(t *testing.T) {
|
||||
result := MonadChainIOK(Of(5), func(x int) IOG.IO[int] {
|
||||
return IOG.Of(x * 2)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainIOK(t *testing.T) {
|
||||
@@ -427,14 +427,14 @@ func TestChainIOK(t *testing.T) {
|
||||
return IOG.Of(x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
result := MonadChainFirstIOK(Of(5), func(x int) IOG.IO[string] {
|
||||
return IOG.Of("ignored")
|
||||
})
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainFirstIOK(t *testing.T) {
|
||||
@@ -442,7 +442,7 @@ func TestChainFirstIOK(t *testing.T) {
|
||||
return IOG.Of("ignored")
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](5), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](5), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestChainIOEitherK(t *testing.T) {
|
||||
@@ -451,7 +451,7 @@ func TestChainIOEitherK(t *testing.T) {
|
||||
return IOE.Of[error](x * 2)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("ChainIOEitherK with error", func(t *testing.T) {
|
||||
@@ -460,7 +460,7 @@ func TestChainIOEitherK(t *testing.T) {
|
||||
return IOE.Left[int](err)
|
||||
})
|
||||
result := chainer(Of(5))
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ func TestDelay(t *testing.T) {
|
||||
start := time.Now()
|
||||
delayed := Delay[int](100 * time.Millisecond)
|
||||
result := delayed(Of(42))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
assert.True(t, E.IsRight(res))
|
||||
@@ -477,7 +477,7 @@ func TestDelay(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Delay with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
delayed := Delay[int](100 * time.Millisecond)
|
||||
result := delayed(Of(42))
|
||||
@@ -500,11 +500,11 @@ func TestDefer(t *testing.T) {
|
||||
})
|
||||
|
||||
// First execution
|
||||
res1 := deferred(context.Background())()
|
||||
res1 := deferred(t.Context())()
|
||||
assert.True(t, E.IsRight(res1))
|
||||
|
||||
// Second execution should generate a new computation
|
||||
res2 := deferred(context.Background())()
|
||||
res2 := deferred(t.Context())()
|
||||
assert.True(t, E.IsRight(res2))
|
||||
|
||||
// Counter should be incremented for each execution
|
||||
@@ -518,7 +518,7 @@ func TestTryCatch(t *testing.T) {
|
||||
return 42, nil
|
||||
}
|
||||
})
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("TryCatch with error", func(t *testing.T) {
|
||||
@@ -528,7 +528,7 @@ func TestTryCatch(t *testing.T) {
|
||||
return 0, err
|
||||
}
|
||||
})
|
||||
assert.Equal(t, E.Left[int](err), result(context.Background())())
|
||||
assert.Equal(t, E.Left[int](err), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
first := Of(42)
|
||||
second := func() ReaderIOResult[int] { return Of(100) }
|
||||
result := MonadAlt(first, second)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Alt with first error", func(t *testing.T) {
|
||||
@@ -545,7 +545,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
first := Left[int](err)
|
||||
second := func() ReaderIOResult[int] { return Of(100) }
|
||||
result := MonadAlt(first, second)
|
||||
assert.Equal(t, E.Right[error](100), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](100), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ func TestAlt(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
alternative := Alt(func() ReaderIOResult[int] { return Of(100) })
|
||||
result := alternative(Left[int](err))
|
||||
assert.Equal(t, E.Right[error](100), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](100), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMemoize(t *testing.T) {
|
||||
@@ -564,13 +564,13 @@ func TestMemoize(t *testing.T) {
|
||||
}))
|
||||
|
||||
// First execution
|
||||
res1 := computation(context.Background())()
|
||||
res1 := computation(t.Context())()
|
||||
assert.True(t, E.IsRight(res1))
|
||||
val1 := E.ToOption(res1)
|
||||
assert.Equal(t, O.Of(1), val1)
|
||||
|
||||
// Second execution should return cached value
|
||||
res2 := computation(context.Background())()
|
||||
res2 := computation(t.Context())()
|
||||
assert.True(t, E.IsRight(res2))
|
||||
val2 := E.ToOption(res2)
|
||||
assert.Equal(t, O.Of(1), val2)
|
||||
@@ -582,19 +582,19 @@ func TestMemoize(t *testing.T) {
|
||||
func TestFlatten(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
result := Flatten(nested)
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fab := Of(N.Mul(2))
|
||||
result := MonadFlap(fab, 5)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
flapper := Flap[int](5)
|
||||
result := flapper(Of(N.Mul(2)))
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
@@ -608,7 +608,7 @@ func TestFold(t *testing.T) {
|
||||
},
|
||||
)
|
||||
result := folder(Of(42))
|
||||
assert.Equal(t, E.Right[error]("success: 42"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("success: 42"), result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("Fold with error", func(t *testing.T) {
|
||||
@@ -622,7 +622,7 @@ func TestFold(t *testing.T) {
|
||||
},
|
||||
)
|
||||
result := folder(Left[int](err))
|
||||
assert.Equal(t, E.Right[error]("error: test error"), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error]("error: test error"), result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -634,7 +634,7 @@ func TestGetOrElse(t *testing.T) {
|
||||
}
|
||||
})
|
||||
result := getter(Of(42))
|
||||
assert.Equal(t, 42, result(context.Background())())
|
||||
assert.Equal(t, 42, result(t.Context())())
|
||||
})
|
||||
|
||||
t.Run("GetOrElse with error", func(t *testing.T) {
|
||||
@@ -645,19 +645,19 @@ func TestGetOrElse(t *testing.T) {
|
||||
}
|
||||
})
|
||||
result := getter(Left[int](err))
|
||||
assert.Equal(t, 0, result(context.Background())())
|
||||
assert.Equal(t, 0, result(t.Context())())
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithContext(t *testing.T) {
|
||||
t.Run("WithContext with valid context", func(t *testing.T) {
|
||||
computation := WithContext(Of(42))
|
||||
result := computation(context.Background())()
|
||||
result := computation(t.Context())()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
})
|
||||
|
||||
t.Run("WithContext with cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
computation := WithContext(Of(42))
|
||||
@@ -672,7 +672,7 @@ func TestEitherize0(t *testing.T) {
|
||||
}
|
||||
eitherized := Eitherize0(f)
|
||||
result := eitherized()
|
||||
assert.Equal(t, E.Right[error](42), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](42), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestUneitherize0(t *testing.T) {
|
||||
@@ -680,7 +680,7 @@ func TestUneitherize0(t *testing.T) {
|
||||
return Of(42)
|
||||
}
|
||||
uneitherized := Uneitherize0(f)
|
||||
result, err := uneitherized(context.Background())
|
||||
result, err := uneitherized(t.Context())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
@@ -691,7 +691,7 @@ func TestEitherize1(t *testing.T) {
|
||||
}
|
||||
eitherized := Eitherize1(f)
|
||||
result := eitherized(5)
|
||||
assert.Equal(t, E.Right[error](10), result(context.Background())())
|
||||
assert.Equal(t, E.Right[error](10), result(t.Context())())
|
||||
}
|
||||
|
||||
func TestUneitherize1(t *testing.T) {
|
||||
@@ -699,14 +699,14 @@ func TestUneitherize1(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
}
|
||||
uneitherized := Uneitherize1(f)
|
||||
result, err := uneitherized(context.Background(), 5)
|
||||
result, err := uneitherized(t.Context(), 5)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result)
|
||||
}
|
||||
|
||||
func TestSequenceT2(t *testing.T) {
|
||||
result := SequenceT2(Of(1), Of(2))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
tuple := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(tuple))
|
||||
@@ -717,13 +717,13 @@ func TestSequenceT2(t *testing.T) {
|
||||
|
||||
func TestSequenceSeqT2(t *testing.T) {
|
||||
result := SequenceSeqT2(Of(1), Of(2))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
}
|
||||
|
||||
func TestSequenceParT2(t *testing.T) {
|
||||
result := SequenceParT2(Of(1), Of(2))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
}
|
||||
|
||||
@@ -734,7 +734,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
})
|
||||
result := traverser(arr)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
arrOpt := E.ToOption(res)
|
||||
assert.Equal(t, O.Of([]int{2, 4, 6}), arrOpt)
|
||||
@@ -750,7 +750,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
})
|
||||
result := traverser(arr)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsLeft(res))
|
||||
})
|
||||
}
|
||||
@@ -758,7 +758,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
arr := []ReaderIOResult[int]{Of(1), Of(2), Of(3)}
|
||||
result := SequenceArray(arr)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
arrOpt := E.ToOption(res)
|
||||
assert.Equal(t, O.Of([]int{1, 2, 3}), arrOpt)
|
||||
@@ -769,7 +769,7 @@ func TestTraverseRecord(t *testing.T) {
|
||||
result := TraverseRecord[string](func(x int) ReaderIOResult[int] {
|
||||
return Of(x * 2)
|
||||
})(rec)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
recOpt := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(recOpt))
|
||||
@@ -784,7 +784,7 @@ func TestSequenceRecord(t *testing.T) {
|
||||
"b": Of(2),
|
||||
}
|
||||
result := SequenceRecord(rec)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
recOpt := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(recOpt))
|
||||
@@ -798,7 +798,7 @@ func TestAltSemigroup(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
|
||||
result := sg.Concat(Left[int](err), Of(42))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.Equal(t, E.Right[error](42), res)
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
))
|
||||
|
||||
result := intAddMonoid.Concat(Of(5), Of(10))
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
assert.Equal(t, E.Right[error](15), res)
|
||||
}
|
||||
|
||||
@@ -835,7 +835,7 @@ func TestBracket(t *testing.T) {
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, released)
|
||||
@@ -863,7 +863,7 @@ func TestBracket(t *testing.T) {
|
||||
}
|
||||
|
||||
result := Bracket(acquire, use, release)
|
||||
res := result(context.Background())()
|
||||
res := result(t.Context())()
|
||||
|
||||
assert.True(t, acquired)
|
||||
assert.True(t, released)
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
func TestInnerContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, parentCancel := context.WithCancel(outer)
|
||||
defer parentCancel()
|
||||
@@ -49,7 +49,7 @@ func TestInnerContextCancelSemantics(t *testing.T) {
|
||||
|
||||
func TestOuterContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, outerCancel := context.WithCancel(outer)
|
||||
defer outerCancel()
|
||||
@@ -69,7 +69,7 @@ func TestOuterContextCancelSemantics(t *testing.T) {
|
||||
|
||||
func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, outerCancel := context.WithCancel(outer)
|
||||
defer outerCancel()
|
||||
@@ -95,7 +95,7 @@ func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
|
||||
|
||||
func TestCancelCauseSemantics(t *testing.T) {
|
||||
// start with a simple context
|
||||
outer := context.Background()
|
||||
outer := t.Context()
|
||||
|
||||
parent, outerCancel := context.WithCancelCause(outer)
|
||||
defer outerCancel(nil)
|
||||
@@ -119,7 +119,7 @@ func TestCancelCauseSemantics(t *testing.T) {
|
||||
func TestTimer(t *testing.T) {
|
||||
delta := 3 * time.Second
|
||||
timer := Timer(delta)
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
t0 := time.Now()
|
||||
res := timer(ctx)()
|
||||
@@ -146,7 +146,7 @@ func TestCanceledApply(t *testing.T) {
|
||||
Ap[string](errValue),
|
||||
)
|
||||
|
||||
res := applied(context.Background())()
|
||||
res := applied(t.Context())()
|
||||
assert.Equal(t, E.Left[string](err), res)
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestRegularApply(t *testing.T) {
|
||||
Ap[string](value),
|
||||
)
|
||||
|
||||
res := applied(context.Background())()
|
||||
res := applied(t.Context())()
|
||||
assert.Equal(t, E.Of[error]("CARSTEN"), res)
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ func TestWithResourceNoErrors(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, countAcquire)
|
||||
assert.Equal(t, 1, countBody)
|
||||
@@ -217,7 +217,7 @@ func TestWithResourceErrorInBody(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, countAcquire)
|
||||
assert.Equal(t, 0, countBody)
|
||||
@@ -247,7 +247,7 @@ func TestWithResourceErrorInAcquire(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 0, countAcquire)
|
||||
assert.Equal(t, 0, countBody)
|
||||
@@ -277,7 +277,7 @@ func TestWithResourceErrorInRelease(t *testing.T) {
|
||||
|
||||
resRIOE := WithResource[int](acquire, release)(body)
|
||||
|
||||
res := resRIOE(context.Background())()
|
||||
res := resRIOE(t.Context())()
|
||||
|
||||
assert.Equal(t, 1, countAcquire)
|
||||
assert.Equal(t, 1, countBody)
|
||||
@@ -286,7 +286,7 @@ func TestWithResourceErrorInRelease(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainFirstLeft(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Left value - function returns Left, always preserves original error
|
||||
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
|
||||
@@ -353,7 +353,7 @@ func TestMonadChainFirstLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChainFirstLeft(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Left value - function returns Left, always preserves original error
|
||||
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {
|
||||
|
||||
@@ -108,7 +108,7 @@ import (
|
||||
// countdown := readerioresult.TailRec(countdownStep)
|
||||
//
|
||||
// // With cancellation
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
|
||||
// defer cancel()
|
||||
// result := countdown(10)(ctx)() // Will be cancelled after ~500ms
|
||||
//
|
||||
@@ -141,7 +141,7 @@ import (
|
||||
// }
|
||||
//
|
||||
// processFiles := readerioresult.TailRec(processStep)
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
//
|
||||
// // Can be cancelled at any point during processing
|
||||
// go func() {
|
||||
@@ -159,7 +159,7 @@ import (
|
||||
//
|
||||
// // Safe for very large inputs with cancellation support
|
||||
// largeCountdown := readerioresult.TailRec(countdownStep)
|
||||
// ctx := context.Background()
|
||||
// ctx := t.Context()
|
||||
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
|
||||
//
|
||||
// # Performance Considerations
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestTailRec_BasicRecursion(t *testing.T) {
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(5)(context.Background())()
|
||||
result := countdown(5)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func TestTailRec_FactorialRecursion(t *testing.T) {
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result := factorial(FactorialState{n: 5, acc: 1})(context.Background())()
|
||||
result := factorial(FactorialState{n: 5, acc: 1})(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](120), result) // 5! = 120
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func TestTailRec_ErrorHandling(t *testing.T) {
|
||||
}
|
||||
|
||||
errorRecursion := TailRec(errorStep)
|
||||
result := errorRecursion(5)(context.Background())()
|
||||
result := errorRecursion(5)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
@@ -125,7 +125,7 @@ func TestTailRec_ContextCancellation(t *testing.T) {
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Create a context that will be cancelled after 100ms
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
@@ -159,7 +159,7 @@ func TestTailRec_ImmediateCancellation(t *testing.T) {
|
||||
countdown := TailRec(countdownStep)
|
||||
|
||||
// Create an already cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := countdown(5)(ctx)()
|
||||
@@ -186,7 +186,7 @@ func TestTailRec_StackSafety(t *testing.T) {
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(largeN)(context.Background())()
|
||||
result := countdown(largeN)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](0), result)
|
||||
}
|
||||
@@ -217,7 +217,7 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
|
||||
countdown := TailRec(countdownStep)
|
||||
|
||||
// Cancel after 50ms to allow some iterations but not all
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
result := countdown(largeN)(ctx)()
|
||||
@@ -274,7 +274,7 @@ func TestTailRec_ComplexState(t *testing.T) {
|
||||
errors: []error{},
|
||||
}
|
||||
|
||||
result := processItems(initialState)(context.Background())()
|
||||
result := processItems(initialState)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error]([]string{"item1", "item2", "item3"}), result)
|
||||
})
|
||||
@@ -286,7 +286,7 @@ func TestTailRec_ComplexState(t *testing.T) {
|
||||
errors: []error{},
|
||||
}
|
||||
|
||||
result := processItems(initialState)(context.Background())()
|
||||
result := processItems(initialState)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
@@ -336,7 +336,7 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
|
||||
}
|
||||
|
||||
// Cancel after 100ms (should allow ~5 files to be processed)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
@@ -366,7 +366,7 @@ func TestTailRec_ZeroIterations(t *testing.T) {
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result := immediate(100)(context.Background())()
|
||||
result := immediate(100)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error]("immediate"), result)
|
||||
}
|
||||
@@ -392,7 +392,7 @@ func TestTailRec_ContextWithDeadline(t *testing.T) {
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Set deadline 80ms from now
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(80*time.Millisecond))
|
||||
ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(80*time.Millisecond))
|
||||
defer cancel()
|
||||
|
||||
result := slowRecursion(10)(ctx)()
|
||||
@@ -427,7 +427,7 @@ func TestTailRec_ContextWithValue(t *testing.T) {
|
||||
}
|
||||
|
||||
valueRecursion := TailRec(valueStep)
|
||||
ctx := context.WithValue(context.Background(), testKey, "test-value")
|
||||
ctx := context.WithValue(t.Context(), testKey, "test-value")
|
||||
result := valueRecursion(3)(ctx)()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
|
||||
@@ -107,7 +107,7 @@ import (
|
||||
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
|
||||
//
|
||||
// // Execute with a cancellable context
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
// defer cancel()
|
||||
// ioResult := retryingFetch(ctx)
|
||||
// finalResult := ioResult()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user