mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-29 10:36:04 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7aa7e3560 | ||
|
|
ff2a4299b2 | ||
|
|
edd66d63e6 | ||
|
|
909aec8eba | ||
|
|
da0344f9bd |
168
v2/context/readerreaderioresult/promap.go
Normal file
168
v2/context/readerreaderioresult/promap.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Local modifies the outer environment before passing it to a computation.
|
||||
// Useful for providing different configurations to sub-computations.
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.Local[context.Context, error, A](f)
|
||||
}
|
||||
|
||||
// LocalIOK transforms the outer environment of a ReaderReaderIOResult using an IO-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through an effectful computation before
|
||||
// passing it to the ReaderReaderIOResult.
|
||||
//
|
||||
// This is useful when the outer environment transformation itself requires IO effects,
|
||||
// such as reading from a file, making a network call, or accessing system resources,
|
||||
// but these effects cannot fail (or failures are not relevant).
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
|
||||
// 2. The resulting R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalIOK[context.Context, error, A](f)
|
||||
}
|
||||
|
||||
// LocalIOEitherK transforms the outer environment of a ReaderReaderIOResult using an IOResult-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through an effectful computation that can fail before
|
||||
// passing it to the ReaderReaderIOResult.
|
||||
//
|
||||
// This is useful when the outer environment transformation itself requires IO effects that can fail,
|
||||
// such as reading from a file that might not exist, making a network call that might timeout,
|
||||
// or parsing data that might be invalid.
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
|
||||
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOEitherK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalIOEitherK[context.Context, A](f)
|
||||
}
|
||||
|
||||
// LocalIOResultK transforms the outer environment of a ReaderReaderIOResult using an IOResult-based Kleisli arrow.
|
||||
// This is a type-safe alias for LocalIOEitherK specialized for Result types (which use error as the error type).
|
||||
//
|
||||
// It allows you to modify the outer environment through an effectful computation that can fail before
|
||||
// passing it to the ReaderReaderIOResult.
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
|
||||
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOResultK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalIOEitherK[context.Context, A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalResultK[A, R1, R2 any](f result.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalEitherK[context.Context, A](f)
|
||||
}
|
||||
|
||||
// LocalReaderIOEitherK transforms the outer environment of a ReaderReaderIOResult using a ReaderIOResult-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through a computation that depends on the inner context
|
||||
// and can perform IO effects that may fail.
|
||||
//
|
||||
// This is useful when the outer environment transformation requires access to the inner context (e.g., context.Context)
|
||||
// and may perform IO operations that can fail, such as database queries, API calls, or file operations.
|
||||
//
|
||||
// The transformation happens in three stages:
|
||||
// 1. The ReaderIOResult effect f is executed with the R2 outer environment and inner context
|
||||
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A ReaderIOResult Kleisli arrow that transforms R2 to R1 with context-aware IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalReaderIOEitherK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalReaderIOEitherK[A](f)
|
||||
}
|
||||
|
||||
// LocalReaderIOResultK transforms the outer environment of a ReaderReaderIOResult using a ReaderIOResult-based Kleisli arrow.
|
||||
// This is a type-safe alias for LocalReaderIOEitherK specialized for Result types (which use error as the error type).
|
||||
//
|
||||
// It allows you to modify the outer environment through a computation that depends on the inner context
|
||||
// and can perform IO effects that may fail.
|
||||
//
|
||||
// The transformation happens in three stages:
|
||||
// 1. The ReaderIOResult effect f is executed with the R2 outer environment and inner context
|
||||
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A ReaderIOResult Kleisli arrow that transforms R2 to R1 with context-aware IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalReaderIOResultK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalReaderIOEitherK[A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalReaderReaderIOEitherK[A, R1, R2 any](f Kleisli[R2, R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalReaderReaderIOEitherK[A](f)
|
||||
}
|
||||
428
v2/context/readerreaderioresult/promap_test.go
Normal file
428
v2/context/readerreaderioresult/promap_test.go
Normal file
@@ -0,0 +1,428 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestLocalIOK tests LocalIOK functionality
|
||||
func TestLocalIOK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic IO transformation", func(t *testing.T) {
|
||||
// IO effect that loads config from a path
|
||||
loadConfig := func(path string) io.IO[SimpleConfig] {
|
||||
return func() SimpleConfig {
|
||||
// Simulate loading config
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalIOK
|
||||
adapted := LocalIOK[string](loadConfig)(useConfig)
|
||||
res := adapted("config.json")(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
})
|
||||
|
||||
t.Run("IO transformation with side effects", func(t *testing.T) {
|
||||
var loadLog []string
|
||||
|
||||
loadData := func(key string) io.IO[int] {
|
||||
return func() int {
|
||||
loadLog = append(loadLog, "Loading: "+key)
|
||||
return len(key) * 10
|
||||
}
|
||||
}
|
||||
|
||||
processData := func(n int) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Processed: %d", n))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string](loadData)(processData)
|
||||
res := adapted("test")(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of("Processed: 40"), res)
|
||||
assert.Equal(t, []string{"Loading: test"}, loadLog)
|
||||
})
|
||||
|
||||
t.Run("error propagation in ReaderReaderIOResult", func(t *testing.T) {
|
||||
loadConfig := func(path string) io.IO[SimpleConfig] {
|
||||
return func() SimpleConfig {
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that returns an error
|
||||
failingOperation := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Left[string](errors.New("operation failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string](loadConfig)(failingOperation)
|
||||
res := adapted("config.json")(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOEitherK tests LocalIOEitherK functionality
|
||||
func TestLocalIOEitherK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic IOResult transformation", func(t *testing.T) {
|
||||
// IOResult effect that loads config from a path (can fail)
|
||||
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if path == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty path"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalIOEitherK
|
||||
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
res := adapted("config.json")(ctx)()
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := adapted("")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
|
||||
t.Run("error propagation from environment transformation", func(t *testing.T) {
|
||||
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
return result.Left[SimpleConfig](errors.New("file not found"))
|
||||
}
|
||||
}
|
||||
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
|
||||
res := adapted("missing.json")(ctx)()
|
||||
|
||||
// Error from loadConfig should propagate
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOResultK tests LocalIOResultK functionality
|
||||
func TestLocalIOResultK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic IOResult transformation", func(t *testing.T) {
|
||||
// IOResult effect that loads config from a path (can fail)
|
||||
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if path == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty path"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalIOResultK
|
||||
adapted := LocalIOResultK[string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
res := adapted("config.json")(ctx)()
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := adapted("")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
|
||||
t.Run("compose multiple LocalIOResultK", func(t *testing.T) {
|
||||
// First transformation: string -> int (can fail)
|
||||
parseID := func(s string) ioresult.IOResult[int] {
|
||||
return func() result.Result[int] {
|
||||
if s == "" {
|
||||
return result.Left[int](errors.New("empty string"))
|
||||
}
|
||||
return result.Of(len(s) * 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Second transformation: int -> SimpleConfig (can fail)
|
||||
loadConfig := func(id int) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if id < 0 {
|
||||
return result.Left[SimpleConfig](errors.New("invalid ID"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 8000 + id})
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
formatConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose transformations
|
||||
step1 := LocalIOResultK[string](loadConfig)(formatConfig)
|
||||
step2 := LocalIOResultK[string](parseID)(step1)
|
||||
|
||||
// Success case
|
||||
res := step2("test")(ctx)()
|
||||
assert.Equal(t, result.Of("Port: 8040"), res)
|
||||
|
||||
// Failure in first transformation
|
||||
resErr1 := step2("")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr1))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalReaderIOEitherK tests LocalReaderIOEitherK functionality
|
||||
func TestLocalReaderIOEitherK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic ReaderIOResult transformation", func(t *testing.T) {
|
||||
// ReaderIOResult effect that loads config from a path (can fail, uses context)
|
||||
loadConfig := func(path string) readerioresult.ReaderIOResult[SimpleConfig] {
|
||||
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if path == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty path"))
|
||||
}
|
||||
// Could use context here for cancellation, logging, etc.
|
||||
return result.Of(SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalReaderIOEitherK
|
||||
adapted := LocalReaderIOEitherK[string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
res := adapted("config.json")(ctx)()
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := adapted("")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
|
||||
t.Run("context propagation", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const key ctxKey = "test-key"
|
||||
|
||||
// ReaderIOResult that reads from context
|
||||
loadFromContext := func(path string) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
if val := ctx.Value(key); val != nil {
|
||||
return result.Of(val.(string))
|
||||
}
|
||||
return result.Left[string](errors.New("key not found in context"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the loaded value
|
||||
useValue := func(val string) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of("Loaded: " + val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalReaderIOEitherK[string](loadFromContext)(useValue)
|
||||
|
||||
// With context value
|
||||
ctxWithValue := context.WithValue(ctx, key, "test-value")
|
||||
res := adapted("ignored")(ctxWithValue)()
|
||||
assert.Equal(t, result.Of("Loaded: test-value"), res)
|
||||
|
||||
// Without context value
|
||||
resErr := adapted("ignored")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalReaderIOResultK tests LocalReaderIOResultK functionality
|
||||
func TestLocalReaderIOResultK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic ReaderIOResult transformation", func(t *testing.T) {
|
||||
// ReaderIOResult effect that loads config from a path (can fail, uses context)
|
||||
loadConfig := func(path string) readerioresult.ReaderIOResult[SimpleConfig] {
|
||||
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if path == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty path"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalReaderIOResultK
|
||||
adapted := LocalReaderIOResultK[string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
res := adapted("config.json")(ctx)()
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := adapted("")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
|
||||
t.Run("real-world: load and validate config with context", func(t *testing.T) {
|
||||
type ConfigFile struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// Read file with context (can fail, uses context for cancellation)
|
||||
readFile := func(cf ConfigFile) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
// Check context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result.Left[string](ctx.Err())
|
||||
default:
|
||||
}
|
||||
|
||||
if cf.Path == "" {
|
||||
return result.Left[string](errors.New("empty path"))
|
||||
}
|
||||
return result.Of(`{"port":9000}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse config with context (can fail)
|
||||
parseConfig := func(content string) readerioresult.ReaderIOResult[SimpleConfig] {
|
||||
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if content == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty content"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 9000})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Using port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose the pipeline
|
||||
step1 := LocalReaderIOResultK[string](parseConfig)(useConfig)
|
||||
step2 := LocalReaderIOResultK[string](readFile)(step1)
|
||||
|
||||
// Success case
|
||||
res := step2(ConfigFile{Path: "app.json"})(ctx)()
|
||||
assert.Equal(t, result.Of("Using port: 9000"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := step2(ConfigFile{Path: ""})(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// FromReaderOption converts a ReaderOption to a ReaderReaderIOResult.
|
||||
@@ -170,6 +171,15 @@ func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B]
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return fromeither.ChainEitherK(
|
||||
Chain[R, A, B],
|
||||
FromEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstEitherK chains a computation that returns an Either but preserves the original value.
|
||||
// Useful for validation or side effects that may fail.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
@@ -837,14 +847,6 @@ func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
|
||||
return RRIOE.MapLeft[R, context.Context, A](f)
|
||||
}
|
||||
|
||||
// Local modifies the outer environment before passing it to a computation.
|
||||
// Useful for providing different configurations to sub-computations.
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.Local[context.Context, error, A](f)
|
||||
}
|
||||
|
||||
// Read provides a specific outer environment value to a computation.
|
||||
// Converts ReaderReaderIOResult[R, A] to ReaderIOResult[context.Context, A].
|
||||
//
|
||||
@@ -892,3 +894,8 @@ func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderReaderIOResult[R, A]
|
||||
func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
|
||||
return reader.Map[R](RIOE.Delay[A](delay))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Defer[R, A any](fa Lazy[ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.Defer(fa)
|
||||
}
|
||||
|
||||
9
v2/context/readerreaderioresult/traverse.go
Normal file
9
v2/context/readerreaderioresult/traverse.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
)
|
||||
|
||||
func TraverseArray[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, []A, []B] {
|
||||
return RRIOE.TraverseArray(f)
|
||||
}
|
||||
@@ -30,8 +30,8 @@ import (
|
||||
|
||||
type (
|
||||
// InjectableFactory is a factory function that can create an untyped instance of a service based on its [Dependency] identifier
|
||||
InjectableFactory = func(Dependency) IOResult[any]
|
||||
ProviderFactory = func(InjectableFactory) IOResult[any]
|
||||
InjectableFactory = ReaderIOResult[Dependency, any]
|
||||
ProviderFactory = ReaderIOResult[InjectableFactory, any]
|
||||
|
||||
paramIndex = map[int]int
|
||||
paramValue = map[int]any
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/record"
|
||||
)
|
||||
|
||||
@@ -12,4 +13,5 @@ type (
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
IOOption[T any] = iooption.IOOption[T]
|
||||
Entry[K comparable, V any] = record.Entry[K, V]
|
||||
ReaderIOResult[R, T any] = readerioresult.ReaderIOResult[R, T]
|
||||
)
|
||||
|
||||
264
v2/effect/bind.go
Normal file
264
v2/effect/bind.go
Normal file
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func Do[C, S any](
|
||||
empty S,
|
||||
) Effect[C, S] {
|
||||
return readerreaderioresult.Of[C](empty)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Bind[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[C, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.Bind(setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Let[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.Let[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LetTo[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.LetTo[C](setter, b)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindTo[C, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[C, T, S1] {
|
||||
return readerreaderioresult.BindTo[C](setter)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Effect[C, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApS[C](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Effect[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApSL[C](lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f func(T) Effect[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindL[C](lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LetL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.LetL[C](lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LetToL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.LetToL[C](lens, b)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOEitherK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f ioeither.Kleisli[error, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindIOEitherK[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOResultK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f ioresult.Kleisli[S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindIOResultK[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f io.Kleisli[S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindIOK[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f reader.Kleisli[C, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindReaderK[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderIOK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f readerio.Kleisli[C, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindReaderIOK[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindEitherK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f either.Kleisli[error, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindEitherK[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOEitherKL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f ioeither.Kleisli[error, T, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindIOEitherKL[C](lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOKL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f io.Kleisli[T, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindIOKL[C](lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderKL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f reader.Kleisli[C, T, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindReaderKL[C](lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderIOKL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f readerio.Kleisli[C, T, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindReaderIOKL[C](lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApIOEitherS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOEither[error, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApIOEitherS[C](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApIOS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IO[T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApIOS[C](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReaderS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Reader[C, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApReaderS[C](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReaderIOS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderIO[C, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApReaderIOS[C](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApEitherS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Either[error, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApEitherS[C](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApIOEitherSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa IOEither[error, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApIOEitherSL[C](lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApIOSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa IO[T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApIOSL[C](lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReaderSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Reader[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApReaderSL[C](lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReaderIOSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa ReaderIO[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApReaderIOSL[C](lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApEitherSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Either[error, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApEitherSL[C](lens, fa)
|
||||
}
|
||||
768
v2/effect/bind_test.go
Normal file
768
v2/effect/bind_test.go
Normal file
@@ -0,0 +1,768 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type BindState struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
t.Run("creates effect with initial state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 30}
|
||||
eff := Do[TestContext](initial)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, initial, result)
|
||||
})
|
||||
|
||||
t.Run("creates effect with empty struct", func(t *testing.T) {
|
||||
type Empty struct{}
|
||||
eff := Do[TestContext](Empty{})
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Empty{}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
t.Run("binds effect result to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := Bind[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("chains multiple binds", func(t *testing.T) {
|
||||
initial := BindState{}
|
||||
|
||||
eff := Bind[TestContext](
|
||||
func(email string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Email = email
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("alice@example.com")
|
||||
},
|
||||
)(Bind[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](30)
|
||||
},
|
||||
)(Bind[TestContext](
|
||||
func(name string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Name = name
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("Alice")
|
||||
},
|
||||
)(Do[TestContext](initial))))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
assert.Equal(t, "alice@example.com", result.Email)
|
||||
})
|
||||
|
||||
t.Run("propagates errors", func(t *testing.T) {
|
||||
expectedErr := errors.New("bind error")
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := Bind[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Fail[TestContext, int](expectedErr)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
t.Run("computes value and binds to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := Let[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) int {
|
||||
return len(s.Name) * 10
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 50, result.Age) // len("Alice") * 10
|
||||
})
|
||||
|
||||
t.Run("chains with Bind", func(t *testing.T) {
|
||||
initial := BindState{Name: "Bob"}
|
||||
|
||||
eff := Let[TestContext](
|
||||
func(email string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Email = email
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) string {
|
||||
return s.Name + "@example.com"
|
||||
},
|
||||
)(Bind[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](25)
|
||||
},
|
||||
)(Do[TestContext](initial)))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Bob", result.Name)
|
||||
assert.Equal(t, 25, result.Age)
|
||||
assert.Equal(t, "Bob@example.com", result.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
t.Run("binds constant value to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := LetTo[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
42,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 42, result.Age)
|
||||
})
|
||||
|
||||
t.Run("chains multiple LetTo", func(t *testing.T) {
|
||||
initial := BindState{}
|
||||
|
||||
eff := LetTo[TestContext](
|
||||
func(email string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Email = email
|
||||
return s
|
||||
}
|
||||
},
|
||||
"test@example.com",
|
||||
)(LetTo[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
30,
|
||||
)(LetTo[TestContext](
|
||||
func(name string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Name = name
|
||||
return s
|
||||
}
|
||||
},
|
||||
"Alice",
|
||||
)(Do[TestContext](initial))))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
assert.Equal(t, "test@example.com", result.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
t.Run("wraps value in state", func(t *testing.T) {
|
||||
type SimpleState struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
eff := BindTo[TestContext](func(v int) SimpleState {
|
||||
return SimpleState{Value: v}
|
||||
})(Of[TestContext, int](42))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result.Value)
|
||||
})
|
||||
|
||||
t.Run("starts a bind chain", func(t *testing.T) {
|
||||
type State struct {
|
||||
X int
|
||||
Y string
|
||||
}
|
||||
|
||||
eff := Let[TestContext](
|
||||
func(y string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Y = y
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s State) string {
|
||||
return "computed"
|
||||
},
|
||||
)(BindTo[TestContext](func(x int) State {
|
||||
return State{X: x}
|
||||
})(Of[TestContext, int](10)))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result.X)
|
||||
assert.Equal(t, "computed", result.Y)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
t.Run("applies effect and binds result to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
ageEffect := Of[TestContext, int](30)
|
||||
|
||||
eff := ApS[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
ageEffect,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("propagates errors from applied effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("aps error")
|
||||
initial := BindState{Name: "Alice"}
|
||||
ageEffect := Fail[TestContext, int](expectedErr)
|
||||
|
||||
eff := ApS[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
ageEffect,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindIOK(t *testing.T) {
|
||||
t.Run("binds IO operation to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindIOK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) io.IO[int] {
|
||||
return func() int {
|
||||
return 30
|
||||
}
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindIOEitherK(t *testing.T) {
|
||||
t.Run("binds successful IOEither to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindIOEitherK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) ioeither.IOEither[error, int] {
|
||||
return ioeither.Of[error, int](30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("propagates IOEither error", func(t *testing.T) {
|
||||
expectedErr := errors.New("ioeither error")
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindIOEitherK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) ioeither.IOEither[error, int] {
|
||||
return ioeither.Left[int, error](expectedErr)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindIOResultK(t *testing.T) {
|
||||
t.Run("binds successful IOResult to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindIOResultK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) ioresult.IOResult[int] {
|
||||
return ioresult.Of[int](30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindReaderK(t *testing.T) {
|
||||
t.Run("binds Reader operation to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindReaderK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) reader.Reader[TestContext, int] {
|
||||
return func(ctx TestContext) int {
|
||||
return 30
|
||||
}
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindReaderIOK(t *testing.T) {
|
||||
t.Run("binds ReaderIO operation to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindReaderIOK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) readerio.ReaderIO[TestContext, int] {
|
||||
return func(ctx TestContext) io.IO[int] {
|
||||
return func() int {
|
||||
return 30
|
||||
}
|
||||
}
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindEitherK(t *testing.T) {
|
||||
t.Run("binds successful Either to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindEitherK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) either.Either[error, int] {
|
||||
return either.Of[error, int](30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("propagates Either error", func(t *testing.T) {
|
||||
expectedErr := errors.New("either error")
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindEitherK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) either.Either[error, int] {
|
||||
return either.Left[int, error](expectedErr)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLensOperations(t *testing.T) {
|
||||
// Create lenses for BindState
|
||||
nameLens := lens.MakeLens(
|
||||
func(s BindState) string { return s.Name },
|
||||
func(s BindState, name string) BindState {
|
||||
s.Name = name
|
||||
return s
|
||||
},
|
||||
)
|
||||
|
||||
ageLens := lens.MakeLens(
|
||||
func(s BindState) int { return s.Age },
|
||||
func(s BindState, age int) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("ApSL applies effect using lens", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 25}
|
||||
ageEffect := Of[TestContext, int](30)
|
||||
|
||||
eff := ApSL[TestContext](ageLens, ageEffect)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("BindL binds effect using lens", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 25}
|
||||
|
||||
eff := BindL[TestContext](
|
||||
ageLens,
|
||||
func(age int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](age + 5)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("LetL computes value using lens", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 25}
|
||||
|
||||
eff := LetL[TestContext](
|
||||
ageLens,
|
||||
func(age int) int {
|
||||
return age * 2
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 50, result.Age)
|
||||
})
|
||||
|
||||
t.Run("LetToL sets constant using lens", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 25}
|
||||
|
||||
eff := LetToL[TestContext](ageLens, 100)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 100, result.Age)
|
||||
})
|
||||
|
||||
t.Run("chains lens operations", func(t *testing.T) {
|
||||
initial := BindState{}
|
||||
|
||||
eff := LetToL[TestContext](
|
||||
ageLens,
|
||||
30,
|
||||
)(LetToL[TestContext](
|
||||
nameLens,
|
||||
"Bob",
|
||||
)(Do[TestContext](initial)))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Bob", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApOperations(t *testing.T) {
|
||||
t.Run("ApIOS applies IO effect", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
ioEffect := func() int { return 30 }
|
||||
|
||||
eff := ApIOS[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
ioEffect,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("ApReaderS applies Reader effect", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
readerEffect := func(ctx TestContext) int { return 30 }
|
||||
|
||||
eff := ApReaderS[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
readerEffect,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("ApEitherS applies Either effect", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
eitherEffect := either.Of[error, int](30)
|
||||
|
||||
eff := ApEitherS[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
eitherEffect,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestComplexBindChain(t *testing.T) {
|
||||
t.Run("builds complex state with multiple operations", func(t *testing.T) {
|
||||
type ComplexState struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
IsAdmin bool
|
||||
Score int
|
||||
}
|
||||
|
||||
eff := LetTo[TestContext](
|
||||
func(score int) func(ComplexState) ComplexState {
|
||||
return func(s ComplexState) ComplexState {
|
||||
s.Score = score
|
||||
return s
|
||||
}
|
||||
},
|
||||
100,
|
||||
)(Let[TestContext](
|
||||
func(isAdmin bool) func(ComplexState) ComplexState {
|
||||
return func(s ComplexState) ComplexState {
|
||||
s.IsAdmin = isAdmin
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s ComplexState) bool {
|
||||
return s.Age >= 18
|
||||
},
|
||||
)(Let[TestContext](
|
||||
func(email string) func(ComplexState) ComplexState {
|
||||
return func(s ComplexState) ComplexState {
|
||||
s.Email = email
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s ComplexState) string {
|
||||
return s.Name + "@example.com"
|
||||
},
|
||||
)(Bind[TestContext](
|
||||
func(age int) func(ComplexState) ComplexState {
|
||||
return func(s ComplexState) ComplexState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s ComplexState) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](25)
|
||||
},
|
||||
)(BindTo[TestContext](func(name string) ComplexState {
|
||||
return ComplexState{Name: name}
|
||||
})(Of[TestContext, string]("Alice"))))))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 25, result.Age)
|
||||
assert.Equal(t, "Alice@example.com", result.Email)
|
||||
assert.True(t, result.IsAdmin)
|
||||
assert.Equal(t, 100, result.Score)
|
||||
})
|
||||
}
|
||||
110
v2/effect/dependencies.go
Normal file
110
v2/effect/dependencies.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
|
||||
return readerreaderioresult.Local[A](acc)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Contramap[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
|
||||
return readerreaderioresult.Local[A](acc)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalIOK[A, C1, C2 any](f io.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalIOK[A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalIOResultK[A, C1, C2 any](f ioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalIOResultK[A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalResultK[A, C1, C2 any](f result.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalResultK[A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalThunkK[A, C1, C2 any](f readerioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalReaderIOResultK[A](f)
|
||||
}
|
||||
|
||||
// LocalEffectK transforms the context of an Effect using an Effect-returning function.
|
||||
// This is the most powerful context transformation function, allowing the transformation
|
||||
// itself to be effectful (can fail, perform I/O, and access the outer context).
|
||||
//
|
||||
// LocalEffectK takes a Kleisli arrow that:
|
||||
// - Accepts the outer context C2
|
||||
// - Returns an Effect that produces the inner context C1
|
||||
// - Can fail with an error during context transformation
|
||||
// - Can perform I/O operations during transformation
|
||||
//
|
||||
// This is useful when:
|
||||
// - Context transformation requires I/O (e.g., loading config from a file)
|
||||
// - Context transformation can fail (e.g., validating or parsing context)
|
||||
// - Context transformation needs to access the outer context
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type produced by the effect
|
||||
// - C1: The inner context type (required by the original effect)
|
||||
// - C2: The outer context type (provided to the transformed effect)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow (C2 -> Effect[C2, C1]) that transforms C2 to C1 effectfully
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms Effect[C1, A] to Effect[C2, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type DatabaseConfig struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// ConfigPath string
|
||||
// }
|
||||
//
|
||||
// // Effect that needs DatabaseConfig
|
||||
// dbEffect := effect.Of[DatabaseConfig, string]("query result")
|
||||
//
|
||||
// // Transform AppConfig to DatabaseConfig effectfully
|
||||
// // (e.g., load config from file, which can fail)
|
||||
// loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
|
||||
// return effect.Chain[AppConfig](func(_ AppConfig) Effect[AppConfig, DatabaseConfig] {
|
||||
// // Simulate loading config from file (can fail)
|
||||
// return effect.Of[AppConfig, DatabaseConfig](DatabaseConfig{
|
||||
// ConnectionString: "loaded from " + app.ConfigPath,
|
||||
// })
|
||||
// })(effect.Of[AppConfig, AppConfig](app))
|
||||
// }
|
||||
//
|
||||
// // Apply the transformation
|
||||
// transform := effect.LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
|
||||
// appEffect := transform(dbEffect)
|
||||
//
|
||||
// // Run with AppConfig
|
||||
// ioResult := effect.Provide(AppConfig{ConfigPath: "/etc/app.conf"})(appEffect)
|
||||
// readerResult := effect.RunSync(ioResult)
|
||||
// result, err := readerResult(context.Background())
|
||||
//
|
||||
// Comparison with other Local functions:
|
||||
// - Local/Contramap: Pure context transformation (C2 -> C1)
|
||||
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
|
||||
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
|
||||
// - LocalReaderIOResultK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
|
||||
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
|
||||
//
|
||||
//go:inline
|
||||
func LocalEffectK[A, C1, C2 any](f Kleisli[C2, C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalReaderReaderIOEitherK[A](f)
|
||||
}
|
||||
620
v2/effect/dependencies_test.go
Normal file
620
v2/effect/dependencies_test.go
Normal file
@@ -0,0 +1,620 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type OuterContext struct {
|
||||
Value string
|
||||
Number int
|
||||
}
|
||||
|
||||
type InnerContext struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
t.Run("transforms context for inner effect", func(t *testing.T) {
|
||||
// Create an effect that uses InnerContext
|
||||
innerEffect := Of[InnerContext, string]("result")
|
||||
|
||||
// Transform OuterContext to InnerContext
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value}
|
||||
}
|
||||
|
||||
// Apply Local to transform the context
|
||||
kleisli := Local[OuterContext, InnerContext, string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
// Run with OuterContext
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
|
||||
t.Run("allows accessing outer context fields", func(t *testing.T) {
|
||||
// Create an effect that reads from InnerContext
|
||||
innerEffect := Chain[InnerContext](func(_ string) Effect[InnerContext, string] {
|
||||
return Of[InnerContext, string]("inner value")
|
||||
})(Of[InnerContext, string]("start"))
|
||||
|
||||
// Transform context
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value + " transformed"}
|
||||
}
|
||||
|
||||
kleisli := Local[OuterContext, InnerContext, string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
// Run with OuterContext
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
Value: "original",
|
||||
Number: 100,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "inner value", result)
|
||||
})
|
||||
|
||||
t.Run("propagates errors from inner effect", func(t *testing.T) {
|
||||
expectedErr := assert.AnError
|
||||
innerEffect := Fail[InnerContext, string](expectedErr)
|
||||
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value}
|
||||
}
|
||||
|
||||
kleisli := Local[OuterContext, InnerContext, string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("chains multiple Local transformations", func(t *testing.T) {
|
||||
type Level1 struct {
|
||||
A string
|
||||
}
|
||||
type Level2 struct {
|
||||
B string
|
||||
}
|
||||
type Level3 struct {
|
||||
C string
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
level3Effect := Of[Level3, string]("deep result")
|
||||
|
||||
// Transform Level2 -> Level3
|
||||
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
|
||||
return Level3{C: l2.B + "-c"}
|
||||
})
|
||||
|
||||
// Transform Level1 -> Level2
|
||||
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
|
||||
return Level2{B: l1.A + "-b"}
|
||||
})
|
||||
|
||||
// Compose transformations
|
||||
level2Effect := local23(level3Effect)
|
||||
level1Effect := local12(level2Effect)
|
||||
|
||||
// Run with Level1 context
|
||||
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "deep result", result)
|
||||
})
|
||||
|
||||
t.Run("works with complex context transformations", func(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Database string
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
DB DatabaseConfig
|
||||
APIKey string
|
||||
Timeout int
|
||||
}
|
||||
|
||||
// Effect that needs only DatabaseConfig
|
||||
dbEffect := Of[DatabaseConfig, string]("connected")
|
||||
|
||||
// Extract DB config from AppConfig
|
||||
accessor := func(app AppConfig) DatabaseConfig {
|
||||
return app.DB
|
||||
}
|
||||
|
||||
kleisli := Local[AppConfig, DatabaseConfig, string](accessor)
|
||||
appEffect := kleisli(dbEffect)
|
||||
|
||||
// Run with full AppConfig
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
DB: DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Database: "mydb",
|
||||
},
|
||||
APIKey: "secret",
|
||||
Timeout: 30,
|
||||
})(appEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "connected", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContramap(t *testing.T) {
|
||||
t.Run("is equivalent to Local", func(t *testing.T) {
|
||||
innerEffect := Of[InnerContext, int](42)
|
||||
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value}
|
||||
}
|
||||
|
||||
// Test Local
|
||||
localKleisli := Local[OuterContext, InnerContext, int](accessor)
|
||||
localEffect := localKleisli(innerEffect)
|
||||
|
||||
// Test Contramap
|
||||
contramapKleisli := Contramap[OuterContext, InnerContext, int](accessor)
|
||||
contramapEffect := contramapKleisli(innerEffect)
|
||||
|
||||
outerCtx := OuterContext{Value: "test", Number: 100}
|
||||
|
||||
// Run both
|
||||
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
|
||||
localReader := RunSync[int](localIO)
|
||||
localResult, localErr := localReader(context.Background())
|
||||
|
||||
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
|
||||
contramapReader := RunSync[int](contramapIO)
|
||||
contramapResult, contramapErr := contramapReader(context.Background())
|
||||
|
||||
assert.NoError(t, localErr)
|
||||
assert.NoError(t, contramapErr)
|
||||
assert.Equal(t, localResult, contramapResult)
|
||||
})
|
||||
|
||||
t.Run("transforms context correctly", func(t *testing.T) {
|
||||
innerEffect := Of[InnerContext, string]("success")
|
||||
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value + " modified"}
|
||||
}
|
||||
|
||||
kleisli := Contramap[OuterContext, InnerContext, string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
Value: "original",
|
||||
Number: 50,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", result)
|
||||
})
|
||||
|
||||
t.Run("handles errors from inner effect", func(t *testing.T) {
|
||||
expectedErr := assert.AnError
|
||||
innerEffect := Fail[InnerContext, int](expectedErr)
|
||||
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value}
|
||||
}
|
||||
|
||||
kleisli := Contramap[OuterContext, InnerContext, int](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterContext, int](OuterContext{
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalAndContramapInteroperability(t *testing.T) {
|
||||
t.Run("can be used interchangeably", func(t *testing.T) {
|
||||
type Config1 struct {
|
||||
Value string
|
||||
}
|
||||
type Config2 struct {
|
||||
Data string
|
||||
}
|
||||
type Config3 struct {
|
||||
Info string
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
effect3 := Of[Config3, string]("result")
|
||||
|
||||
// Use Local for first transformation
|
||||
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
|
||||
return Config3{Info: c2.Data}
|
||||
})
|
||||
|
||||
// Use Contramap for second transformation
|
||||
contramap12 := Contramap[Config1, Config2, string](func(c1 Config1) Config2 {
|
||||
return Config2{Data: c1.Value}
|
||||
})
|
||||
|
||||
// Compose them
|
||||
effect2 := local23(effect3)
|
||||
effect1 := contramap12(effect2)
|
||||
|
||||
// Run
|
||||
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalEffectK(t *testing.T) {
|
||||
t.Run("transforms context using effectful function", func(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
ConfigPath string
|
||||
}
|
||||
|
||||
// Effect that needs DatabaseConfig
|
||||
dbEffect := Of[DatabaseConfig, string]("query result")
|
||||
|
||||
// Transform AppConfig to DatabaseConfig effectfully
|
||||
loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
|
||||
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
|
||||
ConnectionString: "loaded from " + app.ConfigPath,
|
||||
})
|
||||
}
|
||||
|
||||
// Apply the transformation
|
||||
transform := LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
|
||||
appEffect := transform(dbEffect)
|
||||
|
||||
// Run with AppConfig
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
ConfigPath: "/etc/app.conf",
|
||||
})(appEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "query result", result)
|
||||
})
|
||||
|
||||
t.Run("propagates errors from context transformation", func(t *testing.T) {
|
||||
type InnerCtx struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
type OuterCtx struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
innerEffect := Of[InnerCtx, string]("success")
|
||||
|
||||
expectedErr := assert.AnError
|
||||
// Context transformation that fails
|
||||
failingTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
|
||||
return Fail[OuterCtx, InnerCtx](expectedErr)
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string, InnerCtx, OuterCtx](failingTransform)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("propagates errors from inner effect", func(t *testing.T) {
|
||||
type InnerCtx struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
type OuterCtx struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
expectedErr := assert.AnError
|
||||
innerEffect := Fail[InnerCtx, string](expectedErr)
|
||||
|
||||
// Successful context transformation
|
||||
transform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
|
||||
return Of[OuterCtx, InnerCtx](InnerCtx{Value: outer.Path})
|
||||
}
|
||||
|
||||
transformK := LocalEffectK[string, InnerCtx, OuterCtx](transform)
|
||||
outerEffect := transformK(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("allows effectful context transformation with IO operations", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
type AppContext struct {
|
||||
ConfigFile string
|
||||
}
|
||||
|
||||
// Effect that uses Config
|
||||
configEffect := Chain[Config](func(cfg Config) Effect[Config, string] {
|
||||
return Of[Config, string]("processed: " + cfg.Data)
|
||||
})(readerreaderioresult.Ask[Config]())
|
||||
|
||||
// Effectful transformation that simulates loading config
|
||||
loadConfigEffect := func(app AppContext) Effect[AppContext, Config] {
|
||||
// Simulate IO operation (e.g., reading file)
|
||||
return Of[AppContext, Config](Config{
|
||||
Data: "loaded from " + app.ConfigFile,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string, Config, AppContext](loadConfigEffect)
|
||||
appEffect := transform(configEffect)
|
||||
|
||||
ioResult := Provide[AppContext, string](AppContext{
|
||||
ConfigFile: "config.json",
|
||||
})(appEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "processed: loaded from config.json", result)
|
||||
})
|
||||
|
||||
t.Run("chains multiple LocalEffectK transformations", func(t *testing.T) {
|
||||
type Level1 struct {
|
||||
A string
|
||||
}
|
||||
type Level2 struct {
|
||||
B string
|
||||
}
|
||||
type Level3 struct {
|
||||
C string
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
level3Effect := Of[Level3, string]("deep result")
|
||||
|
||||
// Transform Level2 -> Level3 effectfully
|
||||
transform23 := LocalEffectK[string, Level3, Level2](func(l2 Level2) Effect[Level2, Level3] {
|
||||
return Of[Level2, Level3](Level3{C: l2.B + "-c"})
|
||||
})
|
||||
|
||||
// Transform Level1 -> Level2 effectfully
|
||||
transform12 := LocalEffectK[string, Level2, Level1](func(l1 Level1) Effect[Level1, Level2] {
|
||||
return Of[Level1, Level2](Level2{B: l1.A + "-b"})
|
||||
})
|
||||
|
||||
// Compose transformations
|
||||
level2Effect := transform23(level3Effect)
|
||||
level1Effect := transform12(level2Effect)
|
||||
|
||||
// Run with Level1 context
|
||||
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "deep result", result)
|
||||
})
|
||||
|
||||
t.Run("accesses outer context during transformation", func(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Environment string
|
||||
DBHost string
|
||||
DBPort int
|
||||
}
|
||||
|
||||
// Effect that needs DatabaseConfig
|
||||
dbEffect := Chain[DatabaseConfig](func(cfg DatabaseConfig) Effect[DatabaseConfig, string] {
|
||||
return Of[DatabaseConfig, string](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
})(readerreaderioresult.Ask[DatabaseConfig]())
|
||||
|
||||
// Transform using outer context
|
||||
transformWithContext := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
|
||||
// Access outer context to build inner context
|
||||
prefix := ""
|
||||
if app.Environment == "prod" {
|
||||
prefix = "prod-"
|
||||
}
|
||||
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
|
||||
Host: prefix + app.DBHost,
|
||||
Port: app.DBPort,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string, DatabaseConfig, AppConfig](transformWithContext)
|
||||
appEffect := transform(dbEffect)
|
||||
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
Environment: "prod",
|
||||
DBHost: "localhost",
|
||||
DBPort: 5432,
|
||||
})(appEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, result, "prod-localhost")
|
||||
})
|
||||
|
||||
t.Run("validates context during transformation", func(t *testing.T) {
|
||||
type ValidatedConfig struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
type RawConfig struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
innerEffect := Of[ValidatedConfig, string]("success")
|
||||
|
||||
// Validation that can fail
|
||||
validateConfig := func(raw RawConfig) Effect[RawConfig, ValidatedConfig] {
|
||||
if raw.APIKey == "" {
|
||||
return Fail[RawConfig, ValidatedConfig](assert.AnError)
|
||||
}
|
||||
return Of[RawConfig, ValidatedConfig](ValidatedConfig{
|
||||
APIKey: raw.APIKey,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string, ValidatedConfig, RawConfig](validateConfig)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
// Test with invalid config
|
||||
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test with valid config
|
||||
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
|
||||
readerResult2 := RunSync[string](ioResult2)
|
||||
result, err2 := readerResult2(context.Background())
|
||||
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "success", result)
|
||||
})
|
||||
|
||||
t.Run("composes with other Local functions", func(t *testing.T) {
|
||||
type Level1 struct {
|
||||
Value string
|
||||
}
|
||||
type Level2 struct {
|
||||
Data string
|
||||
}
|
||||
type Level3 struct {
|
||||
Info string
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
effect3 := Of[Level3, string]("result")
|
||||
|
||||
// Use LocalEffectK for first transformation (effectful)
|
||||
localEffectK23 := LocalEffectK[string, Level3, Level2](func(l2 Level2) Effect[Level2, Level3] {
|
||||
return Of[Level2, Level3](Level3{Info: l2.Data})
|
||||
})
|
||||
|
||||
// Use Local for second transformation (pure)
|
||||
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
|
||||
return Level2{Data: l1.Value}
|
||||
})
|
||||
|
||||
// Compose them
|
||||
effect2 := localEffectK23(effect3)
|
||||
effect1 := local12(effect2)
|
||||
|
||||
// Run
|
||||
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
|
||||
t.Run("handles complex nested effects in transformation", func(t *testing.T) {
|
||||
type InnerCtx struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type OuterCtx struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Effect that uses InnerCtx
|
||||
innerEffect := Chain[InnerCtx](func(ctx InnerCtx) Effect[InnerCtx, int] {
|
||||
return Of[InnerCtx, int](ctx.Value * 2)
|
||||
})(readerreaderioresult.Ask[InnerCtx]())
|
||||
|
||||
// Complex transformation with nested effects
|
||||
complexTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
|
||||
return Of[OuterCtx, InnerCtx](InnerCtx{
|
||||
Value: outer.Multiplier * 10,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[int, InnerCtx, OuterCtx](complexTransform)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 60, result) // 3 * 10 * 2
|
||||
})
|
||||
}
|
||||
222
v2/effect/doc.go
Normal file
222
v2/effect/doc.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
Package effect provides a functional effect system for managing side effects in Go.
|
||||
|
||||
# Overview
|
||||
|
||||
The effect package is a high-level abstraction for composing effectful computations
|
||||
that may fail, require dependencies (context), and perform I/O operations. It is built
|
||||
on top of ReaderReaderIOResult, providing a clean API for dependency injection and
|
||||
error handling.
|
||||
|
||||
# Naming Conventions
|
||||
|
||||
The naming conventions in this package are modeled after effect-ts (https://effect.website/),
|
||||
a popular TypeScript library for functional effect systems. This alignment helps developers
|
||||
familiar with effect-ts to quickly understand and use this Go implementation.
|
||||
|
||||
# Core Type
|
||||
|
||||
The central type is Effect[C, A], which represents:
|
||||
- C: The context/dependency type required by the effect
|
||||
- A: The success value type produced by the effect
|
||||
|
||||
An Effect can:
|
||||
- Succeed with a value of type A
|
||||
- Fail with an error
|
||||
- Require a context of type C
|
||||
- Perform I/O operations
|
||||
|
||||
# Basic Operations
|
||||
|
||||
Creating Effects:
|
||||
|
||||
// Create a successful effect
|
||||
effect.Succeed[MyContext, string]("hello")
|
||||
|
||||
// Create a failed effect
|
||||
effect.Fail[MyContext, string](errors.New("failed"))
|
||||
|
||||
// Lift a pure value into an effect
|
||||
effect.Of[MyContext, int](42)
|
||||
|
||||
Transforming Effects:
|
||||
|
||||
// Map over the success value
|
||||
effect.Map[MyContext](func(x int) string {
|
||||
return strconv.Itoa(x)
|
||||
})
|
||||
|
||||
// Chain effects together (flatMap)
|
||||
effect.Chain[MyContext](func(x int) Effect[MyContext, string] {
|
||||
return effect.Succeed[MyContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
// Tap into an effect without changing its value
|
||||
effect.Tap[MyContext](func(x int) Effect[MyContext, any] {
|
||||
return effect.Succeed[MyContext, any](fmt.Println(x))
|
||||
})
|
||||
|
||||
# Dependency Injection
|
||||
|
||||
Effects can access their required context:
|
||||
|
||||
// Transform the context before passing it to an effect
|
||||
effect.Local[OuterCtx, InnerCtx](func(outer OuterCtx) InnerCtx {
|
||||
return outer.Inner
|
||||
})
|
||||
|
||||
// Provide a context to run an effect
|
||||
effect.Provide[MyContext, string](myContext)
|
||||
|
||||
# Do Notation
|
||||
|
||||
The package provides "do notation" for composing effects in a sequential, imperative style:
|
||||
|
||||
type State struct {
|
||||
X int
|
||||
Y string
|
||||
}
|
||||
|
||||
result := effect.Do[MyContext](State{}).
|
||||
Bind(func(y string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Y = y
|
||||
return s
|
||||
}
|
||||
}, fetchString).
|
||||
Let(func(x int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.X = x
|
||||
return s
|
||||
}
|
||||
}, func(s State) int {
|
||||
return len(s.Y)
|
||||
})
|
||||
|
||||
# Bind Operations
|
||||
|
||||
The package provides various bind operations for integrating with other effect types:
|
||||
|
||||
- BindIOK: Bind an IO operation
|
||||
- BindIOEitherK: Bind an IOEither operation
|
||||
- BindIOResultK: Bind an IOResult operation
|
||||
- BindReaderK: Bind a Reader operation
|
||||
- BindReaderIOK: Bind a ReaderIO operation
|
||||
- BindEitherK: Bind an Either operation
|
||||
|
||||
Each bind operation has a corresponding "L" variant for working with lenses:
|
||||
- BindL, BindIOKL, BindReaderKL, etc.
|
||||
|
||||
# Applicative Operations
|
||||
|
||||
Apply effects in parallel:
|
||||
|
||||
// Apply a function effect to a value effect
|
||||
effect.Ap[string, MyContext](valueEffect)(functionEffect)
|
||||
|
||||
// Apply effects to build up a structure
|
||||
effect.ApS[MyContext](setter, effect1)
|
||||
|
||||
# Traversal
|
||||
|
||||
Traverse collections with effects:
|
||||
|
||||
// Map an array with an effectful function
|
||||
effect.TraverseArray[MyContext](func(x int) Effect[MyContext, string] {
|
||||
return effect.Succeed[MyContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
# Retry Logic
|
||||
|
||||
Retry effects with configurable policies:
|
||||
|
||||
effect.Retrying[MyContext, string](
|
||||
retryPolicy,
|
||||
func(status retry.RetryStatus) Effect[MyContext, string] {
|
||||
return fetchData()
|
||||
},
|
||||
func(result Result[string]) bool {
|
||||
return result.IsLeft() // retry on error
|
||||
},
|
||||
)
|
||||
|
||||
# Monoids
|
||||
|
||||
Combine effects using monoid operations:
|
||||
|
||||
// Combine effects using applicative semantics
|
||||
effect.ApplicativeMonoid[MyContext](stringMonoid)
|
||||
|
||||
// Combine effects using alternative semantics (first success)
|
||||
effect.AlternativeMonoid[MyContext](stringMonoid)
|
||||
|
||||
# Running Effects
|
||||
|
||||
To execute an effect:
|
||||
|
||||
// Provide the context
|
||||
ioResult := effect.Provide[MyContext, string](myContext)(myEffect)
|
||||
|
||||
// Run synchronously
|
||||
readerResult := effect.RunSync(ioResult)
|
||||
|
||||
// Execute with a context.Context
|
||||
value, err := readerResult(ctx)
|
||||
|
||||
# Integration with Other Packages
|
||||
|
||||
The effect package integrates seamlessly with other fp-go packages:
|
||||
- either: For error handling
|
||||
- io: For I/O operations
|
||||
- reader: For dependency injection
|
||||
- result: For result types
|
||||
- retry: For retry logic
|
||||
- monoid: For combining effects
|
||||
|
||||
# Example
|
||||
|
||||
type Config struct {
|
||||
APIKey string
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
func fetchUser(id int) Effect[Config, User] {
|
||||
return effect.Chain[Config](func(cfg Config) Effect[Config, User] {
|
||||
// Use cfg.APIKey and cfg.BaseURL
|
||||
return effect.Succeed[Config, User](User{ID: id})
|
||||
})(effect.Of[Config, Config](Config{}))
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := Config{APIKey: "key", BaseURL: "https://api.example.com"}
|
||||
userEffect := fetchUser(42)
|
||||
|
||||
// Run the effect
|
||||
ioResult := effect.Provide(cfg)(userEffect)
|
||||
readerResult := effect.RunSync(ioResult)
|
||||
user, err := readerResult(context.Background())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("User: %+v\n", user)
|
||||
}
|
||||
*/
|
||||
package effect
|
||||
|
||||
//go:generate go run ../main.go lens --dir . --filename gen_lens.go --include-test-files
|
||||
51
v2/effect/effect.go
Normal file
51
v2/effect/effect.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func Succeed[C, A any](a A) Effect[C, A] {
|
||||
return readerreaderioresult.Of[C](a)
|
||||
}
|
||||
|
||||
func Fail[C, A any](err error) Effect[C, A] {
|
||||
return readerreaderioresult.Left[C, A](err)
|
||||
}
|
||||
|
||||
func Of[C, A any](a A) Effect[C, A] {
|
||||
return readerreaderioresult.Of[C](a)
|
||||
}
|
||||
|
||||
func Map[C, A, B any](f func(A) B) Operator[C, A, B] {
|
||||
return readerreaderioresult.Map[C](f)
|
||||
}
|
||||
|
||||
func Chain[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, B] {
|
||||
return readerreaderioresult.Chain(f)
|
||||
}
|
||||
|
||||
func Ap[B, C, A any](fa Effect[C, A]) Operator[C, func(A) B, B] {
|
||||
return readerreaderioresult.Ap[B](fa)
|
||||
}
|
||||
|
||||
func Suspend[C, A any](fa Lazy[Effect[C, A]]) Effect[C, A] {
|
||||
return readerreaderioresult.Defer(fa)
|
||||
}
|
||||
|
||||
func Tap[C, A, ANY any](f Kleisli[C, A, ANY]) Operator[C, A, A] {
|
||||
return readerreaderioresult.Tap(f)
|
||||
}
|
||||
|
||||
func Ternary[C, A, B any](pred Predicate[A], onTrue, onFalse Kleisli[C, A, B]) Kleisli[C, A, B] {
|
||||
return function.Ternary(pred, onTrue, onFalse)
|
||||
}
|
||||
|
||||
func ChainResultK[C, A, B any](f result.Kleisli[A, B]) Operator[C, A, B] {
|
||||
return readerreaderioresult.ChainResultK[C](f)
|
||||
}
|
||||
|
||||
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
|
||||
return readerreaderioresult.Read[A](c)
|
||||
}
|
||||
506
v2/effect/effect_test.go
Normal file
506
v2/effect/effect_test.go
Normal file
@@ -0,0 +1,506 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestContext struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func runEffect[A any](eff Effect[TestContext, A], ctx TestContext) (A, error) {
|
||||
ioResult := Provide[TestContext, A](ctx)(eff)
|
||||
readerResult := RunSync[A](ioResult)
|
||||
return readerResult(context.Background())
|
||||
}
|
||||
|
||||
func TestSucceed(t *testing.T) {
|
||||
t.Run("creates successful effect with value", func(t *testing.T) {
|
||||
eff := Succeed[TestContext, int](42)
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("creates successful effect with string", func(t *testing.T) {
|
||||
eff := Succeed[TestContext, string]("hello")
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("creates successful effect with struct", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
eff := Succeed[TestContext, User](user)
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFail(t *testing.T) {
|
||||
t.Run("creates failed effect with error", func(t *testing.T) {
|
||||
expectedErr := errors.New("test error")
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("creates failed effect with custom error", func(t *testing.T) {
|
||||
expectedErr := fmt.Errorf("custom error: %s", "details")
|
||||
eff := Fail[TestContext, string](expectedErr)
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
t.Run("lifts value into effect", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](100)
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
|
||||
t.Run("is equivalent to Succeed", func(t *testing.T) {
|
||||
value := "test value"
|
||||
eff1 := Of[TestContext, string](value)
|
||||
eff2 := Succeed[TestContext, string](value)
|
||||
|
||||
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
|
||||
result2, err2 := runEffect(eff2, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, result1, result2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("maps over successful effect", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](10)
|
||||
mapped := Map[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(mapped, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
|
||||
t.Run("maps to different type", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](42)
|
||||
mapped := Map[TestContext](func(x int) string {
|
||||
return fmt.Sprintf("value: %d", x)
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(mapped, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value: 42", result)
|
||||
})
|
||||
|
||||
t.Run("preserves error in failed effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("original error")
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
mapped := Map[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(mapped, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("chains multiple maps", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](5)
|
||||
result := Map[TestContext](func(x int) int {
|
||||
return x + 1
|
||||
})(Map[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 11, value) // (5 * 2) + 1
|
||||
})
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
t.Run("chains successful effects", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](10)
|
||||
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
|
||||
t.Run("chains to different type", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](42)
|
||||
chained := Chain[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](fmt.Sprintf("number: %d", x))
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "number: 42", result)
|
||||
})
|
||||
|
||||
t.Run("propagates first error", func(t *testing.T) {
|
||||
expectedErr := errors.New("first error")
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("propagates second error", func(t *testing.T) {
|
||||
expectedErr := errors.New("second error")
|
||||
eff := Of[TestContext, int](10)
|
||||
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Fail[TestContext, int](expectedErr)
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("chains multiple operations", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](5)
|
||||
result := Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x + 10)
|
||||
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, value) // (5 * 2) + 10
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("applies function effect to value effect", func(t *testing.T) {
|
||||
fn := Of[TestContext, func(int) int](func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
value := Of[TestContext, int](21)
|
||||
|
||||
result := Ap[int](value)(fn)
|
||||
val, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
|
||||
t.Run("applies function to different type", func(t *testing.T) {
|
||||
fn := Of[TestContext, func(int) string](func(x int) string {
|
||||
return fmt.Sprintf("value: %d", x)
|
||||
})
|
||||
value := Of[TestContext, int](42)
|
||||
|
||||
result := Ap[string](value)(fn)
|
||||
val, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value: 42", val)
|
||||
})
|
||||
|
||||
t.Run("propagates error from function effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("function error")
|
||||
fn := Fail[TestContext, func(int) int](expectedErr)
|
||||
value := Of[TestContext, int](42)
|
||||
|
||||
result := Ap[int](value)(fn)
|
||||
_, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("propagates error from value effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("value error")
|
||||
fn := Of[TestContext, func(int) int](func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
value := Fail[TestContext, int](expectedErr)
|
||||
|
||||
result := Ap[int](value)(fn)
|
||||
_, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSuspend(t *testing.T) {
|
||||
t.Run("suspends effect computation", func(t *testing.T) {
|
||||
callCount := 0
|
||||
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
|
||||
callCount++
|
||||
return Of[TestContext, int](42)
|
||||
})
|
||||
|
||||
// Effect not executed yet
|
||||
assert.Equal(t, 0, callCount)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
|
||||
t.Run("suspends failing effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("suspended error")
|
||||
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
|
||||
return Fail[TestContext, int](expectedErr)
|
||||
})
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("allows lazy evaluation", func(t *testing.T) {
|
||||
var value int
|
||||
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
|
||||
return Of[TestContext, int](value)
|
||||
})
|
||||
|
||||
value = 10
|
||||
result1, err1 := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
value = 20
|
||||
result2, err2 := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, 10, result1)
|
||||
assert.Equal(t, 20, result2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTap(t *testing.T) {
|
||||
t.Run("executes side effect without changing value", func(t *testing.T) {
|
||||
sideEffectValue := 0
|
||||
eff := Of[TestContext, int](42)
|
||||
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
sideEffectValue = x * 2
|
||||
return Of[TestContext, any](nil)
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(tapped, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
assert.Equal(t, 84, sideEffectValue)
|
||||
})
|
||||
|
||||
t.Run("propagates original error", func(t *testing.T) {
|
||||
expectedErr := errors.New("original error")
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
return Of[TestContext, any](nil)
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(tapped, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("propagates tap error", func(t *testing.T) {
|
||||
expectedErr := errors.New("tap error")
|
||||
eff := Of[TestContext, int](42)
|
||||
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
return Fail[TestContext, any](expectedErr)
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(tapped, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("chains multiple taps", func(t *testing.T) {
|
||||
values := []int{}
|
||||
eff := Of[TestContext, int](10)
|
||||
result := Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
values = append(values, x+2)
|
||||
return Of[TestContext, any](nil)
|
||||
})(Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
values = append(values, x+1)
|
||||
return Of[TestContext, any](nil)
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, value)
|
||||
assert.Equal(t, []int{11, 12}, values)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTernary(t *testing.T) {
|
||||
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
|
||||
kleisli := Ternary[TestContext, int, string](
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("greater")
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("less or equal")
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(kleisli(15), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "greater", result)
|
||||
})
|
||||
|
||||
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
|
||||
kleisli := Ternary[TestContext, int, string](
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("greater")
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("less or equal")
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(kleisli(5), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "less or equal", result)
|
||||
})
|
||||
|
||||
t.Run("handles errors in onTrue branch", func(t *testing.T) {
|
||||
expectedErr := errors.New("true branch error")
|
||||
kleisli := Ternary[TestContext, int, string](
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("less or equal")
|
||||
},
|
||||
)
|
||||
|
||||
_, err := runEffect(kleisli(15), TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("handles errors in onFalse branch", func(t *testing.T) {
|
||||
expectedErr := errors.New("false branch error")
|
||||
kleisli := Ternary[TestContext, int, string](
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("greater")
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
},
|
||||
)
|
||||
|
||||
_, err := runEffect(kleisli(5), TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEffectComposition(t *testing.T) {
|
||||
t.Run("composes Map and Chain", func(t *testing.T) {
|
||||
eff := Of[TestContext, int](5)
|
||||
result := Chain[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](fmt.Sprintf("result: %d", x))
|
||||
})(Map[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result: 10", value)
|
||||
})
|
||||
|
||||
t.Run("composes Chain and Tap", func(t *testing.T) {
|
||||
sideEffect := 0
|
||||
eff := Of[TestContext, int](10)
|
||||
result := Tap[TestContext](func(x int) Effect[TestContext, any] {
|
||||
sideEffect = x
|
||||
return Of[TestContext, any](nil)
|
||||
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, value)
|
||||
assert.Equal(t, 20, sideEffect)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEffectWithResult(t *testing.T) {
|
||||
t.Run("converts result to effect", func(t *testing.T) {
|
||||
res := result.Of[int](42)
|
||||
// This demonstrates integration with result package
|
||||
assert.True(t, result.IsRight(res))
|
||||
})
|
||||
}
|
||||
118
v2/effect/gen_lens_test.go
Normal file
118
v2/effect/gen_lens_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package effect
|
||||
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at
|
||||
// 2026-01-27 22:19:41.6840253 +0100 CET m=+0.008579701
|
||||
|
||||
import (
|
||||
__lens "github.com/IBM/fp-go/v2/optics/lens"
|
||||
__option "github.com/IBM/fp-go/v2/option"
|
||||
__prism "github.com/IBM/fp-go/v2/optics/prism"
|
||||
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
|
||||
)
|
||||
|
||||
// ComplexServiceLenses provides lenses for accessing fields of ComplexService
|
||||
type ComplexServiceLenses struct {
|
||||
// mandatory fields
|
||||
service1 __lens.Lens[ComplexService, Service1]
|
||||
service2 __lens.Lens[ComplexService, Service2]
|
||||
// optional fields
|
||||
service1O __lens_option.LensO[ComplexService, Service1]
|
||||
service2O __lens_option.LensO[ComplexService, Service2]
|
||||
}
|
||||
|
||||
// ComplexServiceRefLenses provides lenses for accessing fields of ComplexService via a reference to ComplexService
|
||||
type ComplexServiceRefLenses struct {
|
||||
// mandatory fields
|
||||
service1 __lens.Lens[*ComplexService, Service1]
|
||||
service2 __lens.Lens[*ComplexService, Service2]
|
||||
// optional fields
|
||||
service1O __lens_option.LensO[*ComplexService, Service1]
|
||||
service2O __lens_option.LensO[*ComplexService, Service2]
|
||||
// prisms
|
||||
service1P __prism.Prism[*ComplexService, Service1]
|
||||
service2P __prism.Prism[*ComplexService, Service2]
|
||||
}
|
||||
|
||||
// ComplexServicePrisms provides prisms for accessing fields of ComplexService
|
||||
type ComplexServicePrisms struct {
|
||||
service1 __prism.Prism[ComplexService, Service1]
|
||||
service2 __prism.Prism[ComplexService, Service2]
|
||||
}
|
||||
|
||||
// MakeComplexServiceLenses creates a new ComplexServiceLenses with lenses for all fields
|
||||
func MakeComplexServiceLenses() ComplexServiceLenses {
|
||||
// mandatory lenses
|
||||
lensservice1 := __lens.MakeLensWithName(
|
||||
func(s ComplexService) Service1 { return s.service1 },
|
||||
func(s ComplexService, v Service1) ComplexService { s.service1 = v; return s },
|
||||
"ComplexService.service1",
|
||||
)
|
||||
lensservice2 := __lens.MakeLensWithName(
|
||||
func(s ComplexService) Service2 { return s.service2 },
|
||||
func(s ComplexService, v Service2) ComplexService { s.service2 = v; return s },
|
||||
"ComplexService.service2",
|
||||
)
|
||||
// optional lenses
|
||||
lensservice1O := __lens_option.FromIso[ComplexService](__iso_option.FromZero[Service1]())(lensservice1)
|
||||
lensservice2O := __lens_option.FromIso[ComplexService](__iso_option.FromZero[Service2]())(lensservice2)
|
||||
return ComplexServiceLenses{
|
||||
// mandatory lenses
|
||||
service1: lensservice1,
|
||||
service2: lensservice2,
|
||||
// optional lenses
|
||||
service1O: lensservice1O,
|
||||
service2O: lensservice2O,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeComplexServiceRefLenses creates a new ComplexServiceRefLenses with lenses for all fields
|
||||
func MakeComplexServiceRefLenses() ComplexServiceRefLenses {
|
||||
// mandatory lenses
|
||||
lensservice1 := __lens.MakeLensStrictWithName(
|
||||
func(s *ComplexService) Service1 { return s.service1 },
|
||||
func(s *ComplexService, v Service1) *ComplexService { s.service1 = v; return s },
|
||||
"(*ComplexService).service1",
|
||||
)
|
||||
lensservice2 := __lens.MakeLensStrictWithName(
|
||||
func(s *ComplexService) Service2 { return s.service2 },
|
||||
func(s *ComplexService, v Service2) *ComplexService { s.service2 = v; return s },
|
||||
"(*ComplexService).service2",
|
||||
)
|
||||
// optional lenses
|
||||
lensservice1O := __lens_option.FromIso[*ComplexService](__iso_option.FromZero[Service1]())(lensservice1)
|
||||
lensservice2O := __lens_option.FromIso[*ComplexService](__iso_option.FromZero[Service2]())(lensservice2)
|
||||
return ComplexServiceRefLenses{
|
||||
// mandatory lenses
|
||||
service1: lensservice1,
|
||||
service2: lensservice2,
|
||||
// optional lenses
|
||||
service1O: lensservice1O,
|
||||
service2O: lensservice2O,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeComplexServicePrisms creates a new ComplexServicePrisms with prisms for all fields
|
||||
func MakeComplexServicePrisms() ComplexServicePrisms {
|
||||
_fromNonZeroservice1 := __option.FromNonZero[Service1]()
|
||||
_prismservice1 := __prism.MakePrismWithName(
|
||||
func(s ComplexService) __option.Option[Service1] { return _fromNonZeroservice1(s.service1) },
|
||||
func(v Service1) ComplexService {
|
||||
return ComplexService{ service1: v }
|
||||
},
|
||||
"ComplexService.service1",
|
||||
)
|
||||
_fromNonZeroservice2 := __option.FromNonZero[Service2]()
|
||||
_prismservice2 := __prism.MakePrismWithName(
|
||||
func(s ComplexService) __option.Option[Service2] { return _fromNonZeroservice2(s.service2) },
|
||||
func(v Service2) ComplexService {
|
||||
return ComplexService{ service2: v }
|
||||
},
|
||||
"ComplexService.service2",
|
||||
)
|
||||
return ComplexServicePrisms {
|
||||
service1: _prismservice1,
|
||||
service2: _prismservice2,
|
||||
}
|
||||
}
|
||||
207
v2/effect/injection_test.go
Normal file
207
v2/effect/injection_test.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// Package effect demonstrates dependency injection using the Effect pattern.
|
||||
//
|
||||
// This test file shows how to build a type-safe dependency injection system where:
|
||||
// - An InjectionContainer can resolve services by ID (InjectionToken)
|
||||
// - Services are generic effects that depend on the container
|
||||
// - Lookup methods convert from untyped container to typed dependencies
|
||||
// - Handler functions depend type-safely on specific service interfaces
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
// InjectionToken is a unique identifier for services in the container
|
||||
InjectionToken string
|
||||
|
||||
// InjectionContainer is an Effect that resolves services by their token.
|
||||
// It takes an InjectionToken and returns a Thunk that produces any type.
|
||||
// This allows the container to store and retrieve services of different types.
|
||||
InjectionContainer = Effect[InjectionToken, any]
|
||||
|
||||
// Service is a generic Effect that depends on the InjectionContainer.
|
||||
// It represents a computation that needs access to the dependency injection
|
||||
// container to resolve its dependencies before producing a string result.
|
||||
Service[T any] = Effect[InjectionContainer, T]
|
||||
|
||||
// Service1 is an example service interface that can be resolved from the container
|
||||
Service1 interface {
|
||||
GetService1() string
|
||||
}
|
||||
|
||||
// Service2 is another example service interface
|
||||
Service2 interface {
|
||||
GetService2() string
|
||||
}
|
||||
|
||||
// impl1 is a concrete implementation of Service1
|
||||
impl1 struct{}
|
||||
// impl2 is a concrete implementation of Service2
|
||||
impl2 struct{}
|
||||
)
|
||||
|
||||
// ComplexService demonstrates a more complex dependency injection scenario
|
||||
// where a service depends on multiple other services. This struct aggregates
|
||||
// Service1 and Service2, showing how to compose dependencies.
|
||||
// The fp-go:Lens directive generates lens functions for type-safe field access.
|
||||
//
|
||||
// fp-go:Lens
|
||||
type ComplexService struct {
|
||||
service1 Service1
|
||||
service2 Service2
|
||||
}
|
||||
|
||||
func (_ *impl1) GetService1() string {
|
||||
return "service1"
|
||||
}
|
||||
|
||||
func (_ *impl2) GetService2() string {
|
||||
return "service2"
|
||||
}
|
||||
|
||||
const (
|
||||
// service1 is the injection token for Service1
|
||||
service1 = InjectionToken("service1")
|
||||
// service2 is the injection token for Service2
|
||||
service2 = InjectionToken("service2")
|
||||
)
|
||||
|
||||
var (
|
||||
// complexServiceLenses provides type-safe accessors for ComplexService fields,
|
||||
// generated by the fp-go:Lens directive. These lenses are used in applicative
|
||||
// composition to build the ComplexService from individual dependencies.
|
||||
complexServiceLenses = MakeComplexServiceLenses()
|
||||
)
|
||||
|
||||
// makeSampleInjectionContainer creates an InjectionContainer that can resolve services by ID.
|
||||
// The container maps InjectionTokens to their corresponding service implementations.
|
||||
// It returns an error if a requested service is not available.
|
||||
func makeSampleInjectionContainer() InjectionContainer {
|
||||
|
||||
return func(token InjectionToken) Thunk[any] {
|
||||
switch token {
|
||||
case service1:
|
||||
return readerioresult.Of(any(&impl1{}))
|
||||
case service2:
|
||||
return readerioresult.Of(any(&impl2{}))
|
||||
default:
|
||||
return readerioresult.Left[any](errors.New("dependency not available"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleService1 is an Effect that depends type-safely on Service1.
|
||||
// It demonstrates how to write handlers that work with specific service interfaces
|
||||
// rather than the untyped container, providing compile-time type safety.
|
||||
func handleService1() Effect[Service1, string] {
|
||||
return func(ctx Service1) ReaderIOResult[string] {
|
||||
return readerioresult.Of(fmt.Sprintf("Service1: %s", ctx.GetService1()))
|
||||
}
|
||||
}
|
||||
|
||||
// handleComplexService is an Effect that depends on ComplexService, which itself
|
||||
// aggregates multiple service dependencies (Service1 and Service2).
|
||||
// This demonstrates how to work with composite dependencies in a type-safe manner.
|
||||
func handleComplexService() Effect[ComplexService, string] {
|
||||
return func(ctx ComplexService) ReaderIOResult[string] {
|
||||
return readerioresult.Of(fmt.Sprintf("ComplexService: %s x %s", ctx.service1.GetService1(), ctx.service2.GetService2()))
|
||||
}
|
||||
}
|
||||
|
||||
// lookupService1 is a lookup method that converts from an untyped InjectionContainer
|
||||
// to a typed Service1 dependency. It performs two steps:
|
||||
// 1. Read[any](service1) - retrieves the service from the container by token
|
||||
// 2. ChainResultK(result.InstanceOf[Service1]) - safely casts from any to Service1
|
||||
// This conversion provides type safety when moving from the untyped container to typed handlers.
|
||||
var lookupService1 = F.Flow2(
|
||||
Read[any](service1),
|
||||
readerioresult.ChainResultK(result.InstanceOf[Service1]),
|
||||
)
|
||||
|
||||
// lookupService2 is a lookup method for Service2, following the same pattern as lookupService1.
|
||||
// It retrieves Service2 from the container and safely casts it to the correct type.
|
||||
var lookupService2 = F.Flow2(
|
||||
Read[any](service2),
|
||||
readerioresult.ChainResultK(result.InstanceOf[Service2]),
|
||||
)
|
||||
|
||||
// lookupComplexService demonstrates applicative composition for complex dependency injection.
|
||||
// It builds a ComplexService by composing multiple service lookups:
|
||||
// 1. Do[InjectionContainer](ComplexService{}) - starts with an empty ComplexService in the Effect context
|
||||
// 2. ApSL(complexServiceLenses.service1, lookupService1) - looks up Service1 and sets it using the lens
|
||||
// 3. ApSL(complexServiceLenses.service2, lookupService2) - looks up Service2 and sets it using the lens
|
||||
//
|
||||
// This applicative style allows parallel composition of independent dependencies,
|
||||
// building the complete ComplexService from its constituent parts in a type-safe way.
|
||||
var lookupComplexService = F.Pipe2(
|
||||
Do[InjectionContainer](ComplexService{}),
|
||||
ApSL(complexServiceLenses.service1, lookupService1),
|
||||
ApSL(complexServiceLenses.service2, lookupService2),
|
||||
)
|
||||
|
||||
// handleResult is a curried function that combines results from two services.
|
||||
// It demonstrates how to compose the outputs of multiple effects into a final result.
|
||||
// The curried form allows it to be used with applicative composition (ApS).
|
||||
func handleResult(s1 string) func(string) string {
|
||||
return func(s2 string) string {
|
||||
return fmt.Sprintf("Final Result: %s : %s", s1, s2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDependencyLookup demonstrates both simple and complex dependency injection patterns:
|
||||
//
|
||||
// Simple Pattern (handle1):
|
||||
// 1. Create an InjectionContainer with registered services
|
||||
// 2. Define a handler (handleService1) that depends on a single typed service interface
|
||||
// 3. Use a lookup method (lookupService1) to resolve the dependency from the container
|
||||
// 4. Compose the handler with the lookup using LocalThunkK to inject the dependency
|
||||
//
|
||||
// Complex Pattern (handleComplex):
|
||||
// 1. Define a handler (handleComplexService) that depends on a composite service (ComplexService)
|
||||
// 2. Use applicative composition (lookupComplexService) to build the composite from multiple lookups
|
||||
// 3. Each sub-dependency is resolved independently and combined using lenses
|
||||
// 4. LocalThunkK injects the complete composite dependency into the handler
|
||||
//
|
||||
// Service Composition:
|
||||
// - ApS combines the results of handle1 and handleComplex using handleResult
|
||||
// - This demonstrates how to compose multiple independent effects that share the same container
|
||||
// - The final result aggregates outputs from both simple and complex dependency patterns
|
||||
func TestDependencyLookup(t *testing.T) {
|
||||
|
||||
// Create the dependency injection container
|
||||
container := makeSampleInjectionContainer()
|
||||
|
||||
// Simple dependency injection: single service lookup
|
||||
// LocalThunkK transforms the handler to work with the container
|
||||
handle1 := F.Pipe1(
|
||||
handleService1(),
|
||||
LocalThunkK[string](lookupService1),
|
||||
)
|
||||
|
||||
// Complex dependency injection: composite service with multiple dependencies
|
||||
// lookupComplexService uses applicative composition to build ComplexService
|
||||
handleComplex := F.Pipe1(
|
||||
handleComplexService(),
|
||||
LocalThunkK[string](lookupComplexService),
|
||||
)
|
||||
|
||||
// Compose both services using applicative style
|
||||
// ApS applies handleResult to combine outputs from handle1 and handleComplex
|
||||
result := F.Pipe1(
|
||||
handle1,
|
||||
ApS(handleResult, handleComplex),
|
||||
)
|
||||
|
||||
// Execute: provide container, then context, then run the IO operation
|
||||
res := result(container)(t.Context())()
|
||||
|
||||
fmt.Println(res)
|
||||
|
||||
}
|
||||
14
v2/effect/monoid.go
Normal file
14
v2/effect/monoid.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
func ApplicativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
|
||||
return readerreaderioresult.ApplicativeMonoid[C](m)
|
||||
}
|
||||
|
||||
func AlternativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
|
||||
return readerreaderioresult.AlternativeMonoid[C](m)
|
||||
}
|
||||
350
v2/effect/monoid_test.go
Normal file
350
v2/effect/monoid_test.go
Normal file
@@ -0,0 +1,350 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
t.Run("combines successful effects with string monoid", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext, string]("Hello")
|
||||
eff2 := Of[TestContext, string](" ")
|
||||
eff3 := Of[TestContext, string]("World")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello World", result)
|
||||
})
|
||||
|
||||
t.Run("combines successful effects with int monoid", func(t *testing.T) {
|
||||
intMonoid := monoid.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
|
||||
|
||||
eff1 := Of[TestContext, int](10)
|
||||
eff2 := Of[TestContext, int](20)
|
||||
eff3 := Of[TestContext, int](30)
|
||||
|
||||
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 60, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty value for empty monoid", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"empty",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
|
||||
|
||||
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "empty", result)
|
||||
})
|
||||
|
||||
t.Run("propagates first error", func(t *testing.T) {
|
||||
expectedErr := errors.New("first error")
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, string](expectedErr)
|
||||
eff2 := Of[TestContext, string]("World")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
_, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("propagates second error", func(t *testing.T) {
|
||||
expectedErr := errors.New("second error")
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext, string]("Hello")
|
||||
eff2 := Fail[TestContext, string](expectedErr)
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
_, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("combines multiple effects", func(t *testing.T) {
|
||||
intMonoid := monoid.MakeMonoid(
|
||||
func(a, b int) int { return a * b },
|
||||
1,
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
|
||||
|
||||
effects := []Effect[TestContext, int]{
|
||||
Of[TestContext, int](2),
|
||||
Of[TestContext, int](3),
|
||||
Of[TestContext, int](4),
|
||||
Of[TestContext, int](5),
|
||||
}
|
||||
|
||||
combined := effectMonoid.Empty()
|
||||
for _, eff := range effects {
|
||||
combined = effectMonoid.Concat(combined, eff)
|
||||
}
|
||||
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 120, result) // 1 * 2 * 3 * 4 * 5
|
||||
})
|
||||
|
||||
t.Run("works with custom types", func(t *testing.T) {
|
||||
type Counter struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
counterMonoid := monoid.MakeMonoid(
|
||||
func(a, b Counter) Counter {
|
||||
return Counter{Count: a.Count + b.Count}
|
||||
},
|
||||
Counter{Count: 0},
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext, Counter](counterMonoid)
|
||||
|
||||
eff1 := Of[TestContext, Counter](Counter{Count: 5})
|
||||
eff2 := Of[TestContext, Counter](Counter{Count: 10})
|
||||
eff3 := Of[TestContext, Counter](Counter{Count: 15})
|
||||
|
||||
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result.Count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
t.Run("combines successful effects with monoid", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext, string]("First")
|
||||
eff2 := Of[TestContext, string]("Second")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "FirstSecond", result) // Alternative still combines when both succeed
|
||||
})
|
||||
|
||||
t.Run("tries second effect if first fails", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, string](errors.New("first failed"))
|
||||
eff2 := Of[TestContext, string]("Second")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Second", result)
|
||||
})
|
||||
|
||||
t.Run("returns error if all effects fail", func(t *testing.T) {
|
||||
expectedErr := errors.New("second error")
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, string](errors.New("first error"))
|
||||
eff2 := Fail[TestContext, string](expectedErr)
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
_, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("returns empty value for empty monoid", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"default",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
|
||||
|
||||
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "default", result)
|
||||
})
|
||||
|
||||
t.Run("chains multiple alternatives", func(t *testing.T) {
|
||||
intMonoid := monoid.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, int](intMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, int](errors.New("error 1"))
|
||||
eff2 := Fail[TestContext, int](errors.New("error 2"))
|
||||
eff3 := Of[TestContext, int](42)
|
||||
eff4 := Of[TestContext, int](100)
|
||||
|
||||
combined := effectMonoid.Concat(
|
||||
effectMonoid.Concat(eff1, eff2),
|
||||
effectMonoid.Concat(eff3, eff4),
|
||||
)
|
||||
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 142, result) // Combines successful values: 42 + 100
|
||||
})
|
||||
|
||||
t.Run("works with custom types", func(t *testing.T) {
|
||||
type Result struct {
|
||||
Value string
|
||||
Code int
|
||||
}
|
||||
|
||||
resultMonoid := monoid.MakeMonoid(
|
||||
func(a, b Result) Result {
|
||||
return Result{Value: a.Value + b.Value, Code: a.Code + b.Code}
|
||||
},
|
||||
Result{Value: "", Code: 0},
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext, Result](resultMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, Result](errors.New("failed"))
|
||||
eff2 := Of[TestContext, Result](Result{Value: "success", Code: 200})
|
||||
eff3 := Of[TestContext, Result](Result{Value: "backup", Code: 201})
|
||||
|
||||
combined := effectMonoid.Concat(effectMonoid.Concat(eff1, eff2), eff3)
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "successbackup", result.Value) // Combines both successful values
|
||||
assert.Equal(t, 401, result.Code) // 200 + 201
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonoidComparison(t *testing.T) {
|
||||
t.Run("ApplicativeMonoid vs AlternativeMonoid with all success", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + "," + b },
|
||||
"",
|
||||
)
|
||||
|
||||
applicativeMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
|
||||
alternativeMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext, string]("A")
|
||||
eff2 := Of[TestContext, string]("B")
|
||||
|
||||
// Applicative combines values
|
||||
applicativeResult, err1 := runEffect(
|
||||
applicativeMonoid.Concat(eff1, eff2),
|
||||
TestContext{Value: "test"},
|
||||
)
|
||||
|
||||
// Alternative takes first
|
||||
alternativeResult, err2 := runEffect(
|
||||
alternativeMonoid.Concat(eff1, eff2),
|
||||
TestContext{Value: "test"},
|
||||
)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "A,B", applicativeResult) // Combined with comma separator
|
||||
assert.Equal(t, "A,B", alternativeResult) // Also combined (Alternative uses Alt semigroup)
|
||||
})
|
||||
|
||||
t.Run("ApplicativeMonoid vs AlternativeMonoid with failures", func(t *testing.T) {
|
||||
intMonoid := monoid.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
|
||||
applicativeMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
|
||||
alternativeMonoid := AlternativeMonoid[TestContext, int](intMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, int](errors.New("error 1"))
|
||||
eff2 := Of[TestContext, int](42)
|
||||
|
||||
// Applicative fails on first error
|
||||
_, err1 := runEffect(
|
||||
applicativeMonoid.Concat(eff1, eff2),
|
||||
TestContext{Value: "test"},
|
||||
)
|
||||
|
||||
// Alternative tries second on first failure
|
||||
result2, err2 := runEffect(
|
||||
alternativeMonoid.Concat(eff1, eff2),
|
||||
TestContext{Value: "test"},
|
||||
)
|
||||
|
||||
assert.Error(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, 42, result2)
|
||||
})
|
||||
}
|
||||
14
v2/effect/retry.go
Normal file
14
v2/effect/retry.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
)
|
||||
|
||||
func Retrying[C, A any](
|
||||
policy retry.RetryPolicy,
|
||||
action Kleisli[C, retry.RetryStatus, A],
|
||||
check Predicate[Result[A]],
|
||||
) Effect[C, A] {
|
||||
return readerreaderioresult.Retrying(policy, action, check)
|
||||
}
|
||||
377
v2/effect/retry_test.go
Normal file
377
v2/effect/retry_test.go
Normal file
@@ -0,0 +1,377 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRetrying(t *testing.T) {
|
||||
t.Run("succeeds on first attempt", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
return Of[TestContext, string]("success")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", result)
|
||||
assert.Equal(t, 1, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("retries on failure and eventually succeeds", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
if attemptCount < 3 {
|
||||
return Fail[TestContext, string](errors.New("temporary error"))
|
||||
}
|
||||
return Of[TestContext, string]("success after retries")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success after retries", result)
|
||||
assert.Equal(t, 3, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("exhausts retry limit", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
maxRetries := uint(3)
|
||||
policy := retry.LimitRetries(maxRetries)
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
return Fail[TestContext, string](errors.New("persistent error"))
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
},
|
||||
)
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, int(maxRetries+1), attemptCount) // initial attempt + retries
|
||||
})
|
||||
|
||||
t.Run("does not retry on success", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
eff := Retrying[TestContext, int](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, int] {
|
||||
attemptCount++
|
||||
return Of[TestContext, int](42)
|
||||
},
|
||||
func(res Result[int]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
assert.Equal(t, 1, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("uses custom retry predicate", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
eff := Retrying[TestContext, int](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, int] {
|
||||
attemptCount++
|
||||
return Of[TestContext, int](attemptCount * 10)
|
||||
},
|
||||
func(res Result[int]) bool {
|
||||
// Retry if value is less than 30
|
||||
if result.IsRight(res) {
|
||||
val, _ := result.Unwrap(res)
|
||||
return val < 30
|
||||
}
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result)
|
||||
assert.Equal(t, 3, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("tracks retry status", func(t *testing.T) {
|
||||
var statuses []retry.RetryStatus
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
statuses = append(statuses, status)
|
||||
if len(statuses) < 3 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, string]("done")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "done", result)
|
||||
assert.Len(t, statuses, 3)
|
||||
// First attempt has iteration 0
|
||||
assert.Equal(t, uint(0), statuses[0].IterNumber)
|
||||
assert.Equal(t, uint(1), statuses[1].IterNumber)
|
||||
assert.Equal(t, uint(2), statuses[2].IterNumber)
|
||||
})
|
||||
|
||||
t.Run("works with exponential backoff", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.Monoid.Concat(
|
||||
retry.LimitRetries(3),
|
||||
retry.ExponentialBackoff(10*time.Millisecond),
|
||||
)
|
||||
|
||||
startTime := time.Now()
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
if attemptCount < 3 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, string]("success")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
elapsed := time.Since(startTime)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", result)
|
||||
assert.Equal(t, 3, attemptCount)
|
||||
// Should have some delay due to backoff
|
||||
assert.Greater(t, elapsed, 10*time.Millisecond)
|
||||
})
|
||||
|
||||
t.Run("combines with other effect operations", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
eff := Map[TestContext](func(s string) string {
|
||||
return "mapped: " + s
|
||||
})(Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
if attemptCount < 2 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, string]("success")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "mapped: success", result)
|
||||
assert.Equal(t, 2, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("retries with different error types", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
errors := []error{
|
||||
errors.New("error 1"),
|
||||
errors.New("error 2"),
|
||||
errors.New("error 3"),
|
||||
}
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
if attemptCount < len(errors) {
|
||||
err := errors[attemptCount]
|
||||
attemptCount++
|
||||
return Fail[TestContext, string](err)
|
||||
}
|
||||
attemptCount++
|
||||
return Of[TestContext, string]("finally succeeded")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "finally succeeded", result)
|
||||
assert.Equal(t, 4, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("no retry when predicate returns false", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
return Fail[TestContext, string](errors.New("error"))
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return false // never retry
|
||||
},
|
||||
)
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, attemptCount) // only initial attempt
|
||||
})
|
||||
|
||||
t.Run("retries with context access", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(3)
|
||||
ctx := TestContext{Value: "retry-context"}
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
if attemptCount < 2 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, string]("success with context")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success with context", result)
|
||||
assert.Equal(t, 2, attemptCount)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRetryingWithComplexScenarios(t *testing.T) {
|
||||
t.Run("retry with state accumulation", func(t *testing.T) {
|
||||
type State struct {
|
||||
Attempts []int
|
||||
Value string
|
||||
}
|
||||
|
||||
policy := retry.LimitRetries(4)
|
||||
|
||||
eff := Retrying[TestContext, State](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, State] {
|
||||
state := State{
|
||||
Attempts: make([]int, status.IterNumber+1),
|
||||
Value: "attempt",
|
||||
}
|
||||
for i := uint(0); i <= status.IterNumber; i++ {
|
||||
state.Attempts[i] = int(i)
|
||||
}
|
||||
|
||||
if status.IterNumber < 2 {
|
||||
return Fail[TestContext, State](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, State](state)
|
||||
},
|
||||
func(res Result[State]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "attempt", result.Value)
|
||||
assert.Equal(t, []int{0, 1, 2}, result.Attempts)
|
||||
})
|
||||
|
||||
t.Run("retry with chain operations", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
eff := Chain[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("final: " + string(rune('0'+x)))
|
||||
})(Retrying[TestContext, int](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, int] {
|
||||
attemptCount++
|
||||
if attemptCount < 2 {
|
||||
return Fail[TestContext, int](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext, int](attemptCount)
|
||||
},
|
||||
func(res Result[int]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, result, "final:")
|
||||
})
|
||||
}
|
||||
19
v2/effect/run.go
Normal file
19
v2/effect/run.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func Provide[C, A any](c C) func(Effect[C, A]) ReaderIOResult[A] {
|
||||
return readerreaderioresult.Read[A](c)
|
||||
}
|
||||
|
||||
func RunSync[A any](fa ReaderIOResult[A]) readerresult.ReaderResult[A] {
|
||||
return func(ctx context.Context) (A, error) {
|
||||
return result.Unwrap(fa(ctx)())
|
||||
}
|
||||
}
|
||||
326
v2/effect/run_test.go
Normal file
326
v2/effect/run_test.go
Normal file
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProvide(t *testing.T) {
|
||||
t.Run("provides context to effect", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test-value"}
|
||||
eff := Of[TestContext, string]("result")
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
|
||||
t.Run("provides context with specific values", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
cfg := Config{Host: "localhost", Port: 8080}
|
||||
eff := Of[Config, string]("connected")
|
||||
|
||||
ioResult := Provide[Config, string](cfg)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "connected", result)
|
||||
})
|
||||
|
||||
t.Run("propagates errors", func(t *testing.T) {
|
||||
expectedErr := errors.New("provide error")
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Fail[TestContext, string](expectedErr)
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("works with different context types", func(t *testing.T) {
|
||||
type SimpleContext struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
ctx := SimpleContext{ID: 42}
|
||||
eff := Of[SimpleContext, int](100)
|
||||
|
||||
ioResult := Provide[SimpleContext, int](ctx)(eff)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
|
||||
t.Run("provides context to chained effects", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "base"}
|
||||
|
||||
eff := Chain[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string]("result")
|
||||
})(Of[TestContext, int](42))
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
|
||||
t.Run("provides context to mapped effects", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
|
||||
eff := Map[TestContext](func(x int) string {
|
||||
return "mapped"
|
||||
})(Of[TestContext, int](42))
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "mapped", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunSync(t *testing.T) {
|
||||
t.Run("runs effect synchronously", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext, int](42)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("runs effect with context.Context", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext, string]("hello")
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync[string](ioResult)
|
||||
|
||||
bgCtx := context.Background()
|
||||
result, err := readerResult(bgCtx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("propagates errors synchronously", func(t *testing.T) {
|
||||
expectedErr := errors.New("sync error")
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("runs complex effect chains", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
|
||||
eff := Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x + 10)
|
||||
})(Of[TestContext, int](5)))
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result) // (5 + 10) * 2
|
||||
})
|
||||
|
||||
t.Run("handles multiple sequential runs", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext, int](42)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync[int](ioResult)
|
||||
|
||||
// Run multiple times
|
||||
result1, err1 := readerResult(context.Background())
|
||||
result2, err2 := readerResult(context.Background())
|
||||
result3, err3 := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.NoError(t, err3)
|
||||
assert.Equal(t, 42, result1)
|
||||
assert.Equal(t, 42, result2)
|
||||
assert.Equal(t, 42, result3)
|
||||
})
|
||||
|
||||
t.Run("works with different result types", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
ctx := TestContext{Value: "test"}
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
eff := Of[TestContext, User](user)
|
||||
|
||||
ioResult := Provide[TestContext, User](ctx)(eff)
|
||||
readerResult := RunSync[User](ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
t.Run("complete workflow with success", func(t *testing.T) {
|
||||
type AppConfig struct {
|
||||
APIKey string
|
||||
Timeout int
|
||||
}
|
||||
|
||||
cfg := AppConfig{APIKey: "secret", Timeout: 30}
|
||||
|
||||
// Create an effect that uses the config
|
||||
eff := Of[AppConfig, string]("API call successful")
|
||||
|
||||
// Provide config and run
|
||||
result, err := RunSync[string](Provide[AppConfig, string](cfg)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "API call successful", result)
|
||||
})
|
||||
|
||||
t.Run("complete workflow with error", func(t *testing.T) {
|
||||
type AppConfig struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
expectedErr := errors.New("API error")
|
||||
cfg := AppConfig{APIKey: "secret"}
|
||||
|
||||
eff := Fail[AppConfig, string](expectedErr)
|
||||
|
||||
_, err := RunSync[string](Provide[AppConfig, string](cfg)(eff))(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("workflow with transformations", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
|
||||
eff := Map[TestContext](func(x int) string {
|
||||
return "final"
|
||||
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})(Of[TestContext, int](21)))
|
||||
|
||||
result, err := RunSync[string](Provide[TestContext, string](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "final", result)
|
||||
})
|
||||
|
||||
t.Run("workflow with bind operations", func(t *testing.T) {
|
||||
type State struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
ctx := TestContext{Value: "test"}
|
||||
|
||||
eff := Bind[TestContext](
|
||||
func(y int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Y = y
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s State) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](s.X * 2)
|
||||
},
|
||||
)(BindTo[TestContext](func(x int) State {
|
||||
return State{X: x}
|
||||
})(Of[TestContext, int](10)))
|
||||
|
||||
result, err := RunSync[State](Provide[TestContext, State](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result.X)
|
||||
assert.Equal(t, 20, result.Y)
|
||||
})
|
||||
|
||||
t.Run("workflow with context transformation", func(t *testing.T) {
|
||||
type OuterCtx struct {
|
||||
Value string
|
||||
}
|
||||
type InnerCtx struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
outerCtx := OuterCtx{Value: "outer"}
|
||||
innerEff := Of[InnerCtx, string]("inner result")
|
||||
|
||||
// Transform context
|
||||
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
|
||||
return InnerCtx{Data: outer.Value + "-transformed"}
|
||||
})(innerEff)
|
||||
|
||||
result, err := RunSync[string](Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "inner result", result)
|
||||
})
|
||||
|
||||
t.Run("workflow with array traversal", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
|
||||
eff := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})(input)
|
||||
|
||||
result, err := RunSync[[]int](Provide[TestContext, []int](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
}
|
||||
7
v2/effect/traverse.go
Normal file
7
v2/effect/traverse.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package effect
|
||||
|
||||
import "github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
|
||||
func TraverseArray[C, A, B any](f Kleisli[C, A, B]) Kleisli[C, []A, []B] {
|
||||
return readerreaderioresult.TraverseArray(f)
|
||||
}
|
||||
266
v2/effect/traverse_test.go
Normal file
266
v2/effect/traverse_test.go
Normal file
@@ -0,0 +1,266 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTraverseArray(t *testing.T) {
|
||||
t.Run("traverses empty array", func(t *testing.T) {
|
||||
input := []int{}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("traverses array with single element", func(t *testing.T) {
|
||||
input := []int{42}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"42"}, result)
|
||||
})
|
||||
|
||||
t.Run("traverses array with multiple elements", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, result)
|
||||
})
|
||||
|
||||
t.Run("transforms to different type", func(t *testing.T) {
|
||||
input := []string{"hello", "world", "test"}
|
||||
kleisli := TraverseArray[TestContext](func(s string) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](len(s))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{5, 5, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("stops on first error", func(t *testing.T) {
|
||||
expectedErr := errors.New("traverse error")
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
if x == 3 {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
}
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("handles complex transformations", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3}
|
||||
kleisli := TraverseArray[TestContext](func(id int) Effect[TestContext, User] {
|
||||
return Of[TestContext, User](User{
|
||||
ID: id,
|
||||
Name: fmt.Sprintf("User%d", id),
|
||||
})
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 3)
|
||||
assert.Equal(t, 1, result[0].ID)
|
||||
assert.Equal(t, "User1", result[0].Name)
|
||||
assert.Equal(t, 2, result[1].ID)
|
||||
assert.Equal(t, "User2", result[1].Name)
|
||||
assert.Equal(t, 3, result[2].ID)
|
||||
assert.Equal(t, "User3", result[2].Name)
|
||||
})
|
||||
|
||||
t.Run("chains with other operations", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
eff := Chain[TestContext](func(strings []string) Effect[TestContext, int] {
|
||||
total := 0
|
||||
for _, s := range strings {
|
||||
val, _ := strconv.Atoi(s)
|
||||
total += val
|
||||
}
|
||||
return Of[TestContext, int](total)
|
||||
})(TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x * 2))
|
||||
})(input))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 12, result) // (1*2) + (2*2) + (3*2) = 2 + 4 + 6 = 12
|
||||
})
|
||||
|
||||
t.Run("uses context in transformation", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Chain[TestContext](func(ctx TestContext) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](fmt.Sprintf("%s-%d", ctx.Value, x))
|
||||
})(Of[TestContext, TestContext](TestContext{Value: "prefix"}))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"prefix-1", "prefix-2", "prefix-3"}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves order", func(t *testing.T) {
|
||||
input := []int{5, 3, 8, 1, 9, 2}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 10)
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{50, 30, 80, 10, 90, 20}, result)
|
||||
})
|
||||
|
||||
t.Run("handles large arrays", func(t *testing.T) {
|
||||
size := 1000
|
||||
input := make([]int, size)
|
||||
for i := 0; i < size; i++ {
|
||||
input[i] = i
|
||||
}
|
||||
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](x * 2)
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, size)
|
||||
assert.Equal(t, 0, result[0])
|
||||
assert.Equal(t, 1998, result[999])
|
||||
})
|
||||
|
||||
t.Run("composes multiple traversals", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
// First traversal: int -> string
|
||||
kleisli1 := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
// Second traversal: string -> int (length)
|
||||
kleisli2 := TraverseArray[TestContext](func(s string) Effect[TestContext, int] {
|
||||
return Of[TestContext, int](len(s))
|
||||
})
|
||||
|
||||
eff := Chain[TestContext](kleisli2)(kleisli1(input))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 1, 1}, result) // All single-digit numbers have length 1
|
||||
})
|
||||
|
||||
t.Run("handles nil array", func(t *testing.T) {
|
||||
var input []int
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, result) // TraverseArray returns empty slice for nil input
|
||||
})
|
||||
|
||||
t.Run("works with Map for post-processing", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
eff := Map[TestContext](func(strings []string) string {
|
||||
result := ""
|
||||
for _, s := range strings {
|
||||
result += s + ","
|
||||
}
|
||||
return result
|
||||
})(TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
})(input))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1,2,3,", result)
|
||||
})
|
||||
|
||||
t.Run("error in middle of array", func(t *testing.T) {
|
||||
expectedErr := errors.New("middle error")
|
||||
input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
if x == 5 {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
}
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("error at end of array", func(t *testing.T) {
|
||||
expectedErr := errors.New("end error")
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
|
||||
if x == 5 {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
}
|
||||
return Of[TestContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
37
v2/effect/types.go
Normal file
37
v2/effect/types.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"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/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
IO[A any] = io.IO[A]
|
||||
IOEither[E, A any] = ioeither.IOEither[E, A]
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
|
||||
Thunk[A any] = ReaderIOResult[A]
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
Result[A any] = result.Result[A]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
|
||||
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
|
||||
)
|
||||
@@ -19,6 +19,31 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// MonadSequenceSegment sequences a segment of an array of effects using a divide-and-conquer approach.
|
||||
// It recursively splits the array segment in half, sequences each half, and concatenates the results.
|
||||
//
|
||||
// This function is optimized for performance by using a divide-and-conquer strategy that reduces
|
||||
// the depth of nested function calls compared to a linear fold approach.
|
||||
//
|
||||
// Type parameters:
|
||||
// - HKTB: The higher-kinded type containing values (e.g., Option[B], Either[E, B])
|
||||
// - HKTRB: The higher-kinded type containing an array of values (e.g., Option[[]B], Either[E, []B])
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a single HKTB into HKTRB
|
||||
// - empty: The empty/identity value for HKTRB
|
||||
// - concat: Function to concatenate two HKTRB values
|
||||
// - fbs: The array of effects to sequence
|
||||
// - start: The starting index of the segment (inclusive)
|
||||
// - end: The ending index of the segment (exclusive)
|
||||
//
|
||||
// Returns:
|
||||
// - HKTRB: The sequenced result for the segment
|
||||
//
|
||||
// The function handles three cases:
|
||||
// - Empty segment (end - start == 0): returns empty
|
||||
// - Single element (end - start == 1): returns fof(fbs[start])
|
||||
// - Multiple elements: recursively divides and conquers
|
||||
func MonadSequenceSegment[HKTB, HKTRB any](
|
||||
fof func(HKTB) HKTRB,
|
||||
empty HKTRB,
|
||||
@@ -41,6 +66,23 @@ func MonadSequenceSegment[HKTB, HKTRB any](
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceSegment creates a function that sequences a segment of an array of effects.
|
||||
// Unlike MonadSequenceSegment, this returns a curried function that can be reused.
|
||||
//
|
||||
// This function builds a computation tree at construction time, which can be more efficient
|
||||
// when the same sequencing pattern needs to be applied multiple times to arrays of the same length.
|
||||
//
|
||||
// Type parameters:
|
||||
// - HKTB: The higher-kinded type containing values
|
||||
// - HKTRB: The higher-kinded type containing an array of values
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a single HKTB into HKTRB
|
||||
// - empty: The empty/identity value for HKTRB
|
||||
// - concat: Function to concatenate two HKTRB values
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array of HKTB and returns HKTRB
|
||||
func SequenceSegment[HKTB, HKTRB any](
|
||||
fof func(HKTB) HKTRB,
|
||||
empty HKTRB,
|
||||
@@ -85,14 +127,39 @@ func SequenceSegment[HKTB, HKTRB any](
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
|
||||
|
||||
HKTRB = HKT<GB>
|
||||
HKTB = HKT<B>
|
||||
HKTAB = HKT<func(A)B>
|
||||
*/
|
||||
// MonadTraverse maps each element of an array to an effect, then sequences the results.
|
||||
// This is the monadic version that takes the array as a direct parameter.
|
||||
//
|
||||
// Traverse combines mapping and sequencing in one operation. It's useful when you want to
|
||||
// transform each element of an array into an effect (like Option, Either, IO, etc.) and
|
||||
// then collect all those effects into a single effect containing an array.
|
||||
//
|
||||
// We need to pass the members of the applicative explicitly, because golang does neither
|
||||
// support higher kinded types nor template methods on structs or interfaces.
|
||||
//
|
||||
// Type parameters:
|
||||
// - GA: The input array type (e.g., []A)
|
||||
// - GB: The output array type (e.g., []B)
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
// - HKTB: HKT<B> - The effect containing B (e.g., Option[B])
|
||||
// - HKTAB: HKT<func(B)GB> - Intermediate applicative type
|
||||
// - HKTRB: HKT<GB> - The effect containing the result array (e.g., Option[[]B])
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a value into the effect (Of/Pure)
|
||||
// - fmap: Function to map over the effect (Map)
|
||||
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
|
||||
// - ta: The input array to traverse
|
||||
// - f: The function to apply to each element, producing an effect
|
||||
//
|
||||
// Returns:
|
||||
// - HKTRB: An effect containing the array of transformed values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// If any element produces None, the entire result is None.
|
||||
// If all elements produce Some, the result is Some containing all values.
|
||||
func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -103,14 +170,20 @@ func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
return MonadTraverseReduce(fof, fmap, fap, ta, f, Append[GB, B], Empty[GB]())
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
|
||||
|
||||
HKTRB = HKT<GB>
|
||||
HKTB = HKT<B>
|
||||
HKTAB = HKT<func(A)B>
|
||||
*/
|
||||
// MonadTraverseWithIndex is like MonadTraverse but the transformation function also receives the index.
|
||||
// This is useful when the transformation depends on the element's position in the array.
|
||||
//
|
||||
// Type parameters: Same as MonadTraverse
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a value into the effect (Of/Pure)
|
||||
// - fmap: Function to map over the effect (Map)
|
||||
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
|
||||
// - ta: The input array to traverse
|
||||
// - f: The function to apply to each element with its index, producing an effect
|
||||
//
|
||||
// Returns:
|
||||
// - HKTRB: An effect containing the array of transformed values
|
||||
func MonadTraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -121,6 +194,19 @@ func MonadTraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
return MonadTraverseReduceWithIndex(fof, fmap, fap, ta, f, Append[GB, B], Empty[GB]())
|
||||
}
|
||||
|
||||
// Traverse creates a curried function that maps each element to an effect and sequences the results.
|
||||
// This is the curried version of MonadTraverse, useful for partial application and composition.
|
||||
//
|
||||
// Type parameters: Same as MonadTraverse
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a value into the effect (Of/Pure)
|
||||
// - fmap: Function to map over the effect (Map)
|
||||
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
|
||||
// - f: The function to apply to each element, producing an effect
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns an effect containing the transformed array
|
||||
func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -133,6 +219,19 @@ func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseWithIndex creates a curried function like Traverse but with index-aware transformation.
|
||||
// This is the curried version of MonadTraverseWithIndex.
|
||||
//
|
||||
// Type parameters: Same as MonadTraverse
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a value into the effect (Of/Pure)
|
||||
// - fmap: Function to map over the effect (Map)
|
||||
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
|
||||
// - f: The function to apply to each element with its index, producing an effect
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns an effect containing the transformed array
|
||||
func TraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -231,6 +330,16 @@ func TraverseReduce[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseReduceWithIndex creates a curried function for index-aware custom reduction during traversal.
|
||||
// This is the curried version of MonadTraverseReduceWithIndex.
|
||||
//
|
||||
// Type parameters: Same as MonadTraverseReduce
|
||||
//
|
||||
// Parameters: Same as TraverseReduce, except:
|
||||
// - transform: Function that takes index and element, producing an effect
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns an effect containing the accumulated value
|
||||
func TraverseReduceWithIndex[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
|
||||
@@ -1,10 +1,60 @@
|
||||
// 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 iter provides functional programming utilities for working with Go 1.23+ iterators.
|
||||
// It offers operations for reducing, mapping, concatenating, and transforming iterator sequences
|
||||
// in a functional style, compatible with the range-over-func pattern.
|
||||
package iter
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
func From[A any](as ...A) Seq[A] {
|
||||
return slices.Values(as)
|
||||
}
|
||||
|
||||
// MonadReduceWithIndex reduces an iterator sequence to a single value using a reducer function
|
||||
// that receives the current index, accumulated value, and current element.
|
||||
//
|
||||
// The function iterates through all elements in the sequence, applying the reducer function
|
||||
// at each step with the element's index. This is useful when the position of elements matters
|
||||
// in the reduction logic.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The iterator sequence to reduce
|
||||
// - f: The reducer function that takes (index, accumulator, element) and returns the new accumulator
|
||||
// - initial: The initial value for the accumulator
|
||||
//
|
||||
// Returns:
|
||||
// - The final accumulated value after processing all elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(10)
|
||||
// yield(20)
|
||||
// yield(30)
|
||||
// }
|
||||
// // Sum with index multiplier: 0*10 + 1*20 + 2*30 = 80
|
||||
// result := MonadReduceWithIndex(iter, func(i, acc, val int) int {
|
||||
// return acc + i*val
|
||||
// }, 0)
|
||||
func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(int, B, A) B, initial B) B {
|
||||
current := initial
|
||||
var i int
|
||||
@@ -15,6 +65,29 @@ func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(
|
||||
return current
|
||||
}
|
||||
|
||||
// MonadReduce reduces an iterator sequence to a single value using a reducer function.
|
||||
//
|
||||
// This is similar to MonadReduceWithIndex but without index tracking, making it more
|
||||
// efficient when the position of elements is not needed in the reduction logic.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The iterator sequence to reduce
|
||||
// - f: The reducer function that takes (accumulator, element) and returns the new accumulator
|
||||
// - initial: The initial value for the accumulator
|
||||
//
|
||||
// Returns:
|
||||
// - The final accumulated value after processing all elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// yield(3)
|
||||
// }
|
||||
// sum := MonadReduce(iter, func(acc, val int) int {
|
||||
// return acc + val
|
||||
// }, 0) // Returns: 6
|
||||
func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B, initial B) B {
|
||||
current := initial
|
||||
for a := range fa {
|
||||
@@ -23,7 +96,30 @@ func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B,
|
||||
return current
|
||||
}
|
||||
|
||||
// Concat concatenates two sequences, yielding all elements from left followed by all elements from right.
|
||||
// Concat concatenates two iterator sequences, yielding all elements from left followed by all elements from right.
|
||||
//
|
||||
// The resulting iterator will first yield all elements from the left sequence, then all elements
|
||||
// from the right sequence. If the consumer stops early (yield returns false), iteration stops
|
||||
// immediately without processing remaining elements.
|
||||
//
|
||||
// Parameters:
|
||||
// - left: The first iterator sequence
|
||||
// - right: The second iterator sequence
|
||||
//
|
||||
// Returns:
|
||||
// - A new iterator that yields elements from both sequences in order
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// left := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// }
|
||||
// right := func(yield func(int) bool) {
|
||||
// yield(3)
|
||||
// yield(4)
|
||||
// }
|
||||
// combined := Concat(left, right) // Yields: 1, 2, 3, 4
|
||||
func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
|
||||
return func(yield func(T) bool) {
|
||||
for t := range left {
|
||||
@@ -39,28 +135,129 @@ func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
|
||||
}
|
||||
}
|
||||
|
||||
// Of creates an iterator sequence containing a single element.
|
||||
//
|
||||
// This is the unit/return operation for the iterator monad, lifting a single value
|
||||
// into the iterator context.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The element to wrap in an iterator
|
||||
//
|
||||
// Returns:
|
||||
// - An iterator that yields exactly one element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := Of[func(yield func(int) bool)](42)
|
||||
// // Yields: 42
|
||||
func Of[GA ~func(yield func(A) bool), A any](a A) GA {
|
||||
return func(yield func(A) bool) {
|
||||
yield(a)
|
||||
}
|
||||
}
|
||||
|
||||
// MonadAppend appends a single element to the end of an iterator sequence.
|
||||
//
|
||||
// This creates a new iterator that yields all elements from the original sequence
|
||||
// followed by the tail element.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The original iterator sequence
|
||||
// - tail: The element to append
|
||||
//
|
||||
// Returns:
|
||||
// - A new iterator with the tail element appended
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// }
|
||||
// result := MonadAppend(iter, 3) // Yields: 1, 2, 3
|
||||
func MonadAppend[GA ~func(yield func(A) bool), A any](f GA, tail A) GA {
|
||||
return Concat(f, Of[GA](tail))
|
||||
}
|
||||
|
||||
// Append returns a function that appends a single element to the end of an iterator sequence.
|
||||
//
|
||||
// This is the curried version of MonadAppend, useful for partial application and composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - tail: The element to append
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns a new iterator with the tail element appended
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// appendThree := Append[func(yield func(int) bool)](3)
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// }
|
||||
// result := appendThree(iter) // Yields: 1, 2, 3
|
||||
func Append[GA ~func(yield func(A) bool), A any](tail A) func(GA) GA {
|
||||
return F.Bind2nd(Concat[GA], Of[GA](tail))
|
||||
}
|
||||
|
||||
// Prepend returns a function that prepends a single element to the beginning of an iterator sequence.
|
||||
//
|
||||
// This is the curried version for prepending, useful for partial application and composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - head: The element to prepend
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns a new iterator with the head element prepended
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// prependZero := Prepend[func(yield func(int) bool)](0)
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// }
|
||||
// result := prependZero(iter) // Yields: 0, 1, 2
|
||||
func Prepend[GA ~func(yield func(A) bool), A any](head A) func(GA) GA {
|
||||
return F.Bind1st(Concat[GA], Of[GA](head))
|
||||
}
|
||||
|
||||
// Empty creates an empty iterator sequence that yields no elements.
|
||||
//
|
||||
// This is the identity element for the Concat operation and represents an empty collection
|
||||
// in the iterator context.
|
||||
//
|
||||
// Returns:
|
||||
// - An iterator that yields no elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := Empty[func(yield func(int) bool), int]()
|
||||
// // Yields nothing
|
||||
func Empty[GA ~func(yield func(A) bool), A any]() GA {
|
||||
return func(_ func(A) bool) {}
|
||||
}
|
||||
|
||||
// ToArray collects all elements from an iterator sequence into a slice.
|
||||
//
|
||||
// This eagerly evaluates the entire iterator sequence and materializes all elements
|
||||
// into memory as a slice.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The iterator sequence to collect
|
||||
//
|
||||
// Returns:
|
||||
// - A slice containing all elements from the iterator
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// yield(3)
|
||||
// }
|
||||
// arr := ToArray[func(yield func(int) bool), []int](iter) // Returns: []int{1, 2, 3}
|
||||
func ToArray[GA ~func(yield func(A) bool), GB ~[]A, A any](fa GA) GB {
|
||||
bs := make(GB, 0)
|
||||
for a := range fa {
|
||||
@@ -69,6 +266,28 @@ func ToArray[GA ~func(yield func(A) bool), GB ~[]A, A any](fa GA) GB {
|
||||
return bs
|
||||
}
|
||||
|
||||
// MonadMapToArray maps each element of an iterator sequence through a function and collects the results into a slice.
|
||||
//
|
||||
// This combines mapping and collection into a single operation, eagerly evaluating the entire
|
||||
// iterator sequence and materializing the transformed elements into memory.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The iterator sequence to map and collect
|
||||
// - f: The mapping function to apply to each element
|
||||
//
|
||||
// Returns:
|
||||
// - A slice containing the mapped elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// yield(3)
|
||||
// }
|
||||
// doubled := MonadMapToArray[func(yield func(int) bool), []int](iter, func(x int) int {
|
||||
// return x * 2
|
||||
// }) // Returns: []int{2, 4, 6}
|
||||
func MonadMapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f func(A) B) GB {
|
||||
bs := make(GB, 0)
|
||||
for a := range fa {
|
||||
@@ -77,10 +296,54 @@ func MonadMapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f f
|
||||
return bs
|
||||
}
|
||||
|
||||
// MapToArray returns a function that maps each element through a function and collects the results into a slice.
|
||||
//
|
||||
// This is the curried version of MonadMapToArray, useful for partial application and composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The mapping function to apply to each element
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns a slice of mapped elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := MapToArray[func(yield func(int) bool), []int](func(x int) int {
|
||||
// return x * 2
|
||||
// })
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// }
|
||||
// result := double(iter) // Returns: []int{2, 4}
|
||||
func MapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f func(A) B) func(GA) GB {
|
||||
return F.Bind2nd(MonadMapToArray[GA, GB], f)
|
||||
}
|
||||
|
||||
// MonadMapToArrayWithIndex maps each element of an iterator sequence through a function that receives
|
||||
// the element's index, and collects the results into a slice.
|
||||
//
|
||||
// This is similar to MonadMapToArray but the mapping function also receives the zero-based index
|
||||
// of each element, useful when the position matters in the transformation logic.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The iterator sequence to map and collect
|
||||
// - f: The mapping function that takes (index, element) and returns the transformed element
|
||||
//
|
||||
// Returns:
|
||||
// - A slice containing the mapped elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(string) bool) {
|
||||
// yield("a")
|
||||
// yield("b")
|
||||
// yield("c")
|
||||
// }
|
||||
// indexed := MonadMapToArrayWithIndex[func(yield func(string) bool), []string](iter,
|
||||
// func(i int, s string) string {
|
||||
// return fmt.Sprintf("%d:%s", i, s)
|
||||
// }) // Returns: []string{"0:a", "1:b", "2:c"}
|
||||
func MonadMapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f func(int, A) B) GB {
|
||||
bs := make(GB, 0)
|
||||
var i int
|
||||
@@ -91,10 +354,49 @@ func MonadMapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f
|
||||
return bs
|
||||
}
|
||||
|
||||
// MapToArrayWithIndex returns a function that maps each element through an indexed function
|
||||
// and collects the results into a slice.
|
||||
//
|
||||
// This is the curried version of MonadMapToArrayWithIndex, useful for partial application and composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The mapping function that takes (index, element) and returns the transformed element
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns a slice of mapped elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// addIndex := MapToArrayWithIndex[func(yield func(string) bool), []string](
|
||||
// func(i int, s string) string {
|
||||
// return fmt.Sprintf("%d:%s", i, s)
|
||||
// })
|
||||
// iter := func(yield func(string) bool) {
|
||||
// yield("a")
|
||||
// yield("b")
|
||||
// }
|
||||
// result := addIndex(iter) // Returns: []string{"0:a", "1:b"}
|
||||
func MapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f func(int, A) B) func(GA) GB {
|
||||
return F.Bind2nd(MonadMapToArrayWithIndex[GA, GB], f)
|
||||
}
|
||||
|
||||
// Monoid returns a Monoid instance for iterator sequences.
|
||||
//
|
||||
// The monoid uses Concat as the binary operation and Empty as the identity element,
|
||||
// allowing iterator sequences to be combined in an associative way with a neutral element.
|
||||
// This enables generic operations that work with any monoid, such as folding a collection
|
||||
// of iterators into a single iterator.
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid instance with Concat and Empty operations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// m := Monoid[func(yield func(int) bool), int]()
|
||||
// iter1 := func(yield func(int) bool) { yield(1); yield(2) }
|
||||
// iter2 := func(yield func(int) bool) { yield(3); yield(4) }
|
||||
// combined := m.Concat(iter1, iter2) // Yields: 1, 2, 3, 4
|
||||
// empty := m.Empty() // Yields nothing
|
||||
func Monoid[GA ~func(yield func(A) bool), A any]() M.Monoid[GA] {
|
||||
return M.MakeMonoid(Concat[GA], Empty[GA]())
|
||||
}
|
||||
|
||||
@@ -21,18 +21,50 @@ import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
/*
|
||||
*
|
||||
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
|
||||
|
||||
HKTRB = HKT<GB>
|
||||
HKTB = HKT<B>
|
||||
HKTAB = HKT<func(A)B>
|
||||
*/
|
||||
// MonadTraverse traverses an iterator sequence, applying an effectful function to each element
|
||||
// and collecting the results in an applicative context.
|
||||
//
|
||||
// This is a fundamental operation in functional programming that allows you to "turn inside out"
|
||||
// a structure containing effects. It maps each element through a function that produces an effect,
|
||||
// then sequences all those effects together while preserving the iterator structure.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The output iterator type ~func(yield func(B) bool)
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
// - HKT_B: The higher-kinded type representing an effect containing B
|
||||
// - HKT_GB_GB: The higher-kinded type for a function from GB to GB in the effect context
|
||||
// - HKT_GB: The higher-kinded type representing an effect containing GB (the result iterator)
|
||||
//
|
||||
// Parameters:
|
||||
// - fmap_b: Maps a function over HKT_B to produce HKT_GB
|
||||
// - fof_gb: Lifts a GB value into the effect context (pure/of operation)
|
||||
// - fmap_gb: Maps a function over HKT_GB to produce HKT_GB_GB
|
||||
// - fap_gb: Applies an effectful function to an effectful value (ap operation)
|
||||
// - ta: The input iterator sequence to traverse
|
||||
// - f: The effectful function to apply to each element
|
||||
//
|
||||
// Returns:
|
||||
// - An effect containing an iterator of transformed elements
|
||||
//
|
||||
// Note: We need to pass the applicative operations explicitly because Go doesn't support
|
||||
// higher-kinded types or template methods on structs/interfaces.
|
||||
//
|
||||
// Example (conceptual with Option):
|
||||
//
|
||||
// // Traverse an iterator of strings, parsing each as an integer
|
||||
// // If any parse fails, the whole result is None
|
||||
// iter := func(yield func(string) bool) {
|
||||
// yield("1")
|
||||
// yield("2")
|
||||
// yield("3")
|
||||
// }
|
||||
// result := MonadTraverse(..., iter, parseInt) // Some(iterator of [1,2,3]) or None
|
||||
func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKT_B, HKT_GB_GB, HKT_GB any](
|
||||
fmap_b func(HKT_B, func(B) GB) HKT_GB,
|
||||
|
||||
fof_gb func(GB) HKT_GB,
|
||||
fof_gb OfType[GB, HKT_GB],
|
||||
fmap_gb func(HKT_GB, func(GB) func(GB) GB) HKT_GB_GB,
|
||||
fap_gb func(HKT_GB_GB, HKT_GB) HKT_GB,
|
||||
|
||||
@@ -54,14 +86,43 @@ func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A
|
||||
return INTA.MonadSequenceSegment(fof, empty, concat, hktb, 0, len(hktb))
|
||||
}
|
||||
|
||||
// Traverse is the curried version of MonadTraverse, returning a function that traverses an iterator.
|
||||
//
|
||||
// This version uses type aliases for better readability and is more suitable for partial application
|
||||
// and function composition. It returns a Kleisli arrow (a function from GA to HKT_GB).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The output iterator type ~func(yield func(B) bool)
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
// - HKT_B: The higher-kinded type representing an effect containing B
|
||||
// - HKT_GB_GB: The higher-kinded type for a function from GB to GB in the effect context
|
||||
// - HKT_GB: The higher-kinded type representing an effect containing GB
|
||||
//
|
||||
// Parameters:
|
||||
// - fmap_b: Maps a function over HKT_B to produce HKT_GB
|
||||
// - fof_gb: Lifts a GB value into the effect context
|
||||
// - fmap_gb: Maps a function over HKT_GB to produce HKT_GB_GB
|
||||
// - fap_gb: Applies an effectful function to an effectful value
|
||||
// - f: The effectful function to apply to each element (Kleisli arrow)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns an effect containing an iterator of transformed elements
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// parseInts := Traverse[...](fmap, fof, fmap_gb, fap, parseInt)
|
||||
// iter := func(yield func(string) bool) { yield("1"); yield("2") }
|
||||
// result := parseInts(iter) // Effect containing iterator of integers
|
||||
func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKT_B, HKT_GB_GB, HKT_GB any](
|
||||
fmap_b func(func(B) GB) func(HKT_B) HKT_GB,
|
||||
fmap_b MapType[B, GB, HKT_B, HKT_GB],
|
||||
|
||||
fof_gb func(GB) HKT_GB,
|
||||
fmap_gb func(func(GB) func(GB) GB) func(HKT_GB) HKT_GB_GB,
|
||||
fap_gb func(HKT_GB_GB, HKT_GB) HKT_GB,
|
||||
fof_gb OfType[GB, HKT_GB],
|
||||
fmap_gb MapType[GB, Endomorphism[GB], HKT_GB, HKT_GB_GB],
|
||||
fap_gb ApType[HKT_GB, HKT_GB, HKT_GB_GB],
|
||||
|
||||
f func(A) HKT_B) func(GA) HKT_GB {
|
||||
f Kleisli[A, HKT_B]) Kleisli[GA, HKT_GB] {
|
||||
|
||||
fof := fmap_b(Of[GB])
|
||||
empty := fof_gb(Empty[GB]())
|
||||
@@ -69,18 +130,50 @@ func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B,
|
||||
concat_gb := fmap_gb(cb)
|
||||
|
||||
concat := func(first, second HKT_GB) HKT_GB {
|
||||
return fap_gb(concat_gb(first), second)
|
||||
return fap_gb(second)(concat_gb(first))
|
||||
}
|
||||
|
||||
return func(ma GA) HKT_GB {
|
||||
// return INTA.SequenceSegment(fof, empty, concat)(MapToArray[GA, []HKT_B](f)(ma))
|
||||
hktb := MonadMapToArray[GA, []HKT_B](ma, f)
|
||||
return INTA.MonadSequenceSegment(fof, empty, concat, hktb, 0, len(hktb))
|
||||
}
|
||||
return F.Flow2(
|
||||
MapToArray[GA, []HKT_B](f),
|
||||
INTA.SequenceSegment(fof, empty, concat),
|
||||
)
|
||||
}
|
||||
|
||||
// MonadSequence sequences an iterator of effects into an effect containing an iterator.
|
||||
//
|
||||
// This is a special case of traverse where the transformation function is the identity.
|
||||
// It "flips" the nesting of the iterator and effect types, collecting all effects into
|
||||
// a single effect containing an iterator of values.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(HKTA) bool)
|
||||
// - HKTA: The higher-kinded type representing an effect containing A
|
||||
// - HKTRA: The higher-kinded type representing an effect containing an iterator of A
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts an HKTA value into the HKTRA context
|
||||
// - m: A monoid for combining HKTRA values
|
||||
// - ta: The input iterator of effects to sequence
|
||||
//
|
||||
// Returns:
|
||||
// - An effect containing an iterator of values
|
||||
//
|
||||
// Example (conceptual with Option):
|
||||
//
|
||||
// iter := func(yield func(Option[int]) bool) {
|
||||
// yield(Some(1))
|
||||
// yield(Some(2))
|
||||
// yield(Some(3))
|
||||
// }
|
||||
// result := MonadSequence(..., iter) // Some(iterator of [1,2,3])
|
||||
//
|
||||
// iter2 := func(yield func(Option[int]) bool) {
|
||||
// yield(Some(1))
|
||||
// yield(None)
|
||||
// }
|
||||
// result2 := MonadSequence(..., iter2) // None
|
||||
func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
fof OfType[HKTA, HKTRA],
|
||||
m M.Monoid[HKTRA],
|
||||
|
||||
ta GA) HKTRA {
|
||||
@@ -90,14 +183,37 @@ func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
|
||||
|
||||
HKTRB = HKT<GB>
|
||||
HKTB = HKT<B>
|
||||
HKTAB = HKT<func(A)B>
|
||||
*/
|
||||
// MonadTraverseWithIndex traverses an iterator sequence with index tracking, applying an effectful
|
||||
// function to each element along with its index.
|
||||
//
|
||||
// This is similar to MonadTraverse but the transformation function receives both the element's
|
||||
// zero-based index and the element itself, useful when the position matters in the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - A: The input element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTRB: The higher-kinded type representing an effect containing an iterator of B
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts an HKTB value into the HKTRB context
|
||||
// - m: A monoid for combining HKTRB values
|
||||
// - ta: The input iterator sequence to traverse
|
||||
// - f: The effectful function that takes (index, element) and returns an effect
|
||||
//
|
||||
// Returns:
|
||||
// - An effect containing an iterator of transformed elements
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// iter := func(yield func(string) bool) {
|
||||
// yield("a")
|
||||
// yield("b")
|
||||
// }
|
||||
// // Add index prefix to each element
|
||||
// result := MonadTraverseWithIndex(..., iter, func(i int, s string) Effect[string] {
|
||||
// return Pure(fmt.Sprintf("%d:%s", i, s))
|
||||
// }) // Effect containing iterator of ["0:a", "1:b"]
|
||||
func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
|
||||
fof func(HKTB) HKTRB,
|
||||
m M.Monoid[HKTRB],
|
||||
@@ -110,8 +226,29 @@ func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
|
||||
}
|
||||
|
||||
// Sequence is the curried version of MonadSequence, returning a function that sequences an iterator of effects.
|
||||
//
|
||||
// This version is more suitable for partial application and function composition.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(HKTA) bool)
|
||||
// - HKTA: The higher-kinded type representing an effect containing A
|
||||
// - HKTRA: The higher-kinded type representing an effect containing an iterator of A
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts an HKTA value into the HKTRA context
|
||||
// - m: A monoid for combining HKTRA values
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator of effects and returns an effect containing an iterator
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// sequenceOptions := Sequence[...](fof, monoid)
|
||||
// iter := func(yield func(Option[int]) bool) { yield(Some(1)); yield(Some(2)) }
|
||||
// result := sequenceOptions(iter) // Some(iterator of [1,2])
|
||||
func Sequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
fof OfType[HKTA, HKTRA],
|
||||
m M.Monoid[HKTRA]) func(GA) HKTRA {
|
||||
|
||||
return func(ma GA) HKTRA {
|
||||
@@ -119,6 +256,32 @@ func Sequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseWithIndex is the curried version of MonadTraverseWithIndex, returning a function that
|
||||
// traverses an iterator with index tracking.
|
||||
//
|
||||
// This version is more suitable for partial application and function composition.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - A: The input element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTRB: The higher-kinded type representing an effect containing an iterator of B
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts an HKTB value into the HKTRB context
|
||||
// - m: A monoid for combining HKTRB values
|
||||
// - f: The effectful function that takes (index, element) and returns an effect
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns an effect containing an iterator of transformed elements
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// addIndexPrefix := TraverseWithIndex[...](fof, monoid, func(i int, s string) Effect[string] {
|
||||
// return Pure(fmt.Sprintf("%d:%s", i, s))
|
||||
// })
|
||||
// iter := func(yield func(string) bool) { yield("a"); yield("b") }
|
||||
// result := addIndexPrefix(iter) // Effect containing iterator of ["0:a", "1:b"]
|
||||
func TraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
|
||||
fof func(HKTB) HKTRB,
|
||||
m M.Monoid[HKTRB],
|
||||
@@ -130,6 +293,39 @@ func TraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
|
||||
}
|
||||
}
|
||||
|
||||
// MonadTraverseReduce combines traversal with reduction, applying an effectful transformation
|
||||
// and accumulating results using a reducer function.
|
||||
//
|
||||
// This is a more efficient operation when you want to both transform elements through effects
|
||||
// and reduce them to a single accumulated value, avoiding intermediate collections.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The accumulator type
|
||||
// - A: The input element type
|
||||
// - B: The transformed element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
|
||||
// - HKTRB: The higher-kinded type representing an effect containing GB
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a GB value into the effect context
|
||||
// - fmap: Maps a function over the effect to produce an effectful function
|
||||
// - fap: Applies an effectful function to an effectful value
|
||||
// - ta: The input iterator sequence to traverse and reduce
|
||||
// - transform: The effectful function to apply to each element
|
||||
// - reduce: The reducer function that combines the accumulator with a transformed element
|
||||
// - initial: The initial accumulator value
|
||||
//
|
||||
// Returns:
|
||||
// - An effect containing the final accumulated value
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// iter := func(yield func(string) bool) { yield("1"); yield("2"); yield("3") }
|
||||
// // Parse strings to ints and sum them
|
||||
// result := MonadTraverseReduce(..., iter, parseInt, add, 0)
|
||||
// // Returns: Some(6) or None if any parse fails
|
||||
func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -152,6 +348,44 @@ func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HK
|
||||
}, fof(initial))
|
||||
}
|
||||
|
||||
// MonadTraverseReduceWithIndex combines indexed traversal with reduction, applying an effectful
|
||||
// transformation that receives element indices and accumulating results using a reducer function.
|
||||
//
|
||||
// This is similar to MonadTraverseReduce but the transformation function also receives the
|
||||
// zero-based index of each element, useful when position matters in the transformation logic.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The accumulator type
|
||||
// - A: The input element type
|
||||
// - B: The transformed element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
|
||||
// - HKTRB: The higher-kinded type representing an effect containing GB
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a GB value into the effect context
|
||||
// - fmap: Maps a function over the effect to produce an effectful function
|
||||
// - fap: Applies an effectful function to an effectful value
|
||||
// - ta: The input iterator sequence to traverse and reduce
|
||||
// - transform: The effectful function that takes (index, element) and returns an effect
|
||||
// - reduce: The reducer function that combines the accumulator with a transformed element
|
||||
// - initial: The initial accumulator value
|
||||
//
|
||||
// Returns:
|
||||
// - An effect containing the final accumulated value
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// iter := func(yield func(string) bool) { yield("a"); yield("b"); yield("c") }
|
||||
// // Create indexed strings and concatenate
|
||||
// result := MonadTraverseReduceWithIndex(..., iter,
|
||||
// func(i int, s string) Effect[string] {
|
||||
// return Pure(fmt.Sprintf("%d:%s", i, s))
|
||||
// },
|
||||
// func(acc, s string) string { return acc + "," + s },
|
||||
// "")
|
||||
// // Returns: Effect containing "0:a,1:b,2:c"
|
||||
func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -174,6 +408,36 @@ func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB,
|
||||
}, fof(initial))
|
||||
}
|
||||
|
||||
// TraverseReduce is the curried version of MonadTraverseReduce, returning a function that
|
||||
// traverses and reduces an iterator.
|
||||
//
|
||||
// This version is more suitable for partial application and function composition.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The accumulator type
|
||||
// - A: The input element type
|
||||
// - B: The transformed element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
|
||||
// - HKTRB: The higher-kinded type representing an effect containing GB
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a GB value into the effect context
|
||||
// - fmap: Maps a function over the effect to produce an effectful function
|
||||
// - fap: Applies an effectful function to an effectful value
|
||||
// - transform: The effectful function to apply to each element
|
||||
// - reduce: The reducer function that combines the accumulator with a transformed element
|
||||
// - initial: The initial accumulator value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns an effect containing the accumulated value
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// sumParsedInts := TraverseReduce[...](fof, fmap, fap, parseInt, add, 0)
|
||||
// iter := func(yield func(string) bool) { yield("1"); yield("2"); yield("3") }
|
||||
// result := sumParsedInts(iter) // Some(6) or None if any parse fails
|
||||
func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -188,6 +452,41 @@ func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB a
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseReduceWithIndex is the curried version of MonadTraverseReduceWithIndex, returning a
|
||||
// function that traverses and reduces an iterator with index tracking.
|
||||
//
|
||||
// This version is more suitable for partial application and function composition.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The accumulator type
|
||||
// - A: The input element type
|
||||
// - B: The transformed element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
|
||||
// - HKTRB: The higher-kinded type representing an effect containing GB
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a GB value into the effect context
|
||||
// - fmap: Maps a function over the effect to produce an effectful function
|
||||
// - fap: Applies an effectful function to an effectful value
|
||||
// - transform: The effectful function that takes (index, element) and returns an effect
|
||||
// - reduce: The reducer function that combines the accumulator with a transformed element
|
||||
// - initial: The initial accumulator value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns an effect containing the accumulated value
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// concatIndexed := TraverseReduceWithIndex[...](fof, fmap, fap,
|
||||
// func(i int, s string) Effect[string] {
|
||||
// return Pure(fmt.Sprintf("%d:%s", i, s))
|
||||
// },
|
||||
// func(acc, s string) string { return acc + "," + s },
|
||||
// "")
|
||||
// iter := func(yield func(string) bool) { yield("a"); yield("b") }
|
||||
// result := concatIndexed(iter) // Effect containing "0:a,1:b"
|
||||
func TraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
|
||||
@@ -2,10 +2,23 @@ package iter
|
||||
|
||||
import (
|
||||
I "iter"
|
||||
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
)
|
||||
|
||||
type (
|
||||
// Seq represents Go's standard library iterator type for single values.
|
||||
// It's an alias for iter.Seq[A] and provides interoperability with Go 1.23+ range-over-func.
|
||||
Seq[A any] = I.Seq[A]
|
||||
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
OfType[A, HKT_A any] = pointed.OfType[A, HKT_A]
|
||||
MapType[A, B, HKT_A, HKT_B any] = functor.MapType[A, B, HKT_A, HKT_B]
|
||||
ApType[HKT_A, HKT_B, HKT_AB any] = apply.ApType[HKT_A, HKT_B, HKT_AB]
|
||||
|
||||
Kleisli[A, HKT_B any] = func(A) HKT_B
|
||||
)
|
||||
|
||||
@@ -61,18 +61,105 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseIter applies an IO-returning function to each element of an iterator sequence
|
||||
// and collects the results into an IO of an iterator sequence. Executes in parallel by default.
|
||||
//
|
||||
// This function is useful for processing lazy sequences where each element requires an IO operation.
|
||||
// The resulting iterator is also lazy and will only execute IO operations when iterated.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an element of type A and returns an IO computation producing B
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator sequence of A and returns an IO of an iterator sequence of B
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Fetch user data for each ID in a sequence
|
||||
// fetchUser := func(id int) io.IO[User] {
|
||||
// return func() User {
|
||||
// // Simulate fetching user from database
|
||||
// return User{ID: id, Name: fmt.Sprintf("User%d", id)}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Create an iterator of user IDs
|
||||
// userIDs := func(yield func(int) bool) {
|
||||
// for _, id := range []int{1, 2, 3, 4, 5} {
|
||||
// if !yield(id) { return }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Traverse the iterator, fetching each user
|
||||
// fetchUsers := io.TraverseIter(fetchUser)
|
||||
// usersIO := fetchUsers(userIDs)
|
||||
//
|
||||
// // Execute the IO to get the iterator of users
|
||||
// users := usersIO()
|
||||
// for user := range users {
|
||||
// fmt.Printf("User: %v\n", user)
|
||||
// }
|
||||
func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] {
|
||||
return INTI.Traverse[Seq[A]](
|
||||
Map[B],
|
||||
|
||||
Of[Seq[B]],
|
||||
Map[Seq[B]],
|
||||
MonadAp[Seq[B]],
|
||||
Ap[Seq[B]],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceIter converts an iterator sequence of IO computations into an IO of an iterator sequence of results.
|
||||
// All computations are executed in parallel by default when the resulting IO is invoked.
|
||||
//
|
||||
// This is a special case of TraverseIter where the transformation function is the identity.
|
||||
// It "flips" the nesting of the iterator and IO types, executing all IO operations and collecting
|
||||
// their results into a lazy iterator.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: An iterator sequence where each element is an IO computation
|
||||
//
|
||||
// Returns:
|
||||
// - An IO computation that, when executed, produces an iterator sequence of results
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create an iterator of IO operations
|
||||
// operations := func(yield func(io.IO[int]) bool) {
|
||||
// yield(func() int { return 1 })
|
||||
// yield(func() int { return 2 })
|
||||
// yield(func() int { return 3 })
|
||||
// }
|
||||
//
|
||||
// // Sequence the operations
|
||||
// resultsIO := io.SequenceIter(operations)
|
||||
//
|
||||
// // Execute all IO operations and get the iterator of results
|
||||
// results := resultsIO()
|
||||
// for result := range results {
|
||||
// fmt.Printf("Result: %d\n", result)
|
||||
// }
|
||||
//
|
||||
// Note: The IO operations are executed when resultsIO() is called, not when iterating
|
||||
// over the results. The resulting iterator is lazy but the computations have already
|
||||
// been performed.
|
||||
func SequenceIter[A any](as Seq[IO[A]]) IO[Seq[A]] {
|
||||
return INTI.MonadSequence(
|
||||
Map(INTI.Of[Seq[A]]),
|
||||
ApplicativeMonoid(INTI.Monoid[Seq[A]]()),
|
||||
as,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex is like TraverseArray but the function also receives the index.
|
||||
// Executes in parallel by default.
|
||||
//
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package io
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -36,3 +39,264 @@ func TestTraverseCustomSlice(t *testing.T) {
|
||||
|
||||
assert.Equal(t, res(), []string{"A", "B"})
|
||||
}
|
||||
|
||||
func TestTraverseIter(t *testing.T) {
|
||||
t.Run("transforms all elements successfully", func(t *testing.T) {
|
||||
// Create an iterator of strings
|
||||
input := slices.Values(A.From("hello", "world", "test"))
|
||||
|
||||
// Transform each string to uppercase
|
||||
transform := func(s string) IO[string] {
|
||||
return Of(strings.ToUpper(s))
|
||||
}
|
||||
|
||||
// Traverse the iterator
|
||||
traverseFn := TraverseIter(transform)
|
||||
resultIO := traverseFn(input)
|
||||
|
||||
// Execute the IO and collect results
|
||||
result := resultIO()
|
||||
var collected []string
|
||||
for s := range result {
|
||||
collected = append(collected, s)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"HELLO", "WORLD", "TEST"}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with empty iterator", func(t *testing.T) {
|
||||
// Create an empty iterator
|
||||
input := func(yield func(string) bool) {}
|
||||
|
||||
transform := func(s string) IO[string] {
|
||||
return Of(strings.ToUpper(s))
|
||||
}
|
||||
|
||||
traverseFn := TraverseIter(transform)
|
||||
resultIO := traverseFn(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []string
|
||||
for s := range result {
|
||||
collected = append(collected, s)
|
||||
}
|
||||
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("works with single element", func(t *testing.T) {
|
||||
input := func(yield func(int) bool) {
|
||||
yield(42)
|
||||
}
|
||||
|
||||
transform := func(n int) IO[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
|
||||
traverseFn := TraverseIter(transform)
|
||||
resultIO := traverseFn(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []int
|
||||
for n := range result {
|
||||
collected = append(collected, n)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{84}, collected)
|
||||
})
|
||||
|
||||
t.Run("preserves order of elements", func(t *testing.T) {
|
||||
input := func(yield func(int) bool) {
|
||||
for i := 1; i <= 5; i++ {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transform := func(n int) IO[string] {
|
||||
return Of(fmt.Sprintf("item-%d", n))
|
||||
}
|
||||
|
||||
traverseFn := TraverseIter(transform)
|
||||
resultIO := traverseFn(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []string
|
||||
for s := range result {
|
||||
collected = append(collected, s)
|
||||
}
|
||||
|
||||
expected := []string{"item-1", "item-2", "item-3", "item-4", "item-5"}
|
||||
assert.Equal(t, expected, collected)
|
||||
})
|
||||
|
||||
t.Run("handles complex transformations", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
input := func(yield func(int) bool) {
|
||||
for _, id := range []int{1, 2, 3} {
|
||||
if !yield(id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transform := func(id int) IO[User] {
|
||||
return Of(User{ID: id, Name: fmt.Sprintf("User%d", id)})
|
||||
}
|
||||
|
||||
traverseFn := TraverseIter(transform)
|
||||
resultIO := traverseFn(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []User
|
||||
for user := range result {
|
||||
collected = append(collected, user)
|
||||
}
|
||||
|
||||
expected := []User{
|
||||
{ID: 1, Name: "User1"},
|
||||
{ID: 2, Name: "User2"},
|
||||
{ID: 3, Name: "User3"},
|
||||
}
|
||||
assert.Equal(t, expected, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceIter(t *testing.T) {
|
||||
t.Run("sequences multiple IO operations", func(t *testing.T) {
|
||||
// Create an iterator of IO operations
|
||||
input := slices.Values(A.From(Of(1), Of(2), Of(3)))
|
||||
|
||||
// Sequence the operations
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
// Execute and collect results
|
||||
result := resultIO()
|
||||
var collected []int
|
||||
for n := range result {
|
||||
collected = append(collected, n)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with empty iterator", func(t *testing.T) {
|
||||
input := slices.Values(A.Empty[IO[string]]())
|
||||
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []string
|
||||
for s := range result {
|
||||
collected = append(collected, s)
|
||||
}
|
||||
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("executes all IO operations", func(t *testing.T) {
|
||||
// Track execution order
|
||||
var executed []int
|
||||
|
||||
input := func(yield func(IO[int]) bool) {
|
||||
yield(func() int {
|
||||
executed = append(executed, 1)
|
||||
return 10
|
||||
})
|
||||
yield(func() int {
|
||||
executed = append(executed, 2)
|
||||
return 20
|
||||
})
|
||||
yield(func() int {
|
||||
executed = append(executed, 3)
|
||||
return 30
|
||||
})
|
||||
}
|
||||
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
// Before execution, nothing should be executed
|
||||
assert.Empty(t, executed)
|
||||
|
||||
// Execute the IO
|
||||
result := resultIO()
|
||||
|
||||
// Collect results
|
||||
var collected []int
|
||||
for n := range result {
|
||||
collected = append(collected, n)
|
||||
}
|
||||
|
||||
// All operations should have been executed
|
||||
assert.Equal(t, []int{1, 2, 3}, executed)
|
||||
assert.Equal(t, []int{10, 20, 30}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with single IO operation", func(t *testing.T) {
|
||||
input := func(yield func(IO[string]) bool) {
|
||||
yield(Of("hello"))
|
||||
}
|
||||
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []string
|
||||
for s := range result {
|
||||
collected = append(collected, s)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"hello"}, collected)
|
||||
})
|
||||
|
||||
t.Run("preserves order of results", func(t *testing.T) {
|
||||
input := func(yield func(IO[int]) bool) {
|
||||
for i := 5; i >= 1; i-- {
|
||||
n := i // capture loop variable
|
||||
yield(func() int { return n * 10 })
|
||||
}
|
||||
}
|
||||
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []int
|
||||
for n := range result {
|
||||
collected = append(collected, n)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{50, 40, 30, 20, 10}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with complex types", func(t *testing.T) {
|
||||
type Result struct {
|
||||
Value int
|
||||
Label string
|
||||
}
|
||||
|
||||
input := func(yield func(IO[Result]) bool) {
|
||||
yield(Of(Result{Value: 1, Label: "first"}))
|
||||
yield(Of(Result{Value: 2, Label: "second"}))
|
||||
yield(Of(Result{Value: 3, Label: "third"}))
|
||||
}
|
||||
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []Result
|
||||
for r := range result {
|
||||
collected = append(collected, r)
|
||||
}
|
||||
|
||||
expected := []Result{
|
||||
{Value: 1, Label: "first"},
|
||||
{Value: 2, Label: "second"},
|
||||
{Value: 3, Label: "third"},
|
||||
}
|
||||
assert.Equal(t, expected, collected)
|
||||
})
|
||||
}
|
||||
|
||||
54
v2/iterator/iter/last.go
Normal file
54
v2/iterator/iter/last.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package iter
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Last returns the last element from an [Iterator] wrapped in an [Option].
|
||||
//
|
||||
// This function retrieves the last element from the iterator by consuming the entire
|
||||
// sequence. If the iterator contains at least one element, it returns Some(element).
|
||||
// If the iterator is empty, it returns None.
|
||||
//
|
||||
// RxJS Equivalent: [last] - https://rxjs.dev/api/operators/last
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the iterator
|
||||
//
|
||||
// Parameters:
|
||||
// - it: The input iterator to get the last element from
|
||||
//
|
||||
// Returns:
|
||||
// - Option[U]: Some(last element) if the iterator is non-empty, None otherwise
|
||||
//
|
||||
// Example with non-empty sequence:
|
||||
//
|
||||
// seq := iter.From(1, 2, 3, 4, 5)
|
||||
// last := iter.Last(seq)
|
||||
// // Returns: Some(5)
|
||||
//
|
||||
// Example with empty sequence:
|
||||
//
|
||||
// seq := iter.Empty[int]()
|
||||
// last := iter.Last(seq)
|
||||
// // Returns: None
|
||||
//
|
||||
// Example with filtered sequence:
|
||||
//
|
||||
// seq := iter.From(1, 2, 3, 4, 5)
|
||||
// filtered := iter.Filter(func(x int) bool { return x < 4 })(seq)
|
||||
// last := iter.Last(filtered)
|
||||
// // Returns: Some(3)
|
||||
func Last[U any](it Seq[U]) Option[U] {
|
||||
var last U
|
||||
found := false
|
||||
|
||||
for last = range it {
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
return option.None[U]()
|
||||
}
|
||||
return option.Some(last)
|
||||
}
|
||||
305
v2/iterator/iter/last_test.go
Normal file
305
v2/iterator/iter/last_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestLast test getting the last element from a non-empty sequence
|
||||
func TestLastSimple(t *testing.T) {
|
||||
|
||||
t.Run("returns last element from integer sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(3), last)
|
||||
})
|
||||
|
||||
t.Run("returns last element from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of("c"), last)
|
||||
})
|
||||
|
||||
t.Run("returns last element from single element sequence", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
|
||||
t.Run("returns last element from large sequence", func(t *testing.T) {
|
||||
seq := From(100, 200, 300, 400, 500)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(500), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastEmpty tests getting the last element from an empty sequence
|
||||
func TestLastEmpty(t *testing.T) {
|
||||
|
||||
t.Run("returns None for empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[string](), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty struct sequence", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Value int
|
||||
}
|
||||
seq := Empty[TestStruct]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[TestStruct](), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty sequence of functions", func(t *testing.T) {
|
||||
type TestFunc func(int)
|
||||
seq := Empty[TestFunc]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[TestFunc](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithComplex tests Last with complex types
|
||||
func TestLastWithComplex(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("returns last person", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
last := Last(seq)
|
||||
expected := O.Of(Person{"Charlie", 35})
|
||||
assert.Equal(t, expected, last)
|
||||
})
|
||||
|
||||
t.Run("returns last pointer", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
seq := From(p1, p2)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(p2), last)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLastWithFunctions(t *testing.T) {
|
||||
|
||||
t.Run("return function", func(t *testing.T) {
|
||||
|
||||
want := "last"
|
||||
f1 := function.Constant("first")
|
||||
f2 := function.Constant("last")
|
||||
seq := From(f1, f2)
|
||||
|
||||
getLast := function.Flow2(
|
||||
Last,
|
||||
O.Map(funcReader),
|
||||
)
|
||||
assert.Equal(t, O.Of(want), getLast(seq))
|
||||
})
|
||||
}
|
||||
|
||||
func funcReader(f func() string) string {
|
||||
return f()
|
||||
}
|
||||
|
||||
// TestLastWithChan tests Last with channels
|
||||
func TestLastWithChan(t *testing.T) {
|
||||
t.Run("return function", func(t *testing.T) {
|
||||
want := 30
|
||||
seq := From(intChan(10),
|
||||
intChan(20),
|
||||
intChan(want))
|
||||
|
||||
getLast := function.Flow2(
|
||||
Last,
|
||||
O.Map(chanReader[int]),
|
||||
)
|
||||
assert.Equal(t, O.Of(want), getLast(seq))
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func chanReader[T any](c <-chan T) T {
|
||||
return <-c
|
||||
}
|
||||
|
||||
func intChan(val int) <-chan int {
|
||||
ch := make(chan int, 1)
|
||||
ch <- val
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
// TestLastWithChainedOperations tests Last with multiple chained operations
|
||||
func TestLastWithChainedOperations(t *testing.T) {
|
||||
t.Run("chains filter, map, and last", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, N.MoreThan(5))
|
||||
mapped := MonadMap(filtered, N.Mul(10))
|
||||
result := Last(mapped)
|
||||
assert.Equal(t, O.Of(100), result)
|
||||
})
|
||||
|
||||
t.Run("chains map and filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
filtered := MonadFilter(mapped, N.MoreThan(5))
|
||||
result := Last(filtered)
|
||||
assert.Equal(t, O.Of(10), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithReplicate tests Last with replicated values
|
||||
func TestLastWithReplicate(t *testing.T) {
|
||||
t.Run("returns last from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(5, 42)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
|
||||
t.Run("returns None from zero replications", func(t *testing.T) {
|
||||
seq := Replicate(0, 42)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithMakeBy tests Last with MakeBy
|
||||
func TestLastWithMakeBy(t *testing.T) {
|
||||
t.Run("returns last generated element", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i * i })
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(16), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for zero elements", func(t *testing.T) {
|
||||
seq := MakeBy(0, F.Identity[int])
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithPrepend tests Last with Prepend
|
||||
func TestLastWithPrepend(t *testing.T) {
|
||||
t.Run("returns last element, not prepended", func(t *testing.T) {
|
||||
seq := From(2, 3, 4)
|
||||
prepended := Prepend(1)(seq)
|
||||
last := Last(prepended)
|
||||
assert.Equal(t, O.Of(4), last)
|
||||
})
|
||||
|
||||
t.Run("returns prepended element from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
prepended := Prepend(42)(seq)
|
||||
last := Last(prepended)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithAppend tests Last with Append
|
||||
func TestLastWithAppend(t *testing.T) {
|
||||
t.Run("returns appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
last := Last(appended)
|
||||
assert.Equal(t, O.Of(4), last)
|
||||
})
|
||||
|
||||
t.Run("returns appended element from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
appended := Append(42)(seq)
|
||||
last := Last(appended)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithChain tests Last with Chain (flatMap)
|
||||
func TestLastWithChain(t *testing.T) {
|
||||
t.Run("returns last from chained sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
last := Last(chained)
|
||||
assert.Equal(t, O.Of(30), last)
|
||||
})
|
||||
|
||||
t.Run("returns None when chain produces empty", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return Empty[int]()
|
||||
})
|
||||
last := Last(chained)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithFlatten tests Last with Flatten
|
||||
func TestLastWithFlatten(t *testing.T) {
|
||||
t.Run("returns last from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5))
|
||||
flattened := Flatten(nested)
|
||||
last := Last(flattened)
|
||||
assert.Equal(t, O.Of(5), last)
|
||||
})
|
||||
|
||||
t.Run("returns None from empty nested sequence", func(t *testing.T) {
|
||||
nested := Empty[Seq[int]]()
|
||||
flattened := Flatten(nested)
|
||||
last := Last(flattened)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleLast() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
last := Last(seq)
|
||||
|
||||
if value, ok := O.Unwrap(last); ok {
|
||||
fmt.Printf("Last element: %d\n", value)
|
||||
}
|
||||
// Output: Last element: 5
|
||||
}
|
||||
|
||||
func ExampleLast_empty() {
|
||||
seq := Empty[int]()
|
||||
last := Last(seq)
|
||||
|
||||
if _, ok := O.Unwrap(last); !ok {
|
||||
fmt.Println("Sequence is empty")
|
||||
}
|
||||
// Output: Sequence is empty
|
||||
}
|
||||
|
||||
func ExampleLast_functions() {
|
||||
f1 := function.Constant("first")
|
||||
f2 := function.Constant("middle")
|
||||
f3 := function.Constant("last")
|
||||
seq := From(f1, f2, f3)
|
||||
|
||||
last := Last(seq)
|
||||
|
||||
if fn, ok := O.Unwrap(last); ok {
|
||||
result := fn()
|
||||
fmt.Printf("Last function result: %s\n", result)
|
||||
}
|
||||
// Output: Last function result: last
|
||||
}
|
||||
340
v2/optics/codec/codecs.go
Normal file
340
v2/optics/codec/codecs.go
Normal file
@@ -0,0 +1,340 @@
|
||||
// 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 codec provides pre-built codec implementations for common types.
|
||||
// This package includes codecs for URL parsing, date/time formatting, and other
|
||||
// standard data transformations that require bidirectional encoding/decoding.
|
||||
//
|
||||
// The codecs in this package follow functional programming principles and integrate
|
||||
// with the validation framework to provide type-safe, composable transformations.
|
||||
package codec
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// validateFromParser creates a validation function from a parser that may fail.
|
||||
// It wraps a parser function that returns (A, error) into a Validate[I, A] function
|
||||
// that integrates with the validation framework.
|
||||
//
|
||||
// The returned validation function:
|
||||
// - Calls the parser with the input value
|
||||
// - On success: returns a successful validation containing the parsed value
|
||||
// - On failure: returns a validation failure with the error message and cause
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type to parse into
|
||||
// - I: The input type to parse from
|
||||
//
|
||||
// Parameters:
|
||||
// - parser: A function that attempts to parse input I into type A, returning an error on failure
|
||||
//
|
||||
// Returns:
|
||||
// - A Validate[I, A] function that can be used in codec construction
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a validator for parsing integers from strings
|
||||
// intValidator := validateFromParser(strconv.Atoi)
|
||||
// // Use in a codec
|
||||
// intCodec := MakeType("Int", Is[int](), intValidator, strconv.Itoa)
|
||||
func validateFromParser[A, I any](parser func(I) (A, error)) Validate[I, A] {
|
||||
return func(i I) Decode[Context, A] {
|
||||
// Attempt to parse the input value
|
||||
a, err := parser(i)
|
||||
if err != nil {
|
||||
// On error, create a validation failure with the error details
|
||||
return validation.FailureWithError[A](i, err.Error())(err)
|
||||
}
|
||||
// On success, wrap the parsed value in a successful validation
|
||||
return reader.Of[Context](validation.Success(a))
|
||||
}
|
||||
}
|
||||
|
||||
// URL creates a bidirectional codec for URL parsing and formatting.
|
||||
// This codec can parse strings into *url.URL and encode *url.URL back to strings.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string using url.Parse, validating URL syntax
|
||||
// - Encodes: Converts a *url.URL to its string representation using String()
|
||||
// - Validates: Ensures the input string is a valid URL format
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[*url.URL, string, string] codec that handles URL transformations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// urlCodec := URL()
|
||||
//
|
||||
// // Decode a string to URL
|
||||
// validation := urlCodec.Decode("https://example.com/path?query=value")
|
||||
// // validation is Right(*url.URL{...})
|
||||
//
|
||||
// // Encode a URL to string
|
||||
// u, _ := url.Parse("https://example.com")
|
||||
// str := urlCodec.Encode(u)
|
||||
// // str is "https://example.com"
|
||||
//
|
||||
// // Invalid URL fails validation
|
||||
// validation := urlCodec.Decode("not a valid url")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func URL() Type[*url.URL, string, string] {
|
||||
return MakeType(
|
||||
"URL",
|
||||
Is[*url.URL](),
|
||||
validateFromParser(url.Parse),
|
||||
(*url.URL).String,
|
||||
)
|
||||
}
|
||||
|
||||
// Date creates a bidirectional codec for date/time parsing and formatting with a specific layout.
|
||||
// This codec uses Go's time.Parse and time.Format with the provided layout string.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string into time.Time using the specified layout
|
||||
// - Encodes: Formats a time.Time back to a string using the same layout
|
||||
// - Validates: Ensures the input string matches the expected date/time format
|
||||
//
|
||||
// Parameters:
|
||||
// - layout: The time layout string (e.g., "2006-01-02", time.RFC3339)
|
||||
// See time package documentation for layout format details
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[time.Time, string, string] codec that handles date/time transformations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for ISO 8601 dates
|
||||
// dateCodec := Date("2006-01-02")
|
||||
//
|
||||
// // Decode a string to time.Time
|
||||
// validation := dateCodec.Decode("2024-03-15")
|
||||
// // validation is Right(time.Time{...})
|
||||
//
|
||||
// // Encode a time.Time to string
|
||||
// t := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
|
||||
// str := dateCodec.Encode(t)
|
||||
// // str is "2024-03-15"
|
||||
//
|
||||
// // Create a codec for RFC3339 timestamps
|
||||
// timestampCodec := Date(time.RFC3339)
|
||||
// validation := timestampCodec.Decode("2024-03-15T10:30:00Z")
|
||||
//
|
||||
// // Invalid format fails validation
|
||||
// validation := dateCodec.Decode("15-03-2024")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func Date(layout string) Type[time.Time, string, string] {
|
||||
return MakeType(
|
||||
"Date",
|
||||
Is[time.Time](),
|
||||
validateFromParser(func(s string) (time.Time, error) { return time.Parse(layout, s) }),
|
||||
F.Bind2nd(time.Time.Format, layout),
|
||||
)
|
||||
}
|
||||
|
||||
// Regex creates a bidirectional codec for regex pattern matching with capture groups.
|
||||
// This codec can match strings against a regular expression pattern and extract capture groups,
|
||||
// then reconstruct the original string from the match data.
|
||||
//
|
||||
// The codec uses prism.Match which contains:
|
||||
// - Before: Text before the match
|
||||
// - Groups: Capture groups (index 0 is the full match, 1+ are numbered capture groups)
|
||||
// - After: Text after the match
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Attempts to match the regex against the input string
|
||||
// - Encodes: Reconstructs the original string from a Match structure
|
||||
// - Validates: Ensures the string matches the regex pattern
|
||||
//
|
||||
// Parameters:
|
||||
// - re: A compiled regular expression pattern
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[prism.Match, string, string] codec that handles regex matching
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for matching numbers in text
|
||||
// numberRegex := regexp.MustCompile(`\d+`)
|
||||
// numberCodec := Regex(numberRegex)
|
||||
//
|
||||
// // Decode a string with a number
|
||||
// validation := numberCodec.Decode("Price: 42 dollars")
|
||||
// // validation is Right(Match{Before: "Price: ", Groups: []string{"42"}, After: " dollars"})
|
||||
//
|
||||
// // Encode a Match back to string
|
||||
// match := prism.Match{Before: "Price: ", Groups: []string{"42"}, After: " dollars"}
|
||||
// str := numberCodec.Encode(match)
|
||||
// // str is "Price: 42 dollars"
|
||||
//
|
||||
// // Non-matching string fails validation
|
||||
// validation := numberCodec.Decode("no numbers here")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func Regex(re *regexp.Regexp) Type[prism.Match, string, string] {
|
||||
return FromRefinement(prism.RegexMatcher(re))
|
||||
}
|
||||
|
||||
// RegexNamed creates a bidirectional codec for regex pattern matching with named capture groups.
|
||||
// This codec can match strings against a regular expression with named groups and extract them
|
||||
// by name, then reconstruct the original string from the match data.
|
||||
//
|
||||
// The codec uses prism.NamedMatch which contains:
|
||||
// - Before: Text before the match
|
||||
// - Groups: Map of named capture groups (name -> matched text)
|
||||
// - Full: The complete matched text
|
||||
// - After: Text after the match
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Attempts to match the regex against the input string
|
||||
// - Encodes: Reconstructs the original string from a NamedMatch structure
|
||||
// - Validates: Ensures the string matches the regex pattern with named groups
|
||||
//
|
||||
// Parameters:
|
||||
// - re: A compiled regular expression with named capture groups (e.g., `(?P<name>pattern)`)
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[prism.NamedMatch, string, string] codec that handles named regex matching
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for matching email addresses with named groups
|
||||
// emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
|
||||
// emailCodec := RegexNamed(emailRegex)
|
||||
//
|
||||
// // Decode an email string
|
||||
// validation := emailCodec.Decode("john@example.com")
|
||||
// // validation is Right(NamedMatch{
|
||||
// // Before: "",
|
||||
// // Groups: map[string]string{"user": "john", "domain": "example.com"},
|
||||
// // Full: "john@example.com",
|
||||
// // After: ""
|
||||
// // })
|
||||
//
|
||||
// // Encode a NamedMatch back to string
|
||||
// match := prism.NamedMatch{
|
||||
// Before: "",
|
||||
// Groups: map[string]string{"user": "john", "domain": "example.com"},
|
||||
// Full: "john@example.com",
|
||||
// After: "",
|
||||
// }
|
||||
// str := emailCodec.Encode(match)
|
||||
// // str is "john@example.com"
|
||||
//
|
||||
// // Non-matching string fails validation
|
||||
// validation := emailCodec.Decode("not-an-email")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func RegexNamed(re *regexp.Regexp) Type[prism.NamedMatch, string, string] {
|
||||
return FromRefinement(prism.RegexNamedMatcher(re))
|
||||
}
|
||||
|
||||
// IntFromString creates a bidirectional codec for parsing integers from strings.
|
||||
// This codec converts string representations of integers to int values and vice versa.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string to an int using strconv.Atoi
|
||||
// - Encodes: Converts an int to its string representation using strconv.Itoa
|
||||
// - Validates: Ensures the string contains a valid integer (base 10)
|
||||
//
|
||||
// The codec accepts integers in base 10 format, with optional leading sign (+/-).
|
||||
// It does not accept hexadecimal, octal, or other number formats.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[int, string, string] codec that handles int/string conversions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intCodec := IntFromString()
|
||||
//
|
||||
// // Decode a valid integer string
|
||||
// validation := intCodec.Decode("42")
|
||||
// // validation is Right(42)
|
||||
//
|
||||
// // Decode negative integer
|
||||
// validation := intCodec.Decode("-123")
|
||||
// // validation is Right(-123)
|
||||
//
|
||||
// // Encode an integer to string
|
||||
// str := intCodec.Encode(42)
|
||||
// // str is "42"
|
||||
//
|
||||
// // Invalid integer string fails validation
|
||||
// validation := intCodec.Decode("not a number")
|
||||
// // validation is Left(ValidationError{...})
|
||||
//
|
||||
// // Floating point fails validation
|
||||
// validation := intCodec.Decode("3.14")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func IntFromString() Type[int, string, string] {
|
||||
return MakeType(
|
||||
"IntFromString",
|
||||
Is[int](),
|
||||
validateFromParser(strconv.Atoi),
|
||||
strconv.Itoa,
|
||||
)
|
||||
}
|
||||
|
||||
// Int64FromString creates a bidirectional codec for parsing 64-bit integers from strings.
|
||||
// This codec converts string representations of integers to int64 values and vice versa.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string to an int64 using strconv.ParseInt with base 10
|
||||
// - Encodes: Converts an int64 to its string representation
|
||||
// - Validates: Ensures the string contains a valid 64-bit integer (base 10)
|
||||
//
|
||||
// The codec accepts integers in base 10 format, with optional leading sign (+/-).
|
||||
// It supports the full range of int64 values (-9223372036854775808 to 9223372036854775807).
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[int64, string, string] codec that handles int64/string conversions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// int64Codec := Int64FromString()
|
||||
//
|
||||
// // Decode a valid integer string
|
||||
// validation := int64Codec.Decode("9223372036854775807")
|
||||
// // validation is Right(9223372036854775807)
|
||||
//
|
||||
// // Decode negative integer
|
||||
// validation := int64Codec.Decode("-9223372036854775808")
|
||||
// // validation is Right(-9223372036854775808)
|
||||
//
|
||||
// // Encode an int64 to string
|
||||
// str := int64Codec.Encode(42)
|
||||
// // str is "42"
|
||||
//
|
||||
// // Invalid integer string fails validation
|
||||
// validation := int64Codec.Decode("not a number")
|
||||
// // validation is Left(ValidationError{...})
|
||||
//
|
||||
// // Out of range value fails validation
|
||||
// validation := int64Codec.Decode("9223372036854775808")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func Int64FromString() Type[int64, string, string] {
|
||||
return MakeType(
|
||||
"Int64FromString",
|
||||
Is[int64](),
|
||||
validateFromParser(func(s string) (int64, error) { return strconv.ParseInt(s, 10, 64) }),
|
||||
prism.ParseInt64().ReverseGet,
|
||||
)
|
||||
}
|
||||
908
v2/optics/codec/codecs_test.go
Normal file
908
v2/optics/codec/codecs_test.go
Normal file
@@ -0,0 +1,908 @@
|
||||
// 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 codec
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestURL(t *testing.T) {
|
||||
urlCodec := URL()
|
||||
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, *url.URL](nil))
|
||||
|
||||
t.Run("decodes valid HTTP URL", func(t *testing.T) {
|
||||
result := urlCodec.Decode("https://example.com/path?query=value")
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode valid URL")
|
||||
|
||||
parsedURL := getOrElseNull(result)
|
||||
|
||||
require.NotNil(t, parsedURL)
|
||||
assert.Equal(t, "https", parsedURL.Scheme)
|
||||
assert.Equal(t, "example.com", parsedURL.Host)
|
||||
assert.Equal(t, "/path", parsedURL.Path)
|
||||
assert.Equal(t, "query=value", parsedURL.RawQuery)
|
||||
})
|
||||
|
||||
t.Run("decodes valid HTTP URL without path", func(t *testing.T) {
|
||||
result := urlCodec.Decode("https://example.com")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedURL := getOrElseNull(result)
|
||||
|
||||
require.NotNil(t, parsedURL)
|
||||
assert.Equal(t, "https", parsedURL.Scheme)
|
||||
assert.Equal(t, "example.com", parsedURL.Host)
|
||||
})
|
||||
|
||||
t.Run("decodes URL with port", func(t *testing.T) {
|
||||
result := urlCodec.Decode("http://localhost:8080/api")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedURL := getOrElseNull(result)
|
||||
|
||||
require.NotNil(t, parsedURL)
|
||||
assert.Equal(t, "http", parsedURL.Scheme)
|
||||
assert.Equal(t, "localhost:8080", parsedURL.Host)
|
||||
assert.Equal(t, "/api", parsedURL.Path)
|
||||
})
|
||||
|
||||
t.Run("decodes URL with fragment", func(t *testing.T) {
|
||||
result := urlCodec.Decode("https://example.com/page#section")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedURL := getOrElseNull(result)
|
||||
|
||||
require.NotNil(t, parsedURL)
|
||||
assert.Equal(t, "section", parsedURL.Fragment)
|
||||
})
|
||||
|
||||
t.Run("decodes relative URL", func(t *testing.T) {
|
||||
result := urlCodec.Decode("/path/to/resource")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedURL := getOrElseNull(result)
|
||||
|
||||
require.NotNil(t, parsedURL)
|
||||
assert.Equal(t, "/path/to/resource", parsedURL.Path)
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid URL", func(t *testing.T) {
|
||||
result := urlCodec.Decode("not a valid url ://")
|
||||
|
||||
assert.True(t, either.IsLeft(result), "should fail to decode invalid URL")
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(*url.URL) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("fails to decode URL with invalid characters", func(t *testing.T) {
|
||||
result := urlCodec.Decode("http://example.com/path with spaces")
|
||||
|
||||
// Note: url.Parse actually handles spaces, so let's test a truly invalid URL
|
||||
result = urlCodec.Decode("ht!tp://invalid")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("encodes URL to string", func(t *testing.T) {
|
||||
parsedURL, err := url.Parse("https://example.com/path?query=value")
|
||||
require.NoError(t, err)
|
||||
|
||||
encoded := urlCodec.Encode(parsedURL)
|
||||
|
||||
assert.Equal(t, "https://example.com/path?query=value", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes URL with fragment", func(t *testing.T) {
|
||||
parsedURL, err := url.Parse("https://example.com/page#section")
|
||||
require.NoError(t, err)
|
||||
|
||||
encoded := urlCodec.Encode(parsedURL)
|
||||
|
||||
assert.Equal(t, "https://example.com/page#section", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "https://example.com/path?key=value&foo=bar#fragment"
|
||||
|
||||
// Decode
|
||||
decodeResult := urlCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
parsedURL := getOrElseNull(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := urlCodec.Encode(parsedURL)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("codec has correct name", func(t *testing.T) {
|
||||
assert.Equal(t, "URL", urlCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDate(t *testing.T) {
|
||||
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, time.Time](time.Time{}))
|
||||
|
||||
t.Run("ISO 8601 date format", func(t *testing.T) {
|
||||
dateCodec := Date("2006-01-02")
|
||||
|
||||
t.Run("decodes valid date", func(t *testing.T) {
|
||||
result := dateCodec.Decode("2024-03-15")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedDate := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, 2024, parsedDate.Year())
|
||||
assert.Equal(t, time.March, parsedDate.Month())
|
||||
assert.Equal(t, 15, parsedDate.Day())
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid date format", func(t *testing.T) {
|
||||
result := dateCodec.Decode("15-03-2024")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(time.Time) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid date", func(t *testing.T) {
|
||||
result := dateCodec.Decode("2024-13-45")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode non-date string", func(t *testing.T) {
|
||||
result := dateCodec.Decode("not a date")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("encodes date to string", func(t *testing.T) {
|
||||
date := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
encoded := dateCodec.Encode(date)
|
||||
|
||||
assert.Equal(t, "2024-03-15", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "2024-12-25"
|
||||
|
||||
// Decode
|
||||
decodeResult := dateCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
parsedDate := getOrElseNull(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := dateCodec.Encode(parsedDate)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("RFC3339 timestamp format", func(t *testing.T) {
|
||||
timestampCodec := Date(time.RFC3339)
|
||||
|
||||
t.Run("decodes valid RFC3339 timestamp", func(t *testing.T) {
|
||||
result := timestampCodec.Decode("2024-03-15T10:30:00Z")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedTime := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, 2024, parsedTime.Year())
|
||||
assert.Equal(t, time.March, parsedTime.Month())
|
||||
assert.Equal(t, 15, parsedTime.Day())
|
||||
assert.Equal(t, 10, parsedTime.Hour())
|
||||
assert.Equal(t, 30, parsedTime.Minute())
|
||||
assert.Equal(t, 0, parsedTime.Second())
|
||||
})
|
||||
|
||||
t.Run("decodes RFC3339 with timezone offset", func(t *testing.T) {
|
||||
result := timestampCodec.Decode("2024-03-15T10:30:00+01:00")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedTime := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, 2024, parsedTime.Year())
|
||||
assert.Equal(t, time.March, parsedTime.Month())
|
||||
assert.Equal(t, 15, parsedTime.Day())
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid RFC3339", func(t *testing.T) {
|
||||
result := timestampCodec.Decode("2024-03-15 10:30:00")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("encodes timestamp to RFC3339 string", func(t *testing.T) {
|
||||
timestamp := time.Date(2024, 3, 15, 10, 30, 0, 0, time.UTC)
|
||||
|
||||
encoded := timestampCodec.Encode(timestamp)
|
||||
|
||||
assert.Equal(t, "2024-03-15T10:30:00Z", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "2024-12-25T15:45:30Z"
|
||||
|
||||
// Decode
|
||||
decodeResult := timestampCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
parsedTime := getOrElseNull(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := timestampCodec.Encode(parsedTime)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("custom date format", func(t *testing.T) {
|
||||
customCodec := Date("02/01/2006")
|
||||
|
||||
t.Run("decodes custom format", func(t *testing.T) {
|
||||
result := customCodec.Decode("15/03/2024")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedDate := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, 2024, parsedDate.Year())
|
||||
assert.Equal(t, time.March, parsedDate.Month())
|
||||
assert.Equal(t, 15, parsedDate.Day())
|
||||
})
|
||||
|
||||
t.Run("encodes to custom format", func(t *testing.T) {
|
||||
date := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
encoded := customCodec.Encode(date)
|
||||
|
||||
assert.Equal(t, "15/03/2024", encoded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("codec has correct name", func(t *testing.T) {
|
||||
dateCodec := Date("2006-01-02")
|
||||
assert.Equal(t, "Date", dateCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateFromParser(t *testing.T) {
|
||||
t.Run("successful parsing", func(t *testing.T) {
|
||||
// Create a simple parser that always succeeds
|
||||
parser := func(s string) (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
|
||||
validator := validateFromParser(parser)
|
||||
decode := validator("test")
|
||||
|
||||
// Execute with empty context
|
||||
result := decode(validation.Context{})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("failed parsing", func(t *testing.T) {
|
||||
// Create a parser that always fails
|
||||
parser := func(s string) (int, error) {
|
||||
return 0, assert.AnError
|
||||
}
|
||||
|
||||
validator := validateFromParser(parser)
|
||||
decode := validator("test")
|
||||
|
||||
// Execute with empty context
|
||||
result := decode(validation.Context{})
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
|
||||
// Check that the error contains the input value
|
||||
if len(errors) > 0 {
|
||||
assert.Equal(t, "test", errors[0].Value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parser with context", func(t *testing.T) {
|
||||
parser := func(s string) (string, error) {
|
||||
if s == "" {
|
||||
return "", assert.AnError
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
validator := validateFromParser(parser)
|
||||
|
||||
// Test with context
|
||||
ctx := validation.Context{
|
||||
{Key: "field", Type: "string"},
|
||||
}
|
||||
|
||||
decode := validator("")
|
||||
result := decode(ctx)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(string) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
|
||||
// Verify context is preserved
|
||||
if len(errors) > 0 {
|
||||
assert.Equal(t, ctx, errors[0].Context)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegex(t *testing.T) {
|
||||
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](prism.Match{}))
|
||||
|
||||
t.Run("simple number pattern", func(t *testing.T) {
|
||||
numberRegex := regexp.MustCompile(`\d+`)
|
||||
regexCodec := Regex(numberRegex)
|
||||
|
||||
t.Run("decodes string with number", func(t *testing.T) {
|
||||
result := regexCodec.Decode("Price: 42 dollars")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "Price: ", match.Before)
|
||||
assert.Equal(t, []string{"42"}, match.Groups)
|
||||
assert.Equal(t, " dollars", match.After)
|
||||
})
|
||||
|
||||
t.Run("decodes number at start", func(t *testing.T) {
|
||||
result := regexCodec.Decode("123 items")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "", match.Before)
|
||||
assert.Equal(t, []string{"123"}, match.Groups)
|
||||
assert.Equal(t, " items", match.After)
|
||||
})
|
||||
|
||||
t.Run("decodes number at end", func(t *testing.T) {
|
||||
result := regexCodec.Decode("Total: 999")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "Total: ", match.Before)
|
||||
assert.Equal(t, []string{"999"}, match.Groups)
|
||||
assert.Equal(t, "", match.After)
|
||||
})
|
||||
|
||||
t.Run("fails to decode string without number", func(t *testing.T) {
|
||||
result := regexCodec.Decode("no numbers here")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(prism.Match) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("encodes Match to string", func(t *testing.T) {
|
||||
match := prism.Match{
|
||||
Before: "Price: ",
|
||||
Groups: []string{"42"},
|
||||
After: " dollars",
|
||||
}
|
||||
|
||||
encoded := regexCodec.Encode(match)
|
||||
|
||||
assert.Equal(t, "Price: 42 dollars", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "Count: 789 items"
|
||||
|
||||
// Decode
|
||||
decodeResult := regexCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
match := getOrElseNull(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := regexCodec.Encode(match)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("pattern with capture groups", func(t *testing.T) {
|
||||
// Pattern to match word followed by number
|
||||
wordNumberRegex := regexp.MustCompile(`(\w+)(\d+)`)
|
||||
regexCodec := Regex(wordNumberRegex)
|
||||
|
||||
t.Run("decodes with capture groups", func(t *testing.T) {
|
||||
result := regexCodec.Decode("item42")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "", match.Before)
|
||||
// Groups contains the full match and capture groups
|
||||
require.NotEmpty(t, match.Groups)
|
||||
assert.Equal(t, "item42", match.Groups[0])
|
||||
// Verify we have capture groups
|
||||
if len(match.Groups) > 1 {
|
||||
assert.Contains(t, match.Groups[1], "item")
|
||||
assert.Contains(t, match.Groups[len(match.Groups)-1], "2")
|
||||
}
|
||||
assert.Equal(t, "", match.After)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("codec name contains pattern info", func(t *testing.T) {
|
||||
numberRegex := regexp.MustCompile(`\d+`)
|
||||
regexCodec := Regex(numberRegex)
|
||||
// The name is generated by FromRefinement and includes the pattern
|
||||
assert.Contains(t, regexCodec.Name(), "FromRefinement")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegexNamed(t *testing.T) {
|
||||
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](prism.NamedMatch{}))
|
||||
|
||||
t.Run("email pattern with named groups", func(t *testing.T) {
|
||||
emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
|
||||
emailCodec := RegexNamed(emailRegex)
|
||||
|
||||
t.Run("decodes valid email", func(t *testing.T) {
|
||||
result := emailCodec.Decode("john@example.com")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "", match.Before)
|
||||
assert.Equal(t, "john@example.com", match.Full)
|
||||
assert.Equal(t, "", match.After)
|
||||
require.NotNil(t, match.Groups)
|
||||
assert.Equal(t, "john", match.Groups["user"])
|
||||
assert.Equal(t, "example.com", match.Groups["domain"])
|
||||
})
|
||||
|
||||
t.Run("decodes email with surrounding text", func(t *testing.T) {
|
||||
result := emailCodec.Decode("Contact: alice@test.org for info")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "Contact: ", match.Before)
|
||||
assert.Equal(t, "alice@test.org", match.Full)
|
||||
assert.Equal(t, " for info", match.After)
|
||||
assert.Equal(t, "alice", match.Groups["user"])
|
||||
assert.Equal(t, "test.org", match.Groups["domain"])
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid email", func(t *testing.T) {
|
||||
result := emailCodec.Decode("not-an-email")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(prism.NamedMatch) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("encodes NamedMatch to string", func(t *testing.T) {
|
||||
match := prism.NamedMatch{
|
||||
Before: "Email: ",
|
||||
Groups: map[string]string{"user": "bob", "domain": "example.com"},
|
||||
Full: "bob@example.com",
|
||||
After: "",
|
||||
}
|
||||
|
||||
encoded := emailCodec.Encode(match)
|
||||
|
||||
assert.Equal(t, "Email: bob@example.com", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "Contact: support@company.io"
|
||||
|
||||
// Decode
|
||||
decodeResult := emailCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
match := getOrElseNull(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := emailCodec.Encode(match)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("phone pattern with named groups", func(t *testing.T) {
|
||||
phoneRegex := regexp.MustCompile(`(?P<area>\d{3})-(?P<prefix>\d{3})-(?P<line>\d{4})`)
|
||||
phoneCodec := RegexNamed(phoneRegex)
|
||||
|
||||
t.Run("decodes valid phone number", func(t *testing.T) {
|
||||
result := phoneCodec.Decode("555-123-4567")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "555-123-4567", match.Full)
|
||||
assert.Equal(t, "555", match.Groups["area"])
|
||||
assert.Equal(t, "123", match.Groups["prefix"])
|
||||
assert.Equal(t, "4567", match.Groups["line"])
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid phone format", func(t *testing.T) {
|
||||
result := phoneCodec.Decode("123-45-6789")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("codec name contains refinement info", func(t *testing.T) {
|
||||
emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
|
||||
emailCodec := RegexNamed(emailRegex)
|
||||
// The name is generated by FromRefinement
|
||||
assert.Contains(t, emailCodec.Name(), "FromRefinement")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntFromString(t *testing.T) {
|
||||
intCodec := IntFromString()
|
||||
|
||||
t.Run("decodes positive integer", func(t *testing.T) {
|
||||
result := intCodec.Decode("42")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("decodes negative integer", func(t *testing.T) {
|
||||
result := intCodec.Decode("-123")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, -123, value)
|
||||
})
|
||||
|
||||
t.Run("decodes zero", func(t *testing.T) {
|
||||
result := intCodec.Decode("0")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("decodes integer with plus sign", func(t *testing.T) {
|
||||
result := intCodec.Decode("+456")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 456, value)
|
||||
})
|
||||
|
||||
t.Run("fails to decode floating point", func(t *testing.T) {
|
||||
result := intCodec.Decode("3.14")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("fails to decode non-numeric string", func(t *testing.T) {
|
||||
result := intCodec.Decode("not a number")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode empty string", func(t *testing.T) {
|
||||
result := intCodec.Decode("")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode hexadecimal", func(t *testing.T) {
|
||||
result := intCodec.Decode("0xFF")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode with whitespace", func(t *testing.T) {
|
||||
result := intCodec.Decode(" 42 ")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("encodes positive integer", func(t *testing.T) {
|
||||
encoded := intCodec.Encode(42)
|
||||
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes negative integer", func(t *testing.T) {
|
||||
encoded := intCodec.Encode(-123)
|
||||
|
||||
assert.Equal(t, "-123", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes zero", func(t *testing.T) {
|
||||
encoded := intCodec.Encode(0)
|
||||
|
||||
assert.Equal(t, "0", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "9876"
|
||||
|
||||
// Decode
|
||||
decodeResult := intCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
value := either.MonadFold(decodeResult,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Encode
|
||||
encoded := intCodec.Encode(value)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("codec has correct name", func(t *testing.T) {
|
||||
assert.Equal(t, "IntFromString", intCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestInt64FromString(t *testing.T) {
|
||||
int64Codec := Int64FromString()
|
||||
|
||||
t.Run("decodes positive int64", func(t *testing.T) {
|
||||
result := int64Codec.Decode("9223372036854775807")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int64 { return 0 },
|
||||
F.Identity[int64],
|
||||
)
|
||||
|
||||
assert.Equal(t, int64(9223372036854775807), value)
|
||||
})
|
||||
|
||||
t.Run("decodes negative int64", func(t *testing.T) {
|
||||
result := int64Codec.Decode("-9223372036854775808")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int64 { return 0 },
|
||||
F.Identity[int64],
|
||||
)
|
||||
|
||||
assert.Equal(t, int64(-9223372036854775808), value)
|
||||
})
|
||||
|
||||
t.Run("decodes zero", func(t *testing.T) {
|
||||
result := int64Codec.Decode("0")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int64 { return -1 },
|
||||
F.Identity[int64],
|
||||
)
|
||||
|
||||
assert.Equal(t, int64(0), value)
|
||||
})
|
||||
|
||||
t.Run("decodes small int64", func(t *testing.T) {
|
||||
result := int64Codec.Decode("42")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int64 { return 0 },
|
||||
F.Identity[int64],
|
||||
)
|
||||
|
||||
assert.Equal(t, int64(42), value)
|
||||
})
|
||||
|
||||
t.Run("fails to decode out of range positive", func(t *testing.T) {
|
||||
result := int64Codec.Decode("9223372036854775808")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int64) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("fails to decode out of range negative", func(t *testing.T) {
|
||||
result := int64Codec.Decode("-9223372036854775809")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode floating point", func(t *testing.T) {
|
||||
result := int64Codec.Decode("3.14")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode non-numeric string", func(t *testing.T) {
|
||||
result := int64Codec.Decode("not a number")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode empty string", func(t *testing.T) {
|
||||
result := int64Codec.Decode("")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("encodes positive int64", func(t *testing.T) {
|
||||
encoded := int64Codec.Encode(9223372036854775807)
|
||||
|
||||
assert.Equal(t, "9223372036854775807", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes negative int64", func(t *testing.T) {
|
||||
encoded := int64Codec.Encode(-9223372036854775808)
|
||||
|
||||
assert.Equal(t, "-9223372036854775808", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes zero", func(t *testing.T) {
|
||||
encoded := int64Codec.Encode(0)
|
||||
|
||||
assert.Equal(t, "0", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes small int64", func(t *testing.T) {
|
||||
encoded := int64Codec.Encode(42)
|
||||
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "1234567890123456"
|
||||
|
||||
// Decode
|
||||
decodeResult := int64Codec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
value := either.MonadFold(decodeResult,
|
||||
func(validation.Errors) int64 { return 0 },
|
||||
F.Identity[int64],
|
||||
)
|
||||
|
||||
// Encode
|
||||
encoded := int64Codec.Encode(value)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("codec has correct name", func(t *testing.T) {
|
||||
assert.Equal(t, "Int64FromString", int64Codec.Name())
|
||||
})
|
||||
}
|
||||
264
v2/optics/lenses/doc.go
Normal file
264
v2/optics/lenses/doc.go
Normal file
@@ -0,0 +1,264 @@
|
||||
// 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 lenses provides pre-built lens and prism implementations for common data structures.
|
||||
//
|
||||
// This package offers ready-to-use optics (lenses and prisms) for working with regex match
|
||||
// structures and URL components in a functional programming style. Lenses enable immutable
|
||||
// updates to nested data structures, while prisms provide safe optional access to fields.
|
||||
//
|
||||
// # Overview
|
||||
//
|
||||
// The package includes optics for:
|
||||
// - Match structures: For working with regex match results (indexed capture groups)
|
||||
// - NamedMatch structures: For working with regex matches with named capture groups
|
||||
// - url.Userinfo: For working with URL authentication information
|
||||
//
|
||||
// Each structure has three variants of optics:
|
||||
// - Value lenses: Work with value types (immutable updates)
|
||||
// - Reference lenses: Work with pointer types (mutable updates)
|
||||
// - Prisms: Provide optional access treating zero values as None
|
||||
//
|
||||
// # Lenses vs Prisms
|
||||
//
|
||||
// Lenses provide guaranteed access to a field within a structure:
|
||||
// - Get: Extract a field value
|
||||
// - Set: Update a field value (returns new structure for values, mutates for pointers)
|
||||
//
|
||||
// Prisms provide optional access to fields that may not be present:
|
||||
// - GetOption: Try to extract a field, returning Option[T]
|
||||
// - ReverseGet: Construct a structure from a field value
|
||||
//
|
||||
// # Match Structures
|
||||
//
|
||||
// Match represents a regex match with indexed capture groups:
|
||||
//
|
||||
// type Match struct {
|
||||
// Before string // Text before the match
|
||||
// Groups []string // Capture groups (index 0 is full match)
|
||||
// After string // Text after the match
|
||||
// }
|
||||
//
|
||||
// NamedMatch represents a regex match with named capture groups:
|
||||
//
|
||||
// type NamedMatch struct {
|
||||
// Before string // Text before the match
|
||||
// Groups map[string]string // Named capture groups
|
||||
// Full string // Full matched text
|
||||
// After string // Text after the match
|
||||
// }
|
||||
//
|
||||
// # Usage Examples
|
||||
//
|
||||
// Working with Match (value-based):
|
||||
//
|
||||
// lenses := MakeMatchLenses()
|
||||
// match := Match{
|
||||
// Before: "Hello ",
|
||||
// Groups: []string{"world", "world"},
|
||||
// After: "!",
|
||||
// }
|
||||
//
|
||||
// // Get a field
|
||||
// before := lenses.Before.Get(match) // "Hello "
|
||||
//
|
||||
// // Update a field (returns new Match)
|
||||
// updated := lenses.Before.Set(match, "Hi ")
|
||||
// // updated.Before is "Hi ", original match unchanged
|
||||
//
|
||||
// // Use optional lens (treats empty string as None)
|
||||
// emptyMatch := Match{Before: "", Groups: []string{}, After: ""}
|
||||
// beforeOpt := lenses.BeforeO.GetOption(emptyMatch) // None
|
||||
//
|
||||
// Working with Match (reference-based):
|
||||
//
|
||||
// lenses := MakeMatchRefLenses()
|
||||
// match := &Match{
|
||||
// Before: "Hello ",
|
||||
// Groups: []string{"world"},
|
||||
// After: "!",
|
||||
// }
|
||||
//
|
||||
// // Get a field
|
||||
// before := lenses.Before.Get(match) // "Hello "
|
||||
//
|
||||
// // Update a field (mutates the pointer)
|
||||
// lenses.Before.Set(match, "Hi ")
|
||||
// // match.Before is now "Hi "
|
||||
//
|
||||
// // Use prism for optional access
|
||||
// afterOpt := lenses.AfterP.GetOption(match) // Some("!")
|
||||
//
|
||||
// Working with NamedMatch:
|
||||
//
|
||||
// lenses := MakeNamedMatchLenses()
|
||||
// match := NamedMatch{
|
||||
// Before: "Email: ",
|
||||
// Groups: map[string]string{
|
||||
// "user": "john",
|
||||
// "domain": "example.com",
|
||||
// },
|
||||
// Full: "john@example.com",
|
||||
// After: "",
|
||||
// }
|
||||
//
|
||||
// // Get field values
|
||||
// full := lenses.Full.Get(match) // "john@example.com"
|
||||
// groups := lenses.Groups.Get(match) // map with user and domain
|
||||
//
|
||||
// // Update a field
|
||||
// updated := lenses.Before.Set(match, "Contact: ")
|
||||
//
|
||||
// // Use optional lens
|
||||
// afterOpt := lenses.AfterO.GetOption(match) // None (empty string)
|
||||
//
|
||||
// Working with url.Userinfo:
|
||||
//
|
||||
// lenses := MakeUserinfoRefLenses()
|
||||
// userinfo := url.UserPassword("john", "secret123")
|
||||
//
|
||||
// // Get username
|
||||
// username := lenses.Username.Get(userinfo) // "john"
|
||||
//
|
||||
// // Update password (returns new Userinfo)
|
||||
// updated := lenses.Password.Set(userinfo, "newpass")
|
||||
//
|
||||
// // Use optional lens for password
|
||||
// pwdOpt := lenses.PasswordO.GetOption(userinfo) // Some("secret123")
|
||||
//
|
||||
// // Handle userinfo without password
|
||||
// userOnly := url.User("alice")
|
||||
//
|
||||
// Working with url.URL:
|
||||
//
|
||||
// lenses := MakeURLLenses()
|
||||
// u := url.URL{
|
||||
// Scheme: "https",
|
||||
// Host: "example.com",
|
||||
// Path: "/api/v1/users",
|
||||
// }
|
||||
//
|
||||
// // Get field values
|
||||
// scheme := lenses.Scheme.Get(u) // "https"
|
||||
// host := lenses.Host.Get(u) // "example.com"
|
||||
//
|
||||
// // Update fields (returns new URL)
|
||||
// updated := lenses.Path.Set("/api/v2/users")(u)
|
||||
// // updated.Path is "/api/v2/users", original u unchanged
|
||||
//
|
||||
// // Use optional lens for query string
|
||||
// queryOpt := lenses.RawQueryO.Get(u) // None (no query string)
|
||||
//
|
||||
// // Set query string
|
||||
// withQuery := lenses.RawQuery.Set("page=1&limit=10")(u)
|
||||
//
|
||||
// Working with url.Error:
|
||||
//
|
||||
// lenses := MakeErrorLenses()
|
||||
// urlErr := url.Error{
|
||||
// Op: "Get",
|
||||
// URL: "https://example.com",
|
||||
// Err: errors.New("connection timeout"),
|
||||
// }
|
||||
//
|
||||
// // Get field values
|
||||
// op := lenses.Op.Get(urlErr) // "Get"
|
||||
// urlStr := lenses.URL.Get(urlErr) // "https://example.com"
|
||||
// err := lenses.Err.Get(urlErr) // error: "connection timeout"
|
||||
//
|
||||
// // Update fields (returns new Error)
|
||||
// updated := lenses.Op.Set("Post")(urlErr)
|
||||
// // updated.Op is "Post", original urlErr unchanged
|
||||
// pwdOpt = lenses.PasswordO.GetOption(userOnly) // None
|
||||
//
|
||||
// # Composing Optics
|
||||
//
|
||||
// Lenses and prisms can be composed to access nested structures:
|
||||
//
|
||||
// // Compose lenses to access nested fields
|
||||
// outerLens := MakeSomeLens()
|
||||
// innerLens := MakeSomeOtherLens()
|
||||
// composed := lens.Compose(outerLens, innerLens)
|
||||
//
|
||||
// // Compose prisms for optional nested access
|
||||
// outerPrism := MakeSomePrism()
|
||||
// innerPrism := MakeSomeOtherPrism()
|
||||
// composed := prism.Compose(outerPrism, innerPrism)
|
||||
//
|
||||
// # Optional Lenses
|
||||
//
|
||||
// Optional lenses (suffixed with 'O') treat zero values as None:
|
||||
// - Empty strings become None
|
||||
// - Zero values of other types become None
|
||||
// - Non-zero values become Some(value)
|
||||
//
|
||||
// This is useful for distinguishing between "field not set" and "field set to zero value":
|
||||
//
|
||||
// lenses := MakeMatchLenses()
|
||||
// match := Match{Before: "", Groups: []string{"test"}, After: "!"}
|
||||
//
|
||||
// // Regular lens returns empty string
|
||||
// before := lenses.Before.Get(match) // ""
|
||||
//
|
||||
// // Optional lens returns None
|
||||
// beforeOpt := lenses.BeforeO.GetOption(match) // None
|
||||
//
|
||||
// // Setting None clears the field
|
||||
// cleared := lenses.BeforeO.Set(match, option.None[string]())
|
||||
// // cleared.Before is ""
|
||||
//
|
||||
// // Setting Some updates the field
|
||||
// updated := lenses.BeforeO.Set(match, option.Some("prefix "))
|
||||
// // updated.Before is "prefix "
|
||||
//
|
||||
// # Code Generation
|
||||
//
|
||||
// This package uses code generation for creating lens implementations.
|
||||
// The generate directive at the top of this file triggers the lens generator:
|
||||
//
|
||||
// //go:generate go run ../../main.go lens --dir . --filename gen_lens.go
|
||||
//
|
||||
// To regenerate lenses after modifying structures, run:
|
||||
//
|
||||
// go generate ./optics/lenses
|
||||
//
|
||||
// # Performance Considerations
|
||||
//
|
||||
// Value-based lenses (MatchLenses, NamedMatchLenses):
|
||||
// - Create new structures on each Set operation
|
||||
// - Safe for concurrent use (immutable)
|
||||
// - Suitable for functional programming patterns
|
||||
//
|
||||
// Reference-based lenses (MatchRefLenses, NamedMatchRefLenses):
|
||||
// - Mutate existing structures
|
||||
// - More efficient for repeated updates
|
||||
// - Require careful handling in concurrent contexts
|
||||
//
|
||||
// # Related Packages
|
||||
//
|
||||
// - github.com/IBM/fp-go/v2/optics/lens: Core lens functionality
|
||||
// - github.com/IBM/fp-go/v2/optics/prism: Core prism functionality
|
||||
// - github.com/IBM/fp-go/v2/optics/iso: Isomorphisms for type conversions
|
||||
// - github.com/IBM/fp-go/v2/option: Option type for optional values
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// For more information on functional optics:
|
||||
// - Lens laws: https://github.com/IBM/fp-go/blob/main/optics/lens/README.md
|
||||
// - Prism laws: https://github.com/IBM/fp-go/blob/main/optics/prism/README.md
|
||||
// - Optics tutorial: https://github.com/IBM/fp-go/blob/main/docs/optics.md
|
||||
package lenses
|
||||
|
||||
//go:generate go run ../../main.go lens --dir . --filename gen_lens.go
|
||||
627
v2/optics/lenses/matcher.go
Normal file
627
v2/optics/lenses/matcher.go
Normal file
@@ -0,0 +1,627 @@
|
||||
// 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 lenses
|
||||
|
||||
import (
|
||||
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
|
||||
__lens "github.com/IBM/fp-go/v2/optics/lens"
|
||||
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
__prism "github.com/IBM/fp-go/v2/optics/prism"
|
||||
__option "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// MatchLenses provides lenses for accessing and modifying fields of Match structures.
|
||||
// Lenses enable functional updates to immutable data structures by providing
|
||||
// composable getters and setters.
|
||||
//
|
||||
// This struct contains both mandatory lenses (for direct field access) and optional
|
||||
// lenses (for fields that may be zero values, treating them as Option types).
|
||||
//
|
||||
// Fields:
|
||||
// - Before: Lens for the text before the match
|
||||
// - Groups: Lens for the capture groups array
|
||||
// - After: Lens for the text after the match
|
||||
// - BeforeO: Optional lens treating empty Before as None
|
||||
// - AfterO: Optional lens treating empty After as None
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lenses := MakeMatchLenses()
|
||||
// match := Match{Before: "hello ", Groups: []string{"world"}, After: "!"}
|
||||
//
|
||||
// // Get a field value
|
||||
// before := lenses.Before.Get(match) // "hello "
|
||||
//
|
||||
// // Set a field value (returns new Match)
|
||||
// updated := lenses.Before.Set(match, "hi ")
|
||||
// // updated.Before is now "hi "
|
||||
type MatchLenses struct {
|
||||
// mandatory fields
|
||||
Before __lens.Lens[__prism.Match, string]
|
||||
Groups __lens.Lens[__prism.Match, []string]
|
||||
After __lens.Lens[__prism.Match, string]
|
||||
// optional fields
|
||||
BeforeO __lens_option.LensO[__prism.Match, string]
|
||||
AfterO __lens_option.LensO[__prism.Match, string]
|
||||
}
|
||||
|
||||
// MatchRefLenses provides lenses for accessing and modifying fields of Match structures
|
||||
// via pointers. This is useful when working with mutable references to Match values.
|
||||
//
|
||||
// In addition to standard lenses, this struct also includes prisms for each field,
|
||||
// which provide optional access patterns (useful for validation or conditional updates).
|
||||
//
|
||||
// Fields:
|
||||
// - Before, Groups, After: Standard lenses for pointer-based access
|
||||
// - BeforeO, AfterO: Optional lenses treating zero values as None
|
||||
// - BeforeP, GroupsP, AfterP: Prisms for optional field access
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lenses := MakeMatchRefLenses()
|
||||
// match := &Match{Before: "hello ", Groups: []string{"world"}, After: "!"}
|
||||
//
|
||||
// // Get a field value
|
||||
// before := lenses.Before.Get(match) // "hello "
|
||||
//
|
||||
// // Set a field value (mutates the pointer)
|
||||
// lenses.Before.Set(match, "hi ")
|
||||
// // match.Before is now "hi "
|
||||
type MatchRefLenses struct {
|
||||
// mandatory fields
|
||||
Before __lens.Lens[*__prism.Match, string]
|
||||
Groups __lens.Lens[*__prism.Match, []string]
|
||||
After __lens.Lens[*__prism.Match, string]
|
||||
// optional fields
|
||||
BeforeO __lens_option.LensO[*__prism.Match, string]
|
||||
AfterO __lens_option.LensO[*__prism.Match, string]
|
||||
// prisms
|
||||
BeforeP __prism.Prism[*__prism.Match, string]
|
||||
GroupsP __prism.Prism[*__prism.Match, []string]
|
||||
AfterP __prism.Prism[*__prism.Match, string]
|
||||
}
|
||||
|
||||
// MatchPrisms provides prisms for accessing fields of Match structures.
|
||||
// Prisms enable safe access to fields that may not be present (zero values),
|
||||
// returning Option types instead of direct values.
|
||||
//
|
||||
// Fields:
|
||||
// - Before: Prism for Before field (None if empty string)
|
||||
// - Groups: Prism for Groups field (always Some)
|
||||
// - After: Prism for After field (None if empty string)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// prisms := MakeMatchPrisms()
|
||||
// match := Match{Before: "", Groups: []string{"test"}, After: "!"}
|
||||
//
|
||||
// // Try to get Before (returns None because it's empty)
|
||||
// beforeOpt := prisms.Before.GetOption(match) // None
|
||||
//
|
||||
// // Get After (returns Some because it's non-empty)
|
||||
// afterOpt := prisms.After.GetOption(match) // Some("!")
|
||||
type MatchPrisms struct {
|
||||
Before __prism.Prism[__prism.Match, string]
|
||||
Groups __prism.Prism[__prism.Match, []string]
|
||||
After __prism.Prism[__prism.Match, string]
|
||||
}
|
||||
|
||||
// MakeMatchLenses creates a new MatchLenses with lenses for all fields of Match.
|
||||
// This function constructs both mandatory lenses (for direct field access) and
|
||||
// optional lenses (for treating zero values as Option types).
|
||||
//
|
||||
// The returned lenses enable functional-style updates to Match structures,
|
||||
// allowing you to get and set field values while maintaining immutability.
|
||||
//
|
||||
// Returns:
|
||||
// - A MatchLenses struct with lenses for Before, Groups, After fields,
|
||||
// plus optional lenses BeforeO and AfterO
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lenses := MakeMatchLenses()
|
||||
// match := Match{Before: "start ", Groups: []string{"middle"}, After: " end"}
|
||||
//
|
||||
// // Get field values
|
||||
// before := lenses.Before.Get(match) // "start "
|
||||
// groups := lenses.Groups.Get(match) // []string{"middle"}
|
||||
//
|
||||
// // Update a field (returns new Match)
|
||||
// updated := lenses.After.Set(match, " finish")
|
||||
// // updated is a new Match with After = " finish"
|
||||
//
|
||||
// // Use optional lens (treats empty string as None)
|
||||
// emptyMatch := Match{Before: "", Groups: []string{}, After: ""}
|
||||
// beforeOpt := lenses.BeforeO.GetOption(emptyMatch) // None
|
||||
func MakeMatchLenses() MatchLenses {
|
||||
// mandatory lenses
|
||||
lensBefore := __lens.MakeLensWithName(
|
||||
func(s __prism.Match) string { return s.Before },
|
||||
func(s __prism.Match, v string) __prism.Match { s.Before = v; return s },
|
||||
"Match.Before",
|
||||
)
|
||||
lensGroups := __lens.MakeLensWithName(
|
||||
func(s __prism.Match) []string { return s.Groups },
|
||||
func(s __prism.Match, v []string) __prism.Match { s.Groups = v; return s },
|
||||
"Match.Groups",
|
||||
)
|
||||
lensAfter := __lens.MakeLensWithName(
|
||||
func(s __prism.Match) string { return s.After },
|
||||
func(s __prism.Match, v string) __prism.Match { s.After = v; return s },
|
||||
"Match.After",
|
||||
)
|
||||
// optional lenses
|
||||
lensBeforeO := __lens_option.FromIso[__prism.Match](__iso_option.FromZero[string]())(lensBefore)
|
||||
lensAfterO := __lens_option.FromIso[__prism.Match](__iso_option.FromZero[string]())(lensAfter)
|
||||
return MatchLenses{
|
||||
// mandatory lenses
|
||||
Before: lensBefore,
|
||||
Groups: lensGroups,
|
||||
After: lensAfter,
|
||||
// optional lenses
|
||||
BeforeO: lensBeforeO,
|
||||
AfterO: lensAfterO,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeMatchRefLenses creates a new MatchRefLenses with lenses for all fields of *Match.
|
||||
// This function constructs lenses that work with pointers to Match structures,
|
||||
// enabling both immutable-style updates and direct mutations.
|
||||
//
|
||||
// The returned lenses include:
|
||||
// - Standard lenses for pointer-based field access
|
||||
// - Optional lenses for treating zero values as Option types
|
||||
// - Prisms for safe optional field access
|
||||
//
|
||||
// Returns:
|
||||
// - A MatchRefLenses struct with lenses and prisms for all Match fields
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lenses := MakeMatchRefLenses()
|
||||
// match := &Match{Before: "prefix ", Groups: []string{"data"}, After: " suffix"}
|
||||
//
|
||||
// // Get field value
|
||||
// before := lenses.Before.Get(match) // "prefix "
|
||||
//
|
||||
// // Set field value (mutates the pointer)
|
||||
// lenses.Before.Set(match, "new ")
|
||||
// // match.Before is now "new "
|
||||
//
|
||||
// // Use prism for optional access
|
||||
// afterOpt := lenses.AfterP.GetOption(match) // Some(" suffix")
|
||||
func MakeMatchRefLenses() MatchRefLenses {
|
||||
// mandatory lenses
|
||||
lensBefore := __lens.MakeLensStrictWithName(
|
||||
func(s *__prism.Match) string { return s.Before },
|
||||
func(s *__prism.Match, v string) *__prism.Match { s.Before = v; return s },
|
||||
"(*Match).Before",
|
||||
)
|
||||
lensGroups := __lens.MakeLensRefWithName(
|
||||
func(s *__prism.Match) []string { return s.Groups },
|
||||
func(s *__prism.Match, v []string) *__prism.Match { s.Groups = v; return s },
|
||||
"(*Match).Groups",
|
||||
)
|
||||
lensAfter := __lens.MakeLensStrictWithName(
|
||||
func(s *__prism.Match) string { return s.After },
|
||||
func(s *__prism.Match, v string) *__prism.Match { s.After = v; return s },
|
||||
"(*Match).After",
|
||||
)
|
||||
// optional lenses
|
||||
lensBeforeO := __lens_option.FromIso[*__prism.Match](__iso_option.FromZero[string]())(lensBefore)
|
||||
lensAfterO := __lens_option.FromIso[*__prism.Match](__iso_option.FromZero[string]())(lensAfter)
|
||||
return MatchRefLenses{
|
||||
// mandatory lenses
|
||||
Before: lensBefore,
|
||||
Groups: lensGroups,
|
||||
After: lensAfter,
|
||||
// optional lenses
|
||||
BeforeO: lensBeforeO,
|
||||
AfterO: lensAfterO,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeMatchPrisms creates a new MatchPrisms with prisms for all fields of Match.
|
||||
// This function constructs prisms that provide safe optional access to Match fields,
|
||||
// treating zero values (empty strings) as None.
|
||||
//
|
||||
// The returned prisms enable pattern matching on field presence:
|
||||
// - Before and After prisms return None for empty strings
|
||||
// - Groups prism always returns Some (even for empty slices)
|
||||
//
|
||||
// Returns:
|
||||
// - A MatchPrisms struct with prisms for Before, Groups, and After fields
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// prisms := MakeMatchPrisms()
|
||||
// match := Match{Before: "", Groups: []string{"data"}, After: "!"}
|
||||
//
|
||||
// // Try to get Before (returns None because it's empty)
|
||||
// beforeOpt := prisms.Before.GetOption(match) // None
|
||||
//
|
||||
// // Get Groups (always returns Some)
|
||||
// groupsOpt := prisms.Groups.GetOption(match) // Some([]string{"data"})
|
||||
//
|
||||
// // Get After (returns Some because it's non-empty)
|
||||
// afterOpt := prisms.After.GetOption(match) // Some("!")
|
||||
//
|
||||
// // Construct a Match from a value using ReverseGet
|
||||
// newMatch := prisms.Before.ReverseGet("prefix ")
|
||||
// // newMatch is Match{Before: "prefix ", Groups: nil, After: ""}
|
||||
func MakeMatchPrisms() MatchPrisms {
|
||||
_fromNonZeroBefore := __option.FromNonZero[string]()
|
||||
_prismBefore := __prism.MakePrismWithName(
|
||||
func(s __prism.Match) __option.Option[string] { return _fromNonZeroBefore(s.Before) },
|
||||
func(v string) __prism.Match {
|
||||
return __prism.Match{Before: v}
|
||||
},
|
||||
"Match.Before",
|
||||
)
|
||||
_prismGroups := __prism.MakePrismWithName(
|
||||
func(s __prism.Match) __option.Option[[]string] { return __option.Some(s.Groups) },
|
||||
func(v []string) __prism.Match {
|
||||
return __prism.Match{Groups: v}
|
||||
},
|
||||
"Match.Groups",
|
||||
)
|
||||
_fromNonZeroAfter := __option.FromNonZero[string]()
|
||||
_prismAfter := __prism.MakePrismWithName(
|
||||
func(s __prism.Match) __option.Option[string] { return _fromNonZeroAfter(s.After) },
|
||||
func(v string) __prism.Match {
|
||||
return __prism.Match{After: v}
|
||||
},
|
||||
"Match.After",
|
||||
)
|
||||
return MatchPrisms{
|
||||
Before: _prismBefore,
|
||||
Groups: _prismGroups,
|
||||
After: _prismAfter,
|
||||
}
|
||||
}
|
||||
|
||||
// NamedMatchLenses provides lenses for accessing and modifying fields of NamedMatch structures.
|
||||
// NamedMatch represents regex matches with named capture groups, and these lenses enable
|
||||
// functional updates to its fields.
|
||||
//
|
||||
// This struct contains both mandatory lenses (for direct field access) and optional
|
||||
// lenses (for fields that may be zero values, treating them as Option types).
|
||||
//
|
||||
// Fields:
|
||||
// - Before: Lens for the text before the match
|
||||
// - Groups: Lens for the named capture groups map
|
||||
// - Full: Lens for the complete matched text
|
||||
// - After: Lens for the text after the match
|
||||
// - BeforeO, FullO, AfterO: Optional lenses treating empty strings as None
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lenses := MakeNamedMatchLenses()
|
||||
// match := NamedMatch{
|
||||
// Before: "Email: ",
|
||||
// Groups: map[string]string{"user": "john", "domain": "example.com"},
|
||||
// Full: "john@example.com",
|
||||
// After: "",
|
||||
// }
|
||||
//
|
||||
// // Get a field value
|
||||
// full := lenses.Full.Get(match) // "john@example.com"
|
||||
//
|
||||
// // Set a field value (returns new NamedMatch)
|
||||
// updated := lenses.Before.Set(match, "Contact: ")
|
||||
// // updated.Before is now "Contact: "
|
||||
type NamedMatchLenses struct {
|
||||
// mandatory fields
|
||||
Before __lens.Lens[__prism.NamedMatch, string]
|
||||
Groups __lens.Lens[__prism.NamedMatch, map[string]string]
|
||||
Full __lens.Lens[__prism.NamedMatch, string]
|
||||
After __lens.Lens[__prism.NamedMatch, string]
|
||||
// optional fields
|
||||
BeforeO __lens_option.LensO[__prism.NamedMatch, string]
|
||||
FullO __lens_option.LensO[__prism.NamedMatch, string]
|
||||
AfterO __lens_option.LensO[__prism.NamedMatch, string]
|
||||
}
|
||||
|
||||
// NamedMatchRefLenses provides lenses for accessing and modifying fields of NamedMatch
|
||||
// structures via pointers. This is useful when working with mutable references to
|
||||
// NamedMatch values.
|
||||
//
|
||||
// In addition to standard lenses, this struct also includes prisms for each field,
|
||||
// which provide optional access patterns (useful for validation or conditional updates).
|
||||
//
|
||||
// Fields:
|
||||
// - Before, Groups, Full, After: Standard lenses for pointer-based access
|
||||
// - BeforeO, FullO, AfterO: Optional lenses treating zero values as None
|
||||
// - BeforeP, GroupsP, FullP, AfterP: Prisms for optional field access
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lenses := MakeNamedMatchRefLenses()
|
||||
// match := &NamedMatch{
|
||||
// Before: "Email: ",
|
||||
// Groups: map[string]string{"user": "alice", "domain": "test.org"},
|
||||
// Full: "alice@test.org",
|
||||
// After: " for info",
|
||||
// }
|
||||
//
|
||||
// // Get a field value
|
||||
// full := lenses.Full.Get(match) // "alice@test.org"
|
||||
//
|
||||
// // Set a field value (mutates the pointer)
|
||||
// lenses.After.Set(match, " for contact")
|
||||
// // match.After is now " for contact"
|
||||
type NamedMatchRefLenses struct {
|
||||
// mandatory fields
|
||||
Before __lens.Lens[*__prism.NamedMatch, string]
|
||||
Groups __lens.Lens[*__prism.NamedMatch, map[string]string]
|
||||
Full __lens.Lens[*__prism.NamedMatch, string]
|
||||
After __lens.Lens[*__prism.NamedMatch, string]
|
||||
// optional fields
|
||||
BeforeO __lens_option.LensO[*__prism.NamedMatch, string]
|
||||
FullO __lens_option.LensO[*__prism.NamedMatch, string]
|
||||
AfterO __lens_option.LensO[*__prism.NamedMatch, string]
|
||||
// prisms
|
||||
BeforeP __prism.Prism[*__prism.NamedMatch, string]
|
||||
GroupsP __prism.Prism[*__prism.NamedMatch, map[string]string]
|
||||
FullP __prism.Prism[*__prism.NamedMatch, string]
|
||||
AfterP __prism.Prism[*__prism.NamedMatch, string]
|
||||
}
|
||||
|
||||
// NamedMatchPrisms provides prisms for accessing fields of NamedMatch structures.
|
||||
// Prisms enable safe access to fields that may not be present (zero values),
|
||||
// returning Option types instead of direct values.
|
||||
//
|
||||
// Fields:
|
||||
// - Before: Prism for Before field (None if empty string)
|
||||
// - Groups: Prism for Groups field (always Some)
|
||||
// - Full: Prism for Full field (None if empty string)
|
||||
// - After: Prism for After field (None if empty string)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// prisms := MakeNamedMatchPrisms()
|
||||
// match := NamedMatch{
|
||||
// Before: "",
|
||||
// Groups: map[string]string{"user": "bob"},
|
||||
// Full: "bob@example.com",
|
||||
// After: "",
|
||||
// }
|
||||
//
|
||||
// // Try to get Before (returns None because it's empty)
|
||||
// beforeOpt := prisms.Before.GetOption(match) // None
|
||||
//
|
||||
// // Get Full (returns Some because it's non-empty)
|
||||
// fullOpt := prisms.Full.GetOption(match) // Some("bob@example.com")
|
||||
type NamedMatchPrisms struct {
|
||||
Before __prism.Prism[__prism.NamedMatch, string]
|
||||
Groups __prism.Prism[__prism.NamedMatch, map[string]string]
|
||||
Full __prism.Prism[__prism.NamedMatch, string]
|
||||
After __prism.Prism[__prism.NamedMatch, string]
|
||||
}
|
||||
|
||||
// MakeNamedMatchLenses creates a new NamedMatchLenses with lenses for all fields of NamedMatch.
|
||||
// This function constructs both mandatory lenses (for direct field access) and
|
||||
// optional lenses (for treating zero values as Option types).
|
||||
//
|
||||
// The returned lenses enable functional-style updates to NamedMatch structures,
|
||||
// allowing you to get and set field values while maintaining immutability.
|
||||
//
|
||||
// Returns:
|
||||
// - A NamedMatchLenses struct with lenses for Before, Groups, Full, After fields,
|
||||
// plus optional lenses BeforeO, FullO, and AfterO
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lenses := MakeNamedMatchLenses()
|
||||
// match := NamedMatch{
|
||||
// Before: "Email: ",
|
||||
// Groups: map[string]string{"user": "john", "domain": "example.com"},
|
||||
// Full: "john@example.com",
|
||||
// After: "",
|
||||
// }
|
||||
//
|
||||
// // Get field values
|
||||
// full := lenses.Full.Get(match) // "john@example.com"
|
||||
// groups := lenses.Groups.Get(match) // map[string]string{"user": "john", ...}
|
||||
//
|
||||
// // Update a field (returns new NamedMatch)
|
||||
// updated := lenses.Before.Set(match, "Contact: ")
|
||||
// // updated is a new NamedMatch with Before = "Contact: "
|
||||
//
|
||||
// // Use optional lens (treats empty string as None)
|
||||
// afterOpt := lenses.AfterO.GetOption(match) // None (because After is empty)
|
||||
func MakeNamedMatchLenses() NamedMatchLenses {
|
||||
// mandatory lenses
|
||||
lensBefore := __lens.MakeLensWithName(
|
||||
func(s __prism.NamedMatch) string { return s.Before },
|
||||
func(s __prism.NamedMatch, v string) __prism.NamedMatch { s.Before = v; return s },
|
||||
"NamedMatch.Before",
|
||||
)
|
||||
lensGroups := __lens.MakeLensWithName(
|
||||
func(s __prism.NamedMatch) map[string]string { return s.Groups },
|
||||
func(s __prism.NamedMatch, v map[string]string) __prism.NamedMatch { s.Groups = v; return s },
|
||||
"NamedMatch.Groups",
|
||||
)
|
||||
lensFull := __lens.MakeLensWithName(
|
||||
func(s __prism.NamedMatch) string { return s.Full },
|
||||
func(s __prism.NamedMatch, v string) __prism.NamedMatch { s.Full = v; return s },
|
||||
"NamedMatch.Full",
|
||||
)
|
||||
lensAfter := __lens.MakeLensWithName(
|
||||
func(s __prism.NamedMatch) string { return s.After },
|
||||
func(s __prism.NamedMatch, v string) __prism.NamedMatch { s.After = v; return s },
|
||||
"NamedMatch.After",
|
||||
)
|
||||
// optional lenses
|
||||
lensBeforeO := __lens_option.FromIso[__prism.NamedMatch](__iso_option.FromZero[string]())(lensBefore)
|
||||
lensFullO := __lens_option.FromIso[__prism.NamedMatch](__iso_option.FromZero[string]())(lensFull)
|
||||
lensAfterO := __lens_option.FromIso[__prism.NamedMatch](__iso_option.FromZero[string]())(lensAfter)
|
||||
return NamedMatchLenses{
|
||||
// mandatory lenses
|
||||
Before: lensBefore,
|
||||
Groups: lensGroups,
|
||||
Full: lensFull,
|
||||
After: lensAfter,
|
||||
// optional lenses
|
||||
BeforeO: lensBeforeO,
|
||||
FullO: lensFullO,
|
||||
AfterO: lensAfterO,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeNamedMatchRefLenses creates a new NamedMatchRefLenses with lenses for all fields of *NamedMatch.
|
||||
// This function constructs lenses that work with pointers to NamedMatch structures,
|
||||
// enabling both immutable-style updates and direct mutations.
|
||||
//
|
||||
// The returned lenses include:
|
||||
// - Standard lenses for pointer-based field access
|
||||
// - Optional lenses for treating zero values as Option types
|
||||
// - Prisms for safe optional field access
|
||||
//
|
||||
// Returns:
|
||||
// - A NamedMatchRefLenses struct with lenses and prisms for all NamedMatch fields
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lenses := MakeNamedMatchRefLenses()
|
||||
// match := &NamedMatch{
|
||||
// Before: "Email: ",
|
||||
// Groups: map[string]string{"user": "alice", "domain": "test.org"},
|
||||
// Full: "alice@test.org",
|
||||
// After: "",
|
||||
// }
|
||||
//
|
||||
// // Get field value
|
||||
// full := lenses.Full.Get(match) // "alice@test.org"
|
||||
//
|
||||
// // Set field value (mutates the pointer)
|
||||
// lenses.Before.Set(match, "Contact: ")
|
||||
// // match.Before is now "Contact: "
|
||||
//
|
||||
// // Use prism for optional access
|
||||
// fullOpt := lenses.FullP.GetOption(match) // Some("alice@test.org")
|
||||
func MakeNamedMatchRefLenses() NamedMatchRefLenses {
|
||||
// mandatory lenses
|
||||
lensBefore := __lens.MakeLensStrictWithName(
|
||||
func(s *__prism.NamedMatch) string { return s.Before },
|
||||
func(s *__prism.NamedMatch, v string) *__prism.NamedMatch { s.Before = v; return s },
|
||||
"(*NamedMatch).Before",
|
||||
)
|
||||
lensGroups := __lens.MakeLensRefWithName(
|
||||
func(s *__prism.NamedMatch) map[string]string { return s.Groups },
|
||||
func(s *__prism.NamedMatch, v map[string]string) *__prism.NamedMatch { s.Groups = v; return s },
|
||||
"(*NamedMatch).Groups",
|
||||
)
|
||||
lensFull := __lens.MakeLensStrictWithName(
|
||||
func(s *__prism.NamedMatch) string { return s.Full },
|
||||
func(s *__prism.NamedMatch, v string) *__prism.NamedMatch { s.Full = v; return s },
|
||||
"(*NamedMatch).Full",
|
||||
)
|
||||
lensAfter := __lens.MakeLensStrictWithName(
|
||||
func(s *__prism.NamedMatch) string { return s.After },
|
||||
func(s *__prism.NamedMatch, v string) *__prism.NamedMatch { s.After = v; return s },
|
||||
"(*NamedMatch).After",
|
||||
)
|
||||
// optional lenses
|
||||
lensBeforeO := __lens_option.FromIso[*__prism.NamedMatch](__iso_option.FromZero[string]())(lensBefore)
|
||||
lensFullO := __lens_option.FromIso[*__prism.NamedMatch](__iso_option.FromZero[string]())(lensFull)
|
||||
lensAfterO := __lens_option.FromIso[*__prism.NamedMatch](__iso_option.FromZero[string]())(lensAfter)
|
||||
return NamedMatchRefLenses{
|
||||
// mandatory lenses
|
||||
Before: lensBefore,
|
||||
Groups: lensGroups,
|
||||
Full: lensFull,
|
||||
After: lensAfter,
|
||||
// optional lenses
|
||||
BeforeO: lensBeforeO,
|
||||
FullO: lensFullO,
|
||||
AfterO: lensAfterO,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeNamedMatchPrisms creates a new NamedMatchPrisms with prisms for all fields of NamedMatch.
|
||||
// This function constructs prisms that provide safe optional access to NamedMatch fields,
|
||||
// treating zero values (empty strings) as None.
|
||||
//
|
||||
// The returned prisms enable pattern matching on field presence:
|
||||
// - Before, Full, and After prisms return None for empty strings
|
||||
// - Groups prism always returns Some (even for nil or empty maps)
|
||||
//
|
||||
// Returns:
|
||||
// - A NamedMatchPrisms struct with prisms for Before, Groups, Full, and After fields
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// prisms := MakeNamedMatchPrisms()
|
||||
// match := NamedMatch{
|
||||
// Before: "",
|
||||
// Groups: map[string]string{"user": "bob", "domain": "example.com"},
|
||||
// Full: "bob@example.com",
|
||||
// After: "",
|
||||
// }
|
||||
//
|
||||
// // Try to get Before (returns None because it's empty)
|
||||
// beforeOpt := prisms.Before.GetOption(match) // None
|
||||
//
|
||||
// // Get Groups (always returns Some)
|
||||
// groupsOpt := prisms.Groups.GetOption(match)
|
||||
// // Some(map[string]string{"user": "bob", "domain": "example.com"})
|
||||
//
|
||||
// // Get Full (returns Some because it's non-empty)
|
||||
// fullOpt := prisms.Full.GetOption(match) // Some("bob@example.com")
|
||||
//
|
||||
// // Construct a NamedMatch from a value using ReverseGet
|
||||
// newMatch := prisms.Full.ReverseGet("test@example.com")
|
||||
// // newMatch is NamedMatch{Before: "", Groups: nil, Full: "test@example.com", After: ""}
|
||||
func MakeNamedMatchPrisms() NamedMatchPrisms {
|
||||
_fromNonZeroBefore := __option.FromNonZero[string]()
|
||||
_prismBefore := __prism.MakePrismWithName(
|
||||
func(s __prism.NamedMatch) __option.Option[string] { return _fromNonZeroBefore(s.Before) },
|
||||
func(v string) __prism.NamedMatch {
|
||||
return __prism.NamedMatch{Before: v}
|
||||
},
|
||||
"NamedMatch.Before",
|
||||
)
|
||||
_prismGroups := __prism.MakePrismWithName(
|
||||
func(s __prism.NamedMatch) __option.Option[map[string]string] { return __option.Some(s.Groups) },
|
||||
func(v map[string]string) __prism.NamedMatch {
|
||||
return __prism.NamedMatch{Groups: v}
|
||||
},
|
||||
"NamedMatch.Groups",
|
||||
)
|
||||
_fromNonZeroFull := __option.FromNonZero[string]()
|
||||
_prismFull := __prism.MakePrismWithName(
|
||||
func(s __prism.NamedMatch) __option.Option[string] { return _fromNonZeroFull(s.Full) },
|
||||
func(v string) __prism.NamedMatch {
|
||||
return __prism.NamedMatch{Full: v}
|
||||
},
|
||||
"NamedMatch.Full",
|
||||
)
|
||||
_fromNonZeroAfter := __option.FromNonZero[string]()
|
||||
_prismAfter := __prism.MakePrismWithName(
|
||||
func(s __prism.NamedMatch) __option.Option[string] { return _fromNonZeroAfter(s.After) },
|
||||
func(v string) __prism.NamedMatch {
|
||||
return __prism.NamedMatch{After: v}
|
||||
},
|
||||
"NamedMatch.After",
|
||||
)
|
||||
return NamedMatchPrisms{
|
||||
Before: _prismBefore,
|
||||
Groups: _prismGroups,
|
||||
Full: _prismFull,
|
||||
After: _prismAfter,
|
||||
}
|
||||
}
|
||||
558
v2/optics/lenses/matcher_test.go
Normal file
558
v2/optics/lenses/matcher_test.go
Normal file
@@ -0,0 +1,558 @@
|
||||
// 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 lenses
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
__prism "github.com/IBM/fp-go/v2/optics/prism"
|
||||
__option "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestMatchLenses_Before tests the Before lens for Match
|
||||
func TestMatchLenses_Before(t *testing.T) {
|
||||
lenses := MakeMatchLenses()
|
||||
match := __prism.Match{
|
||||
Before: "prefix ",
|
||||
Groups: []string{"match", "group1"},
|
||||
After: " suffix",
|
||||
}
|
||||
|
||||
// Test Get
|
||||
before := lenses.Before.Get(match)
|
||||
assert.Equal(t, "prefix ", before)
|
||||
|
||||
// Test Set (curried)
|
||||
updated := lenses.Before.Set("new prefix ")(match)
|
||||
assert.Equal(t, "new prefix ", updated.Before)
|
||||
assert.Equal(t, match.Groups, updated.Groups) // Other fields unchanged
|
||||
assert.Equal(t, match.After, updated.After)
|
||||
assert.Equal(t, "prefix ", match.Before) // Original unchanged
|
||||
}
|
||||
|
||||
// TestMatchLenses_Groups tests the Groups lens for Match
|
||||
func TestMatchLenses_Groups(t *testing.T) {
|
||||
lenses := MakeMatchLenses()
|
||||
match := __prism.Match{
|
||||
Before: "prefix ",
|
||||
Groups: []string{"match", "group1"},
|
||||
After: " suffix",
|
||||
}
|
||||
|
||||
// Test Get
|
||||
groups := lenses.Groups.Get(match)
|
||||
assert.Equal(t, []string{"match", "group1"}, groups)
|
||||
|
||||
// Test Set (curried)
|
||||
newGroups := []string{"new", "groups", "here"}
|
||||
updated := lenses.Groups.Set(newGroups)(match)
|
||||
assert.Equal(t, newGroups, updated.Groups)
|
||||
assert.Equal(t, match.Before, updated.Before)
|
||||
assert.Equal(t, match.After, updated.After)
|
||||
}
|
||||
|
||||
// TestMatchLenses_After tests the After lens for Match
|
||||
func TestMatchLenses_After(t *testing.T) {
|
||||
lenses := MakeMatchLenses()
|
||||
match := __prism.Match{
|
||||
Before: "prefix ",
|
||||
Groups: []string{"match"},
|
||||
After: " suffix",
|
||||
}
|
||||
|
||||
// Test Get
|
||||
after := lenses.After.Get(match)
|
||||
assert.Equal(t, " suffix", after)
|
||||
|
||||
// Test Set (curried)
|
||||
updated := lenses.After.Set(" new suffix")(match)
|
||||
assert.Equal(t, " new suffix", updated.After)
|
||||
assert.Equal(t, match.Before, updated.Before)
|
||||
assert.Equal(t, match.Groups, updated.Groups)
|
||||
}
|
||||
|
||||
// TestMatchLenses_BeforeO tests the optional Before lens
|
||||
func TestMatchLenses_BeforeO(t *testing.T) {
|
||||
lenses := MakeMatchLenses()
|
||||
|
||||
t.Run("non-empty Before", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "prefix ", Groups: []string{}, After: ""}
|
||||
opt := lenses.BeforeO.Get(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
value := __option.GetOrElse(func() string { return "" })(opt)
|
||||
assert.Equal(t, "prefix ", value)
|
||||
})
|
||||
|
||||
t.Run("empty Before", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
|
||||
opt := lenses.BeforeO.Get(match)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
})
|
||||
|
||||
t.Run("set Some", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
|
||||
updated := lenses.BeforeO.Set(__option.Some("new "))(match)
|
||||
assert.Equal(t, "new ", updated.Before)
|
||||
})
|
||||
|
||||
t.Run("set None", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "prefix ", Groups: []string{}, After: ""}
|
||||
updated := lenses.BeforeO.Set(__option.None[string]())(match)
|
||||
assert.Equal(t, "", updated.Before)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMatchLenses_AfterO tests the optional After lens
|
||||
func TestMatchLenses_AfterO(t *testing.T) {
|
||||
lenses := MakeMatchLenses()
|
||||
|
||||
t.Run("non-empty After", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "", Groups: []string{}, After: " suffix"}
|
||||
opt := lenses.AfterO.Get(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
value := __option.GetOrElse(func() string { return "" })(opt)
|
||||
assert.Equal(t, " suffix", value)
|
||||
})
|
||||
|
||||
t.Run("empty After", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
|
||||
opt := lenses.AfterO.Get(match)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMatchRefLenses_Before tests the Before lens for *Match
|
||||
func TestMatchRefLenses_Before(t *testing.T) {
|
||||
lenses := MakeMatchRefLenses()
|
||||
match := &__prism.Match{
|
||||
Before: "prefix ",
|
||||
Groups: []string{"match"},
|
||||
After: " suffix",
|
||||
}
|
||||
|
||||
// Test Get
|
||||
before := lenses.Before.Get(match)
|
||||
assert.Equal(t, "prefix ", before)
|
||||
|
||||
// Test Set (creates copy with MakeLensStrictWithName, curried)
|
||||
result := lenses.Before.Set("new prefix ")(match)
|
||||
assert.Equal(t, "new prefix ", result.Before)
|
||||
assert.Equal(t, "prefix ", match.Before) // Original unchanged
|
||||
assert.NotSame(t, match, result) // Returns new pointer
|
||||
}
|
||||
|
||||
// TestMatchRefLenses_Groups tests the Groups lens for *Match
|
||||
func TestMatchRefLenses_Groups(t *testing.T) {
|
||||
lenses := MakeMatchRefLenses()
|
||||
match := &__prism.Match{
|
||||
Before: "prefix ",
|
||||
Groups: []string{"match", "group1"},
|
||||
After: " suffix",
|
||||
}
|
||||
|
||||
// Test Get
|
||||
groups := lenses.Groups.Get(match)
|
||||
assert.Equal(t, []string{"match", "group1"}, groups)
|
||||
|
||||
// Test Set (creates copy with MakeLensRefWithName, curried)
|
||||
newGroups := []string{"new", "groups"}
|
||||
result := lenses.Groups.Set(newGroups)(match)
|
||||
assert.Equal(t, newGroups, result.Groups)
|
||||
assert.Equal(t, []string{"match", "group1"}, match.Groups) // Original unchanged
|
||||
assert.NotSame(t, match, result)
|
||||
}
|
||||
|
||||
// TestMatchRefLenses_After tests the After lens for *Match
|
||||
func TestMatchRefLenses_After(t *testing.T) {
|
||||
lenses := MakeMatchRefLenses()
|
||||
match := &__prism.Match{
|
||||
Before: "prefix ",
|
||||
Groups: []string{"match"},
|
||||
After: " suffix",
|
||||
}
|
||||
|
||||
// Test Get
|
||||
after := lenses.After.Get(match)
|
||||
assert.Equal(t, " suffix", after)
|
||||
|
||||
// Test Set (creates copy with MakeLensStrictWithName, curried)
|
||||
result := lenses.After.Set(" new suffix")(match)
|
||||
assert.Equal(t, " new suffix", result.After)
|
||||
assert.Equal(t, " suffix", match.After) // Original unchanged
|
||||
assert.NotSame(t, match, result)
|
||||
}
|
||||
|
||||
// TestMatchPrisms_Before tests the Before prism
|
||||
func TestMatchPrisms_Before(t *testing.T) {
|
||||
prisms := MakeMatchPrisms()
|
||||
|
||||
t.Run("GetOption with non-empty Before", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "prefix ", Groups: []string{}, After: ""}
|
||||
opt := prisms.Before.GetOption(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
value := __option.GetOrElse(func() string { return "" })(opt)
|
||||
assert.Equal(t, "prefix ", value)
|
||||
})
|
||||
|
||||
t.Run("GetOption with empty Before", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
|
||||
opt := prisms.Before.GetOption(match)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet", func(t *testing.T) {
|
||||
match := prisms.Before.ReverseGet("test ")
|
||||
assert.Equal(t, "test ", match.Before)
|
||||
assert.Nil(t, match.Groups)
|
||||
assert.Equal(t, "", match.After)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMatchPrisms_Groups tests the Groups prism
|
||||
func TestMatchPrisms_Groups(t *testing.T) {
|
||||
prisms := MakeMatchPrisms()
|
||||
|
||||
t.Run("GetOption always returns Some", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "", Groups: []string{"a", "b"}, After: ""}
|
||||
opt := prisms.Groups.GetOption(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
groups := __option.GetOrElse(func() []string { return nil })(opt)
|
||||
assert.Equal(t, []string{"a", "b"}, groups)
|
||||
})
|
||||
|
||||
t.Run("GetOption with nil Groups", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "", Groups: nil, After: ""}
|
||||
opt := prisms.Groups.GetOption(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet", func(t *testing.T) {
|
||||
groups := []string{"test", "groups"}
|
||||
match := prisms.Groups.ReverseGet(groups)
|
||||
assert.Equal(t, groups, match.Groups)
|
||||
assert.Equal(t, "", match.Before)
|
||||
assert.Equal(t, "", match.After)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMatchPrisms_After tests the After prism
|
||||
func TestMatchPrisms_After(t *testing.T) {
|
||||
prisms := MakeMatchPrisms()
|
||||
|
||||
t.Run("GetOption with non-empty After", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "", Groups: []string{}, After: " suffix"}
|
||||
opt := prisms.After.GetOption(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
value := __option.GetOrElse(func() string { return "" })(opt)
|
||||
assert.Equal(t, " suffix", value)
|
||||
})
|
||||
|
||||
t.Run("GetOption with empty After", func(t *testing.T) {
|
||||
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
|
||||
opt := prisms.After.GetOption(match)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
})
|
||||
|
||||
t.Run("ReverseGet", func(t *testing.T) {
|
||||
match := prisms.After.ReverseGet(" test")
|
||||
assert.Equal(t, " test", match.After)
|
||||
assert.Nil(t, match.Groups)
|
||||
assert.Equal(t, "", match.Before)
|
||||
})
|
||||
}
|
||||
|
||||
// TestNamedMatchLenses_Before tests the Before lens for NamedMatch
|
||||
func TestNamedMatchLenses_Before(t *testing.T) {
|
||||
lenses := MakeNamedMatchLenses()
|
||||
match := __prism.NamedMatch{
|
||||
Before: "Email: ",
|
||||
Groups: map[string]string{"user": "john", "domain": "example.com"},
|
||||
Full: "john@example.com",
|
||||
After: "",
|
||||
}
|
||||
|
||||
// Test Get
|
||||
before := lenses.Before.Get(match)
|
||||
assert.Equal(t, "Email: ", before)
|
||||
|
||||
// Test Set (curried)
|
||||
updated := lenses.Before.Set("Contact: ")(match)
|
||||
assert.Equal(t, "Contact: ", updated.Before)
|
||||
assert.Equal(t, match.Groups, updated.Groups)
|
||||
assert.Equal(t, match.Full, updated.Full)
|
||||
assert.Equal(t, match.After, updated.After)
|
||||
assert.Equal(t, "Email: ", match.Before) // Original unchanged
|
||||
}
|
||||
|
||||
// TestNamedMatchLenses_Groups tests the Groups lens for NamedMatch
|
||||
func TestNamedMatchLenses_Groups(t *testing.T) {
|
||||
lenses := MakeNamedMatchLenses()
|
||||
match := __prism.NamedMatch{
|
||||
Before: "Email: ",
|
||||
Groups: map[string]string{"user": "john", "domain": "example.com"},
|
||||
Full: "john@example.com",
|
||||
After: "",
|
||||
}
|
||||
|
||||
// Test Get
|
||||
groups := lenses.Groups.Get(match)
|
||||
assert.Equal(t, map[string]string{"user": "john", "domain": "example.com"}, groups)
|
||||
|
||||
// Test Set (curried)
|
||||
newGroups := map[string]string{"user": "alice", "domain": "test.org"}
|
||||
updated := lenses.Groups.Set(newGroups)(match)
|
||||
assert.Equal(t, newGroups, updated.Groups)
|
||||
assert.Equal(t, match.Before, updated.Before)
|
||||
}
|
||||
|
||||
// TestNamedMatchLenses_Full tests the Full lens for NamedMatch
|
||||
func TestNamedMatchLenses_Full(t *testing.T) {
|
||||
lenses := MakeNamedMatchLenses()
|
||||
match := __prism.NamedMatch{
|
||||
Before: "Email: ",
|
||||
Groups: map[string]string{"user": "john"},
|
||||
Full: "john@example.com",
|
||||
After: "",
|
||||
}
|
||||
|
||||
// Test Get
|
||||
full := lenses.Full.Get(match)
|
||||
assert.Equal(t, "john@example.com", full)
|
||||
|
||||
// Test Set (curried)
|
||||
updated := lenses.Full.Set("alice@test.org")(match)
|
||||
assert.Equal(t, "alice@test.org", updated.Full)
|
||||
assert.Equal(t, match.Before, updated.Before)
|
||||
}
|
||||
|
||||
// TestNamedMatchLenses_After tests the After lens for NamedMatch
|
||||
func TestNamedMatchLenses_After(t *testing.T) {
|
||||
lenses := MakeNamedMatchLenses()
|
||||
match := __prism.NamedMatch{
|
||||
Before: "Email: ",
|
||||
Groups: map[string]string{"user": "john"},
|
||||
Full: "john@example.com",
|
||||
After: " for contact",
|
||||
}
|
||||
|
||||
// Test Get
|
||||
after := lenses.After.Get(match)
|
||||
assert.Equal(t, " for contact", after)
|
||||
|
||||
// Test Set (curried)
|
||||
updated := lenses.After.Set(" for info")(match)
|
||||
assert.Equal(t, " for info", updated.After)
|
||||
assert.Equal(t, match.Before, updated.Before)
|
||||
}
|
||||
|
||||
// TestNamedMatchLenses_Optional tests optional lenses for NamedMatch
|
||||
func TestNamedMatchLenses_Optional(t *testing.T) {
|
||||
lenses := MakeNamedMatchLenses()
|
||||
|
||||
t.Run("BeforeO with non-empty", func(t *testing.T) {
|
||||
match := __prism.NamedMatch{Before: "prefix ", Groups: nil, Full: "", After: ""}
|
||||
opt := lenses.BeforeO.Get(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
})
|
||||
|
||||
t.Run("BeforeO with empty", func(t *testing.T) {
|
||||
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
|
||||
opt := lenses.BeforeO.Get(match)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
})
|
||||
|
||||
t.Run("FullO with non-empty", func(t *testing.T) {
|
||||
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "test@example.com", After: ""}
|
||||
opt := lenses.FullO.Get(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
value := __option.GetOrElse(func() string { return "" })(opt)
|
||||
assert.Equal(t, "test@example.com", value)
|
||||
})
|
||||
|
||||
t.Run("FullO with empty", func(t *testing.T) {
|
||||
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
|
||||
opt := lenses.FullO.Get(match)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
})
|
||||
|
||||
t.Run("AfterO with non-empty", func(t *testing.T) {
|
||||
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: " suffix"}
|
||||
opt := lenses.AfterO.Get(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
})
|
||||
|
||||
t.Run("AfterO with empty", func(t *testing.T) {
|
||||
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
|
||||
opt := lenses.AfterO.Get(match)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
})
|
||||
}
|
||||
|
||||
// TestNamedMatchRefLenses_Immutability tests that reference lenses create copies
|
||||
func TestNamedMatchRefLenses_Immutability(t *testing.T) {
|
||||
lenses := MakeNamedMatchRefLenses()
|
||||
match := &__prism.NamedMatch{
|
||||
Before: "Email: ",
|
||||
Groups: map[string]string{"user": "john"},
|
||||
Full: "john@example.com",
|
||||
After: "",
|
||||
}
|
||||
|
||||
// Test Before (creates copy, curried)
|
||||
updated1 := lenses.Before.Set("Contact: ")(match)
|
||||
assert.Equal(t, "Contact: ", updated1.Before)
|
||||
assert.Equal(t, "Email: ", match.Before) // Original unchanged
|
||||
|
||||
// Test Groups (creates copy, curried)
|
||||
newGroups := map[string]string{"user": "alice"}
|
||||
updated2 := lenses.Groups.Set(newGroups)(match)
|
||||
assert.Equal(t, newGroups, updated2.Groups)
|
||||
assert.Equal(t, map[string]string{"user": "john"}, match.Groups) // Original unchanged
|
||||
|
||||
// Test Full (creates copy, curried)
|
||||
updated3 := lenses.Full.Set("alice@test.org")(match)
|
||||
assert.Equal(t, "alice@test.org", updated3.Full)
|
||||
assert.Equal(t, "john@example.com", match.Full) // Original unchanged
|
||||
|
||||
// Test After (creates copy, curried)
|
||||
updated4 := lenses.After.Set(" for info")(match)
|
||||
assert.Equal(t, " for info", updated4.After)
|
||||
assert.Equal(t, "", match.After) // Original unchanged
|
||||
}
|
||||
|
||||
// TestNamedMatchPrisms tests prisms for NamedMatch
|
||||
func TestNamedMatchPrisms(t *testing.T) {
|
||||
prisms := MakeNamedMatchPrisms()
|
||||
|
||||
t.Run("Before prism", func(t *testing.T) {
|
||||
match := __prism.NamedMatch{Before: "prefix ", Groups: nil, Full: "", After: ""}
|
||||
opt := prisms.Before.GetOption(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
|
||||
emptyMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
|
||||
opt = prisms.Before.GetOption(emptyMatch)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
|
||||
constructed := prisms.Before.ReverseGet("test ")
|
||||
assert.Equal(t, "test ", constructed.Before)
|
||||
})
|
||||
|
||||
t.Run("Groups prism always returns Some", func(t *testing.T) {
|
||||
match := __prism.NamedMatch{
|
||||
Before: "",
|
||||
Groups: map[string]string{"key": "value"},
|
||||
Full: "",
|
||||
After: "",
|
||||
}
|
||||
opt := prisms.Groups.GetOption(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
|
||||
nilMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
|
||||
opt = prisms.Groups.GetOption(nilMatch)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
})
|
||||
|
||||
t.Run("Full prism", func(t *testing.T) {
|
||||
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "test@example.com", After: ""}
|
||||
opt := prisms.Full.GetOption(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
value := __option.GetOrElse(func() string { return "" })(opt)
|
||||
assert.Equal(t, "test@example.com", value)
|
||||
|
||||
emptyMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
|
||||
opt = prisms.Full.GetOption(emptyMatch)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
|
||||
constructed := prisms.Full.ReverseGet("alice@test.org")
|
||||
assert.Equal(t, "alice@test.org", constructed.Full)
|
||||
})
|
||||
|
||||
t.Run("After prism", func(t *testing.T) {
|
||||
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: " suffix"}
|
||||
opt := prisms.After.GetOption(match)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
|
||||
emptyMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
|
||||
opt = prisms.After.GetOption(emptyMatch)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
|
||||
constructed := prisms.After.ReverseGet(" test")
|
||||
assert.Equal(t, " test", constructed.After)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMatchLenses_Immutability verifies that value lenses don't mutate originals
|
||||
func TestMatchLenses_Immutability(t *testing.T) {
|
||||
lenses := MakeMatchLenses()
|
||||
original := __prism.Match{
|
||||
Before: "original ",
|
||||
Groups: []string{"group1", "group2"},
|
||||
After: " original",
|
||||
}
|
||||
|
||||
// Make a copy to compare later
|
||||
originalBefore := original.Before
|
||||
originalGroups := make([]string, len(original.Groups))
|
||||
copy(originalGroups, original.Groups)
|
||||
originalAfter := original.After
|
||||
|
||||
// Perform multiple updates (curried)
|
||||
updated1 := lenses.Before.Set("updated ")(original)
|
||||
updated2 := lenses.Groups.Set([]string{"new"})(updated1)
|
||||
updated3 := lenses.After.Set(" updated")(updated2)
|
||||
|
||||
// Verify original is unchanged
|
||||
assert.Equal(t, originalBefore, original.Before)
|
||||
assert.Equal(t, originalGroups, original.Groups)
|
||||
assert.Equal(t, originalAfter, original.After)
|
||||
|
||||
// Verify updates worked
|
||||
assert.Equal(t, "updated ", updated3.Before)
|
||||
assert.Equal(t, []string{"new"}, updated3.Groups)
|
||||
assert.Equal(t, " updated", updated3.After)
|
||||
}
|
||||
|
||||
// TestNamedMatchLenses_Immutability verifies that value lenses don't mutate originals
|
||||
func TestNamedMatchLenses_Immutability(t *testing.T) {
|
||||
lenses := MakeNamedMatchLenses()
|
||||
original := __prism.NamedMatch{
|
||||
Before: "original ",
|
||||
Groups: map[string]string{"key": "value"},
|
||||
Full: "original@example.com",
|
||||
After: " original",
|
||||
}
|
||||
|
||||
// Make copies to compare later
|
||||
originalBefore := original.Before
|
||||
originalFull := original.Full
|
||||
originalAfter := original.After
|
||||
|
||||
// Perform multiple updates (curried)
|
||||
updated1 := lenses.Before.Set("updated ")(original)
|
||||
updated2 := lenses.Full.Set("updated@test.org")(updated1)
|
||||
updated3 := lenses.After.Set(" updated")(updated2)
|
||||
|
||||
// Verify original is unchanged
|
||||
assert.Equal(t, originalBefore, original.Before)
|
||||
assert.Equal(t, originalFull, original.Full)
|
||||
assert.Equal(t, originalAfter, original.After)
|
||||
|
||||
// Verify updates worked
|
||||
assert.Equal(t, "updated ", updated3.Before)
|
||||
assert.Equal(t, "updated@test.org", updated3.Full)
|
||||
assert.Equal(t, " updated", updated3.After)
|
||||
}
|
||||
409
v2/optics/lenses/url.go
Normal file
409
v2/optics/lenses/url.go
Normal file
@@ -0,0 +1,409 @@
|
||||
package lenses
|
||||
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at
|
||||
// 2026-01-27 16:08:47.5483589 +0100 CET m=+0.003380301
|
||||
|
||||
import (
|
||||
url "net/url"
|
||||
|
||||
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
|
||||
__lens "github.com/IBM/fp-go/v2/optics/lens"
|
||||
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
__option "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// ErrorLenses provides lenses for accessing fields of url.Error
|
||||
type ErrorLenses struct {
|
||||
// mandatory fields
|
||||
Op __lens.Lens[url.Error, string]
|
||||
URL __lens.Lens[url.Error, string]
|
||||
Err __lens.Lens[url.Error, error]
|
||||
// optional fields
|
||||
OpO __lens_option.LensO[url.Error, string]
|
||||
URLO __lens_option.LensO[url.Error, string]
|
||||
ErrO __lens_option.LensO[url.Error, error]
|
||||
}
|
||||
|
||||
// ErrorRefLenses provides lenses for accessing fields of url.Error via a reference to url.Error
|
||||
type ErrorRefLenses struct {
|
||||
// mandatory fields
|
||||
Op __lens.Lens[*url.Error, string]
|
||||
URL __lens.Lens[*url.Error, string]
|
||||
Err __lens.Lens[*url.Error, error]
|
||||
// optional fields
|
||||
OpO __lens_option.LensO[*url.Error, string]
|
||||
URLO __lens_option.LensO[*url.Error, string]
|
||||
ErrO __lens_option.LensO[*url.Error, error]
|
||||
}
|
||||
|
||||
// MakeErrorLenses creates a new ErrorLenses with lenses for all fields
|
||||
func MakeErrorLenses() ErrorLenses {
|
||||
// mandatory lenses
|
||||
lensOp := __lens.MakeLensWithName(
|
||||
func(s url.Error) string { return s.Op },
|
||||
func(s url.Error, v string) url.Error { s.Op = v; return s },
|
||||
"Error.Op",
|
||||
)
|
||||
lensURL := __lens.MakeLensWithName(
|
||||
func(s url.Error) string { return s.URL },
|
||||
func(s url.Error, v string) url.Error { s.URL = v; return s },
|
||||
"Error.URL",
|
||||
)
|
||||
lensErr := __lens.MakeLensWithName(
|
||||
func(s url.Error) error { return s.Err },
|
||||
func(s url.Error, v error) url.Error { s.Err = v; return s },
|
||||
"Error.Err",
|
||||
)
|
||||
// optional lenses
|
||||
lensOpO := __lens_option.FromIso[url.Error](__iso_option.FromZero[string]())(lensOp)
|
||||
lensURLO := __lens_option.FromIso[url.Error](__iso_option.FromZero[string]())(lensURL)
|
||||
lensErrO := __lens_option.FromIso[url.Error](__iso_option.FromZero[error]())(lensErr)
|
||||
return ErrorLenses{
|
||||
// mandatory lenses
|
||||
Op: lensOp,
|
||||
URL: lensURL,
|
||||
Err: lensErr,
|
||||
// optional lenses
|
||||
OpO: lensOpO,
|
||||
URLO: lensURLO,
|
||||
ErrO: lensErrO,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeErrorRefLenses creates a new ErrorRefLenses with lenses for all fields
|
||||
func MakeErrorRefLenses() ErrorRefLenses {
|
||||
// mandatory lenses
|
||||
lensOp := __lens.MakeLensStrictWithName(
|
||||
func(s *url.Error) string { return s.Op },
|
||||
func(s *url.Error, v string) *url.Error { s.Op = v; return s },
|
||||
"(*url.Error).Op",
|
||||
)
|
||||
lensURL := __lens.MakeLensStrictWithName(
|
||||
func(s *url.Error) string { return s.URL },
|
||||
func(s *url.Error, v string) *url.Error { s.URL = v; return s },
|
||||
"(*url.Error).url.URL",
|
||||
)
|
||||
lensErr := __lens.MakeLensStrictWithName(
|
||||
func(s *url.Error) error { return s.Err },
|
||||
func(s *url.Error, v error) *url.Error { s.Err = v; return s },
|
||||
"(*url.Error).Err",
|
||||
)
|
||||
// optional lenses
|
||||
lensOpO := __lens_option.FromIso[*url.Error](__iso_option.FromZero[string]())(lensOp)
|
||||
lensURLO := __lens_option.FromIso[*url.Error](__iso_option.FromZero[string]())(lensURL)
|
||||
lensErrO := __lens_option.FromIso[*url.Error](__iso_option.FromZero[error]())(lensErr)
|
||||
return ErrorRefLenses{
|
||||
// mandatory lenses
|
||||
Op: lensOp,
|
||||
URL: lensURL,
|
||||
Err: lensErr,
|
||||
// optional lenses
|
||||
OpO: lensOpO,
|
||||
URLO: lensURLO,
|
||||
ErrO: lensErrO,
|
||||
}
|
||||
}
|
||||
|
||||
// URLLenses provides lenses for accessing fields of url.URL
|
||||
type URLLenses struct {
|
||||
// mandatory fields
|
||||
Scheme __lens.Lens[url.URL, string]
|
||||
Opaque __lens.Lens[url.URL, string]
|
||||
User __lens.Lens[url.URL, *url.Userinfo]
|
||||
Host __lens.Lens[url.URL, string]
|
||||
Path __lens.Lens[url.URL, string]
|
||||
RawPath __lens.Lens[url.URL, string]
|
||||
OmitHost __lens.Lens[url.URL, bool]
|
||||
ForceQuery __lens.Lens[url.URL, bool]
|
||||
RawQuery __lens.Lens[url.URL, string]
|
||||
Fragment __lens.Lens[url.URL, string]
|
||||
RawFragment __lens.Lens[url.URL, string]
|
||||
// optional fields
|
||||
SchemeO __lens_option.LensO[url.URL, string]
|
||||
OpaqueO __lens_option.LensO[url.URL, string]
|
||||
UserO __lens_option.LensO[url.URL, *url.Userinfo]
|
||||
HostO __lens_option.LensO[url.URL, string]
|
||||
PathO __lens_option.LensO[url.URL, string]
|
||||
RawPathO __lens_option.LensO[url.URL, string]
|
||||
OmitHostO __lens_option.LensO[url.URL, bool]
|
||||
ForceQueryO __lens_option.LensO[url.URL, bool]
|
||||
RawQueryO __lens_option.LensO[url.URL, string]
|
||||
FragmentO __lens_option.LensO[url.URL, string]
|
||||
RawFragmentO __lens_option.LensO[url.URL, string]
|
||||
}
|
||||
|
||||
// URLRefLenses provides lenses for accessing fields of url.URL via a reference to url.URL
|
||||
type URLRefLenses struct {
|
||||
// mandatory fields
|
||||
Scheme __lens.Lens[*url.URL, string]
|
||||
Opaque __lens.Lens[*url.URL, string]
|
||||
User __lens.Lens[*url.URL, *url.Userinfo]
|
||||
Host __lens.Lens[*url.URL, string]
|
||||
Path __lens.Lens[*url.URL, string]
|
||||
RawPath __lens.Lens[*url.URL, string]
|
||||
OmitHost __lens.Lens[*url.URL, bool]
|
||||
ForceQuery __lens.Lens[*url.URL, bool]
|
||||
RawQuery __lens.Lens[*url.URL, string]
|
||||
Fragment __lens.Lens[*url.URL, string]
|
||||
RawFragment __lens.Lens[*url.URL, string]
|
||||
// optional fields
|
||||
SchemeO __lens_option.LensO[*url.URL, string]
|
||||
OpaqueO __lens_option.LensO[*url.URL, string]
|
||||
UserO __lens_option.LensO[*url.URL, *url.Userinfo]
|
||||
HostO __lens_option.LensO[*url.URL, string]
|
||||
PathO __lens_option.LensO[*url.URL, string]
|
||||
RawPathO __lens_option.LensO[*url.URL, string]
|
||||
OmitHostO __lens_option.LensO[*url.URL, bool]
|
||||
ForceQueryO __lens_option.LensO[*url.URL, bool]
|
||||
RawQueryO __lens_option.LensO[*url.URL, string]
|
||||
FragmentO __lens_option.LensO[*url.URL, string]
|
||||
RawFragmentO __lens_option.LensO[*url.URL, string]
|
||||
}
|
||||
|
||||
// MakeURLLenses creates a new URLLenses with lenses for all fields
|
||||
func MakeURLLenses() URLLenses {
|
||||
// mandatory lenses
|
||||
lensScheme := __lens.MakeLensWithName(
|
||||
func(s url.URL) string { return s.Scheme },
|
||||
func(s url.URL, v string) url.URL { s.Scheme = v; return s },
|
||||
"URL.Scheme",
|
||||
)
|
||||
lensOpaque := __lens.MakeLensWithName(
|
||||
func(s url.URL) string { return s.Opaque },
|
||||
func(s url.URL, v string) url.URL { s.Opaque = v; return s },
|
||||
"URL.Opaque",
|
||||
)
|
||||
lensUser := __lens.MakeLensWithName(
|
||||
func(s url.URL) *url.Userinfo { return s.User },
|
||||
func(s url.URL, v *url.Userinfo) url.URL { s.User = v; return s },
|
||||
"URL.User",
|
||||
)
|
||||
lensHost := __lens.MakeLensWithName(
|
||||
func(s url.URL) string { return s.Host },
|
||||
func(s url.URL, v string) url.URL { s.Host = v; return s },
|
||||
"URL.Host",
|
||||
)
|
||||
lensPath := __lens.MakeLensWithName(
|
||||
func(s url.URL) string { return s.Path },
|
||||
func(s url.URL, v string) url.URL { s.Path = v; return s },
|
||||
"URL.Path",
|
||||
)
|
||||
lensRawPath := __lens.MakeLensWithName(
|
||||
func(s url.URL) string { return s.RawPath },
|
||||
func(s url.URL, v string) url.URL { s.RawPath = v; return s },
|
||||
"URL.RawPath",
|
||||
)
|
||||
lensOmitHost := __lens.MakeLensWithName(
|
||||
func(s url.URL) bool { return s.OmitHost },
|
||||
func(s url.URL, v bool) url.URL { s.OmitHost = v; return s },
|
||||
"URL.OmitHost",
|
||||
)
|
||||
lensForceQuery := __lens.MakeLensWithName(
|
||||
func(s url.URL) bool { return s.ForceQuery },
|
||||
func(s url.URL, v bool) url.URL { s.ForceQuery = v; return s },
|
||||
"URL.ForceQuery",
|
||||
)
|
||||
lensRawQuery := __lens.MakeLensWithName(
|
||||
func(s url.URL) string { return s.RawQuery },
|
||||
func(s url.URL, v string) url.URL { s.RawQuery = v; return s },
|
||||
"URL.RawQuery",
|
||||
)
|
||||
lensFragment := __lens.MakeLensWithName(
|
||||
func(s url.URL) string { return s.Fragment },
|
||||
func(s url.URL, v string) url.URL { s.Fragment = v; return s },
|
||||
"URL.Fragment",
|
||||
)
|
||||
lensRawFragment := __lens.MakeLensWithName(
|
||||
func(s url.URL) string { return s.RawFragment },
|
||||
func(s url.URL, v string) url.URL { s.RawFragment = v; return s },
|
||||
"URL.RawFragment",
|
||||
)
|
||||
// optional lenses
|
||||
lensSchemeO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensScheme)
|
||||
lensOpaqueO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensOpaque)
|
||||
lensUserO := __lens_option.FromIso[url.URL](__iso_option.FromZero[*url.Userinfo]())(lensUser)
|
||||
lensHostO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensHost)
|
||||
lensPathO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensPath)
|
||||
lensRawPathO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawPath)
|
||||
lensOmitHostO := __lens_option.FromIso[url.URL](__iso_option.FromZero[bool]())(lensOmitHost)
|
||||
lensForceQueryO := __lens_option.FromIso[url.URL](__iso_option.FromZero[bool]())(lensForceQuery)
|
||||
lensRawQueryO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawQuery)
|
||||
lensFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensFragment)
|
||||
lensRawFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawFragment)
|
||||
return URLLenses{
|
||||
// mandatory lenses
|
||||
Scheme: lensScheme,
|
||||
Opaque: lensOpaque,
|
||||
User: lensUser,
|
||||
Host: lensHost,
|
||||
Path: lensPath,
|
||||
RawPath: lensRawPath,
|
||||
OmitHost: lensOmitHost,
|
||||
ForceQuery: lensForceQuery,
|
||||
RawQuery: lensRawQuery,
|
||||
Fragment: lensFragment,
|
||||
RawFragment: lensRawFragment,
|
||||
// optional lenses
|
||||
SchemeO: lensSchemeO,
|
||||
OpaqueO: lensOpaqueO,
|
||||
UserO: lensUserO,
|
||||
HostO: lensHostO,
|
||||
PathO: lensPathO,
|
||||
RawPathO: lensRawPathO,
|
||||
OmitHostO: lensOmitHostO,
|
||||
ForceQueryO: lensForceQueryO,
|
||||
RawQueryO: lensRawQueryO,
|
||||
FragmentO: lensFragmentO,
|
||||
RawFragmentO: lensRawFragmentO,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeURLRefLenses creates a new URLRefLenses with lenses for all fields
|
||||
func MakeURLRefLenses() URLRefLenses {
|
||||
// mandatory lenses
|
||||
lensScheme := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) string { return s.Scheme },
|
||||
func(s *url.URL, v string) *url.URL { s.Scheme = v; return s },
|
||||
"(*url.URL).Scheme",
|
||||
)
|
||||
lensOpaque := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) string { return s.Opaque },
|
||||
func(s *url.URL, v string) *url.URL { s.Opaque = v; return s },
|
||||
"(*url.URL).Opaque",
|
||||
)
|
||||
lensUser := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) *url.Userinfo { return s.User },
|
||||
func(s *url.URL, v *url.Userinfo) *url.URL { s.User = v; return s },
|
||||
"(*url.URL).User",
|
||||
)
|
||||
lensHost := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) string { return s.Host },
|
||||
func(s *url.URL, v string) *url.URL { s.Host = v; return s },
|
||||
"(*url.URL).Host",
|
||||
)
|
||||
lensPath := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) string { return s.Path },
|
||||
func(s *url.URL, v string) *url.URL { s.Path = v; return s },
|
||||
"(*url.URL).Path",
|
||||
)
|
||||
lensRawPath := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) string { return s.RawPath },
|
||||
func(s *url.URL, v string) *url.URL { s.RawPath = v; return s },
|
||||
"(*url.URL).RawPath",
|
||||
)
|
||||
lensOmitHost := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) bool { return s.OmitHost },
|
||||
func(s *url.URL, v bool) *url.URL { s.OmitHost = v; return s },
|
||||
"(*url.URL).OmitHost",
|
||||
)
|
||||
lensForceQuery := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) bool { return s.ForceQuery },
|
||||
func(s *url.URL, v bool) *url.URL { s.ForceQuery = v; return s },
|
||||
"(*url.URL).ForceQuery",
|
||||
)
|
||||
lensRawQuery := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) string { return s.RawQuery },
|
||||
func(s *url.URL, v string) *url.URL { s.RawQuery = v; return s },
|
||||
"(*url.URL).RawQuery",
|
||||
)
|
||||
lensFragment := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) string { return s.Fragment },
|
||||
func(s *url.URL, v string) *url.URL { s.Fragment = v; return s },
|
||||
"(*url.URL).Fragment",
|
||||
)
|
||||
lensRawFragment := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) string { return s.RawFragment },
|
||||
func(s *url.URL, v string) *url.URL { s.RawFragment = v; return s },
|
||||
"(*url.URL).RawFragment",
|
||||
)
|
||||
// optional lenses
|
||||
lensSchemeO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensScheme)
|
||||
lensOpaqueO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensOpaque)
|
||||
lensUserO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[*url.Userinfo]())(lensUser)
|
||||
lensHostO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensHost)
|
||||
lensPathO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensPath)
|
||||
lensRawPathO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawPath)
|
||||
lensOmitHostO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[bool]())(lensOmitHost)
|
||||
lensForceQueryO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[bool]())(lensForceQuery)
|
||||
lensRawQueryO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawQuery)
|
||||
lensFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensFragment)
|
||||
lensRawFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawFragment)
|
||||
return URLRefLenses{
|
||||
// mandatory lenses
|
||||
Scheme: lensScheme,
|
||||
Opaque: lensOpaque,
|
||||
User: lensUser,
|
||||
Host: lensHost,
|
||||
Path: lensPath,
|
||||
RawPath: lensRawPath,
|
||||
OmitHost: lensOmitHost,
|
||||
ForceQuery: lensForceQuery,
|
||||
RawQuery: lensRawQuery,
|
||||
Fragment: lensFragment,
|
||||
RawFragment: lensRawFragment,
|
||||
// optional lenses
|
||||
SchemeO: lensSchemeO,
|
||||
OpaqueO: lensOpaqueO,
|
||||
UserO: lensUserO,
|
||||
HostO: lensHostO,
|
||||
PathO: lensPathO,
|
||||
RawPathO: lensRawPathO,
|
||||
OmitHostO: lensOmitHostO,
|
||||
ForceQueryO: lensForceQueryO,
|
||||
RawQueryO: lensRawQueryO,
|
||||
FragmentO: lensFragmentO,
|
||||
RawFragmentO: lensRawFragmentO,
|
||||
}
|
||||
}
|
||||
|
||||
// UserinfoRefLenses provides lenses for accessing fields of url.Userinfo via a reference to url.Userinfo
|
||||
type UserinfoRefLenses struct {
|
||||
// mandatory fields
|
||||
Username __lens.Lens[*url.Userinfo, string]
|
||||
Password __lens.Lens[*url.Userinfo, string]
|
||||
// optional fields
|
||||
UsernameO __lens_option.LensO[*url.Userinfo, string]
|
||||
PasswordO __lens_option.LensO[*url.Userinfo, string]
|
||||
}
|
||||
|
||||
// MakeUserinfoRefLenses creates a new UserinfoRefLenses with lenses for all fields
|
||||
func MakeUserinfoRefLenses() UserinfoRefLenses {
|
||||
// mandatory lenses
|
||||
lensUsername := __lens.MakeLensStrictWithName(
|
||||
(*url.Userinfo).Username,
|
||||
func(s *url.Userinfo, v string) *url.Userinfo {
|
||||
pwd, ok := s.Password()
|
||||
if ok {
|
||||
return url.UserPassword(v, pwd)
|
||||
}
|
||||
return url.User(v)
|
||||
},
|
||||
"(*url.Userinfo).Username",
|
||||
)
|
||||
lensPassword := __lens.MakeLensStrictWithName(
|
||||
func(s *url.Userinfo) string {
|
||||
pwd, _ := s.Password()
|
||||
return pwd
|
||||
},
|
||||
func(s *url.Userinfo, v string) *url.Userinfo { return url.UserPassword(s.Username(), v) },
|
||||
"(*url.Userinfo).Password",
|
||||
)
|
||||
// optional lenses
|
||||
lensUsernameO := __lens_option.FromIso[*url.Userinfo](__iso_option.FromZero[string]())(lensUsername)
|
||||
lensPasswordO := __lens.MakeLensStrictWithName(
|
||||
__option.FromValidation((*url.Userinfo).Password),
|
||||
func(s *url.Userinfo, v __option.Option[string]) *url.Userinfo {
|
||||
return __option.MonadFold(v, func() *url.Userinfo { return url.User(s.Username()) }, func(pwd string) *url.Userinfo { return url.UserPassword(s.Username(), pwd) })
|
||||
},
|
||||
"(*url.Userinfo).Password",
|
||||
)
|
||||
return UserinfoRefLenses{
|
||||
// mandatory lenses
|
||||
Username: lensUsername,
|
||||
Password: lensPassword,
|
||||
// optional lenses
|
||||
UsernameO: lensUsernameO,
|
||||
PasswordO: lensPasswordO,
|
||||
}
|
||||
}
|
||||
655
v2/optics/lenses/url_test.go
Normal file
655
v2/optics/lenses/url_test.go
Normal file
@@ -0,0 +1,655 @@
|
||||
// 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 lenses
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
__option "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestUserinfoRefLenses_Username tests the Username lens
|
||||
func TestUserinfoRefLenses_Username(t *testing.T) {
|
||||
lenses := MakeUserinfoRefLenses()
|
||||
|
||||
t.Run("Get username from UserPassword", func(t *testing.T) {
|
||||
userinfo := url.UserPassword("john", "secret123")
|
||||
username := lenses.Username.Get(userinfo)
|
||||
assert.Equal(t, "john", username)
|
||||
})
|
||||
|
||||
t.Run("Get username from User", func(t *testing.T) {
|
||||
userinfo := url.User("alice")
|
||||
username := lenses.Username.Get(userinfo)
|
||||
assert.Equal(t, "alice", username)
|
||||
})
|
||||
|
||||
t.Run("Set username with password", func(t *testing.T) {
|
||||
userinfo := url.UserPassword("john", "secret123")
|
||||
updated := lenses.Username.Set("bob")(userinfo)
|
||||
assert.Equal(t, "bob", updated.Username())
|
||||
// Password should be preserved
|
||||
pwd, ok := updated.Password()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "secret123", pwd)
|
||||
})
|
||||
|
||||
t.Run("Set username without password", func(t *testing.T) {
|
||||
userinfo := url.User("alice")
|
||||
updated := lenses.Username.Set("bob")(userinfo)
|
||||
assert.Equal(t, "bob", updated.Username())
|
||||
// Should still have no password
|
||||
_, ok := updated.Password()
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUserinfoRefLenses_Password tests the Password lens
|
||||
func TestUserinfoRefLenses_Password(t *testing.T) {
|
||||
lenses := MakeUserinfoRefLenses()
|
||||
|
||||
t.Run("Get password when present", func(t *testing.T) {
|
||||
userinfo := url.UserPassword("john", "secret123")
|
||||
password := lenses.Password.Get(userinfo)
|
||||
assert.Equal(t, "secret123", password)
|
||||
})
|
||||
|
||||
t.Run("Get password when absent", func(t *testing.T) {
|
||||
userinfo := url.User("alice")
|
||||
password := lenses.Password.Get(userinfo)
|
||||
assert.Equal(t, "", password)
|
||||
})
|
||||
|
||||
t.Run("Set password", func(t *testing.T) {
|
||||
userinfo := url.UserPassword("john", "secret123")
|
||||
updated := lenses.Password.Set("newpass")(userinfo)
|
||||
assert.Equal(t, "john", updated.Username())
|
||||
pwd, ok := updated.Password()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "newpass", pwd)
|
||||
})
|
||||
|
||||
t.Run("Set password on user without password", func(t *testing.T) {
|
||||
userinfo := url.User("alice")
|
||||
updated := lenses.Password.Set("newpass")(userinfo)
|
||||
assert.Equal(t, "alice", updated.Username())
|
||||
pwd, ok := updated.Password()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "newpass", pwd)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUserinfoRefLenses_UsernameO tests the optional Username lens
|
||||
func TestUserinfoRefLenses_UsernameO(t *testing.T) {
|
||||
lenses := MakeUserinfoRefLenses()
|
||||
|
||||
t.Run("Get non-empty username", func(t *testing.T) {
|
||||
userinfo := url.User("john")
|
||||
opt := lenses.UsernameO.Get(userinfo)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
value := __option.GetOrElse(func() string { return "" })(opt)
|
||||
assert.Equal(t, "john", value)
|
||||
})
|
||||
|
||||
t.Run("Get empty username", func(t *testing.T) {
|
||||
userinfo := url.User("")
|
||||
opt := lenses.UsernameO.Get(userinfo)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
})
|
||||
|
||||
t.Run("Set Some username", func(t *testing.T) {
|
||||
userinfo := url.User("")
|
||||
updated := lenses.UsernameO.Set(__option.Some("alice"))(userinfo)
|
||||
assert.Equal(t, "alice", updated.Username())
|
||||
})
|
||||
|
||||
t.Run("Set None username", func(t *testing.T) {
|
||||
userinfo := url.User("john")
|
||||
updated := lenses.UsernameO.Set(__option.None[string]())(userinfo)
|
||||
assert.Equal(t, "", updated.Username())
|
||||
})
|
||||
}
|
||||
|
||||
// TestUserinfoRefLenses_PasswordO tests the optional Password lens
|
||||
func TestUserinfoRefLenses_PasswordO(t *testing.T) {
|
||||
lenses := MakeUserinfoRefLenses()
|
||||
|
||||
t.Run("Get Some password when present", func(t *testing.T) {
|
||||
userinfo := url.UserPassword("john", "secret123")
|
||||
opt := lenses.PasswordO.Get(userinfo)
|
||||
assert.True(t, __option.IsSome(opt))
|
||||
value := __option.GetOrElse(func() string { return "" })(opt)
|
||||
assert.Equal(t, "secret123", value)
|
||||
})
|
||||
|
||||
t.Run("Get None password when absent", func(t *testing.T) {
|
||||
userinfo := url.User("alice")
|
||||
opt := lenses.PasswordO.Get(userinfo)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
})
|
||||
|
||||
t.Run("Set Some password", func(t *testing.T) {
|
||||
userinfo := url.User("john")
|
||||
updated := lenses.PasswordO.Set(__option.Some("newpass"))(userinfo)
|
||||
assert.Equal(t, "john", updated.Username())
|
||||
pwd, ok := updated.Password()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "newpass", pwd)
|
||||
})
|
||||
|
||||
t.Run("Set None password removes it", func(t *testing.T) {
|
||||
userinfo := url.UserPassword("john", "secret123")
|
||||
updated := lenses.PasswordO.Set(__option.None[string]())(userinfo)
|
||||
assert.Equal(t, "john", updated.Username())
|
||||
_, ok := updated.Password()
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("Set None password on user without password", func(t *testing.T) {
|
||||
userinfo := url.User("alice")
|
||||
updated := lenses.PasswordO.Set(__option.None[string]())(userinfo)
|
||||
assert.Equal(t, "alice", updated.Username())
|
||||
_, ok := updated.Password()
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUserinfoRefLenses_Composition tests composing lens operations
|
||||
func TestUserinfoRefLenses_Composition(t *testing.T) {
|
||||
lenses := MakeUserinfoRefLenses()
|
||||
|
||||
t.Run("Update both username and password", func(t *testing.T) {
|
||||
userinfo := url.UserPassword("john", "secret123")
|
||||
|
||||
// Update username first
|
||||
updated1 := lenses.Username.Set("alice")(userinfo)
|
||||
// Then update password
|
||||
updated2 := lenses.Password.Set("newpass")(updated1)
|
||||
|
||||
assert.Equal(t, "alice", updated2.Username())
|
||||
pwd, ok := updated2.Password()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "newpass", pwd)
|
||||
})
|
||||
|
||||
t.Run("Add password to user without password", func(t *testing.T) {
|
||||
userinfo := url.User("bob")
|
||||
updated := lenses.PasswordO.Set(__option.Some("pass123"))(userinfo)
|
||||
|
||||
assert.Equal(t, "bob", updated.Username())
|
||||
pwd, ok := updated.Password()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "pass123", pwd)
|
||||
})
|
||||
|
||||
t.Run("Remove password from user with password", func(t *testing.T) {
|
||||
userinfo := url.UserPassword("charlie", "oldpass")
|
||||
updated := lenses.PasswordO.Set(__option.None[string]())(userinfo)
|
||||
|
||||
assert.Equal(t, "charlie", updated.Username())
|
||||
_, ok := updated.Password()
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUserinfoRefLenses_EdgeCases tests edge cases
|
||||
func TestUserinfoRefLenses_EdgeCases(t *testing.T) {
|
||||
lenses := MakeUserinfoRefLenses()
|
||||
|
||||
t.Run("Empty username and password", func(t *testing.T) {
|
||||
userinfo := url.UserPassword("", "")
|
||||
|
||||
username := lenses.Username.Get(userinfo)
|
||||
assert.Equal(t, "", username)
|
||||
|
||||
password := lenses.Password.Get(userinfo)
|
||||
assert.Equal(t, "", password)
|
||||
})
|
||||
|
||||
t.Run("Special characters in username", func(t *testing.T) {
|
||||
userinfo := url.User("user@domain.com")
|
||||
username := lenses.Username.Get(userinfo)
|
||||
assert.Equal(t, "user@domain.com", username)
|
||||
|
||||
updated := lenses.Username.Set("new@user.com")(userinfo)
|
||||
assert.Equal(t, "new@user.com", updated.Username())
|
||||
})
|
||||
|
||||
t.Run("Special characters in password", func(t *testing.T) {
|
||||
userinfo := url.UserPassword("john", "p@$$w0rd!")
|
||||
password := lenses.Password.Get(userinfo)
|
||||
assert.Equal(t, "p@$$w0rd!", password)
|
||||
|
||||
updated := lenses.Password.Set("n3w!p@ss")(userinfo)
|
||||
pwd, ok := updated.Password()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "n3w!p@ss", pwd)
|
||||
})
|
||||
|
||||
t.Run("Very long username", func(t *testing.T) {
|
||||
longUsername := "verylongusernamethatexceedsnormallengthbutshouldbehanded"
|
||||
userinfo := url.User(longUsername)
|
||||
username := lenses.Username.Get(userinfo)
|
||||
assert.Equal(t, longUsername, username)
|
||||
})
|
||||
|
||||
t.Run("Very long password", func(t *testing.T) {
|
||||
longPassword := "verylongpasswordthatexceedsnormallengthbutshouldbehanded"
|
||||
userinfo := url.UserPassword("john", longPassword)
|
||||
password := lenses.Password.Get(userinfo)
|
||||
assert.Equal(t, longPassword, password)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUserinfoRefLenses_Immutability tests that operations return new instances
|
||||
func TestUserinfoRefLenses_Immutability(t *testing.T) {
|
||||
lenses := MakeUserinfoRefLenses()
|
||||
|
||||
t.Run("Setting username returns new instance", func(t *testing.T) {
|
||||
original := url.User("john")
|
||||
updated := lenses.Username.Set("alice")(original)
|
||||
|
||||
// Original should be unchanged
|
||||
assert.Equal(t, "john", original.Username())
|
||||
// Updated should have new value
|
||||
assert.Equal(t, "alice", updated.Username())
|
||||
// Should be different instances
|
||||
assert.NotSame(t, original, updated)
|
||||
})
|
||||
|
||||
t.Run("Setting password returns new instance", func(t *testing.T) {
|
||||
original := url.UserPassword("john", "pass1")
|
||||
updated := lenses.Password.Set("pass2")(original)
|
||||
|
||||
// Original should be unchanged
|
||||
pwd, _ := original.Password()
|
||||
assert.Equal(t, "pass1", pwd)
|
||||
// Updated should have new value
|
||||
pwd, _ = updated.Password()
|
||||
assert.Equal(t, "pass2", pwd)
|
||||
// Should be different instances
|
||||
assert.NotSame(t, original, updated)
|
||||
})
|
||||
|
||||
t.Run("Multiple updates create new instances", func(t *testing.T) {
|
||||
original := url.UserPassword("john", "pass1")
|
||||
updated1 := lenses.Username.Set("alice")(original)
|
||||
updated2 := lenses.Password.Set("pass2")(updated1)
|
||||
|
||||
// Original unchanged
|
||||
assert.Equal(t, "john", original.Username())
|
||||
pwd, _ := original.Password()
|
||||
assert.Equal(t, "pass1", pwd)
|
||||
|
||||
// First update has new username, old password
|
||||
assert.Equal(t, "alice", updated1.Username())
|
||||
pwd, _ = updated1.Password()
|
||||
assert.Equal(t, "pass1", pwd)
|
||||
|
||||
// Second update has new username and password
|
||||
assert.Equal(t, "alice", updated2.Username())
|
||||
pwd, _ = updated2.Password()
|
||||
assert.Equal(t, "pass2", pwd)
|
||||
|
||||
// All different instances
|
||||
assert.NotSame(t, original, updated1)
|
||||
assert.NotSame(t, updated1, updated2)
|
||||
assert.NotSame(t, original, updated2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUserinfoRefLenses_PasswordPresence tests password presence detection
|
||||
func TestUserinfoRefLenses_PasswordPresence(t *testing.T) {
|
||||
lenses := MakeUserinfoRefLenses()
|
||||
|
||||
t.Run("Distinguish between no password and empty password", func(t *testing.T) {
|
||||
// User with no password
|
||||
userNoPass := url.User("john")
|
||||
optNoPass := lenses.PasswordO.Get(userNoPass)
|
||||
assert.True(t, __option.IsNone(optNoPass))
|
||||
|
||||
// User with empty password (still has password set)
|
||||
userEmptyPass := url.UserPassword("john", "")
|
||||
optEmptyPass := lenses.PasswordO.Get(userEmptyPass)
|
||||
// Empty password is still Some (password is set, just empty)
|
||||
assert.True(t, __option.IsSome(optEmptyPass))
|
||||
value := __option.GetOrElse(func() string { return "default" })(optEmptyPass)
|
||||
assert.Equal(t, "", value)
|
||||
})
|
||||
|
||||
t.Run("Password lens returns empty string for no password", func(t *testing.T) {
|
||||
userinfo := url.User("john")
|
||||
password := lenses.Password.Get(userinfo)
|
||||
assert.Equal(t, "", password)
|
||||
|
||||
// But PasswordO returns None
|
||||
opt := lenses.PasswordO.Get(userinfo)
|
||||
assert.True(t, __option.IsNone(opt))
|
||||
})
|
||||
}
|
||||
|
||||
// TestErrorLenses tests lenses for url.Error
|
||||
func TestErrorLenses(t *testing.T) {
|
||||
lenses := MakeErrorLenses()
|
||||
|
||||
t.Run("Get and Set Op field", func(t *testing.T) {
|
||||
urlErr := url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://example.com",
|
||||
Err: assert.AnError,
|
||||
}
|
||||
|
||||
// Test Get
|
||||
op := lenses.Op.Get(urlErr)
|
||||
assert.Equal(t, "Get", op)
|
||||
|
||||
// Test Set (curried, returns new Error)
|
||||
updated := lenses.Op.Set("Post")(urlErr)
|
||||
assert.Equal(t, "Post", updated.Op)
|
||||
assert.Equal(t, "Get", urlErr.Op) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("Get and Set URL field", func(t *testing.T) {
|
||||
urlErr := url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://example.com",
|
||||
Err: assert.AnError,
|
||||
}
|
||||
|
||||
// Test Get
|
||||
urlStr := lenses.URL.Get(urlErr)
|
||||
assert.Equal(t, "https://example.com", urlStr)
|
||||
|
||||
// Test Set (curried)
|
||||
updated := lenses.URL.Set("https://newsite.com")(urlErr)
|
||||
assert.Equal(t, "https://newsite.com", updated.URL)
|
||||
assert.Equal(t, "https://example.com", urlErr.URL) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("Get and Set Err field", func(t *testing.T) {
|
||||
originalErr := assert.AnError
|
||||
urlErr := url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://example.com",
|
||||
Err: originalErr,
|
||||
}
|
||||
|
||||
// Test Get
|
||||
err := lenses.Err.Get(urlErr)
|
||||
assert.Equal(t, originalErr, err)
|
||||
|
||||
// Test Set (curried)
|
||||
newErr := errors.New("new error")
|
||||
updated := lenses.Err.Set(newErr)(urlErr)
|
||||
assert.Equal(t, newErr, updated.Err)
|
||||
assert.Equal(t, originalErr, urlErr.Err) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("Optional lenses", func(t *testing.T) {
|
||||
urlErr := url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://example.com",
|
||||
Err: assert.AnError,
|
||||
}
|
||||
|
||||
// Test OpO
|
||||
opOpt := lenses.OpO.Get(urlErr)
|
||||
assert.True(t, __option.IsSome(opOpt))
|
||||
|
||||
// Test with empty Op
|
||||
emptyErr := url.Error{Op: "", URL: "test", Err: nil}
|
||||
opOpt = lenses.OpO.Get(emptyErr)
|
||||
assert.True(t, __option.IsNone(opOpt))
|
||||
|
||||
// Test URLO
|
||||
urlOpt := lenses.URLO.Get(urlErr)
|
||||
assert.True(t, __option.IsSome(urlOpt))
|
||||
|
||||
// Test ErrO
|
||||
errOpt := lenses.ErrO.Get(urlErr)
|
||||
assert.True(t, __option.IsSome(errOpt))
|
||||
|
||||
// Test with nil error
|
||||
nilErrErr := url.Error{Op: "Get", URL: "test", Err: nil}
|
||||
errOpt = lenses.ErrO.Get(nilErrErr)
|
||||
assert.True(t, __option.IsNone(errOpt))
|
||||
})
|
||||
}
|
||||
|
||||
// TestErrorRefLenses tests reference lenses for url.Error
|
||||
func TestErrorRefLenses(t *testing.T) {
|
||||
lenses := MakeErrorRefLenses()
|
||||
|
||||
t.Run("Get and Set creates new instance", func(t *testing.T) {
|
||||
urlErr := &url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://example.com",
|
||||
Err: assert.AnError,
|
||||
}
|
||||
|
||||
// Test Get
|
||||
op := lenses.Op.Get(urlErr)
|
||||
assert.Equal(t, "Get", op)
|
||||
|
||||
// Test Set (creates copy)
|
||||
updated := lenses.Op.Set("Post")(urlErr)
|
||||
assert.Equal(t, "Post", updated.Op)
|
||||
assert.Equal(t, "Get", urlErr.Op) // Original unchanged
|
||||
assert.NotSame(t, urlErr, updated)
|
||||
})
|
||||
}
|
||||
|
||||
// TestURLLenses tests lenses for url.URL
|
||||
func TestURLLenses(t *testing.T) {
|
||||
lenses := MakeURLLenses()
|
||||
|
||||
t.Run("Get and Set Scheme", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com"}
|
||||
|
||||
scheme := lenses.Scheme.Get(u)
|
||||
assert.Equal(t, "https", scheme)
|
||||
|
||||
updated := lenses.Scheme.Set("http")(u)
|
||||
assert.Equal(t, "http", updated.Scheme)
|
||||
assert.Equal(t, "https", u.Scheme) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("Get and Set Host", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com"}
|
||||
|
||||
host := lenses.Host.Get(u)
|
||||
assert.Equal(t, "example.com", host)
|
||||
|
||||
updated := lenses.Host.Set("newsite.com")(u)
|
||||
assert.Equal(t, "newsite.com", updated.Host)
|
||||
assert.Equal(t, "example.com", u.Host)
|
||||
})
|
||||
|
||||
t.Run("Get and Set Path", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com", Path: "/api/v1"}
|
||||
|
||||
path := lenses.Path.Get(u)
|
||||
assert.Equal(t, "/api/v1", path)
|
||||
|
||||
updated := lenses.Path.Set("/api/v2")(u)
|
||||
assert.Equal(t, "/api/v2", updated.Path)
|
||||
assert.Equal(t, "/api/v1", u.Path)
|
||||
})
|
||||
|
||||
t.Run("Get and Set RawQuery", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com", RawQuery: "page=1"}
|
||||
|
||||
query := lenses.RawQuery.Get(u)
|
||||
assert.Equal(t, "page=1", query)
|
||||
|
||||
updated := lenses.RawQuery.Set("page=2&limit=10")(u)
|
||||
assert.Equal(t, "page=2&limit=10", updated.RawQuery)
|
||||
assert.Equal(t, "page=1", u.RawQuery)
|
||||
})
|
||||
|
||||
t.Run("Get and Set Fragment", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com", Fragment: "section1"}
|
||||
|
||||
fragment := lenses.Fragment.Get(u)
|
||||
assert.Equal(t, "section1", fragment)
|
||||
|
||||
updated := lenses.Fragment.Set("section2")(u)
|
||||
assert.Equal(t, "section2", updated.Fragment)
|
||||
assert.Equal(t, "section1", u.Fragment)
|
||||
})
|
||||
|
||||
t.Run("Get and Set User", func(t *testing.T) {
|
||||
userinfo := url.User("john")
|
||||
u := url.URL{Scheme: "https", Host: "example.com", User: userinfo}
|
||||
|
||||
user := lenses.User.Get(u)
|
||||
assert.Equal(t, userinfo, user)
|
||||
|
||||
newUser := url.UserPassword("alice", "pass")
|
||||
updated := lenses.User.Set(newUser)(u)
|
||||
assert.Equal(t, newUser, updated.User)
|
||||
assert.Equal(t, userinfo, u.User)
|
||||
})
|
||||
|
||||
t.Run("Boolean fields", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com", ForceQuery: true}
|
||||
|
||||
forceQuery := lenses.ForceQuery.Get(u)
|
||||
assert.True(t, forceQuery)
|
||||
|
||||
updated := lenses.ForceQuery.Set(false)(u)
|
||||
assert.False(t, updated.ForceQuery)
|
||||
assert.True(t, u.ForceQuery)
|
||||
})
|
||||
|
||||
t.Run("Optional lenses", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com", Path: "/test"}
|
||||
|
||||
// Non-empty scheme
|
||||
schemeOpt := lenses.SchemeO.Get(u)
|
||||
assert.True(t, __option.IsSome(schemeOpt))
|
||||
|
||||
// Empty RawQuery
|
||||
queryOpt := lenses.RawQueryO.Get(u)
|
||||
assert.True(t, __option.IsNone(queryOpt))
|
||||
|
||||
// Set Some
|
||||
withQuery := lenses.RawQueryO.Set(__option.Some("q=test"))(u)
|
||||
assert.Equal(t, "q=test", withQuery.RawQuery)
|
||||
|
||||
// Set None
|
||||
cleared := lenses.RawQueryO.Set(__option.None[string]())(withQuery)
|
||||
assert.Equal(t, "", cleared.RawQuery)
|
||||
})
|
||||
}
|
||||
|
||||
// TestURLRefLenses tests reference lenses for url.URL
|
||||
func TestURLRefLenses(t *testing.T) {
|
||||
lenses := MakeURLRefLenses()
|
||||
|
||||
t.Run("Creates new instances", func(t *testing.T) {
|
||||
u := &url.URL{Scheme: "https", Host: "example.com", Path: "/api"}
|
||||
|
||||
// Test Get
|
||||
scheme := lenses.Scheme.Get(u)
|
||||
assert.Equal(t, "https", scheme)
|
||||
|
||||
// Test Set (creates copy)
|
||||
updated := lenses.Scheme.Set("http")(u)
|
||||
assert.Equal(t, "http", updated.Scheme)
|
||||
assert.Equal(t, "https", u.Scheme) // Original unchanged
|
||||
assert.NotSame(t, u, updated)
|
||||
})
|
||||
|
||||
t.Run("Multiple field updates", func(t *testing.T) {
|
||||
u := &url.URL{Scheme: "https", Host: "example.com"}
|
||||
|
||||
updated1 := lenses.Path.Set("/api/v1")(u)
|
||||
updated2 := lenses.RawQuery.Set("page=1")(updated1)
|
||||
updated3 := lenses.Fragment.Set("top")(updated2)
|
||||
|
||||
// Original unchanged
|
||||
assert.Equal(t, "", u.Path)
|
||||
assert.Equal(t, "", u.RawQuery)
|
||||
assert.Equal(t, "", u.Fragment)
|
||||
|
||||
// Final result has all updates
|
||||
assert.Equal(t, "/api/v1", updated3.Path)
|
||||
assert.Equal(t, "page=1", updated3.RawQuery)
|
||||
assert.Equal(t, "top", updated3.Fragment)
|
||||
})
|
||||
}
|
||||
|
||||
// TestURLLenses_ComplexScenarios tests complex URL manipulation scenarios
|
||||
func TestURLLenses_ComplexScenarios(t *testing.T) {
|
||||
lenses := MakeURLLenses()
|
||||
|
||||
t.Run("Build URL incrementally", func(t *testing.T) {
|
||||
u := url.URL{}
|
||||
|
||||
u = lenses.Scheme.Set("https")(u)
|
||||
u = lenses.Host.Set("api.example.com")(u)
|
||||
u = lenses.Path.Set("/v1/users")(u)
|
||||
u = lenses.RawQuery.Set("limit=10&offset=0")(u)
|
||||
u = lenses.Fragment.Set("results")(u)
|
||||
|
||||
assert.Equal(t, "https", u.Scheme)
|
||||
assert.Equal(t, "api.example.com", u.Host)
|
||||
assert.Equal(t, "/v1/users", u.Path)
|
||||
assert.Equal(t, "limit=10&offset=0", u.RawQuery)
|
||||
assert.Equal(t, "results", u.Fragment)
|
||||
})
|
||||
|
||||
t.Run("Update URL with authentication", func(t *testing.T) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.com",
|
||||
Path: "/api",
|
||||
}
|
||||
|
||||
userinfo := url.UserPassword("admin", "secret")
|
||||
updated := lenses.User.Set(userinfo)(u)
|
||||
|
||||
assert.NotNil(t, updated.User)
|
||||
assert.Equal(t, "admin", updated.User.Username())
|
||||
pwd, ok := updated.User.Password()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "secret", pwd)
|
||||
})
|
||||
|
||||
t.Run("Clear optional fields", func(t *testing.T) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.com",
|
||||
Path: "/api",
|
||||
RawQuery: "page=1",
|
||||
Fragment: "top",
|
||||
}
|
||||
|
||||
// Clear query and fragment
|
||||
u = lenses.RawQueryO.Set(option.None[string]())(u)
|
||||
u = lenses.FragmentO.Set(option.None[string]())(u)
|
||||
|
||||
assert.Equal(t, "", u.RawQuery)
|
||||
assert.Equal(t, "", u.Fragment)
|
||||
assert.Equal(t, "https", u.Scheme) // Other fields unchanged
|
||||
assert.Equal(t, "example.com", u.Host)
|
||||
})
|
||||
}
|
||||
@@ -430,6 +430,8 @@ func FromNonZero[T comparable]() Prism[T, T] {
|
||||
// // Groups: []string{"123"},
|
||||
// // After: "",
|
||||
// // }
|
||||
//
|
||||
// fp-go:Lens
|
||||
type Match struct {
|
||||
Before string // Text before the match
|
||||
Groups []string // Capture groups (index 0 is full match)
|
||||
@@ -599,6 +601,8 @@ func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
|
||||
// // Full: "user@example.com",
|
||||
// // After: "",
|
||||
// // }
|
||||
//
|
||||
// fp-go:Lens
|
||||
type NamedMatch struct {
|
||||
Before string
|
||||
Groups map[string]string
|
||||
@@ -1097,3 +1101,163 @@ func FromOption[T any]() Prism[Option[T], T] {
|
||||
func NonEmptyString() Prism[string, string] {
|
||||
return FromNonZero[string]()
|
||||
}
|
||||
|
||||
// ErrorPrisms provides prisms for accessing fields of url.Error
|
||||
type ErrorPrisms struct {
|
||||
Op Prism[url.Error, string]
|
||||
URL Prism[url.Error, string]
|
||||
Err Prism[url.Error, error]
|
||||
}
|
||||
|
||||
// MakeErrorPrisms creates a new ErrorPrisms with prisms for all fields
|
||||
func MakeErrorPrisms() ErrorPrisms {
|
||||
_fromNonZeroOp := option.FromNonZero[string]()
|
||||
_prismOp := MakePrismWithName(
|
||||
func(s url.Error) Option[string] { return _fromNonZeroOp(s.Op) },
|
||||
func(v string) url.Error {
|
||||
return url.Error{Op: v}
|
||||
},
|
||||
"Error.Op",
|
||||
)
|
||||
_fromNonZeroURL := option.FromNonZero[string]()
|
||||
_prismURL := MakePrismWithName(
|
||||
func(s url.Error) Option[string] { return _fromNonZeroURL(s.URL) },
|
||||
func(v string) url.Error {
|
||||
return url.Error{URL: v}
|
||||
},
|
||||
"Error.URL",
|
||||
)
|
||||
_fromNonZeroErr := option.FromNonZero[error]()
|
||||
_prismErr := MakePrismWithName(
|
||||
func(s url.Error) Option[error] { return _fromNonZeroErr(s.Err) },
|
||||
func(v error) url.Error {
|
||||
return url.Error{Err: v}
|
||||
},
|
||||
"Error.Err",
|
||||
)
|
||||
return ErrorPrisms{
|
||||
Op: _prismOp,
|
||||
URL: _prismURL,
|
||||
Err: _prismErr,
|
||||
}
|
||||
}
|
||||
|
||||
// URLPrisms provides prisms for accessing fields of url.URL
|
||||
type URLPrisms struct {
|
||||
Scheme Prism[url.URL, string]
|
||||
Opaque Prism[url.URL, string]
|
||||
User Prism[url.URL, *url.Userinfo]
|
||||
Host Prism[url.URL, string]
|
||||
Path Prism[url.URL, string]
|
||||
RawPath Prism[url.URL, string]
|
||||
OmitHost Prism[url.URL, bool]
|
||||
ForceQuery Prism[url.URL, bool]
|
||||
RawQuery Prism[url.URL, string]
|
||||
Fragment Prism[url.URL, string]
|
||||
RawFragment Prism[url.URL, string]
|
||||
}
|
||||
|
||||
// MakeURLPrisms creates a new URLPrisms with prisms for all fields
|
||||
func MakeURLPrisms() URLPrisms {
|
||||
_fromNonZeroScheme := option.FromNonZero[string]()
|
||||
_prismScheme := MakePrismWithName(
|
||||
func(s url.URL) Option[string] { return _fromNonZeroScheme(s.Scheme) },
|
||||
func(v string) url.URL {
|
||||
return url.URL{Scheme: v}
|
||||
},
|
||||
"URL.Scheme",
|
||||
)
|
||||
_fromNonZeroOpaque := option.FromNonZero[string]()
|
||||
_prismOpaque := MakePrismWithName(
|
||||
func(s url.URL) Option[string] { return _fromNonZeroOpaque(s.Opaque) },
|
||||
func(v string) url.URL {
|
||||
return url.URL{Opaque: v}
|
||||
},
|
||||
"URL.Opaque",
|
||||
)
|
||||
_fromNonZeroUser := option.FromNonZero[*url.Userinfo]()
|
||||
_prismUser := MakePrismWithName(
|
||||
func(s url.URL) Option[*url.Userinfo] { return _fromNonZeroUser(s.User) },
|
||||
func(v *url.Userinfo) url.URL {
|
||||
return url.URL{User: v}
|
||||
},
|
||||
"URL.User",
|
||||
)
|
||||
_fromNonZeroHost := option.FromNonZero[string]()
|
||||
_prismHost := MakePrismWithName(
|
||||
func(s url.URL) Option[string] { return _fromNonZeroHost(s.Host) },
|
||||
func(v string) url.URL {
|
||||
return url.URL{Host: v}
|
||||
},
|
||||
"URL.Host",
|
||||
)
|
||||
_fromNonZeroPath := option.FromNonZero[string]()
|
||||
_prismPath := MakePrismWithName(
|
||||
func(s url.URL) Option[string] { return _fromNonZeroPath(s.Path) },
|
||||
func(v string) url.URL {
|
||||
return url.URL{Path: v}
|
||||
},
|
||||
"URL.Path",
|
||||
)
|
||||
_fromNonZeroRawPath := option.FromNonZero[string]()
|
||||
_prismRawPath := MakePrismWithName(
|
||||
func(s url.URL) Option[string] { return _fromNonZeroRawPath(s.RawPath) },
|
||||
func(v string) url.URL {
|
||||
return url.URL{RawPath: v}
|
||||
},
|
||||
"URL.RawPath",
|
||||
)
|
||||
_fromNonZeroOmitHost := option.FromNonZero[bool]()
|
||||
_prismOmitHost := MakePrismWithName(
|
||||
func(s url.URL) Option[bool] { return _fromNonZeroOmitHost(s.OmitHost) },
|
||||
func(v bool) url.URL {
|
||||
return url.URL{OmitHost: v}
|
||||
},
|
||||
"URL.OmitHost",
|
||||
)
|
||||
_fromNonZeroForceQuery := option.FromNonZero[bool]()
|
||||
_prismForceQuery := MakePrismWithName(
|
||||
func(s url.URL) Option[bool] { return _fromNonZeroForceQuery(s.ForceQuery) },
|
||||
func(v bool) url.URL {
|
||||
return url.URL{ForceQuery: v}
|
||||
},
|
||||
"URL.ForceQuery",
|
||||
)
|
||||
_fromNonZeroRawQuery := option.FromNonZero[string]()
|
||||
_prismRawQuery := MakePrismWithName(
|
||||
func(s url.URL) Option[string] { return _fromNonZeroRawQuery(s.RawQuery) },
|
||||
func(v string) url.URL {
|
||||
return url.URL{RawQuery: v}
|
||||
},
|
||||
"URL.RawQuery",
|
||||
)
|
||||
_fromNonZeroFragment := option.FromNonZero[string]()
|
||||
_prismFragment := MakePrismWithName(
|
||||
func(s url.URL) Option[string] { return _fromNonZeroFragment(s.Fragment) },
|
||||
func(v string) url.URL {
|
||||
return url.URL{Fragment: v}
|
||||
},
|
||||
"URL.Fragment",
|
||||
)
|
||||
_fromNonZeroRawFragment := option.FromNonZero[string]()
|
||||
_prismRawFragment := MakePrismWithName(
|
||||
func(s url.URL) Option[string] { return _fromNonZeroRawFragment(s.RawFragment) },
|
||||
func(v string) url.URL {
|
||||
return url.URL{RawFragment: v}
|
||||
},
|
||||
"URL.RawFragment",
|
||||
)
|
||||
return URLPrisms{
|
||||
Scheme: _prismScheme,
|
||||
Opaque: _prismOpaque,
|
||||
User: _prismUser,
|
||||
Host: _prismHost,
|
||||
Path: _prismPath,
|
||||
RawPath: _prismRawPath,
|
||||
OmitHost: _prismOmitHost,
|
||||
ForceQuery: _prismForceQuery,
|
||||
RawQuery: _prismRawQuery,
|
||||
Fragment: _prismFragment,
|
||||
RawFragment: _prismRawFragment,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package prism
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -127,4 +128,6 @@ type (
|
||||
Operator[S, A, B any] = func(Prism[S, A]) Prism[S, B]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
)
|
||||
|
||||
106
v2/optics/prism/url_test.go
Normal file
106
v2/optics/prism/url_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package prism
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestURLPrisms tests prisms for url.URL
|
||||
func TestURLPrisms(t *testing.T) {
|
||||
prisms := MakeURLPrisms()
|
||||
|
||||
t.Run("Scheme prism", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com"}
|
||||
opt := prisms.Scheme.GetOption(u)
|
||||
assert.True(t, option.IsSome(opt))
|
||||
|
||||
emptyU := url.URL{Scheme: "", Host: "example.com"}
|
||||
opt = prisms.Scheme.GetOption(emptyU)
|
||||
assert.True(t, option.IsNone(opt))
|
||||
|
||||
// ReverseGet
|
||||
constructed := prisms.Scheme.ReverseGet("ftp")
|
||||
assert.Equal(t, "ftp", constructed.Scheme)
|
||||
})
|
||||
|
||||
t.Run("Host prism", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com"}
|
||||
opt := prisms.Host.GetOption(u)
|
||||
assert.True(t, option.IsSome(opt))
|
||||
value := option.GetOrElse(func() string { return "" })(opt)
|
||||
assert.Equal(t, "example.com", value)
|
||||
})
|
||||
|
||||
t.Run("Path prism", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com", Path: "/api"}
|
||||
opt := prisms.Path.GetOption(u)
|
||||
assert.True(t, option.IsSome(opt))
|
||||
|
||||
emptyPath := url.URL{Scheme: "https", Host: "example.com", Path: ""}
|
||||
opt = prisms.Path.GetOption(emptyPath)
|
||||
assert.True(t, option.IsNone(opt))
|
||||
})
|
||||
|
||||
t.Run("User prism", func(t *testing.T) {
|
||||
userinfo := url.User("john")
|
||||
u := url.URL{Scheme: "https", Host: "example.com", User: userinfo}
|
||||
opt := prisms.User.GetOption(u)
|
||||
assert.True(t, option.IsSome(opt))
|
||||
|
||||
noUser := url.URL{Scheme: "https", Host: "example.com", User: nil}
|
||||
opt = prisms.User.GetOption(noUser)
|
||||
assert.True(t, option.IsNone(opt))
|
||||
})
|
||||
|
||||
t.Run("Boolean prisms", func(t *testing.T) {
|
||||
u := url.URL{Scheme: "https", Host: "example.com", ForceQuery: true}
|
||||
opt := prisms.ForceQuery.GetOption(u)
|
||||
assert.True(t, option.IsSome(opt))
|
||||
|
||||
noForce := url.URL{Scheme: "https", Host: "example.com", ForceQuery: false}
|
||||
opt = prisms.ForceQuery.GetOption(noForce)
|
||||
assert.True(t, option.IsNone(opt))
|
||||
})
|
||||
}
|
||||
|
||||
// TestErrorPrisms tests prisms for url.Error
|
||||
func TestErrorPrisms(t *testing.T) {
|
||||
prisms := MakeErrorPrisms()
|
||||
|
||||
t.Run("Op prism", func(t *testing.T) {
|
||||
urlErr := url.Error{Op: "Get", URL: "test", Err: nil}
|
||||
opt := prisms.Op.GetOption(urlErr)
|
||||
assert.True(t, option.IsSome(opt))
|
||||
|
||||
emptyErr := url.Error{Op: "", URL: "test", Err: nil}
|
||||
opt = prisms.Op.GetOption(emptyErr)
|
||||
assert.True(t, option.IsNone(opt))
|
||||
|
||||
// ReverseGet
|
||||
constructed := prisms.Op.ReverseGet("Post")
|
||||
assert.Equal(t, "Post", constructed.Op)
|
||||
})
|
||||
|
||||
t.Run("URL prism", func(t *testing.T) {
|
||||
urlErr := url.Error{Op: "Get", URL: "https://example.com", Err: nil}
|
||||
opt := prisms.URL.GetOption(urlErr)
|
||||
assert.True(t, option.IsSome(opt))
|
||||
|
||||
emptyErr := url.Error{Op: "Get", URL: "", Err: nil}
|
||||
opt = prisms.URL.GetOption(emptyErr)
|
||||
assert.True(t, option.IsNone(opt))
|
||||
})
|
||||
|
||||
t.Run("Err prism", func(t *testing.T) {
|
||||
urlErr := url.Error{Op: "Get", URL: "test", Err: assert.AnError}
|
||||
opt := prisms.Err.GetOption(urlErr)
|
||||
assert.True(t, option.IsSome(opt))
|
||||
|
||||
nilErr := url.Error{Op: "Get", URL: "test", Err: nil}
|
||||
opt = prisms.Err.GetOption(nilErr)
|
||||
assert.True(t, option.IsNone(opt))
|
||||
})
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] {
|
||||
|
||||
Of[Seq[B]],
|
||||
Map[Seq[B]],
|
||||
MonadAp[Seq[B]],
|
||||
Ap[Seq[B]],
|
||||
|
||||
f,
|
||||
)
|
||||
|
||||
121
v2/reader/iter.go
Normal file
121
v2/reader/iter.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// 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 reader
|
||||
|
||||
import (
|
||||
INTI "github.com/IBM/fp-go/v2/internal/iter"
|
||||
)
|
||||
|
||||
// TraverseIter traverses an iterator sequence, applying a Reader-producing function to each element
|
||||
// and collecting the results in a Reader that produces an iterator.
|
||||
//
|
||||
// This function transforms a sequence of values through a function that produces Readers,
|
||||
// then "flips" the nesting so that instead of having an iterator of Readers, you get a
|
||||
// single Reader that produces an iterator of values. All Readers share the same environment R.
|
||||
//
|
||||
// This is particularly useful when you have a collection of values that each need to be
|
||||
// transformed using environment-dependent logic, and you want to defer the environment
|
||||
// injection until the final execution.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The shared environment/context type
|
||||
// - A: The input element type in the iterator
|
||||
// - B: The output element type in the resulting iterator
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow that transforms each element A into a Reader[R, B]
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes an iterator of A and returns a Reader producing an iterator of B
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Multiplier int }
|
||||
//
|
||||
// // Function that creates a Reader for each number
|
||||
// multiplyByConfig := func(x int) reader.Reader[Config, int] {
|
||||
// return func(c Config) int { return x * c.Multiplier }
|
||||
// }
|
||||
//
|
||||
// // Create an iterator of numbers
|
||||
// numbers := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// yield(3)
|
||||
// }
|
||||
//
|
||||
// // Traverse the iterator
|
||||
// traversed := reader.TraverseIter(multiplyByConfig)(numbers)
|
||||
//
|
||||
// // Execute with config
|
||||
// result := traversed(Config{Multiplier: 10})
|
||||
// // result is an iterator that yields: 10, 20, 30
|
||||
func TraverseIter[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, Seq[A], Seq[B]] {
|
||||
return INTI.Traverse[Seq[A]](
|
||||
Map[R, B],
|
||||
|
||||
Of[R, Seq[B]],
|
||||
Map[R, Seq[B]],
|
||||
Ap[Seq[B]],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceIter sequences an iterator of Readers into a Reader that produces an iterator.
|
||||
//
|
||||
// This function "flips" the nesting of an iterator and Reader types. Given an iterator
|
||||
// where each element is a Reader[R, A], it produces a single Reader[R, Seq[A]] that,
|
||||
// when executed with an environment, evaluates all the Readers with that environment
|
||||
// and collects their results into an iterator.
|
||||
//
|
||||
// This is a special case of TraverseIter where the transformation function is the identity.
|
||||
// All Readers in the input iterator share the same environment R and are evaluated with it.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The shared environment/context type
|
||||
// - A: The result type produced by each Reader
|
||||
//
|
||||
// Parameters:
|
||||
// - as: An iterator sequence where each element is a Reader[R, A]
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that, when executed, produces an iterator of all the Reader results
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Base int }
|
||||
//
|
||||
// // Create an iterator of Readers
|
||||
// readers := func(yield func(reader.Reader[Config, int]) bool) {
|
||||
// yield(func(c Config) int { return c.Base + 1 })
|
||||
// yield(func(c Config) int { return c.Base + 2 })
|
||||
// yield(func(c Config) int { return c.Base + 3 })
|
||||
// }
|
||||
//
|
||||
// // Sequence the iterator
|
||||
// sequenced := reader.SequenceIter(readers)
|
||||
//
|
||||
// // Execute with config
|
||||
// result := sequenced(Config{Base: 10})
|
||||
// // result is an iterator that yields: 11, 12, 13
|
||||
func SequenceIter[R, A any](as Seq[Reader[R, A]]) Reader[R, Seq[A]] {
|
||||
return INTI.MonadSequence(
|
||||
Map[R](INTI.Of[Seq[A]]),
|
||||
ApplicativeMonoid[R](INTI.Monoid[Seq[A]]()),
|
||||
as,
|
||||
)
|
||||
}
|
||||
403
v2/reader/iter_test.go
Normal file
403
v2/reader/iter_test.go
Normal file
@@ -0,0 +1,403 @@
|
||||
// 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 reader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
INTI "github.com/IBM/fp-go/v2/internal/iter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Helper function to collect iterator values into a slice
|
||||
func collectIter[A any](seq Seq[A]) []A {
|
||||
return INTI.ToArray[Seq[A], []A](seq)
|
||||
}
|
||||
|
||||
// Helper function to create an iterator from a slice
|
||||
func fromSlice[A any](items []A) Seq[A] {
|
||||
return slices.Values(items)
|
||||
}
|
||||
|
||||
func TestTraverseIter(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
Prefix string
|
||||
}
|
||||
|
||||
t.Run("traverses empty iterator", func(t *testing.T) {
|
||||
empty := INTI.Empty[Seq[int]]()
|
||||
|
||||
multiplyByConfig := func(x int) Reader[Config, int] {
|
||||
return func(c Config) int { return x * c.Multiplier }
|
||||
}
|
||||
|
||||
traversed := TraverseIter(multiplyByConfig)(empty)
|
||||
result := traversed(Config{Multiplier: 10})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("traverses single element iterator", func(t *testing.T) {
|
||||
single := INTI.Of[Seq[int]](5)
|
||||
|
||||
multiplyByConfig := func(x int) Reader[Config, int] {
|
||||
return func(c Config) int { return x * c.Multiplier }
|
||||
}
|
||||
|
||||
traversed := TraverseIter(multiplyByConfig)(single)
|
||||
result := traversed(Config{Multiplier: 3})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []int{15}, collected)
|
||||
})
|
||||
|
||||
t.Run("traverses multiple elements", func(t *testing.T) {
|
||||
numbers := INTI.From(1, 2, 3, 4)
|
||||
|
||||
multiplyByConfig := func(x int) Reader[Config, int] {
|
||||
return func(c Config) int { return x * c.Multiplier }
|
||||
}
|
||||
|
||||
traversed := TraverseIter(multiplyByConfig)(numbers)
|
||||
result := traversed(Config{Multiplier: 10})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []int{10, 20, 30, 40}, collected)
|
||||
})
|
||||
|
||||
t.Run("transforms types during traversal", func(t *testing.T) {
|
||||
numbers := INTI.From(1, 2, 3)
|
||||
|
||||
intToString := func(x int) Reader[Config, string] {
|
||||
return func(c Config) string {
|
||||
return fmt.Sprintf("%s%d", c.Prefix, x)
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseIter(intToString)(numbers)
|
||||
result := traversed(Config{Prefix: "num-"})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []string{"num-1", "num-2", "num-3"}, collected)
|
||||
})
|
||||
|
||||
t.Run("all readers share same environment", func(t *testing.T) {
|
||||
numbers := INTI.From(1, 2, 3)
|
||||
|
||||
// Each reader accesses the same config
|
||||
addBase := func(x int) Reader[Config, int] {
|
||||
return func(c Config) int {
|
||||
return x + c.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseIter(addBase)(numbers)
|
||||
result := traversed(Config{Multiplier: 100})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []int{101, 102, 103}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with complex transformations", func(t *testing.T) {
|
||||
words := INTI.From("hello", "world")
|
||||
|
||||
wordLength := func(s string) Reader[Config, int] {
|
||||
return func(c Config) int {
|
||||
return len(s) * c.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseIter(wordLength)(words)
|
||||
result := traversed(Config{Multiplier: 2})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []int{10, 10}, collected) // "hello" = 5*2, "world" = 5*2
|
||||
})
|
||||
|
||||
t.Run("preserves order of elements", func(t *testing.T) {
|
||||
numbers := fromSlice([]int{5, 3, 8, 1, 9})
|
||||
|
||||
identity := func(x int) Reader[Config, int] {
|
||||
return Of[Config](x)
|
||||
}
|
||||
|
||||
traversed := TraverseIter(identity)(numbers)
|
||||
result := traversed(Config{})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []int{5, 3, 8, 1, 9}, collected)
|
||||
})
|
||||
|
||||
t.Run("can be used with different config types", func(t *testing.T) {
|
||||
type StringConfig struct {
|
||||
Suffix string
|
||||
}
|
||||
|
||||
words := INTI.From("test", "data")
|
||||
|
||||
addSuffix := func(s string) Reader[StringConfig, string] {
|
||||
return func(c StringConfig) string {
|
||||
return s + c.Suffix
|
||||
}
|
||||
}
|
||||
|
||||
traversed := TraverseIter(addSuffix)(words)
|
||||
result := traversed(StringConfig{Suffix: ".txt"})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []string{"test.txt", "data.txt"}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceIter(t *testing.T) {
|
||||
type Config struct {
|
||||
Base int
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
t.Run("sequences empty iterator", func(t *testing.T) {
|
||||
empty := func(yield func(Reader[Config, int]) bool) {}
|
||||
|
||||
sequenced := SequenceIter(empty)
|
||||
result := sequenced(Config{Base: 10})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("sequences single reader", func(t *testing.T) {
|
||||
single := func(yield func(Reader[Config, int]) bool) {
|
||||
yield(func(c Config) int { return c.Base + 5 })
|
||||
}
|
||||
|
||||
sequenced := SequenceIter(single)
|
||||
result := sequenced(Config{Base: 10})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []int{15}, collected)
|
||||
})
|
||||
|
||||
t.Run("sequences multiple readers", func(t *testing.T) {
|
||||
readers := INTI.From(
|
||||
func(c Config) int { return c.Base + 1 },
|
||||
func(c Config) int { return c.Base + 2 },
|
||||
func(c Config) int { return c.Base + 3 },
|
||||
)
|
||||
|
||||
sequenced := SequenceIter(readers)
|
||||
result := sequenced(Config{Base: 10})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []int{11, 12, 13}, collected)
|
||||
})
|
||||
|
||||
t.Run("all readers receive same environment", func(t *testing.T) {
|
||||
readers := INTI.From(
|
||||
func(c Config) int { return c.Base * c.Multiplier },
|
||||
func(c Config) int { return c.Base + c.Multiplier },
|
||||
func(c Config) int { return c.Base - c.Multiplier },
|
||||
)
|
||||
|
||||
sequenced := SequenceIter(readers)
|
||||
result := sequenced(Config{Base: 10, Multiplier: 3})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []int{30, 13, 7}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with string readers", func(t *testing.T) {
|
||||
type StringConfig struct {
|
||||
Prefix string
|
||||
Suffix string
|
||||
}
|
||||
|
||||
readers := INTI.From(
|
||||
func(c StringConfig) string { return c.Prefix + "first" },
|
||||
func(c StringConfig) string { return c.Prefix + "second" },
|
||||
func(c StringConfig) string { return "third" + c.Suffix },
|
||||
)
|
||||
|
||||
sequenced := SequenceIter(readers)
|
||||
result := sequenced(StringConfig{Prefix: "pre-", Suffix: "-post"})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []string{"pre-first", "pre-second", "third-post"}, collected)
|
||||
})
|
||||
|
||||
t.Run("preserves order of readers", func(t *testing.T) {
|
||||
readers := INTI.From(
|
||||
Of[Config](5),
|
||||
Of[Config](3),
|
||||
Of[Config](8),
|
||||
Of[Config](1),
|
||||
)
|
||||
|
||||
sequenced := SequenceIter(readers)
|
||||
result := sequenced(Config{})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []int{5, 3, 8, 1}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with complex reader logic", func(t *testing.T) {
|
||||
readers := INTI.From(
|
||||
func(c Config) string {
|
||||
return strconv.Itoa(c.Base * 2)
|
||||
},
|
||||
func(c Config) string {
|
||||
return fmt.Sprintf("mult-%d", c.Multiplier)
|
||||
},
|
||||
func(c Config) string {
|
||||
return fmt.Sprintf("sum-%d", c.Base+c.Multiplier)
|
||||
},
|
||||
)
|
||||
|
||||
sequenced := SequenceIter(readers)
|
||||
result := sequenced(Config{Base: 5, Multiplier: 3})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []string{"10", "mult-3", "sum-8"}, collected)
|
||||
})
|
||||
|
||||
t.Run("can handle large number of readers", func(t *testing.T) {
|
||||
|
||||
readers := func(yield func(Reader[Config, int]) bool) {
|
||||
for i := 0; i < 100; i++ {
|
||||
i := i // capture loop variable
|
||||
yield(func(c Config) int { return c.Base + i })
|
||||
}
|
||||
}
|
||||
|
||||
sequenced := SequenceIter(readers)
|
||||
result := sequenced(Config{Base: 1000})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Len(t, collected, 100)
|
||||
assert.Equal(t, 1000, collected[0])
|
||||
assert.Equal(t, 1099, collected[99])
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseIterAndSequenceIterRelationship(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
t.Run("SequenceIter is TraverseIter with identity", func(t *testing.T) {
|
||||
// Create an iterator of readers
|
||||
readers := INTI.From(
|
||||
func(c Config) int { return c.Value + 1 },
|
||||
func(c Config) int { return c.Value + 2 },
|
||||
func(c Config) int { return c.Value + 3 },
|
||||
)
|
||||
|
||||
// Using SequenceIter
|
||||
sequenced := SequenceIter(readers)
|
||||
sequencedResult := sequenced(Config{Value: 10})
|
||||
|
||||
// Using TraverseIter with identity function
|
||||
identity := Asks[Config, int]
|
||||
traversed := TraverseIter(identity)(readers)
|
||||
traversedResult := traversed(Config{Value: 10})
|
||||
|
||||
// Both should produce the same results
|
||||
sequencedCollected := collectIter(sequencedResult)
|
||||
traversedCollected := collectIter(traversedResult)
|
||||
|
||||
assert.Equal(t, sequencedCollected, traversedCollected)
|
||||
assert.Equal(t, []int{11, 12, 13}, sequencedCollected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIteratorIntegration(t *testing.T) {
|
||||
type AppConfig struct {
|
||||
DatabaseURL string
|
||||
APIKey string
|
||||
Port int
|
||||
}
|
||||
|
||||
t.Run("real-world example: processing configuration values", func(t *testing.T) {
|
||||
// Iterator of field names
|
||||
fields := INTI.From(
|
||||
"database",
|
||||
"api",
|
||||
"port",
|
||||
)
|
||||
|
||||
// Function that creates a reader for each field
|
||||
getConfigValue := func(field string) Reader[AppConfig, string] {
|
||||
return func(c AppConfig) string {
|
||||
switch field {
|
||||
case "database":
|
||||
return c.DatabaseURL
|
||||
case "api":
|
||||
return c.APIKey
|
||||
case "port":
|
||||
return strconv.Itoa(c.Port)
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse to get all config values
|
||||
traversed := TraverseIter(getConfigValue)(fields)
|
||||
result := traversed(AppConfig{
|
||||
DatabaseURL: "postgres://localhost",
|
||||
APIKey: "secret-key",
|
||||
Port: 8080,
|
||||
})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []string{
|
||||
"postgres://localhost",
|
||||
"secret-key",
|
||||
"8080",
|
||||
}, collected)
|
||||
})
|
||||
|
||||
t.Run("real-world example: batch processing with shared config", func(t *testing.T) {
|
||||
type ProcessConfig struct {
|
||||
Prefix string
|
||||
Suffix string
|
||||
}
|
||||
|
||||
// Iterator of items to process
|
||||
items := fromSlice([]string{"item1", "item2", "item3"})
|
||||
|
||||
// Processing function that uses config
|
||||
processItem := func(item string) Reader[ProcessConfig, string] {
|
||||
return func(c ProcessConfig) string {
|
||||
return c.Prefix + item + c.Suffix
|
||||
}
|
||||
}
|
||||
|
||||
// Process all items with shared config
|
||||
traversed := TraverseIter(processItem)(items)
|
||||
result := traversed(ProcessConfig{
|
||||
Prefix: "[",
|
||||
Suffix: "]",
|
||||
})
|
||||
|
||||
collected := collectIter(result)
|
||||
assert.Equal(t, []string{"[item1]", "[item2]", "[item3]"}, collected)
|
||||
})
|
||||
}
|
||||
@@ -15,7 +15,11 @@
|
||||
|
||||
package reader
|
||||
|
||||
import "github.com/IBM/fp-go/v2/tailrec"
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
// Reader represents a computation that depends on a shared environment of type R and produces a value of type A.
|
||||
@@ -96,4 +100,7 @@ type (
|
||||
// without stack overflow. It's used for implementing stack-safe recursive algorithms
|
||||
// in the context of Reader computations.
|
||||
Trampoline[B, L any] = tailrec.Trampoline[B, L]
|
||||
|
||||
// Seq represents an iterator sequence over values of type T.
|
||||
Seq[T any] = iter.Seq[T]
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package readerio
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
@@ -234,3 +235,54 @@ func Local[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIO[R1, A], A] {
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIO[R1, A], A] {
|
||||
return reader.Contramap[IO[A]](f)
|
||||
}
|
||||
|
||||
// LocalIOK transforms the environment of a ReaderIO using an IO-based Kleisli arrow.
|
||||
// It allows you to modify the environment through an effectful computation before
|
||||
// passing it to the ReaderIO.
|
||||
//
|
||||
// This is useful when the environment transformation itself requires IO effects,
|
||||
// such as reading from a file, making a network call, or accessing system resources.
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
|
||||
// 2. The resulting R1 value is passed to the ReaderIO[R1, A] to produce the final result
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type produced by the ReaderIO
|
||||
// - R1: The original environment type expected by the ReaderIO
|
||||
// - R2: The new input environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIO[R1, A] and returns a ReaderIO[R2, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Transform a config path into a loaded config
|
||||
// loadConfig := func(path string) IO[Config] {
|
||||
// return func() Config {
|
||||
// // Load config from file
|
||||
// return parseConfig(readFile(path))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the config to perform some operation
|
||||
// useConfig := func(cfg Config) IO[string] {
|
||||
// return Of("Using: " + cfg.Name)
|
||||
// }
|
||||
//
|
||||
// // Compose them using LocalIOK
|
||||
// result := LocalIOK[string, Config, string](loadConfig)(useConfig)
|
||||
// output := result("config.json")() // Loads config and uses it
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) Kleisli[R2, ReaderIO[R1, A], A] {
|
||||
return func(ri ReaderIO[R1, A]) ReaderIO[R2, A] {
|
||||
return F.Flow2(
|
||||
f,
|
||||
io.Chain(ri),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,3 +612,133 @@ func TestRealWorldScenarios(t *testing.T) {
|
||||
assert.Equal(t, "[API] Status: 200, Body: OK", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOK tests LocalIOK functionality
|
||||
func TestLocalIOK(t *testing.T) {
|
||||
t.Run("basic IO transformation", func(t *testing.T) {
|
||||
// IO effect that loads config from a path
|
||||
loadConfig := func(path string) io.IO[SimpleConfig] {
|
||||
return func() SimpleConfig {
|
||||
// Simulate loading config
|
||||
return SimpleConfig{Host: "localhost", Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderIO that uses the config
|
||||
useConfig := func(cfg SimpleConfig) io.IO[string] {
|
||||
return io.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
|
||||
// Compose using LocalIOK
|
||||
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(useConfig)
|
||||
result := adapted("config.json")()
|
||||
|
||||
assert.Equal(t, "localhost:8080", result)
|
||||
})
|
||||
|
||||
t.Run("IO transformation with side effects", func(t *testing.T) {
|
||||
var loadLog []string
|
||||
|
||||
loadData := func(key string) io.IO[int] {
|
||||
return func() int {
|
||||
loadLog = append(loadLog, "Loading: "+key)
|
||||
return len(key) * 10
|
||||
}
|
||||
}
|
||||
|
||||
processData := func(n int) io.IO[string] {
|
||||
return io.Of(fmt.Sprintf("Processed: %d", n))
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string, int, string](loadData)(processData)
|
||||
result := adapted("test")()
|
||||
|
||||
assert.Equal(t, "Processed: 40", result)
|
||||
assert.Equal(t, []string{"Loading: test"}, loadLog)
|
||||
})
|
||||
|
||||
t.Run("compose multiple LocalIOK", func(t *testing.T) {
|
||||
// First transformation: string -> int
|
||||
parseID := func(s string) io.IO[int] {
|
||||
return func() int {
|
||||
id, _ := strconv.Atoi(s)
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
// Second transformation: int -> UserEnv
|
||||
loadUser := func(id int) io.IO[UserEnv] {
|
||||
return func() UserEnv {
|
||||
return UserEnv{UserID: id}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the UserEnv
|
||||
formatUser := func(env UserEnv) io.IO[string] {
|
||||
return io.Of(fmt.Sprintf("User ID: %d", env.UserID))
|
||||
}
|
||||
|
||||
// Compose transformations
|
||||
step1 := LocalIOK[string, UserEnv, int](loadUser)(formatUser)
|
||||
step2 := LocalIOK[string, int, string](parseID)(step1)
|
||||
|
||||
result := step2("42")()
|
||||
assert.Equal(t, "User ID: 42", result)
|
||||
})
|
||||
|
||||
t.Run("environment extraction with IO", func(t *testing.T) {
|
||||
// Extract database config from app config
|
||||
extractDB := func(app AppConfig) io.IO[DatabaseConfig] {
|
||||
return func() DatabaseConfig {
|
||||
// Could perform validation or default setting here
|
||||
cfg := app.Database
|
||||
if cfg.Host == "" {
|
||||
cfg.Host = "localhost"
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
|
||||
// Use the database config
|
||||
connectDB := func(cfg DatabaseConfig) io.IO[string] {
|
||||
return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string, DatabaseConfig, AppConfig](extractDB)(connectDB)
|
||||
result := adapted(AppConfig{
|
||||
Database: DatabaseConfig{Host: "", Port: 5432},
|
||||
})()
|
||||
|
||||
assert.Equal(t, "Connected to localhost:5432", result)
|
||||
})
|
||||
|
||||
t.Run("real-world: load and parse config file", func(t *testing.T) {
|
||||
type ConfigFile struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// Simulate reading file content
|
||||
readFile := func(cf ConfigFile) io.IO[string] {
|
||||
return func() string {
|
||||
return `{"host":"example.com","port":9000}`
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the content
|
||||
parseConfig := func(content string) io.IO[SimpleConfig] {
|
||||
return io.Of(SimpleConfig{Host: "example.com", Port: 9000})
|
||||
}
|
||||
|
||||
// Use the parsed config
|
||||
useConfig := func(cfg SimpleConfig) io.IO[string] {
|
||||
return io.Of(fmt.Sprintf("Using %s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
|
||||
// Compose the pipeline
|
||||
step1 := LocalIOK[string, SimpleConfig, string](parseConfig)(useConfig)
|
||||
step2 := LocalIOK[string, string, ConfigFile](readFile)(step1)
|
||||
|
||||
result := step2(ConfigFile{Path: "app.json"})()
|
||||
assert.Equal(t, "Using example.com:9000", result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
package readerioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOEither.
|
||||
@@ -74,3 +77,117 @@ func Promap[R, E, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, E, ReaderIOE
|
||||
func Contramap[E, A, R1, R2 any](f func(R2) R1) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
|
||||
return reader.Contramap[IOEither[E, A]](f)
|
||||
}
|
||||
|
||||
// LocalIOK transforms the environment of a ReaderIOEither using an IO-based Kleisli arrow.
|
||||
// It allows you to modify the environment through an effectful computation before
|
||||
// passing it to the ReaderIOEither.
|
||||
//
|
||||
// This is useful when the environment transformation itself requires IO effects,
|
||||
// such as reading from a file, making a network call, or accessing system resources,
|
||||
// but these effects cannot fail (or failures are not relevant to the error type E).
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
|
||||
// 2. The resulting R1 value is passed to the ReaderIOEither[R1, E, A] to produce the final result
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error type (unchanged through the transformation)
|
||||
// - A: The success type produced by the ReaderIOEither
|
||||
// - R1: The original environment type expected by the ReaderIOEither
|
||||
// - R2: The new input environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOEither[R1, E, A] and returns a ReaderIOEither[R2, E, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Transform a config path into a loaded config (infallible)
|
||||
// loadConfig := func(path string) IO[Config] {
|
||||
// return func() Config {
|
||||
// return getDefaultConfig() // Always succeeds
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the config to perform an operation that might fail
|
||||
// useConfig := func(cfg Config) IOEither[error, string] {
|
||||
// return func() Either[error, string] {
|
||||
// if cfg.Valid {
|
||||
// return Right[error]("Success")
|
||||
// }
|
||||
// return Left[string](errors.New("invalid config"))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose them using LocalIOK
|
||||
// result := LocalIOK[error, string, Config, string](loadConfig)(useConfig)
|
||||
// output := result("config.json")() // Loads config and uses it
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOK[E, A, R1, R2 any](f io.Kleisli[R2, R1]) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
|
||||
return readerio.LocalIOK[Either[E, A]](f)
|
||||
}
|
||||
|
||||
// LocalIOEitherK transforms the environment of a ReaderIOEither using an IOEither-based Kleisli arrow.
|
||||
// It allows you to modify the environment through an effectful computation that can fail before
|
||||
// passing it to the ReaderIOEither.
|
||||
//
|
||||
// This is useful when the environment transformation itself requires IO effects that can fail,
|
||||
// such as reading from a file that might not exist, making a network call that might timeout,
|
||||
// or parsing data that might be invalid.
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IOEither effect f is executed with the R2 environment to produce Either[E, R1]
|
||||
// 2. If successful (Right), the R1 value is passed to the ReaderIOEither[R1, E, A]
|
||||
// 3. If failed (Left), the error E is propagated without executing the ReaderIOEither
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderIOEither
|
||||
// - R1: The original environment type expected by the ReaderIOEither
|
||||
// - R2: The new input environment type
|
||||
// - E: The error type (shared by both the transformation and the ReaderIOEither)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IOEither Kleisli arrow that transforms R2 to R1 with IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOEither[R1, E, A] and returns a ReaderIOEither[R2, E, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Transform a config path into a loaded config (can fail)
|
||||
// loadConfig := func(path string) IOEither[error, Config] {
|
||||
// return func() Either[error, Config] {
|
||||
// cfg, err := readConfigFile(path)
|
||||
// if err != nil {
|
||||
// return Left[Config](err)
|
||||
// }
|
||||
// return Right[error](cfg)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the config to perform an operation that might fail
|
||||
// useConfig := func(cfg Config) IOEither[error, string] {
|
||||
// return func() Either[error, string] {
|
||||
// if cfg.Valid {
|
||||
// return Right[error]("Success: " + cfg.Name)
|
||||
// }
|
||||
// return Left[string](errors.New("invalid config"))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose them using LocalIOEitherK
|
||||
// result := LocalIOEitherK[string, Config, string](loadConfig)(useConfig)
|
||||
// output := result("config.json")() // Loads config (might fail) and uses it (might fail)
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOEitherK[A, R1, R2, E any](f ioeither.Kleisli[E, R2, R1]) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
|
||||
return func(ri ReaderIOEither[R1, E, A]) ReaderIOEither[R2, E, A] {
|
||||
return function.Flow2(
|
||||
f,
|
||||
ioeither.Chain(ri),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,3 +131,248 @@ func TestPromapWithIO(t *testing.T) {
|
||||
assert.Equal(t, 1, counter) // Side effect occurred
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOK tests LocalIOK functionality
|
||||
func TestLocalIOK(t *testing.T) {
|
||||
t.Run("basic IO transformation", func(t *testing.T) {
|
||||
// IO effect that loads config from a path
|
||||
loadConfig := func(path string) IOE.IO[SimpleConfig] {
|
||||
return func() SimpleConfig {
|
||||
// Simulate loading config
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderIOEither that uses the config
|
||||
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
|
||||
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
|
||||
}
|
||||
|
||||
// Compose using LocalIOK
|
||||
adapted := LocalIOK[string, string, SimpleConfig, string](loadConfig)(useConfig)
|
||||
result := adapted("config.json")()
|
||||
|
||||
assert.Equal(t, E.Of[string]("Port: 8080"), result)
|
||||
})
|
||||
|
||||
t.Run("IO transformation with side effects", func(t *testing.T) {
|
||||
var loadLog []string
|
||||
|
||||
loadData := func(key string) IOE.IO[int] {
|
||||
return func() int {
|
||||
loadLog = append(loadLog, "Loading: "+key)
|
||||
return len(key) * 10
|
||||
}
|
||||
}
|
||||
|
||||
processData := func(n int) IOEither[string, string] {
|
||||
return IOE.Of[string]("Processed: " + strconv.Itoa(n))
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string, string, int, string](loadData)(processData)
|
||||
result := adapted("test")()
|
||||
|
||||
assert.Equal(t, E.Of[string]("Processed: 40"), result)
|
||||
assert.Equal(t, []string{"Loading: test"}, loadLog)
|
||||
})
|
||||
|
||||
t.Run("error propagation in ReaderIOEither", func(t *testing.T) {
|
||||
loadConfig := func(path string) IOE.IO[SimpleConfig] {
|
||||
return func() SimpleConfig {
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderIOEither that returns an error
|
||||
failingOperation := func(cfg SimpleConfig) IOEither[string, string] {
|
||||
return IOE.Left[string]("operation failed")
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string, string, SimpleConfig, string](loadConfig)(failingOperation)
|
||||
result := adapted("config.json")()
|
||||
|
||||
assert.Equal(t, E.Left[string]("operation failed"), result)
|
||||
})
|
||||
|
||||
t.Run("compose multiple LocalIOK", func(t *testing.T) {
|
||||
// First transformation: string -> int
|
||||
parseID := func(s string) IOE.IO[int] {
|
||||
return func() int {
|
||||
id, _ := strconv.Atoi(s)
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
// Second transformation: int -> SimpleConfig
|
||||
loadConfig := func(id int) IOE.IO[SimpleConfig] {
|
||||
return func() SimpleConfig {
|
||||
return SimpleConfig{Port: 8000 + id}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
formatConfig := func(cfg SimpleConfig) IOEither[string, string] {
|
||||
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
|
||||
}
|
||||
|
||||
// Compose transformations
|
||||
step1 := LocalIOK[string, string, SimpleConfig, int](loadConfig)(formatConfig)
|
||||
step2 := LocalIOK[string, string, int, string](parseID)(step1)
|
||||
|
||||
result := step2("42")()
|
||||
assert.Equal(t, E.Of[string]("Port: 8042"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOEitherK tests LocalIOEitherK functionality
|
||||
func TestLocalIOEitherK(t *testing.T) {
|
||||
t.Run("basic IOEither transformation", func(t *testing.T) {
|
||||
// IOEither effect that loads config from a path (can fail)
|
||||
loadConfig := func(path string) IOEither[string, SimpleConfig] {
|
||||
return func() E.Either[string, SimpleConfig] {
|
||||
if path == "" {
|
||||
return E.Left[SimpleConfig]("empty path")
|
||||
}
|
||||
return E.Of[string](SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderIOEither that uses the config
|
||||
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
|
||||
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
|
||||
}
|
||||
|
||||
// Compose using LocalIOEitherK
|
||||
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
result := adapted("config.json")()
|
||||
assert.Equal(t, E.Of[string]("Port: 8080"), result)
|
||||
|
||||
// Failure case
|
||||
resultErr := adapted("")()
|
||||
assert.Equal(t, E.Left[string]("empty path"), resultErr)
|
||||
})
|
||||
|
||||
t.Run("error propagation from environment transformation", func(t *testing.T) {
|
||||
loadConfig := func(path string) IOEither[string, SimpleConfig] {
|
||||
return func() E.Either[string, SimpleConfig] {
|
||||
return E.Left[SimpleConfig]("file not found")
|
||||
}
|
||||
}
|
||||
|
||||
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
|
||||
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
|
||||
}
|
||||
|
||||
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(useConfig)
|
||||
result := adapted("missing.json")()
|
||||
|
||||
// Error from loadConfig should propagate
|
||||
assert.Equal(t, E.Left[string]("file not found"), result)
|
||||
})
|
||||
|
||||
t.Run("error propagation from ReaderIOEither", func(t *testing.T) {
|
||||
loadConfig := func(path string) IOEither[string, SimpleConfig] {
|
||||
return IOE.Of[string](SimpleConfig{Port: 8080})
|
||||
}
|
||||
|
||||
// ReaderIOEither that returns an error
|
||||
failingOperation := func(cfg SimpleConfig) IOEither[string, string] {
|
||||
return IOE.Left[string]("operation failed")
|
||||
}
|
||||
|
||||
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(failingOperation)
|
||||
result := adapted("config.json")()
|
||||
|
||||
// Error from ReaderIOEither should propagate
|
||||
assert.Equal(t, E.Left[string]("operation failed"), result)
|
||||
})
|
||||
|
||||
t.Run("compose multiple LocalIOEitherK", func(t *testing.T) {
|
||||
// First transformation: string -> int (can fail)
|
||||
parseID := func(s string) IOEither[string, int] {
|
||||
return func() E.Either[string, int] {
|
||||
id, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return E.Left[int]("invalid ID")
|
||||
}
|
||||
return E.Of[string](id)
|
||||
}
|
||||
}
|
||||
|
||||
// Second transformation: int -> SimpleConfig (can fail)
|
||||
loadConfig := func(id int) IOEither[string, SimpleConfig] {
|
||||
return func() E.Either[string, SimpleConfig] {
|
||||
if id < 0 {
|
||||
return E.Left[SimpleConfig]("invalid ID")
|
||||
}
|
||||
return E.Of[string](SimpleConfig{Port: 8000 + id})
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
formatConfig := func(cfg SimpleConfig) IOEither[string, string] {
|
||||
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
|
||||
}
|
||||
|
||||
// Compose transformations
|
||||
step1 := LocalIOEitherK[string, SimpleConfig, int, string](loadConfig)(formatConfig)
|
||||
step2 := LocalIOEitherK[string, int, string, string](parseID)(step1)
|
||||
|
||||
// Success case
|
||||
result := step2("42")()
|
||||
assert.Equal(t, E.Of[string]("Port: 8042"), result)
|
||||
|
||||
// Failure in first transformation
|
||||
resultErr1 := step2("invalid")()
|
||||
assert.Equal(t, E.Left[string]("invalid ID"), resultErr1)
|
||||
|
||||
// Failure in second transformation
|
||||
resultErr2 := step2("-5")()
|
||||
assert.Equal(t, E.Left[string]("invalid ID"), resultErr2)
|
||||
})
|
||||
|
||||
t.Run("real-world: load and validate config", func(t *testing.T) {
|
||||
type ConfigFile struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// Read file (can fail)
|
||||
readFile := func(cf ConfigFile) IOEither[string, string] {
|
||||
return func() E.Either[string, string] {
|
||||
if cf.Path == "" {
|
||||
return E.Left[string]("empty path")
|
||||
}
|
||||
return E.Of[string](`{"port":9000}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse config (can fail)
|
||||
parseConfig := func(content string) IOEither[string, SimpleConfig] {
|
||||
return func() E.Either[string, SimpleConfig] {
|
||||
if content == "" {
|
||||
return E.Left[SimpleConfig]("empty content")
|
||||
}
|
||||
return E.Of[string](SimpleConfig{Port: 9000})
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
|
||||
return IOE.Of[string]("Using port: " + strconv.Itoa(cfg.Port))
|
||||
}
|
||||
|
||||
// Compose the pipeline
|
||||
step1 := LocalIOEitherK[string, SimpleConfig, string, string](parseConfig)(useConfig)
|
||||
step2 := LocalIOEitherK[string, string, ConfigFile, string](readFile)(step1)
|
||||
|
||||
// Success case
|
||||
result := step2(ConfigFile{Path: "app.json"})()
|
||||
assert.Equal(t, E.Of[string]("Using port: 9000"), result)
|
||||
|
||||
// Failure case
|
||||
resultErr := step2(ConfigFile{Path: ""})()
|
||||
assert.Equal(t, E.Left[string]("empty path"), resultErr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
RIOE "github.com/IBM/fp-go/v2/readerioeither"
|
||||
)
|
||||
|
||||
@@ -71,3 +74,165 @@ func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderIOResult[
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIOResult[R1, A], A] {
|
||||
return RIOE.Contramap[error, A](f)
|
||||
}
|
||||
|
||||
// LocalIOK transforms the environment of a ReaderIOResult using an IO-based Kleisli arrow.
|
||||
// It allows you to modify the environment through an effectful computation before
|
||||
// passing it to the ReaderIOResult.
|
||||
//
|
||||
// This is useful when the environment transformation itself requires IO effects,
|
||||
// such as reading from a file, making a network call, or accessing system resources,
|
||||
// but these effects cannot fail (or failures are not relevant).
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
|
||||
// 2. The resulting R1 value is passed to the ReaderIOResult[R1, A] to produce the final result
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderIOResult
|
||||
// - R1: The original environment type expected by the ReaderIOResult
|
||||
// - R2: The new input environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Transform a config path into a loaded config (infallible)
|
||||
// loadConfig := func(path string) IO[Config] {
|
||||
// return func() Config {
|
||||
// return getDefaultConfig() // Always succeeds
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the config to perform an operation that might fail
|
||||
// useConfig := func(cfg Config) IOResult[string] {
|
||||
// return func() Result[string] {
|
||||
// if cfg.Valid {
|
||||
// return Ok[string]("Success")
|
||||
// }
|
||||
// return Err[string](errors.New("invalid config"))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose them using LocalIOK
|
||||
// result := LocalIOK[string, Config, string](loadConfig)(useConfig)
|
||||
// output := result("config.json")() // Loads config and uses it
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) Kleisli[R2, ReaderIOResult[R1, A], A] {
|
||||
return RIOE.LocalIOK[error, A](f)
|
||||
}
|
||||
|
||||
// LocalIOEitherK transforms the environment of a ReaderIOResult using an IOEither-based Kleisli arrow.
|
||||
// It allows you to modify the environment through an effectful computation that can fail before
|
||||
// passing it to the ReaderIOResult.
|
||||
//
|
||||
// This is useful when the environment transformation itself requires IO effects that can fail,
|
||||
// such as reading from a file that might not exist, making a network call that might timeout,
|
||||
// or parsing data that might be invalid.
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IOEither effect f is executed with the R2 environment to produce Either[error, R1]
|
||||
// 2. If successful (Right), the R1 value is passed to the ReaderIOResult[R1, A]
|
||||
// 3. If failed (Left), the error is propagated without executing the ReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderIOResult
|
||||
// - R1: The original environment type expected by the ReaderIOResult
|
||||
// - R2: The new input environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IOEither Kleisli arrow that transforms R2 to R1 with IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Transform a config path into a loaded config (can fail)
|
||||
// loadConfig := func(path string) IOEither[error, Config] {
|
||||
// return func() Either[error, Config] {
|
||||
// cfg, err := readConfigFile(path)
|
||||
// if err != nil {
|
||||
// return Left[Config](err)
|
||||
// }
|
||||
// return Right[error](cfg)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the config to perform an operation that might fail
|
||||
// useConfig := func(cfg Config) IOResult[string] {
|
||||
// return func() Result[string] {
|
||||
// if cfg.Valid {
|
||||
// return Ok[string]("Success: " + cfg.Name)
|
||||
// }
|
||||
// return Err[string](errors.New("invalid config"))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose them using LocalIOEitherK
|
||||
// result := LocalIOEitherK[string, Config, string](loadConfig)(useConfig)
|
||||
// output := result("config.json")() // Loads config (might fail) and uses it (might fail)
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOEitherK[A, R1, R2 any](f ioeither.Kleisli[error, R2, R1]) Kleisli[R2, ReaderIOResult[R1, A], A] {
|
||||
return RIOE.LocalIOEitherK[A](f)
|
||||
}
|
||||
|
||||
// LocalIOResultK transforms the environment of a ReaderIOResult using an IOResult-based Kleisli arrow.
|
||||
// It allows you to modify the environment through an effectful computation that can fail before
|
||||
// passing it to the ReaderIOResult.
|
||||
//
|
||||
// This is a type-safe alias for LocalIOEitherK specialized for error type, providing a more
|
||||
// idiomatic API when working with Result types (which use error as the error type).
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
|
||||
// 2. If successful (Ok), the R1 value is passed to the ReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderIOResult
|
||||
// - R1: The original environment type expected by the ReaderIOResult
|
||||
// - R2: The new input environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Transform a config path into a loaded config (can fail)
|
||||
// loadConfig := func(path string) IOResult[Config] {
|
||||
// return func() Result[Config] {
|
||||
// cfg, err := readConfigFile(path)
|
||||
// if err != nil {
|
||||
// return Err[Config](err)
|
||||
// }
|
||||
// return Ok(cfg)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the config to perform an operation that might fail
|
||||
// useConfig := func(cfg Config) IOResult[string] {
|
||||
// return func() Result[string] {
|
||||
// if cfg.Valid {
|
||||
// return Ok("Success: " + cfg.Name)
|
||||
// }
|
||||
// return Err[string](errors.New("invalid config"))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose them using LocalIOResultK
|
||||
// result := LocalIOResultK[string, Config, string](loadConfig)(useConfig)
|
||||
// output := result("config.json")() // Loads config (might fail) and uses it (might fail)
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOResultK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) Kleisli[R2, ReaderIOResult[R1, A], A] {
|
||||
return RIOE.LocalIOEitherK[A](f)
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@ package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
R "github.com/IBM/fp-go/v2/reader"
|
||||
@@ -323,3 +325,297 @@ func TestReadIO(t *testing.T) {
|
||||
assert.Equal(t, "Processing user alice (ID: 123)", result.GetOrElse(func(error) string { return "" })(res))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOK tests LocalIOK functionality
|
||||
func TestLocalIOK(t *testing.T) {
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
t.Run("basic IO transformation", func(t *testing.T) {
|
||||
// IO effect that loads config from a path
|
||||
loadConfig := func(path string) IO[SimpleConfig] {
|
||||
return func() SimpleConfig {
|
||||
// Simulate loading config
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalIOK
|
||||
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(useConfig)
|
||||
res := adapted("config.json")()
|
||||
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
})
|
||||
|
||||
t.Run("IO transformation with side effects", func(t *testing.T) {
|
||||
var loadLog []string
|
||||
|
||||
loadData := func(key string) IO[int] {
|
||||
return func() int {
|
||||
loadLog = append(loadLog, "Loading: "+key)
|
||||
return len(key) * 10
|
||||
}
|
||||
}
|
||||
|
||||
processData := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("Processed: %d", n))
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string, int, string](loadData)(processData)
|
||||
res := adapted("test")()
|
||||
|
||||
assert.Equal(t, result.Of("Processed: 40"), res)
|
||||
assert.Equal(t, []string{"Loading: test"}, loadLog)
|
||||
})
|
||||
|
||||
t.Run("error propagation in ReaderIOResult", func(t *testing.T) {
|
||||
loadConfig := func(path string) IO[SimpleConfig] {
|
||||
return func() SimpleConfig {
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderIOResult that returns an error
|
||||
failingOperation := func(cfg SimpleConfig) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Left[string](errors.New("operation failed"))
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(failingOperation)
|
||||
res := adapted("config.json")()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOEitherK tests LocalIOEitherK functionality
|
||||
func TestLocalIOEitherK(t *testing.T) {
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
t.Run("basic IOEither transformation", func(t *testing.T) {
|
||||
// IOEither effect that loads config from a path (can fail)
|
||||
loadConfig := func(path string) IOEither[error, SimpleConfig] {
|
||||
return func() Either[error, SimpleConfig] {
|
||||
if path == "" {
|
||||
return E.Left[SimpleConfig](errors.New("empty path"))
|
||||
}
|
||||
return E.Of[error](SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalIOEitherK
|
||||
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
res := adapted("config.json")()
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := adapted("")()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
|
||||
t.Run("error propagation from environment transformation", func(t *testing.T) {
|
||||
loadConfig := func(path string) IOEither[error, SimpleConfig] {
|
||||
return func() Either[error, SimpleConfig] {
|
||||
return E.Left[SimpleConfig](errors.New("file not found"))
|
||||
}
|
||||
}
|
||||
|
||||
useConfig := func(cfg SimpleConfig) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(useConfig)
|
||||
res := adapted("missing.json")()
|
||||
|
||||
// Error from loadConfig should propagate
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("error propagation from ReaderIOResult", func(t *testing.T) {
|
||||
loadConfig := func(path string) IOEither[error, SimpleConfig] {
|
||||
return func() Either[error, SimpleConfig] {
|
||||
return E.Of[error](SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderIOResult that returns an error
|
||||
failingOperation := func(cfg SimpleConfig) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Left[string](errors.New("operation failed"))
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(failingOperation)
|
||||
res := adapted("config.json")()
|
||||
|
||||
// Error from ReaderIOResult should propagate
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOResultK tests LocalIOResultK functionality
|
||||
func TestLocalIOResultK(t *testing.T) {
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
t.Run("basic IOResult transformation", func(t *testing.T) {
|
||||
// IOResult effect that loads config from a path (can fail)
|
||||
loadConfig := func(path string) IOResult[SimpleConfig] {
|
||||
return func() Result[SimpleConfig] {
|
||||
if path == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty path"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalIOResultK
|
||||
adapted := LocalIOResultK[string, SimpleConfig, string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
res := adapted("config.json")()
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := adapted("")()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
|
||||
t.Run("error propagation from environment transformation", func(t *testing.T) {
|
||||
loadConfig := func(path string) IOResult[SimpleConfig] {
|
||||
return func() Result[SimpleConfig] {
|
||||
return result.Left[SimpleConfig](errors.New("file not found"))
|
||||
}
|
||||
}
|
||||
|
||||
useConfig := func(cfg SimpleConfig) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOResultK[string, SimpleConfig, string](loadConfig)(useConfig)
|
||||
res := adapted("missing.json")()
|
||||
|
||||
// Error from loadConfig should propagate
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("compose multiple LocalIOResultK", func(t *testing.T) {
|
||||
// First transformation: string -> int (can fail)
|
||||
parseID := func(s string) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
if s == "" {
|
||||
return result.Left[int](errors.New("empty string"))
|
||||
}
|
||||
return result.Of(len(s) * 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Second transformation: int -> SimpleConfig (can fail)
|
||||
loadConfig := func(id int) IOResult[SimpleConfig] {
|
||||
return func() Result[SimpleConfig] {
|
||||
if id < 0 {
|
||||
return result.Left[SimpleConfig](errors.New("invalid ID"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 8000 + id})
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
formatConfig := func(cfg SimpleConfig) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
|
||||
// Compose transformations
|
||||
step1 := LocalIOResultK[string, SimpleConfig, int](loadConfig)(formatConfig)
|
||||
step2 := LocalIOResultK[string, int, string](parseID)(step1)
|
||||
|
||||
// Success case
|
||||
res := step2("test")()
|
||||
assert.Equal(t, result.Of("Port: 8040"), res)
|
||||
|
||||
// Failure in first transformation
|
||||
resErr1 := step2("")()
|
||||
assert.True(t, result.IsLeft(resErr1))
|
||||
})
|
||||
|
||||
t.Run("real-world: load and validate config", func(t *testing.T) {
|
||||
type ConfigFile struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// Read file (can fail)
|
||||
readFile := func(cf ConfigFile) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
if cf.Path == "" {
|
||||
return result.Left[string](errors.New("empty path"))
|
||||
}
|
||||
return result.Of(`{"port":9000}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse config (can fail)
|
||||
parseConfig := func(content string) IOResult[SimpleConfig] {
|
||||
return func() Result[SimpleConfig] {
|
||||
if content == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty content"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 9000})
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
useConfig := func(cfg SimpleConfig) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("Using port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
|
||||
// Compose the pipeline
|
||||
step1 := LocalIOResultK[string, SimpleConfig, string](parseConfig)(useConfig)
|
||||
step2 := LocalIOResultK[string, string, ConfigFile](readFile)(step1)
|
||||
|
||||
// Success case
|
||||
res := step2(ConfigFile{Path: "app.json"})()
|
||||
assert.Equal(t, result.Of("Using port: 9000"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := step2(ConfigFile{Path: ""})()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
}
|
||||
|
||||
72
v2/readerreaderioeither/promap.go
Normal file
72
v2/readerreaderioeither/promap.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package readerreaderioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerioeither"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func Local[C, E, A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return reader.Local[ReaderIOEither[C, E, A]](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalIOK[C, E, A, R1, R2 any](f io.Kleisli[R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return F.Flow4(
|
||||
f,
|
||||
io.Map(rri),
|
||||
readerioeither.FromIO[C],
|
||||
readerioeither.Flatten,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalIOEitherK[C, A, R1, R2, E any](f ioeither.Kleisli[E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return F.Flow4(
|
||||
f,
|
||||
ioeither.Map[E](rri),
|
||||
readerioeither.FromIOEither[C],
|
||||
readerioeither.Flatten,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalEitherK[C, A, R1, R2, E any](f either.Kleisli[E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return F.Flow4(
|
||||
f,
|
||||
either.Map[E](rri),
|
||||
readerioeither.FromEither[C],
|
||||
readerioeither.Flatten,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalReaderIOEitherK[A, C, E, R1, R2 any](f readerioeither.Kleisli[C, E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return F.Flow3(
|
||||
f,
|
||||
readerioeither.Map[C, E](rri),
|
||||
readerioeither.Flatten,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalReaderReaderIOEitherK[A, C, E, R1, R2 any](f Kleisli[R2, C, E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return F.Flow2(
|
||||
reader.AsksReader(f),
|
||||
readerioeither.Chain(rri),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -271,6 +271,15 @@ func ChainReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderIOEitherK[C, R, E, A, B any](f RIOE.Kleisli[R, E, A, B]) Operator[R, C, E, A, B] {
|
||||
return fromreader.ChainReaderK(
|
||||
Chain[R, C, E, A, B],
|
||||
FromReaderIOEither[C, E, R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainFirstReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
|
||||
return fromreader.MonadChainFirstReaderK(
|
||||
@@ -595,11 +604,6 @@ func MapLeft[R, C, A, E1, E2 any](f func(E1) E2) func(ReaderReaderIOEither[R, C,
|
||||
return reader.Map[R](RIOE.MapLeft[C, A](f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Local[C, E, A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
|
||||
return reader.Local[ReaderIOEither[C, E, A]](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Read[C, E, A, R any](r R) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
|
||||
return reader.Read[ReaderIOEither[C, E, A]](r)
|
||||
@@ -659,3 +663,13 @@ func Delay[R, C, E, A any](delay time.Duration) Operator[R, C, E, A, A] {
|
||||
func After[R, C, E, A any](timestamp time.Time) Operator[R, C, E, A, A] {
|
||||
return reader.Map[R](RIOE.After[C, E, A](timestamp))
|
||||
}
|
||||
|
||||
func Defer[R, C, E, A any](fa Lazy[ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
|
||||
return func(r R) ReaderIOEither[C, E, A] {
|
||||
return func(c C) RIOE.IOEither[E, A] {
|
||||
return func() IOE.Either[E, A] {
|
||||
return fa()(r)(c)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
v2/readerreaderioeither/traverse.go
Normal file
15
v2/readerreaderioeither/traverse.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package readerreaderioeither
|
||||
|
||||
import (
|
||||
RA "github.com/IBM/fp-go/v2/internal/array"
|
||||
)
|
||||
|
||||
func TraverseArray[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Kleisli[R, C, E, []A, []B] {
|
||||
return RA.Traverse[[]A, []B](
|
||||
Of,
|
||||
Map,
|
||||
Ap,
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,8 @@
|
||||
package result
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
@@ -603,3 +605,54 @@ func MonadAlt[A any](fa Result[A], that Lazy[Result[A]]) Result[A] {
|
||||
func Zero[A any]() Result[A] {
|
||||
return either.Zero[error, A]()
|
||||
}
|
||||
|
||||
// InstanceOf attempts to perform a type assertion on an any value to convert it to type A.
|
||||
// If the type assertion succeeds, it returns a Right containing the converted value.
|
||||
// If the type assertion fails, it returns a Left containing an error describing the type mismatch.
|
||||
//
|
||||
// This function is useful for safely converting interface{}/any values to concrete types
|
||||
// in a functional programming style, where type assertion failures are represented as
|
||||
// Left values rather than panics or boolean checks.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type to convert to
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The value of type any to be type-asserted
|
||||
//
|
||||
// Returns:
|
||||
// - Result[A]: Right(value) if type assertion succeeds, Left(error) if it fails
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Successful type assertion
|
||||
// var value any = 42
|
||||
// result := result.InstanceOf[int](value) // Right(42)
|
||||
//
|
||||
// // Failed type assertion
|
||||
// var value any = "hello"
|
||||
// result := result.InstanceOf[int](value) // Left(error: "expected int, got string")
|
||||
//
|
||||
// // Using with pipe for safe type conversion
|
||||
// var data any = 3.14
|
||||
// result := F.Pipe1(
|
||||
// data,
|
||||
// result.InstanceOf[float64],
|
||||
// ) // Right(3.14)
|
||||
//
|
||||
// // Chaining with other operations
|
||||
// var value any = 10
|
||||
// result := F.Pipe2(
|
||||
// value,
|
||||
// result.InstanceOf[int],
|
||||
// result.Map(func(n int) int { return n * 2 }),
|
||||
// ) // Right(20)
|
||||
//
|
||||
//go:inline
|
||||
func InstanceOf[A any](a any) Result[A] {
|
||||
var res, ok = a.(A)
|
||||
if ok {
|
||||
return Of(res)
|
||||
}
|
||||
return Left[A](fmt.Errorf("expected %T, got %T", res, a))
|
||||
}
|
||||
|
||||
@@ -183,3 +183,176 @@ func TestZeroEqualsDefaultInitialization(t *testing.T) {
|
||||
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
|
||||
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
|
||||
}
|
||||
|
||||
// TestInstanceOf tests the InstanceOf function for type assertions
|
||||
func TestInstanceOf(t *testing.T) {
|
||||
// Test successful type assertion with int
|
||||
t.Run("successful int assertion", func(t *testing.T) {
|
||||
var value any = 42
|
||||
result := InstanceOf[int](value)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, Right(42), result)
|
||||
})
|
||||
|
||||
// Test successful type assertion with string
|
||||
t.Run("successful string assertion", func(t *testing.T) {
|
||||
var value any = "hello"
|
||||
result := InstanceOf[string](value)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, Right("hello"), result)
|
||||
})
|
||||
|
||||
// Test successful type assertion with float64
|
||||
t.Run("successful float64 assertion", func(t *testing.T) {
|
||||
var value any = 3.14
|
||||
result := InstanceOf[float64](value)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, Right(3.14), result)
|
||||
})
|
||||
|
||||
// Test successful type assertion with pointer
|
||||
t.Run("successful pointer assertion", func(t *testing.T) {
|
||||
val := 42
|
||||
var value any = &val
|
||||
result := InstanceOf[*int](value)
|
||||
assert.True(t, IsRight(result))
|
||||
v, err := UnwrapError(result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, *v)
|
||||
})
|
||||
|
||||
// Test successful type assertion with struct
|
||||
t.Run("successful struct assertion", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
var value any = Person{Name: "Alice", Age: 30}
|
||||
result := InstanceOf[Person](value)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, Right(Person{Name: "Alice", Age: 30}), result)
|
||||
})
|
||||
|
||||
// Test failed type assertion - int to string
|
||||
t.Run("failed int to string assertion", func(t *testing.T) {
|
||||
var value any = 42
|
||||
result := InstanceOf[string](value)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := UnwrapError(result)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "expected")
|
||||
assert.Contains(t, err.Error(), "got")
|
||||
})
|
||||
|
||||
// Test failed type assertion - string to int
|
||||
t.Run("failed string to int assertion", func(t *testing.T) {
|
||||
var value any = "hello"
|
||||
result := InstanceOf[int](value)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, err := UnwrapError(result)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
// Test failed type assertion - int to float64
|
||||
t.Run("failed int to float64 assertion", func(t *testing.T) {
|
||||
var value any = 42
|
||||
result := InstanceOf[float64](value)
|
||||
assert.True(t, IsLeft(result))
|
||||
})
|
||||
|
||||
// Test with nil value
|
||||
t.Run("nil value assertion", func(t *testing.T) {
|
||||
var value any = nil
|
||||
result := InstanceOf[string](value)
|
||||
assert.True(t, IsLeft(result))
|
||||
})
|
||||
|
||||
// Test chaining with Map
|
||||
t.Run("chaining with Map", func(t *testing.T) {
|
||||
var value any = 10
|
||||
result := F.Pipe2(
|
||||
value,
|
||||
InstanceOf[int],
|
||||
Map(func(n int) int { return n * 2 }),
|
||||
)
|
||||
assert.Equal(t, Right(20), result)
|
||||
})
|
||||
|
||||
// Test chaining with Map on failed assertion
|
||||
t.Run("chaining with Map on failed assertion", func(t *testing.T) {
|
||||
var value any = "not a number"
|
||||
result := F.Pipe2(
|
||||
value,
|
||||
InstanceOf[int],
|
||||
Map(func(n int) int { return n * 2 }),
|
||||
)
|
||||
assert.True(t, IsLeft(result))
|
||||
})
|
||||
|
||||
// Test with Chain for dependent operations
|
||||
t.Run("chaining with Chain", func(t *testing.T) {
|
||||
var value any = 5
|
||||
result := F.Pipe2(
|
||||
value,
|
||||
InstanceOf[int],
|
||||
Chain(func(n int) Result[string] {
|
||||
if n > 0 {
|
||||
return Right(fmt.Sprintf("positive: %d", n))
|
||||
}
|
||||
return Left[string](errors.New("not positive"))
|
||||
}),
|
||||
)
|
||||
assert.Equal(t, Right("positive: 5"), result)
|
||||
})
|
||||
|
||||
// Test with GetOrElse for default value
|
||||
t.Run("GetOrElse with failed assertion", func(t *testing.T) {
|
||||
var value any = "not an int"
|
||||
result := F.Pipe2(
|
||||
value,
|
||||
InstanceOf[int],
|
||||
GetOrElse(func(err error) int { return -1 }),
|
||||
)
|
||||
assert.Equal(t, -1, result)
|
||||
})
|
||||
|
||||
// Test with GetOrElse for successful assertion
|
||||
t.Run("GetOrElse with successful assertion", func(t *testing.T) {
|
||||
var value any = 42
|
||||
result := F.Pipe2(
|
||||
value,
|
||||
InstanceOf[int],
|
||||
GetOrElse(func(err error) int { return -1 }),
|
||||
)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
// Test with interface type
|
||||
t.Run("interface type assertion", func(t *testing.T) {
|
||||
var value any = errors.New("test error")
|
||||
result := InstanceOf[error](value)
|
||||
assert.True(t, IsRight(result))
|
||||
v, err := UnwrapError(result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test error", v.Error())
|
||||
})
|
||||
|
||||
// Test with slice type
|
||||
t.Run("slice type assertion", func(t *testing.T) {
|
||||
var value any = []int{1, 2, 3}
|
||||
result := InstanceOf[[]int](value)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, Right([]int{1, 2, 3}), result)
|
||||
})
|
||||
|
||||
// Test with map type
|
||||
t.Run("map type assertion", func(t *testing.T) {
|
||||
var value any = map[string]int{"a": 1, "b": 2}
|
||||
result := InstanceOf[map[string]int](value)
|
||||
assert.True(t, IsRight(result))
|
||||
v, err := UnwrapError(result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, v["a"])
|
||||
assert.Equal(t, 2, v["b"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package builder
|
||||
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at
|
||||
// 2026-01-23 16:15:30.703391 +0100 CET m=+0.003782501
|
||||
// 2026-01-27 10:18:05.2249315 +0100 CET m=+0.004416801
|
||||
|
||||
import (
|
||||
__lens "github.com/IBM/fp-go/v2/optics/lens"
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
|
||||
package lens
|
||||
|
||||
import "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
)
|
||||
|
||||
//go:generate go run ../../main.go lens --dir . --filename gen_lens.go
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package lens
|
||||
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at
|
||||
// 2026-01-23 16:09:35.747264 +0100 CET m=+0.003865601
|
||||
// 2026-01-27 10:33:42.2879434 +0100 CET m=+0.002788201
|
||||
|
||||
import (
|
||||
__lens "github.com/IBM/fp-go/v2/optics/lens"
|
||||
|
||||
Reference in New Issue
Block a user