mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-09 23:11:40 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51adce0c95 | ||
|
|
aa5e908810 | ||
|
|
b3bd5e9ad3 | ||
|
|
178df09ff7 | ||
|
|
92eb9715bd | ||
|
|
41ebb04ae0 |
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
@@ -41,6 +41,14 @@ jobs:
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./coverage.txt
|
||||
flag-name: v1-go-${{ matrix.go-version }}
|
||||
parallel: true
|
||||
|
||||
# - name: Upload coverage to Codecov
|
||||
# uses: codecov/codecov-action@v5
|
||||
# with:
|
||||
@@ -72,6 +80,14 @@ jobs:
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./v2/coverage.txt
|
||||
flag-name: v2-go-${{ matrix.go-version }}
|
||||
parallel: true
|
||||
|
||||
# - name: Upload coverage to Codecov
|
||||
# uses: codecov/codecov-action@v5
|
||||
# with:
|
||||
@@ -82,9 +98,22 @@ jobs:
|
||||
# env:
|
||||
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
coveralls-finish:
|
||||
name: Finish Coveralls
|
||||
needs:
|
||||
- build-v1
|
||||
- build-v2
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel-finished: true
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs:
|
||||
needs:
|
||||
- build-v1
|
||||
- build-v2
|
||||
if: github.repository == 'IBM/fp-go' && github.event_name != 'pull_request'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# fp-go: Functional Programming Library for Go
|
||||
|
||||
[](https://pkg.go.dev/github.com/IBM/fp-go)
|
||||
[](https://coveralls.io/github/IBM/fp-go?branch=main)
|
||||
|
||||
**🚧 Work in progress! 🚧** Despite major version 1 (due to [semantic-release limitations](https://github.com/semantic-release/semantic-release/issues/1507)), we're working to minimize breaking changes.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# fp-go V2: Enhanced Functional Programming for Go 1.24+
|
||||
|
||||
[](https://pkg.go.dev/github.com/IBM/fp-go/v2)
|
||||
[](https://coveralls.io/github/IBM/fp-go?branch=main)
|
||||
|
||||
Version 2 of fp-go leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
|
||||
|
||||
|
||||
@@ -21,14 +21,56 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
// result := generic.Do[[]State, State](State{})
|
||||
func Do[GS ~[]S, S any](
|
||||
empty S,
|
||||
) GS {
|
||||
return Of[GS](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
// For arrays, this produces the cartesian product where later steps can use values from earlier steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// generic.Do[[]State, State](State{}),
|
||||
// generic.Bind[[]State, []State, []int, State, State, int](
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// func(s State) []int {
|
||||
// return []int{1, 2, 3}
|
||||
// },
|
||||
// ),
|
||||
// generic.Bind[[]State, []State, []int, State, State, int](
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) []int {
|
||||
// // This can access s.X from the previous step
|
||||
// return []int{s.X * 10, s.X * 20}
|
||||
// },
|
||||
// ),
|
||||
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
|
||||
func Bind[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) GT,
|
||||
@@ -75,7 +117,39 @@ func BindTo[GS1 ~[]S1, GT ~[]T, S1, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel. For arrays, this produces the cartesian product.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y string
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// xValues := []int{1, 2}
|
||||
// yValues := []string{"a", "b"}
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// generic.Do[[]State, State](State{}),
|
||||
// generic.ApS[[]State, []State, []int, State, State, int](
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// xValues,
|
||||
// ),
|
||||
// generic.ApS[[]State, []State, []string, State, State, string](
|
||||
// func(y string) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// yValues,
|
||||
// ),
|
||||
// ) // [{1,"a"}, {1,"b"}, {2,"a"}, {2,"b"}]
|
||||
func ApS[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa GT,
|
||||
|
||||
@@ -403,5 +403,3 @@ func TestSlicePropertyBased(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -35,5 +35,6 @@ func Commands() []*C.Command {
|
||||
IOCommand(),
|
||||
IOOptionCommand(),
|
||||
DICommand(),
|
||||
LensCommand(),
|
||||
}
|
||||
}
|
||||
|
||||
521
v2/cli/lens.go
Normal file
521
v2/cli/lens.go
Normal file
@@ -0,0 +1,521 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
C "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
keyLensDir = "dir"
|
||||
keyVerbose = "verbose"
|
||||
lensAnnotation = "fp-go:Lens"
|
||||
)
|
||||
|
||||
var (
|
||||
flagLensDir = &C.StringFlag{
|
||||
Name: keyLensDir,
|
||||
Value: ".",
|
||||
Usage: "Directory to scan for Go files",
|
||||
}
|
||||
|
||||
flagVerbose = &C.BoolFlag{
|
||||
Name: keyVerbose,
|
||||
Aliases: []string{"v"},
|
||||
Value: false,
|
||||
Usage: "Enable verbose output",
|
||||
}
|
||||
)
|
||||
|
||||
// structInfo holds information about a struct that needs lens generation
|
||||
type structInfo struct {
|
||||
Name string
|
||||
Fields []fieldInfo
|
||||
Imports map[string]string // package path -> alias
|
||||
}
|
||||
|
||||
// fieldInfo holds information about a struct field
|
||||
type fieldInfo struct {
|
||||
Name string
|
||||
TypeName string
|
||||
BaseType string // TypeName without leading * for pointer types
|
||||
IsOptional bool // true if json tag has omitempty or field is a pointer
|
||||
}
|
||||
|
||||
// templateData holds data for template rendering
|
||||
type templateData struct {
|
||||
PackageName string
|
||||
Structs []structInfo
|
||||
}
|
||||
|
||||
const lensStructTemplate = `
|
||||
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
|
||||
type {{.Name}}Lenses struct {
|
||||
{{- range .Fields}}
|
||||
{{.Name}} {{if .IsOptional}}LO.LensO[{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[{{$.Name}}, {{.TypeName}}]{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
|
||||
type {{.Name}}RefLenses struct {
|
||||
{{- range .Fields}}
|
||||
{{.Name}} {{if .IsOptional}}LO.LensO[*{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[*{{$.Name}}, {{.TypeName}}]{{end}}
|
||||
{{- end}}
|
||||
}
|
||||
`
|
||||
|
||||
const lensConstructorTemplate = `
|
||||
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
|
||||
func Make{{.Name}}Lenses() {{.Name}}Lenses {
|
||||
{{- range .Fields}}
|
||||
{{- if .IsOptional}}
|
||||
getOrElse{{.Name}} := O.GetOrElse(F.ConstNil[{{.BaseType}}])
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}Lenses{
|
||||
{{- range .Fields}}
|
||||
{{- if .IsOptional}}
|
||||
{{.Name}}: L.MakeLens(
|
||||
func(s {{$.Name}}) O.Option[{{.TypeName}}] { return O.FromNillable(s.{{.Name}}) },
|
||||
func(s {{$.Name}}, v O.Option[{{.TypeName}}]) {{$.Name}} { s.{{.Name}} = getOrElse{{.Name}}(v); return s },
|
||||
),
|
||||
{{- else}}
|
||||
{{.Name}}: L.MakeLens(
|
||||
func(s {{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s {{$.Name}}, v {{.TypeName}}) {{$.Name}} { s.{{.Name}} = v; return s },
|
||||
),
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
|
||||
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
|
||||
func Make{{.Name}}RefLenses() {{.Name}}RefLenses {
|
||||
{{- range .Fields}}
|
||||
{{- if .IsOptional}}
|
||||
getOrElse{{.Name}} := O.GetOrElse(F.ConstNil[{{.BaseType}}])
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}RefLenses{
|
||||
{{- range .Fields}}
|
||||
{{- if .IsOptional}}
|
||||
{{.Name}}: L.MakeLensRef(
|
||||
func(s *{{$.Name}}) O.Option[{{.TypeName}}] { return O.FromNillable(s.{{.Name}}) },
|
||||
func(s *{{$.Name}}, v O.Option[{{.TypeName}}]) *{{$.Name}} { s.{{.Name}} = getOrElse{{.Name}}(v); return s },
|
||||
),
|
||||
{{- else}}
|
||||
{{.Name}}: L.MakeLensRef(
|
||||
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
|
||||
),
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var (
|
||||
structTmpl *template.Template
|
||||
constructorTmpl *template.Template
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
structTmpl, err = template.New("struct").Parse(lensStructTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
constructorTmpl, err = template.New("constructor").Parse(lensConstructorTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// hasLensAnnotation checks if a comment group contains the lens annotation
|
||||
func hasLensAnnotation(doc *ast.CommentGroup) bool {
|
||||
if doc == nil {
|
||||
return false
|
||||
}
|
||||
for _, comment := range doc.List {
|
||||
if strings.Contains(comment.Text, lensAnnotation) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getTypeName extracts the type name from a field type expression
|
||||
func getTypeName(expr ast.Expr) string {
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
case *ast.StarExpr:
|
||||
return "*" + getTypeName(t.X)
|
||||
case *ast.ArrayType:
|
||||
return "[]" + getTypeName(t.Elt)
|
||||
case *ast.MapType:
|
||||
return "map[" + getTypeName(t.Key) + "]" + getTypeName(t.Value)
|
||||
case *ast.SelectorExpr:
|
||||
return getTypeName(t.X) + "." + t.Sel.Name
|
||||
case *ast.InterfaceType:
|
||||
return "interface{}"
|
||||
case *ast.IndexExpr:
|
||||
// Generic type with single type parameter (Go 1.18+)
|
||||
// e.g., Option[string]
|
||||
return getTypeName(t.X) + "[" + getTypeName(t.Index) + "]"
|
||||
case *ast.IndexListExpr:
|
||||
// Generic type with multiple type parameters (Go 1.18+)
|
||||
// e.g., Map[string, int]
|
||||
var params []string
|
||||
for _, index := range t.Indices {
|
||||
params = append(params, getTypeName(index))
|
||||
}
|
||||
return getTypeName(t.X) + "[" + strings.Join(params, ", ") + "]"
|
||||
default:
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
|
||||
// extractImports extracts package imports from a type expression
|
||||
// Returns a map of package path -> package name
|
||||
func extractImports(expr ast.Expr, imports map[string]string) {
|
||||
switch t := expr.(type) {
|
||||
case *ast.StarExpr:
|
||||
extractImports(t.X, imports)
|
||||
case *ast.ArrayType:
|
||||
extractImports(t.Elt, imports)
|
||||
case *ast.MapType:
|
||||
extractImports(t.Key, imports)
|
||||
extractImports(t.Value, imports)
|
||||
case *ast.SelectorExpr:
|
||||
// This is a qualified identifier like "option.Option"
|
||||
if ident, ok := t.X.(*ast.Ident); ok {
|
||||
// ident.Name is the package name (e.g., "option")
|
||||
// We need to track this for import resolution
|
||||
imports[ident.Name] = ident.Name
|
||||
}
|
||||
case *ast.IndexExpr:
|
||||
// Generic type with single type parameter
|
||||
extractImports(t.X, imports)
|
||||
extractImports(t.Index, imports)
|
||||
case *ast.IndexListExpr:
|
||||
// Generic type with multiple type parameters
|
||||
extractImports(t.X, imports)
|
||||
for _, index := range t.Indices {
|
||||
extractImports(index, imports)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasOmitEmpty checks if a struct tag contains json omitempty
|
||||
func hasOmitEmpty(tag *ast.BasicLit) bool {
|
||||
if tag == nil {
|
||||
return false
|
||||
}
|
||||
// Parse the struct tag
|
||||
tagValue := strings.Trim(tag.Value, "`")
|
||||
structTag := reflect.StructTag(tagValue)
|
||||
jsonTag := structTag.Get("json")
|
||||
|
||||
// Check if omitempty is present
|
||||
parts := strings.Split(jsonTag, ",")
|
||||
for _, part := range parts {
|
||||
if strings.TrimSpace(part) == "omitempty" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isPointerType checks if a type expression is a pointer
|
||||
func isPointerType(expr ast.Expr) bool {
|
||||
_, ok := expr.(*ast.StarExpr)
|
||||
return ok
|
||||
}
|
||||
|
||||
// parseFile parses a Go file and extracts structs with lens annotations
|
||||
func parseFile(filename string) ([]structInfo, string, error) {
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
var structs []structInfo
|
||||
packageName := node.Name.Name
|
||||
|
||||
// Build import map: package name -> import path
|
||||
fileImports := make(map[string]string)
|
||||
for _, imp := range node.Imports {
|
||||
path := strings.Trim(imp.Path.Value, `"`)
|
||||
var name string
|
||||
if imp.Name != nil {
|
||||
name = imp.Name.Name
|
||||
} else {
|
||||
// Extract package name from path (last component)
|
||||
parts := strings.Split(path, "/")
|
||||
name = parts[len(parts)-1]
|
||||
}
|
||||
fileImports[name] = path
|
||||
}
|
||||
|
||||
// First pass: collect all GenDecls with their doc comments
|
||||
declMap := make(map[*ast.TypeSpec]*ast.CommentGroup)
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
if gd, ok := n.(*ast.GenDecl); ok {
|
||||
for _, spec := range gd.Specs {
|
||||
if ts, ok := spec.(*ast.TypeSpec); ok {
|
||||
declMap[ts] = gd.Doc
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Second pass: process type specs
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
// Look for type declarations
|
||||
typeSpec, ok := n.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if it's a struct type
|
||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Get the doc comment from our map
|
||||
doc := declMap[typeSpec]
|
||||
if !hasLensAnnotation(doc) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Extract field information and collect imports
|
||||
var fields []fieldInfo
|
||||
structImports := make(map[string]string)
|
||||
|
||||
for _, field := range structType.Fields.List {
|
||||
if len(field.Names) == 0 {
|
||||
// Embedded field, skip for now
|
||||
continue
|
||||
}
|
||||
for _, name := range field.Names {
|
||||
// Only export lenses for exported fields
|
||||
if name.IsExported() {
|
||||
typeName := getTypeName(field.Type)
|
||||
isOptional := false
|
||||
baseType := typeName
|
||||
|
||||
// Only pointer types can be optional
|
||||
if isPointerType(field.Type) {
|
||||
isOptional = true
|
||||
// Strip leading * for base type
|
||||
baseType = strings.TrimPrefix(typeName, "*")
|
||||
}
|
||||
|
||||
// Extract imports from this field's type
|
||||
fieldImports := make(map[string]string)
|
||||
extractImports(field.Type, fieldImports)
|
||||
|
||||
// Resolve package names to full import paths
|
||||
for pkgName := range fieldImports {
|
||||
if importPath, ok := fileImports[pkgName]; ok {
|
||||
structImports[importPath] = pkgName
|
||||
}
|
||||
}
|
||||
|
||||
fields = append(fields, fieldInfo{
|
||||
Name: name.Name,
|
||||
TypeName: typeName,
|
||||
BaseType: baseType,
|
||||
IsOptional: isOptional,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) > 0 {
|
||||
structs = append(structs, structInfo{
|
||||
Name: typeSpec.Name.Name,
|
||||
Fields: fields,
|
||||
Imports: structImports,
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return structs, packageName, nil
|
||||
}
|
||||
|
||||
// generateLensHelpers scans a directory for Go files and generates lens code
|
||||
func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
// Get absolute path
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Scanning directory: %s", absDir)
|
||||
}
|
||||
|
||||
// Find all Go files in the directory
|
||||
files, err := filepath.Glob(filepath.Join(absDir, "*.go"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Found %d Go files", len(files))
|
||||
}
|
||||
|
||||
// Parse all files and collect structs
|
||||
var allStructs []structInfo
|
||||
var packageName string
|
||||
|
||||
for _, file := range files {
|
||||
// Skip generated files and test files
|
||||
if strings.HasSuffix(file, "_test.go") || strings.Contains(file, "gen.go") {
|
||||
if verbose {
|
||||
log.Printf("Skipping file: %s", filepath.Base(file))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Parsing file: %s", filepath.Base(file))
|
||||
}
|
||||
|
||||
structs, pkg, err := parseFile(file)
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to parse %s: %v", file, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if verbose && len(structs) > 0 {
|
||||
log.Printf("Found %d annotated struct(s) in %s", len(structs), filepath.Base(file))
|
||||
for _, s := range structs {
|
||||
log.Printf(" - %s (%d fields)", s.Name, len(s.Fields))
|
||||
}
|
||||
}
|
||||
|
||||
if packageName == "" {
|
||||
packageName = pkg
|
||||
}
|
||||
|
||||
allStructs = append(allStructs, structs...)
|
||||
}
|
||||
|
||||
if len(allStructs) == 0 {
|
||||
log.Printf("No structs with %s annotation found in %s", lensAnnotation, absDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect all unique imports from all structs
|
||||
allImports := make(map[string]string) // import path -> alias
|
||||
for _, s := range allStructs {
|
||||
for importPath, alias := range s.Imports {
|
||||
allImports[importPath] = alias
|
||||
}
|
||||
}
|
||||
|
||||
// Create output file
|
||||
outPath := filepath.Join(absDir, filename)
|
||||
f, err := os.Create(filepath.Clean(outPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(allStructs))
|
||||
|
||||
// Write header
|
||||
writePackage(f, packageName)
|
||||
|
||||
// Write imports
|
||||
f.WriteString("import (\n")
|
||||
// Standard fp-go imports always needed
|
||||
f.WriteString("\tF \"github.com/IBM/fp-go/v2/function\"\n")
|
||||
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
|
||||
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
|
||||
f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
|
||||
|
||||
// Add additional imports collected from field types
|
||||
for importPath, alias := range allImports {
|
||||
f.WriteString("\t" + alias + " \"" + importPath + "\"\n")
|
||||
}
|
||||
|
||||
f.WriteString(")\n")
|
||||
|
||||
// Generate lens code for each struct using templates
|
||||
for _, s := range allStructs {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Generate struct type
|
||||
if err := structTmpl.Execute(&buf, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate constructor
|
||||
if err := constructorTmpl.Execute(&buf, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write to file
|
||||
if _, err := f.Write(buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LensCommand creates the CLI command for lens generation
|
||||
func LensCommand() *C.Command {
|
||||
return &C.Command{
|
||||
Name: "lens",
|
||||
Usage: "generate lens code for annotated structs",
|
||||
Description: "Scans Go files for structs annotated with 'fp-go:Lens' and generates lens types. Fields with json omitempty tag or pointer types generate LensO (optional lens).",
|
||||
Flags: []C.Flag{
|
||||
flagLensDir,
|
||||
flagFilename,
|
||||
flagVerbose,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
return generateLensHelpers(
|
||||
ctx.String(keyLensDir),
|
||||
ctx.String(keyFilename),
|
||||
ctx.Bool(keyVerbose),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
411
v2/cli/lens_test.go
Normal file
411
v2/cli/lens_test.go
Normal file
@@ -0,0 +1,411 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHasLensAnnotation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
comment string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "has annotation",
|
||||
comment: "// fp-go:Lens",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "has annotation with other text",
|
||||
comment: "// This is a struct with fp-go:Lens annotation",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no annotation",
|
||||
comment: "// This is just a regular comment",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nil comment",
|
||||
comment: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var doc *ast.CommentGroup
|
||||
if tt.comment != "" {
|
||||
doc = &ast.CommentGroup{
|
||||
List: []*ast.Comment{
|
||||
{Text: tt.comment},
|
||||
},
|
||||
}
|
||||
}
|
||||
result := hasLensAnnotation(doc)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTypeName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple type",
|
||||
code: "type T struct { F string }",
|
||||
expected: "string",
|
||||
},
|
||||
{
|
||||
name: "pointer type",
|
||||
code: "type T struct { F *string }",
|
||||
expected: "*string",
|
||||
},
|
||||
{
|
||||
name: "slice type",
|
||||
code: "type T struct { F []int }",
|
||||
expected: "[]int",
|
||||
},
|
||||
{
|
||||
name: "map type",
|
||||
code: "type T struct { F map[string]int }",
|
||||
expected: "map[string]int",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
var fieldType ast.Expr
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
|
||||
fieldType = field.Type
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
require.NotNil(t, fieldType)
|
||||
result := getTypeName(fieldType)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPointerType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "pointer type",
|
||||
code: "type T struct { F *string }",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "non-pointer type",
|
||||
code: "type T struct { F string }",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "slice type",
|
||||
code: "type T struct { F []string }",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
var fieldType ast.Expr
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
|
||||
fieldType = field.Type
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
require.NotNil(t, fieldType)
|
||||
result := isPointerType(fieldType)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasOmitEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "has omitempty",
|
||||
tag: "`json:\"field,omitempty\"`",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "has omitempty with other options",
|
||||
tag: "`json:\"field,omitempty,string\"`",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no omitempty",
|
||||
tag: "`json:\"field\"`",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no tag",
|
||||
tag: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "different tag",
|
||||
tag: "`xml:\"field\"`",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var tag *ast.BasicLit
|
||||
if tt.tag != "" {
|
||||
tag = &ast.BasicLit{
|
||||
Value: tt.tag,
|
||||
}
|
||||
}
|
||||
result := hasOmitEmpty(tag)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFile(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
Phone *string
|
||||
}
|
||||
|
||||
// fp-go:Lens
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
// Not annotated
|
||||
type Other struct {
|
||||
Field string
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
structs, pkg, err := parseFile(testFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify results
|
||||
assert.Equal(t, "testpkg", pkg)
|
||||
assert.Len(t, structs, 2)
|
||||
|
||||
// Check Person struct
|
||||
person := structs[0]
|
||||
assert.Equal(t, "Person", person.Name)
|
||||
assert.Len(t, person.Fields, 3)
|
||||
|
||||
assert.Equal(t, "Name", person.Fields[0].Name)
|
||||
assert.Equal(t, "string", person.Fields[0].TypeName)
|
||||
assert.False(t, person.Fields[0].IsOptional)
|
||||
|
||||
assert.Equal(t, "Age", person.Fields[1].Name)
|
||||
assert.Equal(t, "int", person.Fields[1].TypeName)
|
||||
assert.False(t, person.Fields[1].IsOptional)
|
||||
|
||||
assert.Equal(t, "Phone", person.Fields[2].Name)
|
||||
assert.Equal(t, "*string", person.Fields[2].TypeName)
|
||||
assert.True(t, person.Fields[2].IsOptional)
|
||||
|
||||
// Check Address struct
|
||||
address := structs[1]
|
||||
assert.Equal(t, "Address", address.Name)
|
||||
assert.Len(t, address.Fields, 2)
|
||||
|
||||
assert.Equal(t, "Street", address.Fields[0].Name)
|
||||
assert.Equal(t, "City", address.Fields[1].Name)
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpers(t *testing.T) {
|
||||
// Create a temporary directory with test files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// fp-go:Lens
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Value *int
|
||||
}
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
genPath := filepath.Join(tmpDir, outputFile)
|
||||
_, err = os.Stat(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read and verify the generated content
|
||||
content, err := os.ReadFile(genPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// Check for expected content
|
||||
assert.Contains(t, contentStr, "package testpkg")
|
||||
assert.Contains(t, contentStr, "Code generated by go generate")
|
||||
assert.Contains(t, contentStr, "TestStructLens")
|
||||
assert.Contains(t, contentStr, "MakeTestStructLens")
|
||||
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
|
||||
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
|
||||
assert.Contains(t, contentStr, "O.FromNillable")
|
||||
assert.Contains(t, contentStr, "O.GetOrElse")
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
|
||||
// Create a temporary directory with test files
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
testCode := `package testpkg
|
||||
|
||||
// No annotation
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
}
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code (should not create file)
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file does not exist
|
||||
genPath := filepath.Join(tmpDir, outputFile)
|
||||
_, err = os.Stat(genPath)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
}
|
||||
|
||||
func TestLensTemplates(t *testing.T) {
|
||||
s := structInfo{
|
||||
Name: "TestStruct",
|
||||
Fields: []fieldInfo{
|
||||
{Name: "Name", TypeName: "string", IsOptional: false},
|
||||
{Name: "Value", TypeName: "*int", IsOptional: true},
|
||||
},
|
||||
}
|
||||
|
||||
// Test struct template
|
||||
var structBuf bytes.Buffer
|
||||
err := structTmpl.Execute(&structBuf, s)
|
||||
require.NoError(t, err)
|
||||
|
||||
structStr := structBuf.String()
|
||||
assert.Contains(t, structStr, "type TestStructLenses struct")
|
||||
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
|
||||
assert.Contains(t, structStr, "Value LO.LensO[TestStruct, *int]")
|
||||
|
||||
// Test constructor template
|
||||
var constructorBuf bytes.Buffer
|
||||
err = constructorTmpl.Execute(&constructorBuf, s)
|
||||
require.NoError(t, err)
|
||||
|
||||
constructorStr := constructorBuf.String()
|
||||
assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses")
|
||||
assert.Contains(t, constructorStr, "return TestStructLenses{")
|
||||
assert.Contains(t, constructorStr, "Name: L.MakeLens(")
|
||||
assert.Contains(t, constructorStr, "Value: L.MakeLens(")
|
||||
assert.Contains(t, constructorStr, "O.FromNillable")
|
||||
assert.Contains(t, constructorStr, "O.GetOrElse")
|
||||
}
|
||||
|
||||
func TestLensCommandFlags(t *testing.T) {
|
||||
cmd := LensCommand()
|
||||
|
||||
assert.Equal(t, "lens", cmd.Name)
|
||||
assert.Equal(t, "generate lens code for annotated structs", cmd.Usage)
|
||||
assert.Contains(t, strings.ToLower(cmd.Description), "fp-go:lens")
|
||||
assert.Contains(t, strings.ToLower(cmd.Description), "lenso")
|
||||
|
||||
// Check flags
|
||||
assert.Len(t, cmd.Flags, 3)
|
||||
|
||||
var hasDir, hasFilename, hasVerbose bool
|
||||
for _, flag := range cmd.Flags {
|
||||
switch flag.Names()[0] {
|
||||
case "dir":
|
||||
hasDir = true
|
||||
case "filename":
|
||||
hasFilename = true
|
||||
case "verbose":
|
||||
hasVerbose = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasDir, "should have dir flag")
|
||||
assert.True(t, hasFilename, "should have filename flag")
|
||||
assert.True(t, hasVerbose, "should have verbose flag")
|
||||
}
|
||||
@@ -18,12 +18,12 @@ package readereither
|
||||
import "github.com/IBM/fp-go/v2/readereither"
|
||||
|
||||
// TraverseArray transforms an array
|
||||
func TraverseArray[A, B any](f func(A) ReaderEither[B]) func([]A) ReaderEither[[]B] {
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return readereither.TraverseArray(f)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex transforms an array
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderEither[B]) func([]A) ReaderEither[[]B] {
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderEither[B]) Kleisli[[]A, []B] {
|
||||
return readereither.TraverseArrayWithIndex(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,21 +18,71 @@ package readereither
|
||||
import (
|
||||
"context"
|
||||
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// UserID string
|
||||
// TenantID string
|
||||
// }
|
||||
// result := readereither.Do(State{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderEither[S] {
|
||||
return G.Do[ReaderEither[S], context.Context, error, S](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access the context.Context from the environment.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// UserID string
|
||||
// TenantID string
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do(State{}),
|
||||
// readereither.Bind(
|
||||
// func(uid string) func(State) State {
|
||||
// return func(s State) State { s.UserID = uid; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderEither[string] {
|
||||
// return func(ctx context.Context) either.Either[error, string] {
|
||||
// if uid, ok := ctx.Value("userID").(string); ok {
|
||||
// return either.Right[error](uid)
|
||||
// }
|
||||
// return either.Left[string](errors.New("no userID"))
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// readereither.Bind(
|
||||
// func(tid string) func(State) State {
|
||||
// return func(s State) State { s.TenantID = tid; return s }
|
||||
// },
|
||||
// func(s State) readereither.ReaderEither[string] {
|
||||
// // This can access s.UserID from the previous step
|
||||
// return func(ctx context.Context) either.Either[error, string] {
|
||||
// return either.Right[error]("tenant-" + s.UserID)
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) ReaderEither[T],
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[ReaderEither[S1], S2] {
|
||||
return G.Bind[ReaderEither[S1], ReaderEither[S2], ReaderEither[T], context.Context, error, S1, S2, T](setter, f)
|
||||
}
|
||||
|
||||
@@ -40,7 +90,7 @@ func Bind[S1, S2, T any](
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
) Kleisli[ReaderEither[S1], S2] {
|
||||
return G.Let[ReaderEither[S1], ReaderEither[S2], context.Context, error, S1, S2, T](setter, f)
|
||||
}
|
||||
|
||||
@@ -48,21 +98,212 @@ func Let[S1, S2, T any](
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
) Kleisli[ReaderEither[S1], S2] {
|
||||
return G.LetTo[ReaderEither[S1], ReaderEither[S2], context.Context, error, S1, S2, T](setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(ReaderEither[T]) ReaderEither[S1] {
|
||||
) Kleisli[ReaderEither[T], S1] {
|
||||
return G.BindTo[ReaderEither[S1], ReaderEither[T], context.Context, error, S1, T](setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// UserID string
|
||||
// TenantID string
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUserID := func(ctx context.Context) either.Either[error, string] {
|
||||
// return either.Right[error](ctx.Value("userID").(string))
|
||||
// }
|
||||
// getTenantID := func(ctx context.Context) either.Either[error, string] {
|
||||
// return either.Right[error](ctx.Value("tenantID").(string))
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readereither.Do(State{}),
|
||||
// readereither.ApS(
|
||||
// func(uid string) func(State) State {
|
||||
// return func(s State) State { s.UserID = uid; return s }
|
||||
// },
|
||||
// getUserID,
|
||||
// ),
|
||||
// readereither.ApS(
|
||||
// func(tid string) func(State) State {
|
||||
// return func(s State) State { s.TenantID = tid; return s }
|
||||
// },
|
||||
// getTenantID,
|
||||
// ),
|
||||
// )
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderEither[T],
|
||||
) func(ReaderEither[S1]) ReaderEither[S2] {
|
||||
) Kleisli[ReaderEither[S1], S2] {
|
||||
return G.ApS[ReaderEither[S1], ReaderEither[S2], ReaderEither[T], context.Context, error, S1, S2, T](setter, fa)
|
||||
}
|
||||
|
||||
// ApSL is a variant of ApS that uses a lens to focus on a specific field in the state.
|
||||
// Instead of providing a setter function, you provide a lens that knows how to get and set
|
||||
// the field. This is more convenient when working with nested structures.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens that focuses on a field of type T within state S
|
||||
// - fa: A ReaderEither computation that produces a value of type T
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms ReaderEither[S] to ReaderEither[S] by setting the focused field
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(p Person) int { return p.Age },
|
||||
// func(p Person, a int) Person { p.Age = a; return p },
|
||||
// )
|
||||
//
|
||||
// getAge := func(ctx context.Context) either.Either[error, int] {
|
||||
// return either.Right[error](30)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readereither.Do(Person{Name: "Alice", Age: 25}),
|
||||
// readereither.ApSL(ageLens, getAge),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderEither[T],
|
||||
) Kleisli[ReaderEither[S], S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific field in the state.
|
||||
// It combines the lens-based field access with monadic composition, allowing you to:
|
||||
// 1. Extract a field value using the lens
|
||||
// 2. Use that value in a computation that may fail
|
||||
// 3. Update the field with the result
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens that focuses on a field of type T within state S
|
||||
// - f: A function that takes the current field value and returns a ReaderEither computation
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms ReaderEither[S] to ReaderEither[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// increment := func(v int) readereither.ReaderEither[int] {
|
||||
// return func(ctx context.Context) either.Either[error, int] {
|
||||
// if v >= 100 {
|
||||
// return either.Left[int](errors.New("value too large"))
|
||||
// }
|
||||
// return either.Right[error](v + 1)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readereither.Of[error](Counter{Value: 42}),
|
||||
// readereither.BindL(valueLens, increment),
|
||||
// )
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Kleisli[ReaderEither[S], S] {
|
||||
return Bind[S, S, T](lens.Set, func(s S) ReaderEither[T] {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
|
||||
// It applies a pure transformation to the focused field without any effects.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens that focuses on a field of type T within state S
|
||||
// - f: A pure function that transforms the field value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms ReaderEither[S] to ReaderEither[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readereither.Of[error](Counter{Value: 21}),
|
||||
// readereither.LetL(valueLens, double),
|
||||
// )
|
||||
// // result when executed will be Right(Counter{Value: 42})
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Kleisli[ReaderEither[S], S] {
|
||||
return Let[S, S, T](lens.Set, func(s S) T {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.
|
||||
// It sets the focused field to a constant value.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens that focuses on a field of type T within state S
|
||||
// - b: The constant value to set
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms ReaderEither[S] to ReaderEither[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readereither.Of[error](Config{Debug: true, Timeout: 30}),
|
||||
// readereither.LetToL(debugLens, false),
|
||||
// )
|
||||
// // result when executed will be Right(Config{Debug: false, Timeout: 30})
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Kleisli[ReaderEither[S], S] {
|
||||
return LetTo[S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -28,26 +28,26 @@ func Curry0[A any](f func(context.Context) (A, error)) ReaderEither[A] {
|
||||
return readereither.Curry0(f)
|
||||
}
|
||||
|
||||
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderEither[A] {
|
||||
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) Kleisli[T1, A] {
|
||||
return readereither.Curry1(f)
|
||||
}
|
||||
|
||||
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) func(T2) ReaderEither[A] {
|
||||
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) Kleisli[T2, A] {
|
||||
return readereither.Curry2(f)
|
||||
}
|
||||
|
||||
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) func(T3) ReaderEither[A] {
|
||||
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) Kleisli[T3, A] {
|
||||
return readereither.Curry3(f)
|
||||
}
|
||||
|
||||
func Uncurry1[T1, A any](f func(T1) ReaderEither[A]) func(context.Context, T1) (A, error) {
|
||||
func Uncurry1[T1, A any](f Kleisli[T1, A]) func(context.Context, T1) (A, error) {
|
||||
return readereither.Uncurry1(f)
|
||||
}
|
||||
|
||||
func Uncurry2[T1, T2, A any](f func(T1) func(T2) ReaderEither[A]) func(context.Context, T1, T2) (A, error) {
|
||||
func Uncurry2[T1, T2, A any](f func(T1) Kleisli[T2, A]) func(context.Context, T1, T2) (A, error) {
|
||||
return readereither.Uncurry2(f)
|
||||
}
|
||||
|
||||
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderEither[A]) func(context.Context, T1, T2, T3) (A, error) {
|
||||
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) Kleisli[T3, A]) func(context.Context, T1, T2, T3) (A, error) {
|
||||
return readereither.Uncurry3(f)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func From0[A any](f func(context.Context) (A, error)) func() ReaderEither[A] {
|
||||
return readereither.From0(f)
|
||||
}
|
||||
|
||||
func From1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderEither[A] {
|
||||
func From1[T1, A any](f func(context.Context, T1) (A, error)) Kleisli[T1, A] {
|
||||
return readereither.From1(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,11 +41,11 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return readereither.Map[context.Context, error](f)
|
||||
}
|
||||
|
||||
func MonadChain[A, B any](ma ReaderEither[A], f func(A) ReaderEither[B]) ReaderEither[B] {
|
||||
func MonadChain[A, B any](ma ReaderEither[A], f Kleisli[A, B]) ReaderEither[B] {
|
||||
return readereither.MonadChain(ma, f)
|
||||
}
|
||||
|
||||
func Chain[A, B any](f func(A) ReaderEither[B]) Operator[A, B] {
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return readereither.Chain(f)
|
||||
}
|
||||
|
||||
@@ -61,11 +61,11 @@ func Ap[A, B any](fa ReaderEither[A]) func(ReaderEither[func(A) B]) ReaderEither
|
||||
return readereither.Ap[B](fa)
|
||||
}
|
||||
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) ReaderEither[A] {
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
|
||||
return readereither.FromPredicate[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
func OrElse[A any](onLeft func(error) ReaderEither[A]) func(ReaderEither[A]) ReaderEither[A] {
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderEither[A], A] {
|
||||
return readereither.OrElse(onLeft)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,5 +31,6 @@ type (
|
||||
// ReaderEither is a specialization of the Reader monad for the typical golang scenario
|
||||
ReaderEither[A any] = readereither.ReaderEither[context.Context, error, A]
|
||||
|
||||
Operator[A, B any] = reader.Reader[ReaderEither[A], ReaderEither[B]]
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderEither[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderEither[A], B]
|
||||
)
|
||||
|
||||
@@ -19,20 +19,75 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
// result := readerioeither.Do(State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderIOEither[S] {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access the context.Context from the environment.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) readerioeither.ReaderIOEither[User] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// readerioeither.Bind(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// func(s State) readerioeither.ReaderIOEither[Config] {
|
||||
// // This can access s.User from the previous step
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, Config] {
|
||||
// return ioeither.TryCatch(func() (Config, error) {
|
||||
// return fetchConfigForUser(ctx, s.User.ID)
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) ReaderIOEither[T],
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return chain.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[T, S2],
|
||||
@@ -42,10 +97,12 @@ func Bind[S1, S2, T any](
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
) Operator[S1, S2] {
|
||||
return functor.Let(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
@@ -54,10 +111,12 @@ func Let[S1, S2, T any](
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
) Operator[S1, S2] {
|
||||
return functor.LetTo(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
@@ -66,6 +125,8 @@ func LetTo[S1, S2, T any](
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[T, S1] {
|
||||
@@ -75,11 +136,53 @@ func BindTo[S1, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// getConfig := func(ctx context.Context) ioeither.IOEither[error, Config] {
|
||||
// return ioeither.TryCatch(func() (Config, error) {
|
||||
// return fetchConfig(ctx)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.ApS(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// readerioeither.ApS(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// getConfig,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderIOEither[T],
|
||||
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
|
||||
) Operator[S1, S2] {
|
||||
return apply.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
@@ -87,3 +190,152 @@ func ApS[S1, S2, T any](
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Of(State{}),
|
||||
// readerioeither.ApSL(userLens, getUser),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderIOEither[T],
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a ReaderIOEither computation that produces an updated value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.BindL(userLens, func(user User) readerioeither.ReaderIOEither[User] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser(ctx)
|
||||
// })
|
||||
// }
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return Bind[S, S, T](lens.Set, func(s S) ReaderIOEither[T] {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new value (without wrapping in a ReaderIOEither).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{User: User{Name: "Alice"}}),
|
||||
// readerioeither.LetL(userLens, func(user User) User {
|
||||
// user.Name = "Bob"
|
||||
// return user
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[S, S] {
|
||||
return Let[S, S, T](lens.Set, func(s S) T {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The value b is set directly to the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// userLens := lens.MakeLens(
|
||||
// func(s State) User { return s.User },
|
||||
// func(s State, u User) State { s.User = u; return s },
|
||||
// )
|
||||
//
|
||||
// newUser := User{Name: "Bob", ID: 123}
|
||||
// result := F.Pipe2(
|
||||
// readerioeither.Do(State{}),
|
||||
// readerioeither.LetToL(userLens, newUser),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return LetTo[S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -42,7 +43,7 @@ func TestBind(t *testing.T) {
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, res(context.Background())(), E.Of[error]("John Doe"))
|
||||
assert.Equal(t, res(t.Context())(), E.Of[error]("John Doe"))
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
@@ -54,5 +55,221 @@ func TestApS(t *testing.T) {
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
assert.Equal(t, res(context.Background())(), E.Of[error]("John Doe"))
|
||||
assert.Equal(t, res(t.Context())(), E.Of[error]("John Doe"))
|
||||
}
|
||||
|
||||
func TestApS_WithError(t *testing.T) {
|
||||
// Test that ApS propagates errors correctly
|
||||
testErr := assert.AnError
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(utils.Empty),
|
||||
ApS(utils.SetLastName, Left[string](testErr)),
|
||||
ApS(utils.SetGivenName, Of("John")),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
assert.Equal(t, testErr, E.ToError(result))
|
||||
}
|
||||
|
||||
func TestApS_WithSecondError(t *testing.T) {
|
||||
// Test that ApS propagates errors from the second operation
|
||||
testErr := assert.AnError
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(utils.Empty),
|
||||
ApS(utils.SetLastName, Of("Doe")),
|
||||
ApS(utils.SetGivenName, Left[string](testErr)),
|
||||
Map(utils.GetFullName),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
assert.Equal(t, testErr, E.ToError(result))
|
||||
}
|
||||
|
||||
func TestApS_MultipleFields(t *testing.T) {
|
||||
// Test ApS with more than two fields
|
||||
type Person struct {
|
||||
FirstName string
|
||||
MiddleName string
|
||||
LastName string
|
||||
Age int
|
||||
}
|
||||
|
||||
setFirstName := func(s string) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.FirstName = s
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
setMiddleName := func(s string) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.MiddleName = s
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
setLastName := func(s string) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.LastName = s
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
setAge := func(a int) func(Person) Person {
|
||||
return func(p Person) Person {
|
||||
p.Age = a
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe5(
|
||||
Do(Person{}),
|
||||
ApS(setFirstName, Of("John")),
|
||||
ApS(setMiddleName, Of("Q")),
|
||||
ApS(setLastName, Of("Doe")),
|
||||
ApS(setAge, Of(42)),
|
||||
Map(func(p Person) Person { return p }),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
person := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(person))
|
||||
p, _ := O.Unwrap(person)
|
||||
assert.Equal(t, "John", p.FirstName)
|
||||
assert.Equal(t, "Q", p.MiddleName)
|
||||
assert.Equal(t, "Doe", p.LastName)
|
||||
assert.Equal(t, 42, p.Age)
|
||||
}
|
||||
|
||||
func TestApS_WithDifferentTypes(t *testing.T) {
|
||||
// Test ApS with different value types
|
||||
type State struct {
|
||||
Name string
|
||||
Count int
|
||||
Flag bool
|
||||
}
|
||||
|
||||
setName := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Name = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
setCount := func(c int) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Count = c
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
setFlag := func(f bool) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Flag = f
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe4(
|
||||
Do(State{}),
|
||||
ApS(setName, Of("test")),
|
||||
ApS(setCount, Of(100)),
|
||||
ApS(setFlag, Of(true)),
|
||||
Map(func(s State) State { return s }),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
stateOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(stateOpt))
|
||||
state, _ := O.Unwrap(stateOpt)
|
||||
assert.Equal(t, "test", state.Name)
|
||||
assert.Equal(t, 100, state.Count)
|
||||
assert.True(t, state.Flag)
|
||||
}
|
||||
|
||||
func TestApS_EmptyState(t *testing.T) {
|
||||
// Test ApS starting with an empty state
|
||||
type Empty struct{}
|
||||
|
||||
res := Do(Empty{})
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
emptyOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(emptyOpt))
|
||||
empty, _ := O.Unwrap(emptyOpt)
|
||||
assert.Equal(t, Empty{}, empty)
|
||||
}
|
||||
|
||||
func TestApS_ChainedWithBind(t *testing.T) {
|
||||
// Test mixing ApS with Bind operations
|
||||
type State struct {
|
||||
Independent string
|
||||
Dependent string
|
||||
}
|
||||
|
||||
setIndependent := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Independent = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
setDependent := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Dependent = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
getDependentValue := func(s State) ReaderIOEither[string] {
|
||||
// This depends on the Independent field
|
||||
return Of(s.Independent + "-dependent")
|
||||
}
|
||||
|
||||
res := F.Pipe3(
|
||||
Do(State{}),
|
||||
ApS(setIndependent, Of("value")),
|
||||
Bind(setDependent, getDependentValue),
|
||||
Map(func(s State) State { return s }),
|
||||
)
|
||||
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
stateOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(stateOpt))
|
||||
state, _ := O.Unwrap(stateOpt)
|
||||
assert.Equal(t, "value", state.Independent)
|
||||
assert.Equal(t, "value-dependent", state.Dependent)
|
||||
}
|
||||
|
||||
func TestApS_WithContextCancellation(t *testing.T) {
|
||||
// Test that ApS respects context cancellation
|
||||
type State struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
setValue := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Value = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
// Create a computation that would succeed
|
||||
computation := ApS(setValue, Of("test"))(Do(State{}))
|
||||
|
||||
// Create a cancelled context
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
result := computation(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func Bracket[
|
||||
A, B, ANY any](
|
||||
|
||||
acquire ReaderIOEither[A],
|
||||
use func(A) ReaderIOEither[B],
|
||||
use Kleisli[A, B],
|
||||
release func(A, Either[B]) ReaderIOEither[ANY],
|
||||
) ReaderIOEither[B] {
|
||||
return bracket.Bracket[ReaderIOEither[A], ReaderIOEither[B], ReaderIOEither[ANY], Either[B], A, B](
|
||||
|
||||
@@ -39,6 +39,8 @@ import (
|
||||
// eqRIE := Eq(eqInt)
|
||||
// ctx := context.Background()
|
||||
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
|
||||
//
|
||||
//go:inline
|
||||
func Eq[A any](eq eq.Eq[Either[A]]) func(context.Context) eq.Eq[ReaderIOEither[A]] {
|
||||
return RIOE.Eq[context.Context](eq)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,36 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package builder provides utilities for building HTTP requests in a functional way
|
||||
// using the ReaderIOEither monad. It integrates with the http/builder package to
|
||||
// create composable, type-safe HTTP request builders with proper error handling
|
||||
// and context support.
|
||||
//
|
||||
// The main function, Requester, converts a Builder from the http/builder package
|
||||
// into a ReaderIOEither that produces HTTP requests. This allows for:
|
||||
// - Immutable request building with method chaining
|
||||
// - Automatic header management including Content-Length
|
||||
// - Support for requests with and without bodies
|
||||
// - Proper error handling wrapped in Either
|
||||
// - Context propagation for cancellation and timeouts
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// import (
|
||||
// "context"
|
||||
// B "github.com/IBM/fp-go/v2/http/builder"
|
||||
// RB "github.com/IBM/fp-go/v2/context/readerioeither/http/builder"
|
||||
// )
|
||||
//
|
||||
// builder := F.Pipe3(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("POST"),
|
||||
// B.WithJSONBody(userData),
|
||||
// )
|
||||
//
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
package builder
|
||||
|
||||
import (
|
||||
@@ -31,6 +61,59 @@ import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Requester converts an http/builder.Builder into a ReaderIOEither that produces HTTP requests.
|
||||
// It handles both requests with and without bodies, automatically managing headers including
|
||||
// Content-Length for requests with bodies.
|
||||
//
|
||||
// The function performs the following operations:
|
||||
// 1. Extracts the request body (if present) from the builder
|
||||
// 2. Creates appropriate request constructor (with or without body)
|
||||
// 3. Applies the target URL from the builder
|
||||
// 4. Applies the HTTP method from the builder
|
||||
// 5. Merges headers from the builder into the request
|
||||
// 6. Handles any errors that occur during request construction
|
||||
//
|
||||
// For requests with a body:
|
||||
// - Sets the Content-Length header automatically
|
||||
// - Uses bytes.NewReader to create the request body
|
||||
// - Merges builder headers into the request
|
||||
//
|
||||
// For requests without a body:
|
||||
// - Creates a request with nil body
|
||||
// - Merges builder headers into the request
|
||||
//
|
||||
// Parameters:
|
||||
// - builder: A pointer to an http/builder.Builder containing request configuration
|
||||
//
|
||||
// Returns:
|
||||
// - A Requester (ReaderIOEither[*http.Request]) that, when executed with a context,
|
||||
// produces either an error or a configured *http.Request
|
||||
//
|
||||
// Example with body:
|
||||
//
|
||||
// import (
|
||||
// B "github.com/IBM/fp-go/v2/http/builder"
|
||||
// RB "github.com/IBM/fp-go/v2/context/readerioeither/http/builder"
|
||||
// )
|
||||
//
|
||||
// builder := F.Pipe3(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("POST"),
|
||||
// B.WithJSONBody(map[string]string{"name": "John"}),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
//
|
||||
// Example without body:
|
||||
//
|
||||
// builder := F.Pipe2(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("GET"),
|
||||
// )
|
||||
// requester := RB.Requester(builder)
|
||||
// result := requester(context.Background())()
|
||||
func Requester(builder *R.Builder) RIOEH.Requester {
|
||||
|
||||
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOEither[*http.Request] {
|
||||
|
||||
@@ -57,3 +57,231 @@ func TestBuilderWithQuery(t *testing.T) {
|
||||
|
||||
assert.True(t, E.IsRight(req(context.Background())()))
|
||||
}
|
||||
|
||||
// TestBuilderWithoutBody tests creating a request without a body
|
||||
func TestBuilderWithoutBody(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
R.WithMethod("GET"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "GET", req.Method)
|
||||
assert.Equal(t, "https://api.example.com/users", req.URL.String())
|
||||
assert.Nil(t, req.Body, "Expected nil body for GET request")
|
||||
}
|
||||
|
||||
// TestBuilderWithBody tests creating a request with a body
|
||||
func TestBuilderWithBody(t *testing.T) {
|
||||
bodyData := []byte(`{"name":"John","age":30}`)
|
||||
|
||||
builder := F.Pipe3(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
R.WithMethod("POST"),
|
||||
R.WithBytes(bodyData),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://api.example.com/users", req.URL.String())
|
||||
assert.NotNil(t, req.Body, "Expected non-nil body for POST request")
|
||||
assert.Equal(t, "24", req.Header.Get("Content-Length"))
|
||||
}
|
||||
|
||||
// TestBuilderWithHeaders tests that headers are properly set
|
||||
func TestBuilderWithHeaders(t *testing.T) {
|
||||
builder := F.Pipe3(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/data"),
|
||||
R.WithHeader("Authorization")("Bearer token123"),
|
||||
R.WithHeader("Accept")("application/json"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "Bearer token123", req.Header.Get("Authorization"))
|
||||
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
||||
}
|
||||
|
||||
// TestBuilderWithInvalidURL tests error handling for invalid URLs
|
||||
func TestBuilderWithInvalidURL(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
R.Default,
|
||||
R.WithURL("://invalid-url"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestBuilderWithEmptyMethod tests creating a request with empty method
|
||||
func TestBuilderWithEmptyMethod(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
R.WithMethod(""),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
// Empty method should still work (defaults to GET in http.NewRequest)
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
}
|
||||
|
||||
// TestBuilderWithMultipleHeaders tests setting multiple headers
|
||||
func TestBuilderWithMultipleHeaders(t *testing.T) {
|
||||
builder := F.Pipe4(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/data"),
|
||||
R.WithHeader("X-Custom-Header-1")("value1"),
|
||||
R.WithHeader("X-Custom-Header-2")("value2"),
|
||||
R.WithHeader("X-Custom-Header-3")("value3"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "value1", req.Header.Get("X-Custom-Header-1"))
|
||||
assert.Equal(t, "value2", req.Header.Get("X-Custom-Header-2"))
|
||||
assert.Equal(t, "value3", req.Header.Get("X-Custom-Header-3"))
|
||||
}
|
||||
|
||||
// TestBuilderWithBodyAndHeaders tests combining body and headers
|
||||
func TestBuilderWithBodyAndHeaders(t *testing.T) {
|
||||
bodyData := []byte(`{"test":"data"}`)
|
||||
|
||||
builder := F.Pipe4(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/submit"),
|
||||
R.WithMethod("PUT"),
|
||||
R.WithBytes(bodyData),
|
||||
R.WithHeader("X-Request-ID")("12345"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "PUT", req.Method)
|
||||
assert.NotNil(t, req.Body, "Expected non-nil body")
|
||||
assert.Equal(t, "12345", req.Header.Get("X-Request-ID"))
|
||||
assert.Equal(t, "15", req.Header.Get("Content-Length"))
|
||||
}
|
||||
|
||||
// TestBuilderContextCancellation tests that context cancellation is respected
|
||||
func TestBuilderContextCancellation(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/users"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
|
||||
// Create a cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
result := requester(ctx)()
|
||||
|
||||
// The request should still be created (cancellation affects execution, not creation)
|
||||
// But we verify the context is properly passed
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
if req != nil {
|
||||
assert.Equal(t, ctx, req.Context(), "Expected context to be set in request")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuilderWithDifferentMethods tests various HTTP methods
|
||||
func TestBuilderWithDifferentMethods(t *testing.T) {
|
||||
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range methods {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/resource"),
|
||||
R.WithMethod(method),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result for method %s", method)
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request for method %s", method)
|
||||
assert.Equal(t, method, req.Method)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuilderWithJSON tests creating a request with JSON body
|
||||
func TestBuilderWithJSON(t *testing.T) {
|
||||
data := map[string]string{"username": "testuser", "email": "test@example.com"}
|
||||
|
||||
builder := F.Pipe3(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/v1/users"),
|
||||
R.WithMethod("POST"),
|
||||
R.WithJSON(data),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, "https://api.example.com/v1/users", req.URL.String())
|
||||
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||
assert.NotNil(t, req.Body)
|
||||
}
|
||||
|
||||
// TestBuilderWithBearer tests adding Bearer token
|
||||
func TestBuilderWithBearer(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
R.Default,
|
||||
R.WithURL("https://api.example.com/protected"),
|
||||
R.WithBearer("my-secret-token"),
|
||||
)
|
||||
|
||||
requester := Requester(builder)
|
||||
result := requester(context.Background())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
|
||||
assert.NotNil(t, req, "Expected non-nil request")
|
||||
assert.Equal(t, "Bearer my-secret-token", req.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
15
v2/context/readerioeither/http/builder/coverage.out
Normal file
15
v2/context/readerioeither/http/builder/coverage.out
Normal file
@@ -0,0 +1,15 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:117.52,119.103 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:119.103,120.80 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:120.80,121.41 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:121.41,123.19 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:123.19,126.6 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:127.5,127.20 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:132.2,132.93 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:132.93,133.80 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:133.80,134.41 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:134.41,136.19 2 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:136.19,138.6 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:139.5,139.20 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:144.2,150.50 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:150.50,153.4 2 1
|
||||
11
v2/context/readerioeither/http/coverage.out
Normal file
11
v2/context/readerioeither/http/coverage.out
Normal file
@@ -0,0 +1,11 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:111.76,116.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:134.49,136.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:161.90,162.65 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:162.65,166.76 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:166.76,176.5 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:198.73,203.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:222.74,227.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:234.76,236.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:245.74,254.2 1 1
|
||||
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:281.76,286.2 1 1
|
||||
@@ -13,6 +13,22 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package http provides functional HTTP client utilities built on top of ReaderIOEither monad.
|
||||
// It offers a composable way to make HTTP requests with context support, error handling,
|
||||
// and response parsing capabilities. The package follows functional programming principles
|
||||
// to ensure type-safe, testable, and maintainable HTTP operations.
|
||||
//
|
||||
// The main abstractions include:
|
||||
// - Requester: A reader that constructs HTTP requests with context
|
||||
// - Client: An interface for executing HTTP requests
|
||||
// - Response readers: Functions to parse responses as bytes, text, or JSON
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// result := ReadJSON[MyType](client)(request)
|
||||
// response := result(context.Background())()
|
||||
package http
|
||||
|
||||
import (
|
||||
@@ -30,14 +46,31 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Requester is a reader that constructs a request
|
||||
// Requester is a reader that constructs an HTTP request with context support.
|
||||
// It represents a computation that, given a context, produces either an error
|
||||
// or an HTTP request. This allows for composable request building with proper
|
||||
// error handling and context propagation.
|
||||
Requester = RIOE.ReaderIOEither[*http.Request]
|
||||
|
||||
// Client is an interface for executing HTTP requests in a functional way.
|
||||
// It wraps the standard http.Client and provides a Do method that works
|
||||
// with the ReaderIOEither monad for composable, type-safe HTTP operations.
|
||||
Client interface {
|
||||
// Do can send an HTTP request considering a context
|
||||
// Do executes an HTTP request and returns the response wrapped in a ReaderIOEither.
|
||||
// It takes a Requester (which builds the request) and returns a computation that,
|
||||
// when executed with a context, performs the HTTP request and returns either
|
||||
// an error or the HTTP response.
|
||||
//
|
||||
// Parameters:
|
||||
// - req: A Requester that builds the HTTP request
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderIOEither that produces either an error or an *http.Response
|
||||
Do(Requester) RIOE.ReaderIOEither[*http.Response]
|
||||
}
|
||||
|
||||
// client is the internal implementation of the Client interface.
|
||||
// It wraps a standard http.Client and provides functional HTTP operations.
|
||||
client struct {
|
||||
delegate *http.Client
|
||||
doIOE func(*http.Request) IOE.IOEither[error, *http.Response]
|
||||
@@ -45,11 +78,33 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
// MakeRequest is an eitherized version of [http.NewRequestWithContext]
|
||||
// MakeRequest is an eitherized version of http.NewRequestWithContext.
|
||||
// It creates a Requester that builds an HTTP request with the given method, URL, and body.
|
||||
// This function properly handles errors and wraps them in the Either monad.
|
||||
//
|
||||
// Parameters:
|
||||
// - method: HTTP method (GET, POST, PUT, DELETE, etc.)
|
||||
// - url: The target URL for the request
|
||||
// - body: Optional request body (can be nil)
|
||||
//
|
||||
// Returns:
|
||||
// - A Requester that produces either an error or an *http.Request
|
||||
MakeRequest = RIOE.Eitherize3(http.NewRequestWithContext)
|
||||
|
||||
// makeRequest is a partially applied version of MakeRequest with the context parameter bound.
|
||||
makeRequest = F.Bind13of3(MakeRequest)
|
||||
|
||||
// specialize
|
||||
// MakeGetRequest creates a GET request for the specified URL.
|
||||
// It's a convenience function that specializes MakeRequest for GET requests with no body.
|
||||
//
|
||||
// Parameters:
|
||||
// - url: The target URL for the GET request
|
||||
//
|
||||
// Returns:
|
||||
// - A Requester that produces either an error or an *http.Request
|
||||
//
|
||||
// Example:
|
||||
// req := MakeGetRequest("https://api.example.com/users")
|
||||
MakeGetRequest = makeRequest("GET", nil)
|
||||
)
|
||||
|
||||
@@ -60,12 +115,49 @@ func (client client) Do(req Requester) RIOE.ReaderIOEither[*http.Response] {
|
||||
)
|
||||
}
|
||||
|
||||
// MakeClient creates an HTTP client proxy
|
||||
// MakeClient creates a functional HTTP client wrapper around a standard http.Client.
|
||||
// The returned Client provides methods for executing HTTP requests in a functional,
|
||||
// composable way using the ReaderIOEither monad.
|
||||
//
|
||||
// Parameters:
|
||||
// - httpClient: A standard *http.Client to wrap (e.g., http.DefaultClient)
|
||||
//
|
||||
// Returns:
|
||||
// - A Client that can execute HTTP requests functionally
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// // or with custom client
|
||||
// customClient := &http.Client{Timeout: 10 * time.Second}
|
||||
// client := MakeClient(customClient)
|
||||
func MakeClient(httpClient *http.Client) Client {
|
||||
return client{delegate: httpClient, doIOE: IOE.Eitherize1(httpClient.Do)}
|
||||
}
|
||||
|
||||
// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple
|
||||
// ReadFullResponse sends an HTTP request, reads the complete response body as a byte array,
|
||||
// and returns both the response and body as a tuple (FullResponse).
|
||||
// It validates the HTTP status code and handles errors appropriately.
|
||||
//
|
||||
// The function performs the following steps:
|
||||
// 1. Executes the HTTP request using the provided client
|
||||
// 2. Validates the response status code (checks for HTTP errors)
|
||||
// 3. Reads the entire response body into a byte array
|
||||
// 4. Returns a tuple containing the response and body
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOEither[FullResponse]
|
||||
// where FullResponse is a tuple of (*http.Response, []byte)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// fullResp := ReadFullResponse(client)(request)
|
||||
// result := fullResp(context.Background())()
|
||||
func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOEither[H.FullResponse] {
|
||||
return func(req Requester) RIOE.ReaderIOEither[H.FullResponse] {
|
||||
return F.Flow3(
|
||||
@@ -86,7 +178,23 @@ func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOEither[H.FullR
|
||||
}
|
||||
}
|
||||
|
||||
// ReadAll sends a request and reads the response as bytes
|
||||
// ReadAll sends an HTTP request and reads the complete response body as a byte array.
|
||||
// It validates the HTTP status code and returns the raw response body bytes.
|
||||
// This is useful when you need to process the response body in a custom way.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOEither[[]byte]
|
||||
// containing the response body as bytes
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// readBytes := ReadAll(client)
|
||||
// result := readBytes(request)(context.Background())()
|
||||
func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
@@ -94,7 +202,23 @@ func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
)
|
||||
}
|
||||
|
||||
// ReadText sends a request, reads the response and represents the response as a text string
|
||||
// ReadText sends an HTTP request, reads the response body, and converts it to a string.
|
||||
// It validates the HTTP status code and returns the response body as a UTF-8 string.
|
||||
// This is convenient for APIs that return plain text responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOEither[string]
|
||||
// containing the response body as a string
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/text")
|
||||
// readText := ReadText(client)
|
||||
// result := readText(request)(context.Background())()
|
||||
func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
@@ -102,13 +226,22 @@ func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
|
||||
)
|
||||
}
|
||||
|
||||
// ReadJson sends a request, reads the response and parses the response as JSON
|
||||
// ReadJson sends an HTTP request, reads the response, and parses it as JSON.
|
||||
//
|
||||
// Deprecated: use [ReadJSON] instead
|
||||
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
|
||||
// but will be removed in a future version. The capitalized version follows Go naming
|
||||
// conventions for acronyms.
|
||||
func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
|
||||
return ReadJSON[A](client)
|
||||
}
|
||||
|
||||
// readJSON is an internal helper that reads the response body and validates JSON content type.
|
||||
// It performs the following validations:
|
||||
// 1. Validates HTTP status code
|
||||
// 2. Validates that the response Content-Type is application/json
|
||||
// 3. Reads the response body as bytes
|
||||
//
|
||||
// This function is used internally by ReadJSON to ensure proper JSON response handling.
|
||||
func readJSON(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
return F.Flow3(
|
||||
ReadFullResponse(client),
|
||||
@@ -120,7 +253,31 @@ func readJSON(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
|
||||
)
|
||||
}
|
||||
|
||||
// ReadJSON sends a request, reads the response and parses the response as JSON
|
||||
// ReadJSON sends an HTTP request, reads the response, and parses it as JSON into type A.
|
||||
// It validates both the HTTP status code and the Content-Type header to ensure the
|
||||
// response is valid JSON before attempting to unmarshal.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type to unmarshal the JSON response into
|
||||
//
|
||||
// Parameters:
|
||||
// - client: The HTTP client to use for executing the request
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Requester and returns a ReaderIOEither[A]
|
||||
// containing the parsed JSON data
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// }
|
||||
//
|
||||
// client := MakeClient(http.DefaultClient)
|
||||
// request := MakeGetRequest("https://api.example.com/user/1")
|
||||
// readUser := ReadJSON[User](client)
|
||||
// result := readUser(request)(context.Background())()
|
||||
func ReadJSON[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
|
||||
@@ -88,7 +88,7 @@ func TestSendSingleRequest(t *testing.T) {
|
||||
|
||||
resp1 := readItem(req1)
|
||||
|
||||
resE := resp1(context.TODO())()
|
||||
resE := resp1(t.Context())()
|
||||
|
||||
fmt.Println(resE)
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func TestSendSingleRequestWithHeaderUnsafe(t *testing.T) {
|
||||
)
|
||||
|
||||
res := F.Pipe1(
|
||||
resp1(context.TODO())(),
|
||||
resp1(t.Context())(),
|
||||
E.GetOrElse(errors.ToString),
|
||||
)
|
||||
|
||||
@@ -149,9 +149,167 @@ func TestSendSingleRequestWithHeaderSafe(t *testing.T) {
|
||||
)
|
||||
|
||||
res := F.Pipe1(
|
||||
response(context.TODO())(),
|
||||
response(t.Context())(),
|
||||
E.GetOrElse(errors.ToString),
|
||||
)
|
||||
|
||||
assert.Equal(t, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", res)
|
||||
}
|
||||
|
||||
// TestReadAll tests the ReadAll function which reads response as bytes
|
||||
func TestReadAll(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readBytes := ReadAll(client)
|
||||
|
||||
result := readBytes(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
bytes := E.GetOrElse(func(error) []byte { return nil })(result)
|
||||
assert.NotNil(t, bytes, "Expected non-nil bytes")
|
||||
assert.Greater(t, len(bytes), 0, "Expected non-empty byte array")
|
||||
|
||||
// Verify it contains expected JSON content
|
||||
content := string(bytes)
|
||||
assert.Contains(t, content, "userId")
|
||||
assert.Contains(t, content, "title")
|
||||
}
|
||||
|
||||
// TestReadText tests the ReadText function which reads response as string
|
||||
func TestReadText(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readText := ReadText(client)
|
||||
|
||||
result := readText(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
text := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.NotEmpty(t, text, "Expected non-empty text")
|
||||
|
||||
// Verify it contains expected JSON content as text
|
||||
assert.Contains(t, text, "userId")
|
||||
assert.Contains(t, text, "title")
|
||||
assert.Contains(t, text, "sunt aut facere")
|
||||
}
|
||||
|
||||
// TestReadJson tests the deprecated ReadJson function
|
||||
func TestReadJson(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readItem := ReadJson[PostItem](client)
|
||||
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
item := E.GetOrElse(func(error) PostItem { return PostItem{} })(result)
|
||||
assert.Equal(t, uint(1), item.UserID, "Expected UserID to be 1")
|
||||
assert.Equal(t, uint(1), item.Id, "Expected Id to be 1")
|
||||
assert.NotEmpty(t, item.Title, "Expected non-empty title")
|
||||
assert.NotEmpty(t, item.Body, "Expected non-empty body")
|
||||
}
|
||||
|
||||
// TestReadAllWithInvalidURL tests ReadAll with an invalid URL
|
||||
func TestReadAllWithInvalidURL(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
|
||||
readBytes := ReadAll(client)
|
||||
|
||||
result := readBytes(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestReadTextWithInvalidURL tests ReadText with an invalid URL
|
||||
func TestReadTextWithInvalidURL(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
|
||||
readText := ReadText(client)
|
||||
|
||||
result := readText(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestReadJSONWithInvalidURL tests ReadJSON with an invalid URL
|
||||
func TestReadJSONWithInvalidURL(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestReadJSONWithInvalidJSON tests ReadJSON with non-JSON response
|
||||
func TestReadJSONWithInvalidJSON(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
// This URL returns HTML, not JSON
|
||||
request := MakeGetRequest("https://www.google.com")
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
// Should fail because content-type is not application/json
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for non-JSON response")
|
||||
}
|
||||
|
||||
// TestMakeClientWithCustomClient tests MakeClient with a custom http.Client
|
||||
func TestMakeClientWithCustomClient(t *testing.T) {
|
||||
customClient := H.DefaultClient
|
||||
|
||||
client := MakeClient(customClient)
|
||||
assert.NotNil(t, client, "Expected non-nil client")
|
||||
|
||||
// Verify it works
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
readItem := ReadJSON[PostItem](client)
|
||||
result := readItem(request)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
}
|
||||
|
||||
// TestReadAllComposition tests composing ReadAll with other operations
|
||||
func TestReadAllComposition(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
|
||||
// Compose ReadAll with a map operation to get byte length
|
||||
readBytes := ReadAll(client)(request)
|
||||
readLength := R.Map(func(bytes []byte) int { return len(bytes) })(readBytes)
|
||||
|
||||
result := readLength(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
length := E.GetOrElse(func(error) int { return 0 })(result)
|
||||
assert.Greater(t, length, 0, "Expected positive byte length")
|
||||
}
|
||||
|
||||
// TestReadTextComposition tests composing ReadText with other operations
|
||||
func TestReadTextComposition(t *testing.T) {
|
||||
client := MakeClient(H.DefaultClient)
|
||||
|
||||
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
|
||||
|
||||
// Compose ReadText with a map operation to get string length
|
||||
readText := ReadText(client)(request)
|
||||
readLength := R.Map(func(text string) int { return len(text) })(readText)
|
||||
|
||||
result := readLength(t.Context())()
|
||||
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
length := E.GetOrElse(func(error) int { return 0 })(result)
|
||||
assert.Greater(t, length, 0, "Expected positive string length")
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ const (
|
||||
// - e: The Either value to lift into ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither that produces the given Either value.
|
||||
//
|
||||
//go:inline
|
||||
func FromEither[A any](e Either[A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromEither[context.Context](e)
|
||||
}
|
||||
@@ -59,6 +61,8 @@ func Left[A any](l error) ReaderIOEither[A] {
|
||||
// - r: The success value
|
||||
//
|
||||
// Returns a ReaderIOEither that always succeeds with the given value.
|
||||
//
|
||||
//go:inline
|
||||
func Right[A any](r A) ReaderIOEither[A] {
|
||||
return readerioeither.Right[context.Context, error](r)
|
||||
}
|
||||
@@ -71,6 +75,8 @@ func Right[A any](r A) ReaderIOEither[A] {
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a new ReaderIOEither with the transformed value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[A, B any](fa ReaderIOEither[A], f func(A) B) ReaderIOEither[B] {
|
||||
return readerioeither.MonadMap(fa, f)
|
||||
}
|
||||
@@ -82,6 +88,8 @@ func MonadMap[A, B any](fa ReaderIOEither[A], f func(A) B) ReaderIOEither[B] {
|
||||
// - f: The transformation function
|
||||
//
|
||||
// Returns a function that transforms a ReaderIOEither.
|
||||
//
|
||||
//go:inline
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return readerioeither.Map[context.Context, error](f)
|
||||
}
|
||||
@@ -94,6 +102,8 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a new ReaderIOEither with the constant value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[A, B any](fa ReaderIOEither[A], b B) ReaderIOEither[B] {
|
||||
return readerioeither.MonadMapTo(fa, b)
|
||||
}
|
||||
@@ -105,6 +115,8 @@ func MonadMapTo[A, B any](fa ReaderIOEither[A], b B) ReaderIOEither[B] {
|
||||
// - b: The constant value to use
|
||||
//
|
||||
// Returns a function that transforms a ReaderIOEither.
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return readerioeither.MapTo[context.Context, error, A](b)
|
||||
}
|
||||
@@ -117,7 +129,9 @@ func MapTo[A, B any](b B) Operator[A, B] {
|
||||
// - f: Function that produces the second ReaderIOEither based on the first's result
|
||||
//
|
||||
// Returns a new ReaderIOEither representing the sequenced computation.
|
||||
func MonadChain[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) ReaderIOEither[B] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[A, B any](ma ReaderIOEither[A], f Kleisli[A, B]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadChain(ma, f)
|
||||
}
|
||||
|
||||
@@ -128,7 +142,9 @@ func MonadChain[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) Rea
|
||||
// - f: Function that produces the second ReaderIOEither based on the first's result
|
||||
//
|
||||
// Returns a function that sequences ReaderIOEither computations.
|
||||
func Chain[A, B any](f func(A) ReaderIOEither[B]) Operator[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return readerioeither.Chain(f)
|
||||
}
|
||||
|
||||
@@ -140,7 +156,9 @@ func Chain[A, B any](f func(A) ReaderIOEither[B]) Operator[A, B] {
|
||||
// - f: Function that produces the second ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither with the result of the first computation.
|
||||
func MonadChainFirst[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) ReaderIOEither[A] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirst[A, B any](ma ReaderIOEither[A], f Kleisli[A, B]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadChainFirst(ma, f)
|
||||
}
|
||||
|
||||
@@ -151,7 +169,9 @@ func MonadChainFirst[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]
|
||||
// - f: Function that produces the second ReaderIOEither
|
||||
//
|
||||
// Returns a function that sequences ReaderIOEither computations.
|
||||
func ChainFirst[A, B any](f func(A) ReaderIOEither[B]) Operator[A, A] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return readerioeither.ChainFirst(f)
|
||||
}
|
||||
|
||||
@@ -162,6 +182,8 @@ func ChainFirst[A, B any](f func(A) ReaderIOEither[B]) Operator[A, A] {
|
||||
// - a: The value to wrap
|
||||
//
|
||||
// Returns a ReaderIOEither that always succeeds with the given value.
|
||||
//
|
||||
//go:inline
|
||||
func Of[A any](a A) ReaderIOEither[A] {
|
||||
return readerioeither.Of[context.Context, error](a)
|
||||
}
|
||||
@@ -240,6 +262,8 @@ func MonadAp[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) Read
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a ReaderIOEither with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadApSeq[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadApSeq(fab, fa)
|
||||
}
|
||||
@@ -251,6 +275,8 @@ func MonadApSeq[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) R
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOEither function to the value.
|
||||
//
|
||||
//go:inline
|
||||
func Ap[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadAp[B, A], fa)
|
||||
}
|
||||
@@ -262,6 +288,8 @@ func Ap[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOEither function to the value sequentially.
|
||||
//
|
||||
//go:inline
|
||||
func ApSeq[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApSeq[B, A], fa)
|
||||
}
|
||||
@@ -273,6 +301,8 @@ func ApSeq[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
// - fa: ReaderIOEither containing a value
|
||||
//
|
||||
// Returns a function that applies a ReaderIOEither function to the value in parallel.
|
||||
//
|
||||
//go:inline
|
||||
func ApPar[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
return function.Bind2nd(MonadApPar[B, A], fa)
|
||||
}
|
||||
@@ -285,7 +315,9 @@ func ApPar[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
|
||||
// - onFalse: Function to generate an error when predicate fails
|
||||
//
|
||||
// Returns a function that converts a value to ReaderIOEither based on the predicate.
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) ReaderIOEither[A] {
|
||||
//
|
||||
//go:inline
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
|
||||
return readerioeither.FromPredicate[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
@@ -296,7 +328,9 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) Read
|
||||
// - onLeft: Function that produces an alternative ReaderIOEither from the error
|
||||
//
|
||||
// Returns a function that provides fallback behavior for failed computations.
|
||||
func OrElse[A any](onLeft func(error) ReaderIOEither[A]) Operator[A, A] {
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
|
||||
return readerioeither.OrElse[context.Context](onLeft)
|
||||
}
|
||||
|
||||
@@ -304,6 +338,8 @@ func OrElse[A any](onLeft func(error) ReaderIOEither[A]) Operator[A, A] {
|
||||
// This is useful for accessing the [context.Context] within a computation.
|
||||
//
|
||||
// Returns a ReaderIOEither that produces the context.
|
||||
//
|
||||
//go:inline
|
||||
func Ask() ReaderIOEither[context.Context] {
|
||||
return readerioeither.Ask[context.Context, error]()
|
||||
}
|
||||
@@ -316,6 +352,8 @@ func Ask() ReaderIOEither[context.Context] {
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a new ReaderIOEither with the chained computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadChainEitherK[context.Context](ma, f)
|
||||
}
|
||||
@@ -327,7 +365,9 @@ func MonadChainEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) Read
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a function that chains the Either-returning function.
|
||||
func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
|
||||
return readerioeither.ChainEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -339,6 +379,8 @@ func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) Read
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a ReaderIOEither with the original value if both computations succeed.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadChainFirstEitherK[context.Context](ma, f)
|
||||
}
|
||||
@@ -350,7 +392,9 @@ func MonadChainFirstEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B])
|
||||
// - f: Function that produces an Either
|
||||
//
|
||||
// Returns a function that chains the Either-returning function.
|
||||
func ChainFirstEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
|
||||
return readerioeither.ChainFirstEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -361,6 +405,8 @@ func ChainFirstEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A])
|
||||
// - onNone: Function to generate an error when Option is None
|
||||
//
|
||||
// Returns a function that chains Option-returning functions into ReaderIOEither.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
|
||||
return readerioeither.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
@@ -372,6 +418,8 @@ func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operato
|
||||
// - t: The IOEither to convert
|
||||
//
|
||||
// Returns a ReaderIOEither that executes the IOEither.
|
||||
//
|
||||
//go:inline
|
||||
func FromIOEither[A any](t ioeither.IOEither[error, A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromIOEither[context.Context](t)
|
||||
}
|
||||
@@ -383,6 +431,8 @@ func FromIOEither[A any](t ioeither.IOEither[error, A]) ReaderIOEither[A] {
|
||||
// - t: The IO to convert
|
||||
//
|
||||
// Returns a ReaderIOEither that executes the IO and wraps the result in Right.
|
||||
//
|
||||
//go:inline
|
||||
func FromIO[A any](t IO[A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromIO[context.Context, error](t)
|
||||
}
|
||||
@@ -395,6 +445,8 @@ func FromIO[A any](t IO[A]) ReaderIOEither[A] {
|
||||
// - t: The Lazy computation to convert
|
||||
//
|
||||
// Returns a ReaderIOEither that executes the Lazy computation and wraps the result in Right.
|
||||
//
|
||||
//go:inline
|
||||
func FromLazy[A any](t Lazy[A]) ReaderIOEither[A] {
|
||||
return readerioeither.FromIO[context.Context, error](t)
|
||||
}
|
||||
@@ -420,6 +472,8 @@ func Never[A any]() ReaderIOEither[A] {
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a new ReaderIOEither with the chained IO computation.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEither[B] {
|
||||
return readerioeither.MonadChainIOK(ma, f)
|
||||
}
|
||||
@@ -431,7 +485,9 @@ func MonadChainIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEith
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
func ChainIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
|
||||
return readerioeither.ChainIOK[context.Context, error](f)
|
||||
}
|
||||
|
||||
@@ -443,6 +499,8 @@ func ChainIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEith
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a ReaderIOEither with the original value after executing the IO.
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadChainFirstIOK(ma, f)
|
||||
}
|
||||
@@ -454,7 +512,9 @@ func MonadChainFirstIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderI
|
||||
// - f: Function that produces an IO
|
||||
//
|
||||
// Returns a function that chains the IO-returning function.
|
||||
func ChainFirstIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
return readerioeither.ChainFirstIOK[context.Context, error](f)
|
||||
}
|
||||
|
||||
@@ -465,7 +525,9 @@ func ChainFirstIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderI
|
||||
// - f: Function that produces an IOEither
|
||||
//
|
||||
// Returns a function that chains the IOEither-returning function.
|
||||
func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
|
||||
//
|
||||
//go:inline
|
||||
func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) Operator[A, B] {
|
||||
return readerioeither.ChainIOEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
@@ -476,7 +538,7 @@ func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) func(ma Rea
|
||||
// - delay: The duration to wait before executing the computation
|
||||
//
|
||||
// Returns a function that delays a ReaderIOEither computation.
|
||||
func Delay[A any](delay time.Duration) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
func Delay[A any](delay time.Duration) Operator[A, A] {
|
||||
return func(ma ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return func(ctx context.Context) IOEither[A] {
|
||||
return func() Either[A] {
|
||||
@@ -517,6 +579,8 @@ func Timer(delay time.Duration) ReaderIOEither[time.Time] {
|
||||
// - gen: Lazy generator function that produces a ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither that generates a fresh computation on each execution.
|
||||
//
|
||||
//go:inline
|
||||
func Defer[A any](gen Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
return readerioeither.Defer(gen)
|
||||
}
|
||||
@@ -528,6 +592,8 @@ func Defer[A any](gen Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
// - f: Function that takes a context and returns a function producing (value, error)
|
||||
//
|
||||
// Returns a ReaderIOEither that wraps the error-returning function.
|
||||
//
|
||||
//go:inline
|
||||
func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOEither[A] {
|
||||
return readerioeither.TryCatch(f, errors.IdentityError)
|
||||
}
|
||||
@@ -540,6 +606,8 @@ func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOEither[A
|
||||
// - second: Lazy alternative ReaderIOEither to use if first fails
|
||||
//
|
||||
// Returns a ReaderIOEither that tries the first, then the second if first fails.
|
||||
//
|
||||
//go:inline
|
||||
func MonadAlt[A any](first ReaderIOEither[A], second Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
return readerioeither.MonadAlt(first, second)
|
||||
}
|
||||
@@ -551,6 +619,8 @@ func MonadAlt[A any](first ReaderIOEither[A], second Lazy[ReaderIOEither[A]]) Re
|
||||
// - second: Lazy alternative ReaderIOEither to use if first fails
|
||||
//
|
||||
// Returns a function that provides fallback behavior.
|
||||
//
|
||||
//go:inline
|
||||
func Alt[A any](second Lazy[ReaderIOEither[A]]) Operator[A, A] {
|
||||
return readerioeither.Alt(second)
|
||||
}
|
||||
@@ -563,6 +633,8 @@ func Alt[A any](second Lazy[ReaderIOEither[A]]) Operator[A, A] {
|
||||
// - rdr: The ReaderIOEither to memoize
|
||||
//
|
||||
// Returns a ReaderIOEither that caches its result after the first execution.
|
||||
//
|
||||
//go:inline
|
||||
func Memoize[A any](rdr ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
return readerioeither.Memoize(rdr)
|
||||
}
|
||||
@@ -574,6 +646,8 @@ func Memoize[A any](rdr ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
// - rdr: The nested ReaderIOEither to flatten
|
||||
//
|
||||
// Returns a flattened ReaderIOEither.
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[A any](rdr ReaderIOEither[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
return readerioeither.Flatten(rdr)
|
||||
}
|
||||
@@ -586,6 +660,8 @@ func Flatten[A any](rdr ReaderIOEither[ReaderIOEither[A]]) ReaderIOEither[A] {
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a ReaderIOEither with the function applied to the value.
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[B, A any](fab ReaderIOEither[func(A) B], a A) ReaderIOEither[B] {
|
||||
return readerioeither.MonadFlap(fab, a)
|
||||
}
|
||||
@@ -597,6 +673,8 @@ func MonadFlap[B, A any](fab ReaderIOEither[func(A) B], a A) ReaderIOEither[B] {
|
||||
// - a: The value to apply to the function
|
||||
//
|
||||
// Returns a function that applies the value to a ReaderIOEither function.
|
||||
//
|
||||
//go:inline
|
||||
func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
return readerioeither.Flap[context.Context, error, B](a)
|
||||
}
|
||||
@@ -609,7 +687,9 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
// - onRight: Handler for success case
|
||||
//
|
||||
// Returns a function that folds a ReaderIOEither into a new ReaderIOEither.
|
||||
func Fold[A, B any](onLeft func(error) ReaderIOEither[B], onRight func(A) ReaderIOEither[B]) Operator[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] {
|
||||
return readerioeither.Fold(onLeft, onRight)
|
||||
}
|
||||
|
||||
@@ -620,6 +700,8 @@ func Fold[A, B any](onLeft func(error) ReaderIOEither[B], onRight func(A) Reader
|
||||
// - onLeft: Function to provide a default value from the error
|
||||
//
|
||||
// Returns a function that converts a ReaderIOEither to a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOEither[A]) ReaderIO[A] {
|
||||
return readerioeither.GetOrElse(onLeft)
|
||||
}
|
||||
@@ -631,6 +713,8 @@ func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOEither[A]) Re
|
||||
// - onLeft: Function to transform the error
|
||||
//
|
||||
// Returns a function that transforms the error of a ReaderIOEither.
|
||||
//
|
||||
//go:inline
|
||||
func OrLeft[A any](onLeft func(error) ReaderIO[error]) Operator[A, A] {
|
||||
return readerioeither.OrLeft[A](onLeft)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
rightVal := E.Right[error](42)
|
||||
@@ -43,7 +43,7 @@ func TestFromEither(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLeftRight(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test Left
|
||||
err := errors.New("test error")
|
||||
@@ -58,13 +58,13 @@ func TestLeftRight(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
result := Of(42)(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadMap(Right(42), func(x int) int { return x * 2 })(ctx)()
|
||||
@@ -77,7 +77,7 @@ func TestMonadMap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadMapTo(Right(42), "hello")(ctx)()
|
||||
@@ -90,7 +90,7 @@ func TestMonadMapTo(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChain(Right(42), func(x int) ReaderIOEither[int] {
|
||||
@@ -113,7 +113,7 @@ func TestMonadChain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainFirst(Right(42), func(x int) ReaderIOEither[string] {
|
||||
@@ -136,7 +136,7 @@ func TestMonadChainFirst(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadApSeq(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with both Right
|
||||
fct := Right(func(x int) int { return x * 2 })
|
||||
@@ -159,7 +159,7 @@ func TestMonadApSeq(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadApPar(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with both Right
|
||||
fct := Right(func(x int) int { return x * 2 })
|
||||
@@ -169,7 +169,7 @@ func TestMonadApPar(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
pred := func(x int) bool { return x > 0 }
|
||||
onFalse := func(x int) error { return fmt.Errorf("value %d is not positive", x) }
|
||||
@@ -184,7 +184,7 @@ func TestFromPredicate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "key", "value")
|
||||
ctx := context.WithValue(t.Context(), "key", "value")
|
||||
result := Ask()(ctx)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
retrievedCtx, _ := E.Unwrap(result)
|
||||
@@ -192,7 +192,7 @@ func TestAsk(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainEitherK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainEitherK(Right(42), func(x int) E.Either[error, int] {
|
||||
@@ -208,7 +208,7 @@ func TestMonadChainEitherK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainFirstEitherK(Right(42), func(x int) E.Either[error, string] {
|
||||
@@ -224,7 +224,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChainOptionKFunc(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
onNone := func() error { return errors.New("none error") }
|
||||
|
||||
@@ -243,7 +243,7 @@ func TestChainOptionKFunc(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromIOEither(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
ioe := func() E.Either[error, int] {
|
||||
@@ -262,7 +262,7 @@ func TestFromIOEither(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
io := func() int { return 42 }
|
||||
result := FromIO(io)(ctx)()
|
||||
@@ -270,7 +270,7 @@ func TestFromIO(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFromLazy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
lazy := func() int { return 42 }
|
||||
result := FromLazy(lazy)(ctx)()
|
||||
@@ -278,7 +278,7 @@ func TestFromLazy(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNeverWithCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
// Start Never in a goroutine
|
||||
done := make(chan E.Either[error, int])
|
||||
@@ -295,7 +295,7 @@ func TestNeverWithCancel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainIOK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainIOK(Right(42), func(x int) func() int {
|
||||
@@ -305,7 +305,7 @@ func TestMonadChainIOK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right
|
||||
result := MonadChainFirstIOK(Right(42), func(x int) func() string {
|
||||
@@ -315,7 +315,7 @@ func TestMonadChainFirstIOK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDelayFunc(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
delay := 100 * time.Millisecond
|
||||
|
||||
start := time.Now()
|
||||
@@ -328,7 +328,7 @@ func TestDelayFunc(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDefer(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
count := 0
|
||||
|
||||
gen := func() ReaderIOEither[int] {
|
||||
@@ -348,7 +348,7 @@ func TestDefer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTryCatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test success
|
||||
result := TryCatch(func(ctx context.Context) func() (int, error) {
|
||||
@@ -369,7 +369,7 @@ func TestTryCatch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadAlt(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with Right (alternative not called)
|
||||
result := MonadAlt(Right(42), func() ReaderIOEither[int] {
|
||||
@@ -386,7 +386,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMemoize(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
count := 0
|
||||
|
||||
rdr := Memoize(FromLazy(func() int {
|
||||
@@ -404,7 +404,7 @@ func TestMemoize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
nested := Right(Right(42))
|
||||
result := Flatten(nested)(ctx)()
|
||||
@@ -412,7 +412,7 @@ func TestFlatten(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
fab := Right(func(x int) int { return x * 2 })
|
||||
result := MonadFlap(fab, 42)(ctx)()
|
||||
assert.Equal(t, E.Right[error](84), result)
|
||||
@@ -420,19 +420,19 @@ func TestMonadFlap(t *testing.T) {
|
||||
|
||||
func TestWithContext(t *testing.T) {
|
||||
// Test with non-canceled context
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
result := WithContext(Right(42))(ctx)()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test with canceled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
result = WithContext(Right(42))(ctx)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with both Right
|
||||
fct := Right(func(x int) int { return x * 2 })
|
||||
@@ -443,7 +443,7 @@ func TestMonadAp(t *testing.T) {
|
||||
|
||||
// Test traverse functions
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with all Right
|
||||
arr := []ReaderIOEither[int]{Right(1), Right(2), Right(3)}
|
||||
@@ -460,7 +460,7 @@ func TestSequenceArray(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTraverseArray(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test transformation
|
||||
arr := []int{1, 2, 3}
|
||||
@@ -473,7 +473,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSequenceRecord(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test with all Right
|
||||
rec := map[string]ReaderIOEither[int]{
|
||||
@@ -488,7 +488,7 @@ func TestSequenceRecord(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTraverseRecord(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
// Test transformation
|
||||
rec := map[string]int{"a": 1, "b": 2}
|
||||
@@ -503,7 +503,7 @@ func TestTraverseRecord(t *testing.T) {
|
||||
|
||||
// Test monoid functions
|
||||
func TestAltSemigroup(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
sg := AltSemigroup[int]()
|
||||
|
||||
@@ -519,7 +519,7 @@ func TestAltSemigroup(t *testing.T) {
|
||||
|
||||
// Test Do notation
|
||||
func TestDo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx := t.Context()
|
||||
|
||||
type State struct {
|
||||
Value int
|
||||
|
||||
@@ -55,7 +55,7 @@ import (
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
func WithResource[A, R, ANY any](onCreate ReaderIOEither[R], onRelease func(R) ReaderIOEither[ANY]) func(func(R) ReaderIOEither[A]) ReaderIOEither[A] {
|
||||
func WithResource[A, R, ANY any](onCreate ReaderIOEither[R], onRelease func(R) ReaderIOEither[ANY]) Kleisli[Kleisli[R, A], A] {
|
||||
return function.Flow2(
|
||||
function.Bind2nd(function.Flow2[func(R) ReaderIOEither[A], Operator[A, A], R, ReaderIOEither[A], ReaderIOEither[A]], WithContext[A]),
|
||||
RIE.WithResource[A, context.Context, error, R](WithContext(onCreate), onRelease),
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArray[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -45,7 +45,7 @@ func TraverseArray[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEith
|
||||
// - f: Function that transforms each element with its index into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderIOEither[B]) Kleisli[[]A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -72,7 +72,7 @@ func SequenceArray[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
|
||||
// - f: Function that transforms each value into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms a map into a ReaderIOEither of a map.
|
||||
func TraverseRecord[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -89,7 +89,7 @@ func TraverseRecord[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(ma
|
||||
// - f: Function that transforms each key-value pair into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms a map into a ReaderIOEither of a map.
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -117,7 +117,7 @@ func SequenceRecord[K comparable, A any](ma map[K]ReaderIOEither[A]) ReaderIOEit
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither containing an array of transformed values.
|
||||
func MonadTraverseArraySeq[A, B any](as []A, f func(A) ReaderIOEither[B]) ReaderIOEither[[]B] {
|
||||
func MonadTraverseArraySeq[A, B any](as []A, f Kleisli[A, B]) ReaderIOEither[[]B] {
|
||||
return array.MonadTraverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -134,7 +134,7 @@ func MonadTraverseArraySeq[A, B any](as []A, f func(A) ReaderIOEither[B]) Reader
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArraySeq[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -145,7 +145,7 @@ func TraverseArraySeq[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOE
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndexSeq uses transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]]
|
||||
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) ReaderIOEither[B]) Kleisli[[]A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -167,7 +167,7 @@ func SequenceArraySeq[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
|
||||
}
|
||||
|
||||
// MonadTraverseRecordSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f func(A) ReaderIOEither[B]) ReaderIOEither[map[K]B] {
|
||||
func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f Kleisli[A, B]) ReaderIOEither[map[K]B] {
|
||||
return record.MonadTraverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -178,7 +178,7 @@ func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f func(A) Reader
|
||||
}
|
||||
|
||||
// TraverseRecordSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordSeq[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -189,7 +189,7 @@ func TraverseRecordSeq[K comparable, A, B any](f func(A) ReaderIOEither[B]) func
|
||||
}
|
||||
|
||||
// TraverseRecordWithIndexSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordWithIndexSeq[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecordWithIndexSeq[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -212,7 +212,7 @@ func SequenceRecordSeq[K comparable, A any](ma map[K]ReaderIOEither[A]) ReaderIO
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a ReaderIOEither containing an array of transformed values.
|
||||
func MonadTraverseArrayPar[A, B any](as []A, f func(A) ReaderIOEither[B]) ReaderIOEither[[]B] {
|
||||
func MonadTraverseArrayPar[A, B any](as []A, f Kleisli[A, B]) ReaderIOEither[[]B] {
|
||||
return array.MonadTraverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -229,7 +229,7 @@ func MonadTraverseArrayPar[A, B any](as []A, f func(A) ReaderIOEither[B]) Reader
|
||||
// - f: Function that transforms each element into a ReaderIOEither
|
||||
//
|
||||
// Returns a function that transforms an array into a ReaderIOEither of an array.
|
||||
func TraverseArrayPar[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return array.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -240,7 +240,7 @@ func TraverseArrayPar[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOE
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndexPar uses transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]]
|
||||
func TraverseArrayWithIndexPar[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
|
||||
func TraverseArrayWithIndexPar[A, B any](f func(int, A) ReaderIOEither[B]) Kleisli[[]A, []B] {
|
||||
return array.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -262,7 +262,7 @@ func SequenceArrayPar[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
|
||||
}
|
||||
|
||||
// TraverseRecordPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordPar[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecordPar[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -273,7 +273,7 @@ func TraverseRecordPar[K comparable, A, B any](f func(A) ReaderIOEither[B]) func
|
||||
}
|
||||
|
||||
// TraverseRecordWithIndexPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
|
||||
func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return record.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -284,7 +284,7 @@ func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) ReaderIOEit
|
||||
}
|
||||
|
||||
// MonadTraverseRecordPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
|
||||
func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f func(A) ReaderIOEither[B]) ReaderIOEither[map[K]B] {
|
||||
func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f Kleisli[A, B]) ReaderIOEither[map[K]B] {
|
||||
return record.MonadTraverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
|
||||
@@ -99,10 +99,12 @@ type (
|
||||
// result := fetchUser("123")(ctx)()
|
||||
ReaderIOEither[A any] = readerioeither.ReaderIOEither[context.Context, error, A]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderIOEither[B]]
|
||||
|
||||
// Operator represents a transformation from one ReaderIOEither to another.
|
||||
// This is useful for point-free style composition and building reusable transformations.
|
||||
//
|
||||
// Operator[A, B] is equivalent to func(ReaderIOEither[A]) ReaderIOEither[B]
|
||||
// Operator[A, B] is equivalent to Kleisli[ReaderIOEither[A], B]
|
||||
//
|
||||
// Example usage:
|
||||
// // Define a reusable transformation
|
||||
@@ -110,5 +112,5 @@ type (
|
||||
//
|
||||
// // Apply the transformation
|
||||
// result := toUpper(computation)
|
||||
Operator[A, B any] = Reader[ReaderIOEither[A], ReaderIOEither[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderIOEither[A], B]
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package either
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
@@ -171,3 +172,204 @@ func ApS[E, S1, S2, T any](
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions and enables working with
|
||||
// nested fields in a type-safe manner.
|
||||
//
|
||||
// Unlike BindL, ApSL uses applicative semantics, meaning the computation fa is independent
|
||||
// of the current state and can be evaluated concurrently.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - fa: An Either[E, T] computation that produces the value to set
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that updates the focused field in the Either context
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(p Person) int { return p.Age },
|
||||
// func(p Person, a int) Person { p.Age = a; return p },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// either.Right[error](Person{Name: "Alice", Age: 25}),
|
||||
// either.ApSL(ageLens, either.Right[error](30)),
|
||||
// ) // Right(Person{Name: "Alice", Age: 30})
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[E, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Either[E, T],
|
||||
) Endomorphism[Either[E, S]] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// an Either that produces the new value.
|
||||
//
|
||||
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
|
||||
// the current value of the focused field.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - f: A function that takes the current field value and returns an Either[E, T]
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that updates the focused field based on its current value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but fail if it would exceed 100
|
||||
// increment := func(v int) either.Either[error, int] {
|
||||
// if v >= 100 {
|
||||
// return either.Left[int](errors.New("counter overflow"))
|
||||
// }
|
||||
// return either.Right[error](v + 1)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// either.Right[error](Counter{Value: 42}),
|
||||
// either.BindL(valueLens, increment),
|
||||
// ) // Right(Counter{Value: 43})
|
||||
//
|
||||
//go:inline
|
||||
func BindL[E, S, T any](
|
||||
lens Lens[S, T],
|
||||
f func(T) Either[E, T],
|
||||
) Endomorphism[Either[E, S]] {
|
||||
return Bind[E, S, S, T](lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in Either).
|
||||
//
|
||||
// This is useful for pure transformations that cannot fail, such as mathematical operations,
|
||||
// string manipulations, or other deterministic updates.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - f: An endomorphism (T → T) that transforms the current field value
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that updates the focused field with the transformed value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// either.Right[error](Counter{Value: 21}),
|
||||
// either.LetL(valueLens, double),
|
||||
// ) // Right(Counter{Value: 42})
|
||||
//
|
||||
//go:inline
|
||||
func LetL[E, S, T any](
|
||||
lens Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Endomorphism[Either[E, S]] {
|
||||
return Let[E, S, S, T](lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// This is useful for resetting fields, initializing values, or setting fields to
|
||||
// predetermined constants.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Error type for the Either
|
||||
// - S: Structure type containing the field to update
|
||||
// - T: Type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
|
||||
// - b: The constant value to set the field to
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that sets the focused field to the constant value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// either.Right[error](Config{Debug: true, Timeout: 30}),
|
||||
// either.LetToL(debugLens, false),
|
||||
// ) // Right(Config{Debug: false, Timeout: 30})
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[E, S, T any](
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Endomorphism[Either[E, S]] {
|
||||
return LetTo[E, S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -54,3 +55,307 @@ func TestApS(t *testing.T) {
|
||||
|
||||
assert.Equal(t, res, Of[error]("John Doe"))
|
||||
}
|
||||
|
||||
// Test types for lens-based operations
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Timeout int
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
// Create a lens for the Age field
|
||||
ageLens := L.MakeLens(
|
||||
func(p Person) int { return p.Age },
|
||||
func(p Person, a int) Person { p.Age = a; return p },
|
||||
)
|
||||
|
||||
t.Run("ApSL with Right value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Right[error](Person{Name: "Alice", Age: 25}),
|
||||
ApSL(ageLens, Right[error](30)),
|
||||
)
|
||||
|
||||
expected := Right[error](Person{Name: "Alice", Age: 30})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("ApSL with Left in context", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Left[Person](assert.AnError),
|
||||
ApSL(ageLens, Right[error](30)),
|
||||
)
|
||||
|
||||
expected := Left[Person](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("ApSL with Left in value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Right[error](Person{Name: "Alice", Age: 25}),
|
||||
ApSL(ageLens, Left[int](assert.AnError)),
|
||||
)
|
||||
|
||||
expected := Left[Person](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("ApSL with both Left", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Left[Person](assert.AnError),
|
||||
ApSL(ageLens, Left[int](assert.AnError)),
|
||||
)
|
||||
|
||||
expected := Left[Person](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
// Create a lens for the Value field
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("BindL with successful transformation", func(t *testing.T) {
|
||||
// Increment the counter, but fail if it would exceed 100
|
||||
increment := func(v int) Either[error, int] {
|
||||
if v >= 100 {
|
||||
return Left[int](assert.AnError)
|
||||
}
|
||||
return Right[error](v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](Counter{Value: 42}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 43})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("BindL with failing transformation", func(t *testing.T) {
|
||||
increment := func(v int) Either[error, int] {
|
||||
if v >= 100 {
|
||||
return Left[int](assert.AnError)
|
||||
}
|
||||
return Right[error](v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](Counter{Value: 100}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
expected := Left[Counter](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("BindL with Left input", func(t *testing.T) {
|
||||
increment := func(v int) Either[error, int] {
|
||||
return Right[error](v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[Counter](assert.AnError),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
expected := Left[Counter](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("BindL with multiple operations", func(t *testing.T) {
|
||||
double := func(v int) Either[error, int] {
|
||||
return Right[error](v * 2)
|
||||
}
|
||||
|
||||
addTen := func(v int) Either[error, int] {
|
||||
return Right[error](v + 10)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 5}),
|
||||
BindL(valueLens, double),
|
||||
BindL(valueLens, addTen),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 20})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
// Create a lens for the Value field
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("LetL with pure transformation", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](Counter{Value: 21}),
|
||||
LetL[error](valueLens, double),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 42})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetL with Left input", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[Counter](assert.AnError),
|
||||
LetL[error](valueLens, double),
|
||||
)
|
||||
|
||||
expected := Left[Counter](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetL with multiple transformations", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
addTen := func(v int) int { return v + 10 }
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 5}),
|
||||
LetL[error](valueLens, double),
|
||||
LetL[error](valueLens, addTen),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 20})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetL with identity transformation", func(t *testing.T) {
|
||||
identity := func(v int) int { return v }
|
||||
|
||||
result := F.Pipe1(
|
||||
Right[error](Counter{Value: 42}),
|
||||
LetL[error](valueLens, identity),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 42})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
// Create a lens for the Debug field
|
||||
debugLens := L.MakeLens(
|
||||
func(c Config) bool { return c.Debug },
|
||||
func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
)
|
||||
|
||||
t.Run("LetToL with constant value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Right[error](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[error](debugLens, false),
|
||||
)
|
||||
|
||||
expected := Right[error](Config{Debug: false, Timeout: 30})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetToL with Left input", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Left[Config](assert.AnError),
|
||||
LetToL[error](debugLens, false),
|
||||
)
|
||||
|
||||
expected := Left[Config](assert.AnError)
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetToL with multiple fields", func(t *testing.T) {
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) int { return c.Timeout },
|
||||
func(c Config, t int) Config { c.Timeout = t; return c },
|
||||
)
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[error](debugLens, false),
|
||||
LetToL[error](timeoutLens, 60),
|
||||
)
|
||||
|
||||
expected := Right[error](Config{Debug: false, Timeout: 60})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("LetToL setting same value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Right[error](Config{Debug: false, Timeout: 30}),
|
||||
LetToL[error](debugLens, false),
|
||||
)
|
||||
|
||||
expected := Right[error](Config{Debug: false, Timeout: 30})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLensOperationsCombined(t *testing.T) {
|
||||
// Test combining different lens operations
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("Combine LetToL and LetL", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 100}),
|
||||
LetToL[error](valueLens, 10),
|
||||
LetL[error](valueLens, double),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 20})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Combine LetL and BindL", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
validate := func(v int) Either[error, int] {
|
||||
if v > 100 {
|
||||
return Left[int](assert.AnError)
|
||||
}
|
||||
return Right[error](v)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 25}),
|
||||
LetL[error](valueLens, double),
|
||||
BindL(valueLens, validate),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 50})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Combine ApSL and LetL", func(t *testing.T) {
|
||||
addFive := func(v int) int { return v + 5 }
|
||||
|
||||
result := F.Pipe2(
|
||||
Right[error](Counter{Value: 10}),
|
||||
ApSL(valueLens, Right[error](20)),
|
||||
LetL[error](valueLens, addFive),
|
||||
)
|
||||
|
||||
expected := Right[error](Counter{Value: 25})
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func MonadAp[B, E, A any](fab Either[E, func(a A) B], fa Either[E, A]) Either[E,
|
||||
|
||||
// Ap is the curried version of [MonadAp].
|
||||
// Returns a function that applies a wrapped function to the given wrapped value.
|
||||
func Ap[B, E, A any](fa Either[E, A]) func(fab Either[E, func(a A) B]) Either[E, B] {
|
||||
func Ap[B, E, A any](fa Either[E, A]) Operator[E, func(A) B, B] {
|
||||
return F.Bind2nd(MonadAp[B, E, A], fa)
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func MonadMapTo[E, A, B any](fa Either[E, A], b B) Either[E, B] {
|
||||
}
|
||||
|
||||
// MapTo is the curried version of [MonadMapTo].
|
||||
func MapTo[E, A, B any](b B) func(Either[E, A]) Either[E, B] {
|
||||
func MapTo[E, A, B any](b B) Operator[E, A, B] {
|
||||
return Map[E](F.Constant1[A](b))
|
||||
}
|
||||
|
||||
@@ -211,26 +211,26 @@ func MonadChainOptionK[A, B, E any](onNone func() E, ma Either[E, A], f func(A)
|
||||
}
|
||||
|
||||
// ChainOptionK is the curried version of [MonadChainOptionK].
|
||||
func ChainOptionK[A, B, E any](onNone func() E) func(func(A) Option[B]) func(Either[E, A]) Either[E, B] {
|
||||
func ChainOptionK[A, B, E any](onNone func() E) func(func(A) Option[B]) Operator[E, A, B] {
|
||||
from := FromOption[B](onNone)
|
||||
return func(f func(A) Option[B]) func(Either[E, A]) Either[E, B] {
|
||||
return func(f func(A) Option[B]) Operator[E, A, B] {
|
||||
return Chain(F.Flow2(f, from))
|
||||
}
|
||||
}
|
||||
|
||||
// ChainTo is the curried version of [MonadChainTo].
|
||||
func ChainTo[A, E, B any](mb Either[E, B]) func(Either[E, A]) Either[E, B] {
|
||||
func ChainTo[A, E, B any](mb Either[E, B]) Operator[E, A, B] {
|
||||
return F.Constant1[Either[E, A]](mb)
|
||||
}
|
||||
|
||||
// Chain is the curried version of [MonadChain].
|
||||
// Sequences two computations where the second depends on the first.
|
||||
func Chain[E, A, B any](f func(a A) Either[E, B]) func(Either[E, A]) Either[E, B] {
|
||||
func Chain[E, A, B any](f func(a A) Either[E, B]) Operator[E, A, B] {
|
||||
return Fold(Left[B, E], f)
|
||||
}
|
||||
|
||||
// ChainFirst is the curried version of [MonadChainFirst].
|
||||
func ChainFirst[E, A, B any](f func(a A) Either[E, B]) func(Either[E, A]) Either[E, A] {
|
||||
func ChainFirst[E, A, B any](f func(a A) Either[E, B]) Operator[E, A, A] {
|
||||
return C.ChainFirst(
|
||||
Chain[E, A, A],
|
||||
Map[E, B, A],
|
||||
@@ -437,7 +437,7 @@ func AltW[E, E1, A any](that L.Lazy[Either[E1, A]]) func(Either[E, A]) Either[E1
|
||||
// return either.Right[error](99)
|
||||
// })
|
||||
// result := alternative(either.Left[int](errors.New("fail"))) // Right(99)
|
||||
func Alt[E, A any](that L.Lazy[Either[E, A]]) func(Either[E, A]) Either[E, A] {
|
||||
func Alt[E, A any](that L.Lazy[Either[E, A]]) Operator[E, A, A] {
|
||||
return AltW[E](that)
|
||||
}
|
||||
|
||||
@@ -449,7 +449,7 @@ func Alt[E, A any](that L.Lazy[Either[E, A]]) func(Either[E, A]) Either[E, A] {
|
||||
// return either.Right[error](0) // default value
|
||||
// })
|
||||
// result := recover(either.Left[int](errors.New("fail"))) // Right(0)
|
||||
func OrElse[E, A any](onLeft func(e E) Either[E, A]) func(Either[E, A]) Either[E, A] {
|
||||
func OrElse[E, A any](onLeft func(e E) Either[E, A]) Operator[E, A, A] {
|
||||
return Fold(onLeft, Of[E, A])
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ func MonadFlap[E, B, A any](fab Either[E, func(A) B], a A) Either[E, B] {
|
||||
}
|
||||
|
||||
// Flap is the curried version of [MonadFlap].
|
||||
func Flap[E, B, A any](a A) func(Either[E, func(A) B]) Either[E, B] {
|
||||
func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
|
||||
return FC.Flap(Map[E, func(A) B, B], a)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
type eitherFunctor[E, A, B any] struct{}
|
||||
|
||||
func (o *eitherFunctor[E, A, B]) Map(f func(A) B) func(Either[E, A]) Either[E, B] {
|
||||
func (o *eitherFunctor[E, A, B]) Map(f func(A) B) Operator[E, A, B] {
|
||||
return Map[E, A, B](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
L "github.com/IBM/fp-go/v2/logging"
|
||||
)
|
||||
|
||||
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) func(Either[E, A]) Either[E, A] {
|
||||
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[E, A, A] {
|
||||
return Fold(
|
||||
func(e E) Either[E, A] {
|
||||
left("%s: %v", prefix, e)
|
||||
@@ -50,9 +50,9 @@ func _log[E, A any](left func(string, ...any), right func(string, ...any), prefi
|
||||
// )
|
||||
// // Logs: "Processing: 42"
|
||||
// // result is Right(84)
|
||||
func Logger[E, A any](loggers ...*log.Logger) func(string) func(Either[E, A]) Either[E, A] {
|
||||
func Logger[E, A any](loggers ...*log.Logger) func(string) Operator[E, A, A] {
|
||||
left, right := L.LoggingCallbacks(loggers...)
|
||||
return func(prefix string) func(Either[E, A]) Either[E, A] {
|
||||
return func(prefix string) Operator[E, A, A] {
|
||||
delegate := _log[E, A](left, right, prefix)
|
||||
return func(ma Either[E, A]) Either[E, A] {
|
||||
return F.Pipe1(
|
||||
|
||||
@@ -25,15 +25,15 @@ func (o *eitherMonad[E, A, B]) Of(a A) Either[E, A] {
|
||||
return Of[E, A](a)
|
||||
}
|
||||
|
||||
func (o *eitherMonad[E, A, B]) Map(f func(A) B) func(Either[E, A]) Either[E, B] {
|
||||
func (o *eitherMonad[E, A, B]) Map(f func(A) B) Operator[E, A, B] {
|
||||
return Map[E, A, B](f)
|
||||
}
|
||||
|
||||
func (o *eitherMonad[E, A, B]) Chain(f func(A) Either[E, B]) func(Either[E, A]) Either[E, B] {
|
||||
func (o *eitherMonad[E, A, B]) Chain(f func(A) Either[E, B]) Operator[E, A, B] {
|
||||
return Chain[E, A, B](f)
|
||||
}
|
||||
|
||||
func (o *eitherMonad[E, A, B]) Ap(fa Either[E, A]) func(Either[E, func(A) B]) Either[E, B] {
|
||||
func (o *eitherMonad[E, A, B]) Ap(fa Either[E, A]) Operator[E, func(A) B, B] {
|
||||
return Ap[B, E, A](fa)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
// m := either.AlternativeMonoid[error](intAdd)
|
||||
// result := m.Concat(either.Right[error](1), either.Right[error](2))
|
||||
// // result is Right(3)
|
||||
func AlternativeMonoid[E, A any](m M.Monoid[A]) M.Monoid[Either[E, A]] {
|
||||
func AlternativeMonoid[E, A any](m M.Monoid[A]) Monoid[E, A] {
|
||||
return M.AlternativeMonoid(
|
||||
Of[E, A],
|
||||
MonadMap[E, A, func(A) A],
|
||||
@@ -51,7 +51,7 @@ func AlternativeMonoid[E, A any](m M.Monoid[A]) M.Monoid[Either[E, A]] {
|
||||
// m := either.AltMonoid[error, int](zero)
|
||||
// result := m.Concat(either.Left[int](errors.New("err1")), either.Right[error](42))
|
||||
// // result is Right(42)
|
||||
func AltMonoid[E, A any](zero L.Lazy[Either[E, A]]) M.Monoid[Either[E, A]] {
|
||||
func AltMonoid[E, A any](zero L.Lazy[Either[E, A]]) Monoid[E, A] {
|
||||
return M.AltMonoid(
|
||||
zero,
|
||||
MonadAlt[E, A],
|
||||
|
||||
@@ -15,10 +15,22 @@
|
||||
|
||||
package either
|
||||
|
||||
import "github.com/IBM/fp-go/v2/option"
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Option is a type alias for option.Option, provided for convenience
|
||||
// when working with Either and Option together.
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Option[A any] = option.Option[A]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Endomorphism[T any] = endomorphism.Endomorphism[T]
|
||||
|
||||
Kleisli[E, A, B any] = reader.Reader[A, Either[E, B]]
|
||||
Operator[E, A, B any] = Kleisli[E, Either[E, A], B]
|
||||
Monoid[E, A any] = monoid.Monoid[Either[E, A]]
|
||||
)
|
||||
|
||||
@@ -13,6 +13,62 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package builder provides a functional, immutable HTTP request builder with composable operations.
|
||||
// It follows functional programming principles to construct HTTP requests in a type-safe,
|
||||
// testable, and maintainable way.
|
||||
//
|
||||
// The Builder type is immutable - all operations return a new builder instance rather than
|
||||
// modifying the existing one. This ensures thread-safety and makes the code easier to reason about.
|
||||
//
|
||||
// Key Features:
|
||||
// - Immutable builder pattern with method chaining
|
||||
// - Lens-based access to builder properties
|
||||
// - Support for headers, query parameters, request body, and HTTP methods
|
||||
// - JSON and form data encoding
|
||||
// - URL construction with query parameter merging
|
||||
// - Hash generation for caching
|
||||
// - Bearer token authentication helpers
|
||||
//
|
||||
// Basic Usage:
|
||||
//
|
||||
// import (
|
||||
// B "github.com/IBM/fp-go/v2/http/builder"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// )
|
||||
//
|
||||
// // Build a simple GET request
|
||||
// builder := F.Pipe2(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithHeader("Accept")("application/json"),
|
||||
// )
|
||||
//
|
||||
// // Build a POST request with JSON body
|
||||
// builder := F.Pipe3(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/users"),
|
||||
// B.WithMethod("POST"),
|
||||
// B.WithJSON(map[string]string{"name": "John"}),
|
||||
// )
|
||||
//
|
||||
// // Build a request with query parameters
|
||||
// builder := F.Pipe3(
|
||||
// B.Default,
|
||||
// B.WithURL("https://api.example.com/search"),
|
||||
// B.WithQueryArg("q")("golang"),
|
||||
// B.WithQueryArg("limit")("10"),
|
||||
// )
|
||||
//
|
||||
// The package provides several convenience functions for common HTTP methods:
|
||||
// - WithGet, WithPost, WithPut, WithDelete for setting HTTP methods
|
||||
// - WithBearer for adding Bearer token authentication
|
||||
// - WithJSON for JSON payloads
|
||||
// - WithFormData for form-encoded payloads
|
||||
//
|
||||
// Lenses are provided for advanced use cases:
|
||||
// - URL, Method, Body, Headers, Query for accessing builder properties
|
||||
// - Header(name) for accessing individual headers
|
||||
// - QueryArg(name) for accessing individual query parameters
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -17,8 +17,11 @@ package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
C "github.com/IBM/fp-go/v2/http/content"
|
||||
FD "github.com/IBM/fp-go/v2/http/form"
|
||||
@@ -91,3 +94,351 @@ func TestHash(t *testing.T) {
|
||||
|
||||
fmt.Println(MakeHash(b1))
|
||||
}
|
||||
|
||||
// TestGetTargetURL tests URL construction with query parameters
|
||||
func TestGetTargetURL(t *testing.T) {
|
||||
builder := F.Pipe3(
|
||||
Default,
|
||||
WithURL("http://www.example.com?existing=param"),
|
||||
WithQueryArg("limit")("10"),
|
||||
WithQueryArg("offset")("20"),
|
||||
)
|
||||
|
||||
result := builder.GetTargetURL()
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
url := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Contains(t, url, "limit=10")
|
||||
assert.Contains(t, url, "offset=20")
|
||||
assert.Contains(t, url, "existing=param")
|
||||
}
|
||||
|
||||
// TestGetTargetURLWithInvalidURL tests error handling for invalid URLs
|
||||
func TestGetTargetURLWithInvalidURL(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithURL("://invalid-url"),
|
||||
)
|
||||
|
||||
result := builder.GetTargetURL()
|
||||
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
|
||||
}
|
||||
|
||||
// TestGetTargetUrl tests the deprecated GetTargetUrl function
|
||||
func TestGetTargetUrl(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
Default,
|
||||
WithURL("http://www.example.com"),
|
||||
WithQueryArg("test")("value"),
|
||||
)
|
||||
|
||||
result := builder.GetTargetUrl()
|
||||
assert.True(t, E.IsRight(result), "Expected Right result")
|
||||
|
||||
url := E.GetOrElse(func(error) string { return "" })(result)
|
||||
assert.Contains(t, url, "test=value")
|
||||
}
|
||||
|
||||
// TestSetMethod tests the SetMethod function
|
||||
func TestSetMethod(t *testing.T) {
|
||||
builder := Default.SetMethod("POST")
|
||||
|
||||
assert.Equal(t, "POST", builder.GetMethod())
|
||||
}
|
||||
|
||||
// TestSetQuery tests the SetQuery function
|
||||
func TestSetQuery(t *testing.T) {
|
||||
query := make(url.Values)
|
||||
query.Set("key1", "value1")
|
||||
query.Set("key2", "value2")
|
||||
|
||||
builder := Default.SetQuery(query)
|
||||
|
||||
assert.Equal(t, "value1", builder.GetQuery().Get("key1"))
|
||||
assert.Equal(t, "value2", builder.GetQuery().Get("key2"))
|
||||
}
|
||||
|
||||
// TestSetHeaders tests the SetHeaders function
|
||||
func TestSetHeaders(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("X-Custom-Header", "custom-value")
|
||||
headers.Set("Authorization", "Bearer token")
|
||||
|
||||
builder := Default.SetHeaders(headers)
|
||||
|
||||
assert.Equal(t, "custom-value", builder.GetHeaders().Get("X-Custom-Header"))
|
||||
assert.Equal(t, "Bearer token", builder.GetHeaders().Get("Authorization"))
|
||||
}
|
||||
|
||||
// TestGetHeaderValues tests the GetHeaderValues function
|
||||
func TestGetHeaderValues(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
Default,
|
||||
WithHeader("Accept")("application/json"),
|
||||
WithHeader("Accept")("text/html"),
|
||||
)
|
||||
|
||||
values := builder.GetHeaderValues("Accept")
|
||||
assert.Contains(t, values, "text/html")
|
||||
}
|
||||
|
||||
// TestGetUrl tests the deprecated GetUrl function
|
||||
func TestGetUrl(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithURL("http://www.example.com"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "http://www.example.com", builder.GetUrl())
|
||||
}
|
||||
|
||||
// TestSetUrl tests the deprecated SetUrl function
|
||||
func TestSetUrl(t *testing.T) {
|
||||
builder := Default.SetUrl("http://www.example.com")
|
||||
|
||||
assert.Equal(t, "http://www.example.com", builder.GetURL())
|
||||
}
|
||||
|
||||
// TestWithJson tests the deprecated WithJson function
|
||||
func TestWithJson(t *testing.T) {
|
||||
data := map[string]string{"key": "value"}
|
||||
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithJson(data),
|
||||
)
|
||||
|
||||
contentType := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.ContentType))
|
||||
assert.Equal(t, C.JSON, contentType)
|
||||
assert.True(t, O.IsSome(builder.GetBody()))
|
||||
}
|
||||
|
||||
// TestQueryArg tests the QueryArg lens
|
||||
func TestQueryArg(t *testing.T) {
|
||||
lens := QueryArg("test")
|
||||
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
lens.Set(O.Some("value")),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Some("value"), lens.Get(builder))
|
||||
assert.Equal(t, "value", builder.GetQuery().Get("test"))
|
||||
}
|
||||
|
||||
// TestWithQueryArg tests the WithQueryArg function
|
||||
func TestWithQueryArg(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
Default,
|
||||
WithQueryArg("param1")("value1"),
|
||||
WithQueryArg("param2")("value2"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "value1", builder.GetQuery().Get("param1"))
|
||||
assert.Equal(t, "value2", builder.GetQuery().Get("param2"))
|
||||
}
|
||||
|
||||
// TestWithoutQueryArg tests the WithoutQueryArg function
|
||||
func TestWithoutQueryArg(t *testing.T) {
|
||||
builder := F.Pipe3(
|
||||
Default,
|
||||
WithQueryArg("param1")("value1"),
|
||||
WithQueryArg("param2")("value2"),
|
||||
WithoutQueryArg("param1"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "", builder.GetQuery().Get("param1"))
|
||||
assert.Equal(t, "value2", builder.GetQuery().Get("param2"))
|
||||
}
|
||||
|
||||
// TestGetHash tests the GetHash method
|
||||
func TestGetHash(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
Default,
|
||||
WithURL("http://www.example.com"),
|
||||
WithMethod("POST"),
|
||||
)
|
||||
|
||||
hash := builder.GetHash()
|
||||
assert.NotEmpty(t, hash)
|
||||
assert.Equal(t, MakeHash(builder), hash)
|
||||
}
|
||||
|
||||
// TestWithBytes tests the WithBytes function
|
||||
func TestWithBytes(t *testing.T) {
|
||||
data := []byte("test data")
|
||||
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithBytes(data),
|
||||
)
|
||||
|
||||
body := builder.GetBody()
|
||||
assert.True(t, O.IsSome(body))
|
||||
}
|
||||
|
||||
// TestWithoutBody tests the WithoutBody function
|
||||
func TestWithoutBody(t *testing.T) {
|
||||
builder := F.Pipe2(
|
||||
Default,
|
||||
WithBytes([]byte("data")),
|
||||
WithoutBody,
|
||||
)
|
||||
|
||||
assert.True(t, O.IsNone(builder.GetBody()))
|
||||
}
|
||||
|
||||
// TestWithGet tests the WithGet convenience function
|
||||
func TestWithGet(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithGet,
|
||||
)
|
||||
|
||||
assert.Equal(t, "GET", builder.GetMethod())
|
||||
}
|
||||
|
||||
// TestWithPost tests the WithPost convenience function
|
||||
func TestWithPost(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithPost,
|
||||
)
|
||||
|
||||
assert.Equal(t, "POST", builder.GetMethod())
|
||||
}
|
||||
|
||||
// TestWithPut tests the WithPut convenience function
|
||||
func TestWithPut(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithPut,
|
||||
)
|
||||
|
||||
assert.Equal(t, "PUT", builder.GetMethod())
|
||||
}
|
||||
|
||||
// TestWithDelete tests the WithDelete convenience function
|
||||
func TestWithDelete(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithDelete,
|
||||
)
|
||||
|
||||
assert.Equal(t, "DELETE", builder.GetMethod())
|
||||
}
|
||||
|
||||
// TestWithBearer tests the WithBearer function
|
||||
func TestWithBearer(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithBearer("my-token"),
|
||||
)
|
||||
|
||||
auth := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.Authorization))
|
||||
assert.Equal(t, "Bearer my-token", auth)
|
||||
}
|
||||
|
||||
// TestWithContentType tests the WithContentType function
|
||||
func TestWithContentType(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithContentType(C.TextPlain),
|
||||
)
|
||||
|
||||
contentType := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.ContentType))
|
||||
assert.Equal(t, C.TextPlain, contentType)
|
||||
}
|
||||
|
||||
// TestWithAuthorization tests the WithAuthorization function
|
||||
func TestWithAuthorization(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithAuthorization("Basic abc123"),
|
||||
)
|
||||
|
||||
auth := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.Authorization))
|
||||
assert.Equal(t, "Basic abc123", auth)
|
||||
}
|
||||
|
||||
// TestBuilderChaining tests that builder operations can be chained
|
||||
func TestBuilderChaining(t *testing.T) {
|
||||
builder := F.Pipe3(
|
||||
Default,
|
||||
WithURL("http://www.example.com"),
|
||||
WithMethod("POST"),
|
||||
WithHeader("X-Test")("test-value"),
|
||||
)
|
||||
|
||||
// Verify all operations were applied
|
||||
assert.Equal(t, "http://www.example.com", builder.GetURL())
|
||||
assert.Equal(t, "POST", builder.GetMethod())
|
||||
|
||||
testHeader := O.GetOrElse(F.Constant(""))(builder.GetHeader("X-Test"))
|
||||
assert.Equal(t, "test-value", testHeader)
|
||||
}
|
||||
|
||||
// TestWithQuery tests the WithQuery function
|
||||
func TestWithQuery(t *testing.T) {
|
||||
query := make(url.Values)
|
||||
query.Set("key1", "value1")
|
||||
query.Set("key2", "value2")
|
||||
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithQuery(query),
|
||||
)
|
||||
|
||||
assert.Equal(t, "value1", builder.GetQuery().Get("key1"))
|
||||
assert.Equal(t, "value2", builder.GetQuery().Get("key2"))
|
||||
}
|
||||
|
||||
// TestWithHeaders tests the WithHeaders function
|
||||
func TestWithHeaders(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("X-Test", "test-value")
|
||||
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithHeaders(headers),
|
||||
)
|
||||
|
||||
assert.Equal(t, "test-value", builder.GetHeaders().Get("X-Test"))
|
||||
}
|
||||
|
||||
// TestWithUrl tests the deprecated WithUrl function
|
||||
func TestWithUrl(t *testing.T) {
|
||||
builder := F.Pipe1(
|
||||
Default,
|
||||
WithUrl("http://www.example.com"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "http://www.example.com", builder.GetURL())
|
||||
}
|
||||
|
||||
// TestComplexBuilderComposition tests a complex builder composition
|
||||
func TestComplexBuilderComposition(t *testing.T) {
|
||||
builder := F.Pipe5(
|
||||
Default,
|
||||
WithURL("http://api.example.com/users"),
|
||||
WithPost,
|
||||
WithJSON(map[string]interface{}{
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
}),
|
||||
WithBearer("secret-token"),
|
||||
WithQueryArg("notify")("true"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "http://api.example.com/users", builder.GetURL())
|
||||
assert.Equal(t, "POST", builder.GetMethod())
|
||||
|
||||
contentType := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.ContentType))
|
||||
assert.Equal(t, C.JSON, contentType)
|
||||
|
||||
auth := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.Authorization))
|
||||
assert.Equal(t, "Bearer secret-token", auth)
|
||||
|
||||
assert.Equal(t, "true", builder.GetQuery().Get("notify"))
|
||||
assert.True(t, O.IsSome(builder.GetBody()))
|
||||
}
|
||||
|
||||
36
v2/http/builder/coverage.out
Normal file
36
v2/http/builder/coverage.out
Normal file
@@ -0,0 +1,36 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:208.51,211.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:213.37,215.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:217.42,221.2 3 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:226.64,228.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:231.64,257.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:260.41,262.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:264.41,266.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:268.44,273.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:275.50,277.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:279.47,281.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:283.61,286.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:288.69,290.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:292.59,295.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:298.53,301.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:303.53,306.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:308.66,311.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:313.82,316.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:318.64,321.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:323.57,326.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:328.65,334.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:336.63,338.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:341.42,343.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:346.61,354.75 4 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:354.75,360.3 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:364.62,369.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:372.46,374.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:379.43,381.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:384.43,393.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:396.63,401.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:404.64,409.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:412.48,414.2 1 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:416.68,419.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:421.84,424.2 2 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:426.35,438.2 7 1
|
||||
github.com/IBM/fp-go/v2/http/builder/builder.go:441.34,443.2 1 1
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
ENDO "github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
LA "github.com/IBM/fp-go/v2/optics/lens/array"
|
||||
LO "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
LRG "github.com/IBM/fp-go/v2/optics/lens/record/generic"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
RG "github.com/IBM/fp-go/v2/record/generic"
|
||||
@@ -50,7 +50,7 @@ var (
|
||||
|
||||
composeHead = F.Pipe1(
|
||||
LA.AtHead[string](),
|
||||
L.ComposeOptions[url.Values, string](A.Empty[string]()),
|
||||
LO.Compose[url.Values, string](A.Empty[string]()),
|
||||
)
|
||||
|
||||
// AtValue is a [L.Lens] that focusses on first value in form fields
|
||||
|
||||
@@ -13,6 +13,56 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package headers provides constants and utilities for working with HTTP headers
|
||||
// in a functional programming style. It offers type-safe header name constants,
|
||||
// monoid operations for combining headers, and lens-based access to header values.
|
||||
//
|
||||
// The package follows functional programming principles by providing:
|
||||
// - Immutable operations through lenses
|
||||
// - Monoid for combining header maps
|
||||
// - Type-safe header name constants
|
||||
// - Functional composition of header operations
|
||||
//
|
||||
// Constants:
|
||||
//
|
||||
// The package defines commonly used HTTP header names as constants:
|
||||
// - Accept: The Accept request header
|
||||
// - Authorization: The Authorization request header
|
||||
// - ContentType: The Content-Type header
|
||||
// - ContentLength: The Content-Length header
|
||||
//
|
||||
// Monoid:
|
||||
//
|
||||
// The Monoid provides a way to combine multiple http.Header maps:
|
||||
//
|
||||
// headers1 := make(http.Header)
|
||||
// headers1.Set("X-Custom", "value1")
|
||||
//
|
||||
// headers2 := make(http.Header)
|
||||
// headers2.Set("Authorization", "Bearer token")
|
||||
//
|
||||
// combined := Monoid.Concat(headers1, headers2)
|
||||
// // combined now contains both headers
|
||||
//
|
||||
// Lenses:
|
||||
//
|
||||
// AtValues and AtValue provide lens-based access to header values:
|
||||
//
|
||||
// // AtValues focuses on all values of a header ([]string)
|
||||
// contentTypeLens := AtValues("Content-Type")
|
||||
// values := contentTypeLens.Get(headers)
|
||||
//
|
||||
// // AtValue focuses on the first value of a header (Option[string])
|
||||
// authLens := AtValue("Authorization")
|
||||
// token := authLens.Get(headers) // Returns Option[string]
|
||||
//
|
||||
// The lenses support functional updates:
|
||||
//
|
||||
// // Set a header value
|
||||
// newHeaders := AtValue("Content-Type").Set(O.Some("application/json"))(headers)
|
||||
//
|
||||
// // Remove a header
|
||||
// newHeaders := AtValue("X-Custom").Set(O.None[string]())(headers)
|
||||
package headers
|
||||
|
||||
import (
|
||||
@@ -21,36 +71,94 @@ import (
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
LA "github.com/IBM/fp-go/v2/optics/lens/array"
|
||||
LO "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
LRG "github.com/IBM/fp-go/v2/optics/lens/record/generic"
|
||||
RG "github.com/IBM/fp-go/v2/record/generic"
|
||||
)
|
||||
|
||||
// HTTP headers
|
||||
// Common HTTP header name constants.
|
||||
// These constants provide type-safe access to standard HTTP header names.
|
||||
const (
|
||||
Accept = "Accept"
|
||||
// Accept specifies the media types that are acceptable for the response.
|
||||
// Example: "Accept: application/json"
|
||||
Accept = "Accept"
|
||||
|
||||
// Authorization contains credentials for authenticating the client with the server.
|
||||
// Example: "Authorization: Bearer token123"
|
||||
Authorization = "Authorization"
|
||||
ContentType = "Content-Type"
|
||||
|
||||
// ContentType indicates the media type of the resource or data.
|
||||
// Example: "Content-Type: application/json"
|
||||
ContentType = "Content-Type"
|
||||
|
||||
// ContentLength indicates the size of the entity-body in bytes.
|
||||
// Example: "Content-Length: 348"
|
||||
ContentLength = "Content-Length"
|
||||
)
|
||||
|
||||
var (
|
||||
// Monoid is a [M.Monoid] to concatenate [http.Header] maps
|
||||
// Monoid is a Monoid for combining http.Header maps.
|
||||
// It uses a union operation where values from both headers are preserved.
|
||||
// When the same header exists in both maps, the values are concatenated.
|
||||
//
|
||||
// Example:
|
||||
// h1 := make(http.Header)
|
||||
// h1.Set("X-Custom", "value1")
|
||||
//
|
||||
// h2 := make(http.Header)
|
||||
// h2.Set("Authorization", "Bearer token")
|
||||
//
|
||||
// combined := Monoid.Concat(h1, h2)
|
||||
// // combined contains both X-Custom and Authorization headers
|
||||
Monoid = RG.UnionMonoid[http.Header](A.Semigroup[string]())
|
||||
|
||||
// AtValues is a [L.Lens] that focusses on the values of a header
|
||||
// AtValues is a Lens that focuses on all values of a specific header.
|
||||
// It returns a lens that accesses the []string slice of header values.
|
||||
// The header name is automatically canonicalized using MIME header key rules.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The header name (will be canonicalized)
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[http.Header, []string] focusing on the header's values
|
||||
//
|
||||
// Example:
|
||||
// lens := AtValues("Content-Type")
|
||||
// values := lens.Get(headers) // Returns []string
|
||||
// newHeaders := lens.Set([]string{"application/json"})(headers)
|
||||
AtValues = F.Flow2(
|
||||
textproto.CanonicalMIMEHeaderKey,
|
||||
LRG.AtRecord[http.Header, []string],
|
||||
)
|
||||
|
||||
// composeHead is an internal helper that composes a lens to focus on the first
|
||||
// element of a string array, returning an Option[string].
|
||||
composeHead = F.Pipe1(
|
||||
LA.AtHead[string](),
|
||||
L.ComposeOptions[http.Header, string](A.Empty[string]()),
|
||||
LO.Compose[http.Header, string](A.Empty[string]()),
|
||||
)
|
||||
|
||||
// AtValue is a [L.Lens] that focusses on first value of a header
|
||||
// AtValue is a Lens that focuses on the first value of a specific header.
|
||||
// It returns a lens that accesses an Option[string] representing the first
|
||||
// header value, or None if the header doesn't exist.
|
||||
// The header name is automatically canonicalized using MIME header key rules.
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The header name (will be canonicalized)
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[http.Header, Option[string]] focusing on the first header value
|
||||
//
|
||||
// Example:
|
||||
// lens := AtValue("Authorization")
|
||||
// token := lens.Get(headers) // Returns Option[string]
|
||||
//
|
||||
// // Set a header value
|
||||
// newHeaders := lens.Set(O.Some("Bearer token"))(headers)
|
||||
//
|
||||
// // Remove a header
|
||||
// newHeaders := lens.Set(O.None[string]())(headers)
|
||||
AtValue = F.Flow2(
|
||||
AtValues,
|
||||
composeHead,
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/eq"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
LT "github.com/IBM/fp-go/v2/optics/lens/testing"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
RG "github.com/IBM/fp-go/v2/record/generic"
|
||||
@@ -56,3 +57,281 @@ func TestLaws(t *testing.T) {
|
||||
assert.True(t, fieldLaws(v1, s1))
|
||||
assert.True(t, fieldLaws(v2, s1))
|
||||
}
|
||||
|
||||
// TestMonoidEmpty tests the Monoid empty (identity) element
|
||||
func TestMonoidEmpty(t *testing.T) {
|
||||
empty := Monoid.Empty()
|
||||
assert.NotNil(t, empty)
|
||||
assert.Equal(t, 0, len(empty))
|
||||
}
|
||||
|
||||
// TestMonoidConcat tests concatenating two header maps
|
||||
func TestMonoidConcat(t *testing.T) {
|
||||
h1 := make(http.Header)
|
||||
h1.Set("X-Custom-1", "value1")
|
||||
h1.Set("Authorization", "Bearer token1")
|
||||
|
||||
h2 := make(http.Header)
|
||||
h2.Set("X-Custom-2", "value2")
|
||||
h2.Set("Content-Type", "application/json")
|
||||
|
||||
result := Monoid.Concat(h1, h2)
|
||||
|
||||
assert.Equal(t, "value1", result.Get("X-Custom-1"))
|
||||
assert.Equal(t, "value2", result.Get("X-Custom-2"))
|
||||
assert.Equal(t, "Bearer token1", result.Get("Authorization"))
|
||||
assert.Equal(t, "application/json", result.Get("Content-Type"))
|
||||
}
|
||||
|
||||
// TestMonoidConcatWithOverlap tests concatenating headers with overlapping keys
|
||||
func TestMonoidConcatWithOverlap(t *testing.T) {
|
||||
h1 := make(http.Header)
|
||||
h1.Set("X-Custom", "value1")
|
||||
|
||||
h2 := make(http.Header)
|
||||
h2.Add("X-Custom", "value2")
|
||||
|
||||
result := Monoid.Concat(h1, h2)
|
||||
|
||||
// Both values should be present
|
||||
values := result.Values("X-Custom")
|
||||
assert.Contains(t, values, "value1")
|
||||
assert.Contains(t, values, "value2")
|
||||
}
|
||||
|
||||
// TestMonoidIdentity tests that concatenating with empty is identity
|
||||
func TestMonoidIdentity(t *testing.T) {
|
||||
h := make(http.Header)
|
||||
h.Set("X-Test", "value")
|
||||
|
||||
empty := Monoid.Empty()
|
||||
|
||||
// Left identity: empty + h = h
|
||||
leftResult := Monoid.Concat(empty, h)
|
||||
assert.Equal(t, "value", leftResult.Get("X-Test"))
|
||||
|
||||
// Right identity: h + empty = h
|
||||
rightResult := Monoid.Concat(h, empty)
|
||||
assert.Equal(t, "value", rightResult.Get("X-Test"))
|
||||
}
|
||||
|
||||
// TestAtValuesGet tests getting header values using AtValues lens
|
||||
func TestAtValuesGet(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Add("Accept", "application/json")
|
||||
headers.Add("Accept", "text/html")
|
||||
|
||||
// Get Content-Type values
|
||||
ctLens := AtValues("Content-Type")
|
||||
ctValuesOpt := ctLens.Get(headers)
|
||||
assert.True(t, O.IsSome(ctValuesOpt))
|
||||
ctValues := O.GetOrElse(F.Constant([]string{}))(ctValuesOpt)
|
||||
assert.Equal(t, []string{"application/json"}, ctValues)
|
||||
|
||||
// Get Accept values (multiple)
|
||||
acceptLens := AtValues("Accept")
|
||||
acceptValuesOpt := acceptLens.Get(headers)
|
||||
assert.True(t, O.IsSome(acceptValuesOpt))
|
||||
acceptValues := O.GetOrElse(F.Constant([]string{}))(acceptValuesOpt)
|
||||
assert.Equal(t, 2, len(acceptValues))
|
||||
assert.Contains(t, acceptValues, "application/json")
|
||||
assert.Contains(t, acceptValues, "text/html")
|
||||
}
|
||||
|
||||
// TestAtValuesSet tests setting header values using AtValues lens
|
||||
func TestAtValuesSet(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("X-Old", "old-value")
|
||||
|
||||
lens := AtValues("Content-Type")
|
||||
newHeaders := lens.Set(O.Some([]string{"application/json", "text/plain"}))(headers)
|
||||
|
||||
// New header should be set
|
||||
values := newHeaders.Values("Content-Type")
|
||||
assert.Equal(t, 2, len(values))
|
||||
assert.Contains(t, values, "application/json")
|
||||
assert.Contains(t, values, "text/plain")
|
||||
|
||||
// Old header should still exist
|
||||
assert.Equal(t, "old-value", newHeaders.Get("X-Old"))
|
||||
}
|
||||
|
||||
// TestAtValuesCanonical tests that header names are canonicalized
|
||||
func TestAtValuesCanonical(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("content-type", "application/json")
|
||||
|
||||
// Access with different casing
|
||||
lens := AtValues("Content-Type")
|
||||
valuesOpt := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsSome(valuesOpt))
|
||||
values := O.GetOrElse(F.Constant([]string{}))(valuesOpt)
|
||||
assert.Equal(t, []string{"application/json"}, values)
|
||||
}
|
||||
|
||||
// TestAtValueGet tests getting first header value using AtValue lens
|
||||
func TestAtValueGet(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("Authorization", "Bearer token123")
|
||||
|
||||
lens := AtValue("Authorization")
|
||||
value := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsSome(value))
|
||||
token := O.GetOrElse(F.Constant(""))(value)
|
||||
assert.Equal(t, "Bearer token123", token)
|
||||
}
|
||||
|
||||
// TestAtValueGetNone tests getting non-existent header returns None
|
||||
func TestAtValueGetNone(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
|
||||
lens := AtValue("X-Non-Existent")
|
||||
value := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsNone(value))
|
||||
}
|
||||
|
||||
// TestAtValueSet tests setting header value using AtValue lens
|
||||
func TestAtValueSet(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
|
||||
lens := AtValue("Content-Type")
|
||||
newHeaders := lens.Set(O.Some("application/json"))(headers)
|
||||
|
||||
value := lens.Get(newHeaders)
|
||||
assert.True(t, O.IsSome(value))
|
||||
|
||||
ct := O.GetOrElse(F.Constant(""))(value)
|
||||
assert.Equal(t, "application/json", ct)
|
||||
}
|
||||
|
||||
// TestAtValueSetNone tests removing header using AtValue lens
|
||||
func TestAtValueSetNone(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set("X-Custom", "value")
|
||||
|
||||
lens := AtValue("X-Custom")
|
||||
newHeaders := lens.Set(O.None[string]())(headers)
|
||||
|
||||
value := lens.Get(newHeaders)
|
||||
assert.True(t, O.IsNone(value))
|
||||
}
|
||||
|
||||
// TestAtValueMultipleValues tests AtValue with multiple header values
|
||||
func TestAtValueMultipleValues(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Add("Accept", "application/json")
|
||||
headers.Add("Accept", "text/html")
|
||||
|
||||
lens := AtValue("Accept")
|
||||
value := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsSome(value))
|
||||
// Should get the first value
|
||||
first := O.GetOrElse(F.Constant(""))(value)
|
||||
assert.Equal(t, "application/json", first)
|
||||
}
|
||||
|
||||
// TestHeaderConstants tests that header constants are correct
|
||||
func TestHeaderConstants(t *testing.T) {
|
||||
assert.Equal(t, "Accept", Accept)
|
||||
assert.Equal(t, "Authorization", Authorization)
|
||||
assert.Equal(t, "Content-Type", ContentType)
|
||||
assert.Equal(t, "Content-Length", ContentLength)
|
||||
}
|
||||
|
||||
// TestHeaderConstantsUsage tests using header constants with http.Header
|
||||
func TestHeaderConstantsUsage(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
|
||||
headers.Set(Accept, "application/json")
|
||||
headers.Set(Authorization, "Bearer token")
|
||||
headers.Set(ContentType, "application/json")
|
||||
headers.Set(ContentLength, "1234")
|
||||
|
||||
assert.Equal(t, "application/json", headers.Get(Accept))
|
||||
assert.Equal(t, "Bearer token", headers.Get(Authorization))
|
||||
assert.Equal(t, "application/json", headers.Get(ContentType))
|
||||
assert.Equal(t, "1234", headers.Get(ContentLength))
|
||||
}
|
||||
|
||||
// TestAtValueWithConstants tests using AtValue with header constants
|
||||
func TestAtValueWithConstants(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Set(ContentType, "application/json")
|
||||
|
||||
lens := AtValue(ContentType)
|
||||
value := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsSome(value))
|
||||
ct := O.GetOrElse(F.Constant(""))(value)
|
||||
assert.Equal(t, "application/json", ct)
|
||||
}
|
||||
|
||||
// TestMonoidAssociativity tests that Monoid concatenation is associative
|
||||
func TestMonoidAssociativity(t *testing.T) {
|
||||
h1 := make(http.Header)
|
||||
h1.Set("X-1", "value1")
|
||||
|
||||
h2 := make(http.Header)
|
||||
h2.Set("X-2", "value2")
|
||||
|
||||
h3 := make(http.Header)
|
||||
h3.Set("X-3", "value3")
|
||||
|
||||
// (h1 + h2) + h3
|
||||
left := Monoid.Concat(Monoid.Concat(h1, h2), h3)
|
||||
|
||||
// h1 + (h2 + h3)
|
||||
right := Monoid.Concat(h1, Monoid.Concat(h2, h3))
|
||||
|
||||
// Both should have all three headers
|
||||
assert.Equal(t, "value1", left.Get("X-1"))
|
||||
assert.Equal(t, "value2", left.Get("X-2"))
|
||||
assert.Equal(t, "value3", left.Get("X-3"))
|
||||
|
||||
assert.Equal(t, "value1", right.Get("X-1"))
|
||||
assert.Equal(t, "value2", right.Get("X-2"))
|
||||
assert.Equal(t, "value3", right.Get("X-3"))
|
||||
}
|
||||
|
||||
// TestAtValuesEmptyHeader tests AtValues with empty headers
|
||||
func TestAtValuesEmptyHeader(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
|
||||
lens := AtValues("X-Non-Existent")
|
||||
valuesOpt := lens.Get(headers)
|
||||
|
||||
assert.True(t, O.IsNone(valuesOpt))
|
||||
}
|
||||
|
||||
// TestComplexHeaderOperations tests complex operations combining lenses and monoid
|
||||
func TestComplexHeaderOperations(t *testing.T) {
|
||||
// Create initial headers
|
||||
h1 := make(http.Header)
|
||||
h1.Set("X-Initial", "initial")
|
||||
|
||||
// Use lens to add Content-Type
|
||||
ctLens := AtValue(ContentType)
|
||||
h2 := ctLens.Set(O.Some("application/json"))(h1)
|
||||
|
||||
// Use lens to add Authorization
|
||||
authLens := AtValue(Authorization)
|
||||
h3 := authLens.Set(O.Some("Bearer token"))(h2)
|
||||
|
||||
// Create additional headers
|
||||
h4 := make(http.Header)
|
||||
h4.Set("X-Additional", "additional")
|
||||
|
||||
// Combine using Monoid
|
||||
final := Monoid.Concat(h3, h4)
|
||||
|
||||
// Verify all headers are present
|
||||
assert.Equal(t, "initial", final.Get("X-Initial"))
|
||||
assert.Equal(t, "application/json", final.Get(ContentType))
|
||||
assert.Equal(t, "Bearer token", final.Get(Authorization))
|
||||
assert.Equal(t, "additional", final.Get("X-Additional"))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,53 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package http provides functional programming utilities for working with HTTP
|
||||
// requests and responses. It offers type-safe abstractions, validation functions,
|
||||
// and utilities for handling HTTP operations in a functional style.
|
||||
//
|
||||
// The package includes:
|
||||
// - Type definitions for HTTP responses with bodies
|
||||
// - Validation functions for HTTP responses
|
||||
// - JSON content type validation
|
||||
// - Error handling with detailed HTTP error information
|
||||
// - Functional utilities for accessing response components
|
||||
//
|
||||
// Types:
|
||||
//
|
||||
// FullResponse represents a complete HTTP response including both the response
|
||||
// object and the body as a byte array. It's implemented as a Pair for functional
|
||||
// composition:
|
||||
//
|
||||
// type FullResponse = Pair[*http.Response, []byte]
|
||||
//
|
||||
// The Response and Body functions provide lens-like access to the components:
|
||||
//
|
||||
// resp := Response(fullResponse) // Get *http.Response
|
||||
// body := Body(fullResponse) // Get []byte
|
||||
//
|
||||
// Validation:
|
||||
//
|
||||
// ValidateResponse checks if an HTTP response has a successful status code (2xx):
|
||||
//
|
||||
// result := ValidateResponse(response)
|
||||
// // Returns Either[error, *http.Response]
|
||||
//
|
||||
// ValidateJSONResponse validates both the status code and Content-Type header:
|
||||
//
|
||||
// result := ValidateJSONResponse(response)
|
||||
// // Returns Either[error, *http.Response]
|
||||
//
|
||||
// Error Handling:
|
||||
//
|
||||
// HttpError provides detailed information about HTTP failures:
|
||||
//
|
||||
// err := StatusCodeError(response)
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// code := httpErr.StatusCode()
|
||||
// headers := httpErr.Headers()
|
||||
// body := httpErr.Body()
|
||||
// url := httpErr.URL()
|
||||
// }
|
||||
package http
|
||||
|
||||
import (
|
||||
@@ -22,11 +69,38 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// FullResponse represents a full http response, including headers and body
|
||||
// FullResponse represents a complete HTTP response including both the
|
||||
// *http.Response object and the response body as a byte slice.
|
||||
//
|
||||
// It's implemented as a Pair to enable functional composition and
|
||||
// transformation of HTTP responses. This allows you to work with both
|
||||
// the response metadata (status, headers) and body content together.
|
||||
//
|
||||
// Example:
|
||||
// fullResp := MakePair(response, bodyBytes)
|
||||
// resp := Response(fullResp) // Extract *http.Response
|
||||
// body := Body(fullResp) // Extract []byte
|
||||
FullResponse = P.Pair[*H.Response, []byte]
|
||||
)
|
||||
|
||||
var (
|
||||
// Response is a lens-like accessor that extracts the *http.Response
|
||||
// from a FullResponse. It provides functional access to the response
|
||||
// metadata including status code, headers, and other HTTP response fields.
|
||||
//
|
||||
// Example:
|
||||
// fullResp := MakePair(response, bodyBytes)
|
||||
// resp := Response(fullResp)
|
||||
// statusCode := resp.StatusCode
|
||||
Response = P.Head[*H.Response, []byte]
|
||||
Body = P.Tail[*H.Response, []byte]
|
||||
|
||||
// Body is a lens-like accessor that extracts the response body bytes
|
||||
// from a FullResponse. It provides functional access to the raw body
|
||||
// content without needing to read from an io.Reader.
|
||||
//
|
||||
// Example:
|
||||
// fullResp := MakePair(response, bodyBytes)
|
||||
// body := Body(fullResp)
|
||||
// content := string(body)
|
||||
Body = P.Tail[*H.Response, []byte]
|
||||
)
|
||||
|
||||
185
v2/http/utils.go
185
v2/http/utils.go
@@ -33,8 +33,29 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// ParsedMediaType represents a parsed MIME media type as a Pair.
|
||||
// The first element is the media type string (e.g., "application/json"),
|
||||
// and the second element is a map of parameters (e.g., {"charset": "utf-8"}).
|
||||
//
|
||||
// Example:
|
||||
// parsed := ParseMediaType("application/json; charset=utf-8")
|
||||
// mediaType := P.Head(parsed) // "application/json"
|
||||
// params := P.Tail(parsed) // map[string]string{"charset": "utf-8"}
|
||||
ParsedMediaType = P.Pair[string, map[string]string]
|
||||
|
||||
// HttpError represents an HTTP error with detailed information about
|
||||
// the failed request. It includes the status code, response headers,
|
||||
// response body, and the URL that was accessed.
|
||||
//
|
||||
// This error type is created by StatusCodeError when an HTTP response
|
||||
// has a non-successful status code (not 2xx).
|
||||
//
|
||||
// Example:
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// fmt.Printf("Status: %d\n", httpErr.StatusCode())
|
||||
// fmt.Printf("URL: %s\n", httpErr.URL())
|
||||
// fmt.Printf("Body: %s\n", string(httpErr.Body()))
|
||||
// }
|
||||
HttpError struct {
|
||||
statusCode int
|
||||
headers H.Header
|
||||
@@ -44,11 +65,28 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
// mime type to check if a media type matches
|
||||
// isJSONMimeType is a regex matcher that checks if a media type is a valid JSON type.
|
||||
// It matches "application/json" and variants like "application/vnd.api+json".
|
||||
isJSONMimeType = regexp.MustCompile(`application/(?:\w+\+)?json`).MatchString
|
||||
// ValidateResponse validates an HTTP response and returns an [E.Either] if the response is not a success
|
||||
|
||||
// ValidateResponse validates an HTTP response and returns an Either.
|
||||
// It checks if the response has a successful status code (2xx range).
|
||||
//
|
||||
// Returns:
|
||||
// - Right(*http.Response) if status code is 2xx
|
||||
// - Left(error) with HttpError if status code is not 2xx
|
||||
//
|
||||
// Example:
|
||||
// result := ValidateResponse(response)
|
||||
// E.Fold(
|
||||
// func(err error) { /* handle error */ },
|
||||
// func(resp *http.Response) { /* handle success */ },
|
||||
// )(result)
|
||||
ValidateResponse = E.FromPredicate(isValidStatus, StatusCodeError)
|
||||
// alidateJsonContentTypeString parses a content type a validates that it is valid JSON
|
||||
|
||||
// validateJSONContentTypeString parses a content type string and validates
|
||||
// that it represents a valid JSON media type. This is an internal helper
|
||||
// used by ValidateJSONResponse.
|
||||
validateJSONContentTypeString = F.Flow2(
|
||||
ParseMediaType,
|
||||
E.ChainFirst(F.Flow2(
|
||||
@@ -56,7 +94,21 @@ var (
|
||||
E.FromPredicate(isJSONMimeType, errors.OnSome[string]("mimetype [%s] is not a valid JSON content type")),
|
||||
)),
|
||||
)
|
||||
// ValidateJSONResponse checks if an HTTP response is a valid JSON response
|
||||
|
||||
// ValidateJSONResponse validates that an HTTP response is a valid JSON response.
|
||||
// It checks both the status code (must be 2xx) and the Content-Type header
|
||||
// (must be a JSON media type like "application/json").
|
||||
//
|
||||
// Returns:
|
||||
// - Right(*http.Response) if response is valid JSON with 2xx status
|
||||
// - Left(error) if status is not 2xx or Content-Type is not JSON
|
||||
//
|
||||
// Example:
|
||||
// result := ValidateJSONResponse(response)
|
||||
// E.Fold(
|
||||
// func(err error) { /* handle non-JSON or error response */ },
|
||||
// func(resp *http.Response) { /* handle valid JSON response */ },
|
||||
// )(result)
|
||||
ValidateJSONResponse = F.Flow2(
|
||||
E.Of[error, *H.Response],
|
||||
E.ChainFirst(F.Flow5(
|
||||
@@ -66,60 +118,175 @@ var (
|
||||
E.FromOption[string](errors.OnNone("unable to access the [%s] header", HeaderContentType)),
|
||||
E.ChainFirst(validateJSONContentTypeString),
|
||||
)))
|
||||
// ValidateJsonResponse checks if an HTTP response is a valid JSON response
|
||||
|
||||
// ValidateJsonResponse checks if an HTTP response is a valid JSON response.
|
||||
//
|
||||
// Deprecated: use [ValidateJSONResponse] instead
|
||||
// Deprecated: use ValidateJSONResponse instead (note the capitalization).
|
||||
ValidateJsonResponse = ValidateJSONResponse
|
||||
)
|
||||
|
||||
const (
|
||||
// HeaderContentType is the standard HTTP Content-Type header name.
|
||||
// It indicates the media type of the resource or data being sent.
|
||||
//
|
||||
// Example values:
|
||||
// - "application/json"
|
||||
// - "text/html; charset=utf-8"
|
||||
// - "application/xml"
|
||||
HeaderContentType = "Content-Type"
|
||||
)
|
||||
|
||||
// ParseMediaType parses a media type into a tuple
|
||||
// ParseMediaType parses a MIME media type string into its components.
|
||||
// It returns a ParsedMediaType (Pair) containing the media type and its parameters.
|
||||
//
|
||||
// Parameters:
|
||||
// - mediaType: A media type string (e.g., "application/json; charset=utf-8")
|
||||
//
|
||||
// Returns:
|
||||
// - Right(ParsedMediaType) with the parsed media type and parameters
|
||||
// - Left(error) if the media type string is invalid
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := ParseMediaType("application/json; charset=utf-8")
|
||||
// E.Map(func(parsed ParsedMediaType) {
|
||||
// mediaType := P.Head(parsed) // "application/json"
|
||||
// params := P.Tail(parsed) // map[string]string{"charset": "utf-8"}
|
||||
// })(result)
|
||||
func ParseMediaType(mediaType string) E.Either[error, ParsedMediaType] {
|
||||
m, p, err := mime.ParseMediaType(mediaType)
|
||||
return E.TryCatchError(P.MakePair(m, p), err)
|
||||
}
|
||||
|
||||
// Error fulfills the error interface
|
||||
// Error implements the error interface for HttpError.
|
||||
// It returns a formatted error message including the status code and URL.
|
||||
func (r *HttpError) Error() string {
|
||||
return fmt.Sprintf("invalid status code [%d] when accessing URL [%s]", r.statusCode, r.url)
|
||||
}
|
||||
|
||||
// String returns the string representation of the HttpError.
|
||||
// It's equivalent to calling Error().
|
||||
func (r *HttpError) String() string {
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// StatusCode returns the HTTP status code from the failed response.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// code := httpErr.StatusCode() // e.g., 404, 500
|
||||
// }
|
||||
func (r *HttpError) StatusCode() int {
|
||||
return r.statusCode
|
||||
}
|
||||
|
||||
// Headers returns a clone of the HTTP headers from the failed response.
|
||||
// The headers are cloned to prevent modification of the original response.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// headers := httpErr.Headers()
|
||||
// contentType := headers.Get("Content-Type")
|
||||
// }
|
||||
func (r *HttpError) Headers() H.Header {
|
||||
return r.headers
|
||||
}
|
||||
|
||||
// URL returns the URL that was accessed when the error occurred.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// url := httpErr.URL()
|
||||
// fmt.Printf("Failed to access: %s\n", url)
|
||||
// }
|
||||
func (r *HttpError) URL() *url.URL {
|
||||
return r.url
|
||||
}
|
||||
|
||||
// Body returns the response body bytes from the failed response.
|
||||
// This can be useful for debugging or displaying error messages from the server.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// if httpErr, ok := err.(*HttpError); ok {
|
||||
// body := httpErr.Body()
|
||||
// fmt.Printf("Error response: %s\n", string(body))
|
||||
// }
|
||||
func (r *HttpError) Body() []byte {
|
||||
return r.body
|
||||
}
|
||||
|
||||
// GetHeader extracts the HTTP headers from an http.Response.
|
||||
// This is a functional accessor for the Header field.
|
||||
//
|
||||
// Parameters:
|
||||
// - resp: The HTTP response
|
||||
//
|
||||
// Returns:
|
||||
// - The http.Header map from the response
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// headers := GetHeader(response)
|
||||
// contentType := headers.Get("Content-Type")
|
||||
func GetHeader(resp *H.Response) H.Header {
|
||||
return resp.Header
|
||||
}
|
||||
|
||||
// GetBody extracts the response body reader from an http.Response.
|
||||
// This is a functional accessor for the Body field.
|
||||
//
|
||||
// Parameters:
|
||||
// - resp: The HTTP response
|
||||
//
|
||||
// Returns:
|
||||
// - The io.ReadCloser for reading the response body
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// body := GetBody(response)
|
||||
// defer body.Close()
|
||||
// data, err := io.ReadAll(body)
|
||||
func GetBody(resp *H.Response) io.ReadCloser {
|
||||
return resp.Body
|
||||
}
|
||||
|
||||
// isValidStatus checks if an HTTP response has a successful status code.
|
||||
// A status code is considered valid if it's in the 2xx range (200-299).
|
||||
//
|
||||
// Parameters:
|
||||
// - resp: The HTTP response to check
|
||||
//
|
||||
// Returns:
|
||||
// - true if status code is 2xx, false otherwise
|
||||
func isValidStatus(resp *H.Response) bool {
|
||||
return resp.StatusCode >= H.StatusOK && resp.StatusCode < H.StatusMultipleChoices
|
||||
}
|
||||
|
||||
// StatusCodeError creates an instance of [HttpError] filled with information from the response
|
||||
// StatusCodeError creates an HttpError from an http.Response with a non-successful status code.
|
||||
// It reads the response body and captures all relevant information for debugging.
|
||||
//
|
||||
// The function:
|
||||
// - Reads and stores the response body
|
||||
// - Clones the response headers
|
||||
// - Captures the request URL
|
||||
// - Creates a comprehensive error with all this information
|
||||
//
|
||||
// Parameters:
|
||||
// - resp: The HTTP response with a non-successful status code
|
||||
//
|
||||
// Returns:
|
||||
// - An error (specifically *HttpError) with detailed information
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// if !isValidStatus(response) {
|
||||
// err := StatusCodeError(response)
|
||||
// return err
|
||||
// }
|
||||
func StatusCodeError(resp *H.Response) error {
|
||||
// read the body
|
||||
bodyRdr := GetBody(resp)
|
||||
|
||||
@@ -16,12 +16,18 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
H "net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
C "github.com/IBM/fp-go/v2/http/content"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func NoError[A any](t *testing.T) func(E.Either[error, A]) bool {
|
||||
@@ -37,21 +43,351 @@ func Error[A any](t *testing.T) func(E.Either[error, A]) bool {
|
||||
}
|
||||
|
||||
func TestValidateJsonContentTypeString(t *testing.T) {
|
||||
|
||||
res := F.Pipe1(
|
||||
validateJSONContentTypeString(C.JSON),
|
||||
NoError[ParsedMediaType](t),
|
||||
)
|
||||
|
||||
assert.True(t, res)
|
||||
}
|
||||
|
||||
func TestValidateInvalidJsonContentTypeString(t *testing.T) {
|
||||
|
||||
res := F.Pipe1(
|
||||
validateJSONContentTypeString("application/xml"),
|
||||
Error[ParsedMediaType](t),
|
||||
)
|
||||
|
||||
assert.True(t, res)
|
||||
}
|
||||
|
||||
// TestParseMediaType tests parsing valid media types
|
||||
func TestParseMediaType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mediaType string
|
||||
wantType string
|
||||
wantParam map[string]string
|
||||
}{
|
||||
{
|
||||
name: "simple JSON",
|
||||
mediaType: "application/json",
|
||||
wantType: "application/json",
|
||||
wantParam: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "JSON with charset",
|
||||
mediaType: "application/json; charset=utf-8",
|
||||
wantType: "application/json",
|
||||
wantParam: map[string]string{"charset": "utf-8"},
|
||||
},
|
||||
{
|
||||
name: "HTML with charset",
|
||||
mediaType: "text/html; charset=iso-8859-1",
|
||||
wantType: "text/html",
|
||||
wantParam: map[string]string{"charset": "iso-8859-1"},
|
||||
},
|
||||
{
|
||||
name: "multipart with boundary",
|
||||
mediaType: "multipart/form-data; boundary=----WebKitFormBoundary",
|
||||
wantType: "multipart/form-data",
|
||||
wantParam: map[string]string{"boundary": "----WebKitFormBoundary"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParseMediaType(tt.mediaType)
|
||||
require.True(t, E.IsRight(result), "ParseMediaType should succeed")
|
||||
|
||||
parsed := E.GetOrElse(func(error) ParsedMediaType {
|
||||
return P.MakePair("", map[string]string{})
|
||||
})(result)
|
||||
mediaType := P.Head(parsed)
|
||||
params := P.Tail(parsed)
|
||||
|
||||
assert.Equal(t, tt.wantType, mediaType)
|
||||
assert.Equal(t, tt.wantParam, params)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseMediaTypeInvalid tests parsing invalid media types
|
||||
func TestParseMediaTypeInvalid(t *testing.T) {
|
||||
result := ParseMediaType("invalid media type")
|
||||
assert.True(t, E.IsLeft(result), "ParseMediaType should fail for invalid input")
|
||||
}
|
||||
|
||||
// TestHttpErrorMethods tests all HttpError methods
|
||||
func TestHttpErrorMethods(t *testing.T) {
|
||||
testURL, _ := url.Parse("https://example.com/api/test")
|
||||
headers := make(H.Header)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Set("X-Custom", "value")
|
||||
body := []byte(`{"error": "not found"}`)
|
||||
|
||||
httpErr := &HttpError{
|
||||
statusCode: 404,
|
||||
headers: headers,
|
||||
body: body,
|
||||
url: testURL,
|
||||
}
|
||||
|
||||
// Test StatusCode
|
||||
assert.Equal(t, 404, httpErr.StatusCode())
|
||||
|
||||
// Test Headers
|
||||
returnedHeaders := httpErr.Headers()
|
||||
assert.Equal(t, "application/json", returnedHeaders.Get("Content-Type"))
|
||||
assert.Equal(t, "value", returnedHeaders.Get("X-Custom"))
|
||||
|
||||
// Test Body
|
||||
assert.Equal(t, body, httpErr.Body())
|
||||
assert.Equal(t, `{"error": "not found"}`, string(httpErr.Body()))
|
||||
|
||||
// Test URL
|
||||
assert.Equal(t, testURL, httpErr.URL())
|
||||
assert.Equal(t, "https://example.com/api/test", httpErr.URL().String())
|
||||
|
||||
// Test Error
|
||||
errMsg := httpErr.Error()
|
||||
assert.Contains(t, errMsg, "404")
|
||||
assert.Contains(t, errMsg, "https://example.com/api/test")
|
||||
|
||||
// Test String
|
||||
assert.Equal(t, errMsg, httpErr.String())
|
||||
}
|
||||
|
||||
// TestGetHeader tests the GetHeader function
|
||||
func TestGetHeader(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
resp.Header.Set("Authorization", "Bearer token")
|
||||
|
||||
headers := GetHeader(resp)
|
||||
assert.Equal(t, "application/json", headers.Get("Content-Type"))
|
||||
assert.Equal(t, "Bearer token", headers.Get("Authorization"))
|
||||
}
|
||||
|
||||
// TestGetBody tests the GetBody function
|
||||
func TestGetBody(t *testing.T) {
|
||||
bodyContent := []byte("test body content")
|
||||
resp := &H.Response{
|
||||
Body: io.NopCloser(bytes.NewReader(bodyContent)),
|
||||
}
|
||||
|
||||
body := GetBody(resp)
|
||||
defer body.Close()
|
||||
|
||||
data, err := io.ReadAll(body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, bodyContent, data)
|
||||
}
|
||||
|
||||
// TestIsValidStatus tests the isValidStatus function
|
||||
func TestIsValidStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
want bool
|
||||
}{
|
||||
{"200 OK", H.StatusOK, true},
|
||||
{"201 Created", H.StatusCreated, true},
|
||||
{"204 No Content", H.StatusNoContent, true},
|
||||
{"299 (edge of 2xx)", 299, true},
|
||||
{"300 Multiple Choices", H.StatusMultipleChoices, false},
|
||||
{"301 Moved Permanently", H.StatusMovedPermanently, false},
|
||||
{"400 Bad Request", H.StatusBadRequest, false},
|
||||
{"404 Not Found", H.StatusNotFound, false},
|
||||
{"500 Internal Server Error", H.StatusInternalServerError, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resp := &H.Response{StatusCode: tt.statusCode}
|
||||
assert.Equal(t, tt.want, isValidStatus(resp))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateResponse tests the ValidateResponse function
|
||||
func TestValidateResponse(t *testing.T) {
|
||||
t.Run("successful response", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
|
||||
result := ValidateResponse(resp)
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
validResp := E.GetOrElse(func(error) *H.Response { return nil })(result)
|
||||
assert.Equal(t, resp, validResp)
|
||||
})
|
||||
|
||||
t.Run("error response", func(t *testing.T) {
|
||||
testURL, _ := url.Parse("https://example.com/test")
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusNotFound,
|
||||
Header: make(H.Header),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte("not found"))),
|
||||
Request: &H.Request{URL: testURL},
|
||||
}
|
||||
|
||||
result := ValidateResponse(resp)
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
// Extract error using Fold
|
||||
var httpErr *HttpError
|
||||
E.Fold(
|
||||
func(err error) *H.Response {
|
||||
var ok bool
|
||||
httpErr, ok = err.(*HttpError)
|
||||
require.True(t, ok, "error should be *HttpError")
|
||||
return nil
|
||||
},
|
||||
func(r *H.Response) *H.Response { return r },
|
||||
)(result)
|
||||
assert.Equal(t, 404, httpErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
// TestStatusCodeError tests the StatusCodeError function
|
||||
func TestStatusCodeError(t *testing.T) {
|
||||
testURL, _ := url.Parse("https://api.example.com/users/123")
|
||||
bodyContent := []byte(`{"error": "user not found"}`)
|
||||
|
||||
headers := make(H.Header)
|
||||
headers.Set("Content-Type", "application/json")
|
||||
headers.Set("X-Request-ID", "abc123")
|
||||
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusNotFound,
|
||||
Header: headers,
|
||||
Body: io.NopCloser(bytes.NewReader(bodyContent)),
|
||||
Request: &H.Request{URL: testURL},
|
||||
}
|
||||
|
||||
err := StatusCodeError(resp)
|
||||
require.Error(t, err)
|
||||
|
||||
httpErr, ok := err.(*HttpError)
|
||||
require.True(t, ok, "error should be *HttpError")
|
||||
|
||||
// Verify all fields
|
||||
assert.Equal(t, 404, httpErr.StatusCode())
|
||||
assert.Equal(t, testURL, httpErr.URL())
|
||||
assert.Equal(t, bodyContent, httpErr.Body())
|
||||
|
||||
// Verify headers are cloned
|
||||
returnedHeaders := httpErr.Headers()
|
||||
assert.Equal(t, "application/json", returnedHeaders.Get("Content-Type"))
|
||||
assert.Equal(t, "abc123", returnedHeaders.Get("X-Request-ID"))
|
||||
|
||||
// Verify error message
|
||||
errMsg := httpErr.Error()
|
||||
assert.Contains(t, errMsg, "404")
|
||||
assert.Contains(t, errMsg, "https://api.example.com/users/123")
|
||||
}
|
||||
|
||||
// TestValidateJSONResponse tests the ValidateJSONResponse function
|
||||
func TestValidateJSONResponse(t *testing.T) {
|
||||
t.Run("valid JSON response", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
assert.True(t, E.IsRight(result), "should accept valid JSON response")
|
||||
})
|
||||
|
||||
t.Run("JSON with charset", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
assert.True(t, E.IsRight(result), "should accept JSON with charset")
|
||||
})
|
||||
|
||||
t.Run("JSON variant (hal+json)", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/hal+json")
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
assert.True(t, E.IsRight(result), "should accept JSON variants")
|
||||
})
|
||||
|
||||
t.Run("non-JSON content type", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "text/html")
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
assert.True(t, E.IsLeft(result), "should reject non-JSON content type")
|
||||
})
|
||||
|
||||
t.Run("missing Content-Type header", func(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
assert.True(t, E.IsLeft(result), "should reject missing Content-Type")
|
||||
})
|
||||
|
||||
t.Run("valid JSON with error status code", func(t *testing.T) {
|
||||
// Note: ValidateJSONResponse only validates Content-Type, not status code
|
||||
// It wraps the response in Right(response) first, then validates headers
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusInternalServerError,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result := ValidateJSONResponse(resp)
|
||||
// This actually succeeds because ValidateJSONResponse doesn't check status
|
||||
assert.True(t, E.IsRight(result), "ValidateJSONResponse only checks Content-Type, not status")
|
||||
})
|
||||
}
|
||||
|
||||
// TestFullResponseAccessors tests Response and Body accessors
|
||||
func TestFullResponseAccessors(t *testing.T) {
|
||||
resp := &H.Response{
|
||||
StatusCode: H.StatusOK,
|
||||
Header: make(H.Header),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
bodyContent := []byte(`{"message": "success"}`)
|
||||
fullResp := P.MakePair(resp, bodyContent)
|
||||
|
||||
// Test Response accessor
|
||||
extractedResp := Response(fullResp)
|
||||
assert.Equal(t, resp, extractedResp)
|
||||
assert.Equal(t, H.StatusOK, extractedResp.StatusCode)
|
||||
|
||||
// Test Body accessor
|
||||
extractedBody := Body(fullResp)
|
||||
assert.Equal(t, bodyContent, extractedBody)
|
||||
assert.Equal(t, `{"message": "success"}`, string(extractedBody))
|
||||
}
|
||||
|
||||
// TestHeaderContentTypeConstant tests the HeaderContentType constant
|
||||
func TestHeaderContentTypeConstant(t *testing.T) {
|
||||
assert.Equal(t, "Content-Type", HeaderContentType)
|
||||
|
||||
// Test usage with http.Header
|
||||
headers := make(H.Header)
|
||||
headers.Set(HeaderContentType, "application/json")
|
||||
assert.Equal(t, "application/json", headers.Get(HeaderContentType))
|
||||
}
|
||||
|
||||
@@ -21,14 +21,55 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
// result := identity.Do(State{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) S {
|
||||
return empty
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// identity.Do(State{}),
|
||||
// identity.Bind(
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// func(s State) int {
|
||||
// return 42
|
||||
// },
|
||||
// ),
|
||||
// identity.Bind(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) int {
|
||||
// // This can access s.X from the previous step
|
||||
// return s.X * 2
|
||||
// },
|
||||
// ),
|
||||
// ) // State{X: 42, Y: 84}
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
@@ -75,7 +116,36 @@ func BindTo[S1, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// result := F.Pipe2(
|
||||
// identity.Do(State{}),
|
||||
// identity.ApS(
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// 42,
|
||||
// ),
|
||||
// identity.ApS(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// 100,
|
||||
// ),
|
||||
// ) // State{X: 42, Y: 100}
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa T,
|
||||
|
||||
@@ -49,19 +49,19 @@ func Of[A any](a A) A {
|
||||
return a
|
||||
}
|
||||
|
||||
func MonadChain[A, B any](ma A, f func(A) B) B {
|
||||
func MonadChain[A, B any](ma A, f Kleisli[A, B]) B {
|
||||
return f(ma)
|
||||
}
|
||||
|
||||
func Chain[A, B any](f func(A) B) Operator[A, B] {
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return f
|
||||
}
|
||||
|
||||
func MonadChainFirst[A, B any](fa A, f func(A) B) A {
|
||||
func MonadChainFirst[A, B any](fa A, f Kleisli[A, B]) A {
|
||||
return chain.MonadChainFirst(MonadChain[A, A], MonadMap[B, A], fa, f)
|
||||
}
|
||||
|
||||
func ChainFirst[A, B any](f func(A) B) Operator[A, A] {
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return chain.ChainFirst(Chain[A, A], Map[B, A], f)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,5 +16,6 @@
|
||||
package identity
|
||||
|
||||
type (
|
||||
Operator[A, B any] = func(A) B
|
||||
Kleisli[A, B any] = func(A) B
|
||||
Operator[A, B any] = Kleisli[A, B]
|
||||
)
|
||||
|
||||
136
v2/io/bind.go
136
v2/io/bind.go
@@ -19,6 +19,7 @@ import (
|
||||
INTA "github.com/IBM/fp-go/v2/internal/apply"
|
||||
INTC "github.com/IBM/fp-go/v2/internal/chain"
|
||||
INTF "github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type S to be used with the Bind operation.
|
||||
@@ -58,7 +59,7 @@ func Do[S any](
|
||||
// }, fetchUser)
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) IO[T],
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return INTC.Bind(
|
||||
Chain[S1, S2],
|
||||
@@ -152,3 +153,136 @@ func ApS[S1, S2, T any](
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// portLens := lens.MakeLens(
|
||||
// func(c Config) int { return c.Port },
|
||||
// func(c Config, p int) Config { c.Port = p; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// io.Of(Config{Host: "localhost"}),
|
||||
// io.ApSL(portLens, io.Of(8080)),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa IO[T],
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// an IO that produces the new value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter asynchronously
|
||||
// increment := func(v int) io.IO[int] {
|
||||
// return io.Of(v + 1)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// io.Of(Counter{Value: 42}),
|
||||
// io.BindL(valueLens, increment),
|
||||
// ) // IO[Counter{Value: 43}]
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return Bind[S, S, T](lens.Set, func(s S) IO[T] {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in IO).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// io.Of(Counter{Value: 21}),
|
||||
// io.LetL(valueLens, double),
|
||||
// ) // IO[Counter{Value: 42}]
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[S, S] {
|
||||
return Let[S, S, T](lens.Set, func(s S) T {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// io.Of(Config{Debug: true, Timeout: 30}),
|
||||
// io.LetToL(debugLens, false),
|
||||
// ) // IO[Config{Debug: false, Timeout: 30}]
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return LetTo[S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -54,3 +55,144 @@ func TestApS(t *testing.T) {
|
||||
|
||||
assert.Equal(t, res(), "John Doe")
|
||||
}
|
||||
|
||||
// Test types for lens-based operations
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("BindL with successful transformation", func(t *testing.T) {
|
||||
increment := func(v int) IO[int] {
|
||||
return Of(v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Of(Counter{Value: 42}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
assert.Equal(t, Counter{Value: 43}, result())
|
||||
})
|
||||
|
||||
t.Run("BindL with multiple operations", func(t *testing.T) {
|
||||
double := func(v int) IO[int] {
|
||||
return Of(v * 2)
|
||||
}
|
||||
|
||||
addTen := func(v int) IO[int] {
|
||||
return Of(v + 10)
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(Counter{Value: 5}),
|
||||
BindL(valueLens, double),
|
||||
BindL(valueLens, addTen),
|
||||
)
|
||||
|
||||
assert.Equal(t, Counter{Value: 20}, result())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("LetL with pure transformation", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe1(
|
||||
Of(Counter{Value: 21}),
|
||||
LetL(valueLens, double),
|
||||
)
|
||||
|
||||
assert.Equal(t, Counter{Value: 42}, result())
|
||||
})
|
||||
|
||||
t.Run("LetL with multiple transformations", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
addTen := func(v int) int { return v + 10 }
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(Counter{Value: 5}),
|
||||
LetL(valueLens, double),
|
||||
LetL(valueLens, addTen),
|
||||
)
|
||||
|
||||
assert.Equal(t, Counter{Value: 20}, result())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
ageLens := L.MakeLens(
|
||||
func(p Person) int { return p.Age },
|
||||
func(p Person, a int) Person { p.Age = a; return p },
|
||||
)
|
||||
|
||||
t.Run("LetToL with constant value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(Person{Name: "Alice", Age: 25}),
|
||||
LetToL(ageLens, 30),
|
||||
)
|
||||
|
||||
assert.Equal(t, Person{Name: "Alice", Age: 30}, result())
|
||||
})
|
||||
|
||||
t.Run("LetToL with multiple fields", func(t *testing.T) {
|
||||
nameLens := L.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, n string) Person { p.Name = n; return p },
|
||||
)
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(Person{Name: "Alice", Age: 25}),
|
||||
LetToL(ageLens, 30),
|
||||
LetToL(nameLens, "Bob"),
|
||||
)
|
||||
|
||||
assert.Equal(t, Person{Name: "Bob", Age: 30}, result())
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
ageLens := L.MakeLens(
|
||||
func(p Person) int { return p.Age },
|
||||
func(p Person, a int) Person { p.Age = a; return p },
|
||||
)
|
||||
|
||||
t.Run("ApSL with value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of(Person{Name: "Alice", Age: 25}),
|
||||
ApSL(ageLens, Of(30)),
|
||||
)
|
||||
|
||||
assert.Equal(t, Person{Name: "Alice", Age: 30}, result())
|
||||
})
|
||||
|
||||
t.Run("ApSL with chaining", func(t *testing.T) {
|
||||
nameLens := L.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, n string) Person { p.Name = n; return p },
|
||||
)
|
||||
|
||||
result := F.Pipe2(
|
||||
Of(Person{Name: "Alice", Age: 25}),
|
||||
ApSL(ageLens, Of(30)),
|
||||
ApSL(nameLens, Of("Bob")),
|
||||
)
|
||||
|
||||
assert.Equal(t, Person{Name: "Bob", Age: 30}, result())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
// whether the body action returns and error or not.
|
||||
func Bracket[A, B, ANY any](
|
||||
acquire IO[A],
|
||||
use func(A) IO[B],
|
||||
use Kleisli[A, B],
|
||||
release func(A, B) IO[ANY],
|
||||
) IO[B] {
|
||||
return INTB.Bracket[IO[A], IO[B], IO[ANY], B, A, B](
|
||||
|
||||
2643
v2/io/gen.go
2643
v2/io/gen.go
File diff suppressed because it is too large
Load Diff
11
v2/io/io.go
11
v2/io/io.go
@@ -44,7 +44,8 @@ type (
|
||||
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioltagt] for more details
|
||||
IO[A any] = func() A
|
||||
|
||||
Operator[A, B any] = R.Reader[IO[A], IO[B]]
|
||||
Kleisli[A, B any] = R.Reader[A, IO[B]]
|
||||
Operator[A, B any] = Kleisli[IO[A], B]
|
||||
Monoid[A any] = M.Monoid[IO[A]]
|
||||
Semigroup[A any] = S.Semigroup[IO[A]]
|
||||
)
|
||||
@@ -121,14 +122,14 @@ func MapTo[A, B any](b B) Operator[A, B] {
|
||||
}
|
||||
|
||||
// MonadChain composes computations in sequence, using the return value of one computation to determine the next computation.
|
||||
func MonadChain[A, B any](fa IO[A], f func(A) IO[B]) IO[B] {
|
||||
func MonadChain[A, B any](fa IO[A], f Kleisli[A, B]) IO[B] {
|
||||
return func() B {
|
||||
return f(fa())()
|
||||
}
|
||||
}
|
||||
|
||||
// Chain composes computations in sequence, using the return value of one computation to determine the next computation.
|
||||
func Chain[A, B any](f func(A) IO[B]) Operator[A, B] {
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return F.Bind2nd(MonadChain[A, B], f)
|
||||
}
|
||||
|
||||
@@ -201,13 +202,13 @@ func Memoize[A any](ma IO[A]) IO[A] {
|
||||
|
||||
// MonadChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
|
||||
// keeping only the result of the first.
|
||||
func MonadChainFirst[A, B any](fa IO[A], f func(A) IO[B]) IO[A] {
|
||||
func MonadChainFirst[A, B any](fa IO[A], f Kleisli[A, B]) IO[A] {
|
||||
return chain.MonadChainFirst(MonadChain[A, A], MonadMap[B, A], fa, f)
|
||||
}
|
||||
|
||||
// ChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
|
||||
// keeping only the result of the first.
|
||||
func ChainFirst[A, B any](f func(A) IO[B]) Operator[A, A] {
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return chain.ChainFirst(
|
||||
Chain[A, A],
|
||||
Map[B, A],
|
||||
|
||||
@@ -32,9 +32,9 @@ import (
|
||||
// io.ChainFirst(io.Logger[User]()("Fetched user")),
|
||||
// processUser,
|
||||
// )
|
||||
func Logger[A any](loggers ...*log.Logger) func(string) func(A) IO[any] {
|
||||
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
|
||||
_, right := L.LoggingCallbacks(loggers...)
|
||||
return func(prefix string) func(A) IO[any] {
|
||||
return func(prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
right("%s: %v", prefix, a)
|
||||
@@ -53,7 +53,7 @@ func Logger[A any](loggers ...*log.Logger) func(string) func(A) IO[any] {
|
||||
// io.ChainFirst(io.Logf[User]("User: %+v")),
|
||||
// processUser,
|
||||
// )
|
||||
func Logf[A any](prefix string) func(A) IO[any] {
|
||||
func Logf[A any](prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
log.Printf(prefix, a)
|
||||
@@ -72,7 +72,7 @@ func Logf[A any](prefix string) func(A) IO[any] {
|
||||
// io.ChainFirst(io.Printf[User]("User: %+v\n")),
|
||||
// processUser,
|
||||
// )
|
||||
func Printf[A any](prefix string) func(A) IO[any] {
|
||||
func Printf[A any](prefix string) Kleisli[A, any] {
|
||||
return func(a A) IO[any] {
|
||||
return FromImpure(func() {
|
||||
fmt.Printf(prefix, a)
|
||||
|
||||
@@ -35,7 +35,7 @@ func (o *ioMonad[A, B]) Map(f func(A) B) Operator[A, B] {
|
||||
return Map(f)
|
||||
}
|
||||
|
||||
func (o *ioMonad[A, B]) Chain(f func(A) IO[B]) Operator[A, B] {
|
||||
func (o *ioMonad[A, B]) Chain(f Kleisli[A, B]) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
// return readData(f)
|
||||
// })
|
||||
func WithResource[
|
||||
R, A, ANY any](onCreate IO[R], onRelease func(R) IO[ANY]) func(func(R) IO[A]) IO[A] {
|
||||
R, A, ANY any](onCreate IO[R], onRelease func(R) IO[ANY]) Kleisli[Kleisli[R, A], A] {
|
||||
// simply map to implementation of bracket
|
||||
return function.Bind13of3(Bracket[R, A, ANY])(onCreate, function.Ignore2of2[A](onRelease))
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ type (
|
||||
// )
|
||||
func Retrying[A any](
|
||||
policy R.RetryPolicy,
|
||||
action func(R.RetryStatus) IO[A],
|
||||
action Kleisli[R.RetryStatus, A],
|
||||
check func(A) bool,
|
||||
) IO[A] {
|
||||
// get an implementation for the types
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
// fetchUsers := func(id int) io.IO[User] { return fetchUser(id) }
|
||||
// users := io.MonadTraverseArray([]int{1, 2, 3}, fetchUsers)
|
||||
// result := users() // []User with all fetched users
|
||||
func MonadTraverseArray[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
|
||||
func MonadTraverseArray[A, B any](tas []A, f Kleisli[A, B]) IO[[]B] {
|
||||
return INTA.MonadTraverse(
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -50,7 +50,7 @@ func MonadTraverseArray[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
|
||||
// return fetchUser(id)
|
||||
// })
|
||||
// users := fetchUsers([]int{1, 2, 3})
|
||||
func TraverseArray[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return INTA.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -68,7 +68,7 @@ func TraverseArray[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
|
||||
// numbered := io.TraverseArrayWithIndex(func(i int, s string) io.IO[string] {
|
||||
// return io.Of(fmt.Sprintf("%d: %s", i, s))
|
||||
// })
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) IO[B]) func([]A) IO[[]B] {
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) IO[B]) Kleisli[[]A, []B] {
|
||||
return INTA.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -98,7 +98,7 @@ func SequenceArray[A any](tas []IO[A]) IO[[]A] {
|
||||
// fetchData := func(url string) io.IO[Data] { return fetch(url) }
|
||||
// urls := map[string]string{"a": "http://a.com", "b": "http://b.com"}
|
||||
// data := io.MonadTraverseRecord(urls, fetchData)
|
||||
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) IO[B]) IO[map[K]B] {
|
||||
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) IO[map[K]B] {
|
||||
return INTR.MonadTraverse(
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -112,7 +112,7 @@ func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) IO[B]) I
|
||||
// TraverseRecord returns a function that applies an IO-returning function to each value
|
||||
// in a map and collects the results. This is the curried version of MonadTraverseRecord.
|
||||
// Executes in parallel by default.
|
||||
func TraverseRecord[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO[map[K]B] {
|
||||
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return INTR.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -124,7 +124,7 @@ func TraverseRecord[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO[ma
|
||||
|
||||
// TraverseRecordWithIndex is like TraverseRecord but the function also receives the key.
|
||||
// Executes in parallel by default.
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) IO[B]) func(map[K]A) IO[map[K]B] {
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) IO[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return INTR.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -153,7 +153,7 @@ func SequenceRecord[K comparable, A any](tas map[K]IO[A]) IO[map[K]A] {
|
||||
//
|
||||
// fetchUsers := func(id int) io.IO[User] { return fetchUser(id) }
|
||||
// users := io.MonadTraverseArraySeq([]int{1, 2, 3}, fetchUsers)
|
||||
func MonadTraverseArraySeq[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
|
||||
func MonadTraverseArraySeq[A, B any](tas []A, f Kleisli[A, B]) IO[[]B] {
|
||||
return INTA.MonadTraverse(
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -167,7 +167,7 @@ func MonadTraverseArraySeq[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
|
||||
// TraverseArraySeq returns a function that applies an IO-returning function to each element
|
||||
// of an array and collects the results. Executes sequentially (one after another).
|
||||
// Use this when operations must be performed in order or when parallel execution is not desired.
|
||||
func TraverseArraySeq[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
|
||||
func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return INTA.Traverse[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -179,7 +179,7 @@ func TraverseArraySeq[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
|
||||
|
||||
// TraverseArrayWithIndexSeq is like TraverseArraySeq but the function also receives the index.
|
||||
// Executes sequentially (one after another).
|
||||
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) IO[B]) func([]A) IO[[]B] {
|
||||
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) IO[B]) Kleisli[[]A, []B] {
|
||||
return INTA.TraverseWithIndex[[]A](
|
||||
Of[[]B],
|
||||
Map[[]B, func(B) []B],
|
||||
@@ -197,7 +197,7 @@ func SequenceArraySeq[A any](tas []IO[A]) IO[[]A] {
|
||||
|
||||
// MonadTraverseRecordSeq applies an IO-returning function to each value in a map
|
||||
// and collects the results into an IO of a map. Executes sequentially.
|
||||
func MonadTraverseRecordSeq[K comparable, A, B any](tas map[K]A, f func(A) IO[B]) IO[map[K]B] {
|
||||
func MonadTraverseRecordSeq[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) IO[map[K]B] {
|
||||
return INTR.MonadTraverse(
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -210,7 +210,7 @@ func MonadTraverseRecordSeq[K comparable, A, B any](tas map[K]A, f func(A) IO[B]
|
||||
|
||||
// TraverseRecordSeq returns a function that applies an IO-returning function to each value
|
||||
// in a map and collects the results. Executes sequentially (one after another).
|
||||
func TraverseRecordSeq[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO[map[K]B] {
|
||||
func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return INTR.Traverse[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
@@ -223,7 +223,7 @@ func TraverseRecordSeq[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO
|
||||
// TraverseRecordWithIndeSeq is like TraverseRecordSeq but the function also receives the key.
|
||||
// Executes sequentially (one after another).
|
||||
// Note: There's a typo in the function name (Inde instead of Index) for backward compatibility.
|
||||
func TraverseRecordWithIndeSeq[K comparable, A, B any](f func(K, A) IO[B]) func(map[K]A) IO[map[K]B] {
|
||||
func TraverseRecordWithIndeSeq[K comparable, A, B any](f func(K, A) IO[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return INTR.TraverseWithIndex[map[K]A](
|
||||
Of[map[K]B],
|
||||
Map[map[K]B, func(B) map[K]B],
|
||||
|
||||
@@ -19,16 +19,62 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
// result := ioeither.Do[error](State{})
|
||||
func Do[E, S any](
|
||||
empty S,
|
||||
) IOEither[E, S] {
|
||||
return Of[E](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// ioeither.Do[error](State{}),
|
||||
// ioeither.Bind(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// func(s State) ioeither.IOEither[error, User] {
|
||||
// return ioeither.TryCatch(func() (User, error) {
|
||||
// return fetchUser()
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// ioeither.Bind(
|
||||
// func(posts []Post) func(State) State {
|
||||
// return func(s State) State { s.Posts = posts; return s }
|
||||
// },
|
||||
// func(s State) ioeither.IOEither[error, []Post] {
|
||||
// // This can access s.User from the previous step
|
||||
// return ioeither.TryCatch(func() ([]Post, error) {
|
||||
// return fetchPostsForUser(s.User.ID)
|
||||
// })
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) IOEither[E, T],
|
||||
@@ -75,7 +121,39 @@ func BindTo[E, S1, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getUser := ioeither.Right[error](User{ID: 1, Name: "Alice"})
|
||||
// getPosts := ioeither.Right[error]([]Post{{ID: 1, Title: "Hello"}})
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// ioeither.Do[error](State{}),
|
||||
// ioeither.ApS(
|
||||
// func(user User) func(State) State {
|
||||
// return func(s State) State { s.User = user; return s }
|
||||
// },
|
||||
// getUser,
|
||||
// ),
|
||||
// ioeither.ApS(
|
||||
// func(posts []Post) func(State) State {
|
||||
// return func(s State) State { s.Posts = posts; return s }
|
||||
// },
|
||||
// getPosts,
|
||||
// ),
|
||||
// )
|
||||
func ApS[E, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOEither[E, T],
|
||||
@@ -87,3 +165,139 @@ func ApS[E, S1, S2, T any](
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// portLens := lens.MakeLens(
|
||||
// func(c Config) int { return c.Port },
|
||||
// func(c Config, p int) Config { c.Port = p; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// ioeither.Of[error](Config{Host: "localhost"}),
|
||||
// ioeither.ApSL(portLens, ioeither.Of[error](8080)),
|
||||
// )
|
||||
func ApSL[E, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa IOEither[E, T],
|
||||
) Operator[E, S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// an IOEither that produces the new value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// increment := func(v int) ioeither.IOEither[error, int] {
|
||||
// return ioeither.TryCatch(func() (int, error) {
|
||||
// if v >= 100 {
|
||||
// return 0, errors.New("overflow")
|
||||
// }
|
||||
// return v + 1, nil
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// ioeither.Of[error](Counter{Value: 42}),
|
||||
// ioeither.BindL(valueLens, increment),
|
||||
// )
|
||||
func BindL[E, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) IOEither[E, T],
|
||||
) Operator[E, S, S] {
|
||||
return Bind[E, S, S, T](lens.Set, func(s S) IOEither[E, T] {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in IOEither).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// ioeither.Of[error](Counter{Value: 21}),
|
||||
// ioeither.LetL(valueLens, double),
|
||||
// )
|
||||
func LetL[E, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[E, S, S] {
|
||||
return Let[E, S, S, T](lens.Set, func(s S) T {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// ioeither.Of[error](Config{Debug: true, Timeout: 30}),
|
||||
// ioeither.LetToL(debugLens, false),
|
||||
// )
|
||||
func LetToL[E, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[E, S, S] {
|
||||
return LetTo[E, S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
// TraverseArray transforms an array
|
||||
func TraverseArray[A, B any](f func(A) IOOption[B]) func([]A) IOOption[[]B] {
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return function.Flow2(
|
||||
io.TraverseArray(f),
|
||||
io.Map(option.SequenceArray[B]),
|
||||
@@ -30,7 +30,7 @@ func TraverseArray[A, B any](f func(A) IOOption[B]) func([]A) IOOption[[]B] {
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex transforms an array
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) IOOption[B]) func([]A) IOOption[[]B] {
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) IOOption[B]) Kleisli[[]A, []B] {
|
||||
return function.Flow2(
|
||||
io.TraverseArrayWithIndex(f),
|
||||
io.Map(option.SequenceArray[B]),
|
||||
|
||||
@@ -19,20 +19,62 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
// result := iooption.Do(State{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) IOOption[S] {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// iooption.Do(State{}),
|
||||
// iooption.Bind(
|
||||
// func(name string) func(State) State {
|
||||
// return func(s State) State { s.Name = name; return s }
|
||||
// },
|
||||
// func(s State) iooption.IOOption[string] {
|
||||
// return iooption.FromIO(io.Of("Alice"))
|
||||
// },
|
||||
// ),
|
||||
// iooption.Bind(
|
||||
// func(age int) func(State) State {
|
||||
// return func(s State) State { s.Age = age; return s }
|
||||
// },
|
||||
// func(s State) iooption.IOOption[int] {
|
||||
// // This can access s.Name from the previous step
|
||||
// return iooption.FromIO(io.Of(len(s.Name) * 10))
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) IOOption[T],
|
||||
) func(IOOption[S1]) IOOption[S2] {
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
return chain.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[T, S2],
|
||||
@@ -45,7 +87,7 @@ func Bind[S1, S2, T any](
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(IOOption[S1]) IOOption[S2] {
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
return functor.Let(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
@@ -57,7 +99,7 @@ func Let[S1, S2, T any](
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(IOOption[S1]) IOOption[S2] {
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
return functor.LetTo(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
@@ -68,18 +110,50 @@ func LetTo[S1, S2, T any](
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(IOOption[T]) IOOption[S1] {
|
||||
) Kleisli[IOOption[T], S1] {
|
||||
return chain.BindTo(
|
||||
Map[T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getName := iooption.Some("Alice")
|
||||
// getAge := iooption.Some(30)
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// iooption.Do(State{}),
|
||||
// iooption.ApS(
|
||||
// func(name string) func(State) State {
|
||||
// return func(s State) State { s.Name = name; return s }
|
||||
// },
|
||||
// getName,
|
||||
// ),
|
||||
// iooption.ApS(
|
||||
// func(age int) func(State) State {
|
||||
// return func(s State) State { s.Age = age; return s }
|
||||
// },
|
||||
// getAge,
|
||||
// ),
|
||||
// )
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOOption[T],
|
||||
) func(IOOption[S1]) IOOption[S2] {
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
return apply.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
@@ -87,3 +161,136 @@ func ApS[S1, S2, T any](
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(s State) int { return s.Age },
|
||||
// func(s State, a int) State { s.Age = a; return s },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// iooption.Of(State{Name: "Alice"}),
|
||||
// iooption.ApSL(ageLens, iooption.Some(30)),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa IOOption[T],
|
||||
) Kleisli[IOOption[S], S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// an IOOption that produces the new value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but return None if it would exceed 100
|
||||
// increment := func(v int) iooption.IOOption[int] {
|
||||
// return iooption.FromIO(io.Of(v + 1))
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// iooption.Of(Counter{Value: 42}),
|
||||
// iooption.BindL(valueLens, increment),
|
||||
// ) // IOOption[Counter{Value: 43}]
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Kleisli[IOOption[S], S] {
|
||||
return Bind[S, S, T](lens.Set, func(s S) IOOption[T] {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in IOOption).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// iooption.Of(Counter{Value: 21}),
|
||||
// iooption.LetL(valueLens, double),
|
||||
// ) // IOOption[Counter{Value: 42}]
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Kleisli[IOOption[S], S] {
|
||||
return Let[S, S, T](lens.Set, func(s S) T {
|
||||
return f(lens.Get(s))
|
||||
})
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// iooption.Of(Config{Debug: true, Timeout: 30}),
|
||||
// iooption.LetToL(debugLens, false),
|
||||
// ) // IOOption[Config{Debug: false, Timeout: 30}]
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Kleisli[IOOption[S], S] {
|
||||
return LetTo[S, S, T](lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
// whether the body action returns and error or not.
|
||||
func Bracket[A, B, ANY any](
|
||||
acquire IOOption[A],
|
||||
use func(A) IOOption[B],
|
||||
use Kleisli[A, B],
|
||||
release func(A, Option[B]) IOOption[ANY],
|
||||
) IOOption[B] {
|
||||
return G.Bracket[IOOption[A], IOOption[B], IOOption[ANY], Option[B], A, B](
|
||||
|
||||
2643
v2/iooption/gen.go
2643
v2/iooption/gen.go
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ import "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource
|
||||
func WithResource[
|
||||
R, A, ANY any](onCreate IOOption[R], onRelease func(R) IOOption[ANY]) func(func(R) IOOption[A]) IOOption[A] {
|
||||
R, A, ANY any](onCreate IOOption[R], onRelease func(R) IOOption[ANY]) Kleisli[Kleisli[R, A], A] {
|
||||
// simply map to implementation of bracket
|
||||
return function.Bind13of3(Bracket[R, A, ANY])(onCreate, function.Ignore2of2[Option[A]](onRelease))
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
// Retrying will retry the actions according to the check policy
|
||||
func Retrying[A any](
|
||||
policy R.RetryPolicy,
|
||||
action func(R.RetryStatus) IOOption[A],
|
||||
action Kleisli[R.RetryStatus, A],
|
||||
check func(A) bool,
|
||||
) IOOption[A] {
|
||||
// get an implementation for the types
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -31,4 +32,7 @@ type (
|
||||
// IOOption represents a synchronous computation that may fail
|
||||
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioeitherlte-agt] for more details
|
||||
IOOption[A any] = io.IO[Option[A]]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, IOOption[B]]
|
||||
Operator[A, B any] = Kleisli[IOOption[A], B]
|
||||
)
|
||||
|
||||
@@ -19,18 +19,60 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
// result := stateless.Do(State{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) Iterator[S] {
|
||||
return G.Do[Iterator[S]](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
// For iterators, this produces the cartesian product of all values.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// stateless.Do(State{}),
|
||||
// stateless.Bind(
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// func(s State) stateless.Iterator[int] {
|
||||
// return stateless.Of(1, 2, 3)
|
||||
// },
|
||||
// ),
|
||||
// stateless.Bind(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) stateless.Iterator[int] {
|
||||
// // This can access s.X from the previous step
|
||||
// return stateless.Of(s.X * 10, s.X * 20)
|
||||
// },
|
||||
// ),
|
||||
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) Iterator[T],
|
||||
) func(Iterator[S1]) Iterator[S2] {
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[Iterator[S1], S2] {
|
||||
return G.Bind[Iterator[S1], Iterator[S2], Iterator[T], S1, S2, T](setter, f)
|
||||
}
|
||||
|
||||
@@ -38,7 +80,7 @@ func Bind[S1, S2, T any](
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(Iterator[S1]) Iterator[S2] {
|
||||
) Kleisli[Iterator[S1], S2] {
|
||||
return G.Let[Iterator[S1], Iterator[S2], S1, S2, T](setter, f)
|
||||
}
|
||||
|
||||
@@ -46,21 +88,53 @@ func Let[S1, S2, T any](
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(Iterator[S1]) Iterator[S2] {
|
||||
) Kleisli[Iterator[S1], S2] {
|
||||
return G.LetTo[Iterator[S1], Iterator[S2], S1, S2, T](setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Iterator[T]) Iterator[S1] {
|
||||
) Kleisli[Iterator[T], S1] {
|
||||
return G.BindTo[Iterator[S1], Iterator[T], S1, T](setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// xValues := stateless.Of(1, 2, 3)
|
||||
// yValues := stateless.Of(10, 20)
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// stateless.Do(State{}),
|
||||
// stateless.ApS(
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// xValues,
|
||||
// ),
|
||||
// stateless.ApS(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// yValues,
|
||||
// ),
|
||||
// ) // Produces all combinations: {1,10}, {1,20}, {2,10}, {2,20}, {3,10}, {3,20}
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Iterator[T],
|
||||
) func(Iterator[S1]) Iterator[S2] {
|
||||
) Kleisli[Iterator[S1], S2] {
|
||||
return G.ApS[Iterator[func(T) S2], Iterator[S1], Iterator[S2], Iterator[T], S1, S2, T](setter, fa)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,6 @@ import (
|
||||
|
||||
// Compress returns an [Iterator] that filters elements from a data [Iterator] returning only those that have a corresponding element in selector [Iterator] that evaluates to `true`.
|
||||
// Stops when either the data or selectors iterator has been exhausted.
|
||||
func Compress[U any](sel Iterator[bool]) func(Iterator[U]) Iterator[U] {
|
||||
func Compress[U any](sel Iterator[bool]) Kleisli[Iterator[U], U] {
|
||||
return G.Compress[Iterator[U], Iterator[bool], Iterator[P.Pair[U, bool]]](sel)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@ import (
|
||||
|
||||
// DropWhile creates an [Iterator] that drops elements from the [Iterator] as long as the predicate is true; afterwards, returns every element.
|
||||
// Note, the [Iterator] does not produce any output until the predicate first becomes false
|
||||
func DropWhile[U any](pred func(U) bool) func(Iterator[U]) Iterator[U] {
|
||||
func DropWhile[U any](pred func(U) bool) Kleisli[Iterator[U], U] {
|
||||
return G.DropWhile[Iterator[U]](pred)
|
||||
}
|
||||
|
||||
@@ -23,14 +23,56 @@ import (
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
// result := generic.Do[Iterator[State]](State{})
|
||||
func Do[GS ~func() O.Option[P.Pair[GS, S]], S any](
|
||||
empty S,
|
||||
) GS {
|
||||
return Of[GS](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
// For iterators, this produces the cartesian product where later steps can use values from earlier steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// generic.Do[Iterator[State]](State{}),
|
||||
// generic.Bind[Iterator[State], Iterator[State], Iterator[int], State, State, int](
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// func(s State) Iterator[int] {
|
||||
// return generic.Of[Iterator[int]](1, 2, 3)
|
||||
// },
|
||||
// ),
|
||||
// generic.Bind[Iterator[State], Iterator[State], Iterator[int], State, State, int](
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) Iterator[int] {
|
||||
// // This can access s.X from the previous step
|
||||
// return generic.Of[Iterator[int]](s.X * 10, s.X * 20)
|
||||
// },
|
||||
// ),
|
||||
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
|
||||
func Bind[GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], GA ~func() O.Option[P.Pair[GA, A]], S1, S2, A any](
|
||||
setter func(A) func(S1) S2,
|
||||
f func(S1) GA,
|
||||
@@ -78,7 +120,39 @@ func BindTo[GS1 ~func() O.Option[P.Pair[GS1, S1]], GA ~func() O.Option[P.Pair[GA
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel. For iterators, this produces the cartesian product.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y string
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// xIter := generic.Of[Iterator[int]](1, 2, 3)
|
||||
// yIter := generic.Of[Iterator[string]]("a", "b")
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// generic.Do[Iterator[State]](State{}),
|
||||
// generic.ApS[Iterator[func(int) State], Iterator[State], Iterator[State], Iterator[int], State, State, int](
|
||||
// func(x int) func(State) State {
|
||||
// return func(s State) State { s.X = x; return s }
|
||||
// },
|
||||
// xIter,
|
||||
// ),
|
||||
// generic.ApS[Iterator[func(string) State], Iterator[State], Iterator[State], Iterator[string], State, State, string](
|
||||
// func(y string) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// yIter,
|
||||
// ),
|
||||
// ) // Produces: {1,"a"}, {1,"b"}, {2,"a"}, {2,"b"}, {3,"a"}, {3,"b"}
|
||||
func ApS[GAS2 ~func() O.Option[P.Pair[GAS2, func(A) S2]], GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], GA ~func() O.Option[P.Pair[GA, A]], S1, S2, A any](
|
||||
setter func(A) func(S1) S2,
|
||||
fa GA,
|
||||
|
||||
@@ -18,15 +18,11 @@ package stateless
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
|
||||
L "github.com/IBM/fp-go/v2/lazy"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Iterator represents a stateless, pure way to iterate over a sequence
|
||||
type Iterator[U any] L.Lazy[O.Option[pair.Pair[Iterator[U], U]]]
|
||||
|
||||
// Next returns the [Iterator] for the next element in an iterator [pair.Pair]
|
||||
func Next[U any](m pair.Pair[Iterator[U], U]) Iterator[U] {
|
||||
return pair.Head(m)
|
||||
@@ -68,15 +64,15 @@ func MonadMap[U, V any](ma Iterator[U], f func(U) V) Iterator[V] {
|
||||
}
|
||||
|
||||
// Map transforms an [Iterator] of type [U] into an [Iterator] of type [V] via a mapping function
|
||||
func Map[U, V any](f func(U) V) func(ma Iterator[U]) Iterator[V] {
|
||||
func Map[U, V any](f func(U) V) Operator[U, V] {
|
||||
return G.Map[Iterator[V], Iterator[U]](f)
|
||||
}
|
||||
|
||||
func MonadChain[U, V any](ma Iterator[U], f func(U) Iterator[V]) Iterator[V] {
|
||||
func MonadChain[U, V any](ma Iterator[U], f Kleisli[U, V]) Iterator[V] {
|
||||
return G.MonadChain[Iterator[V], Iterator[U]](ma, f)
|
||||
}
|
||||
|
||||
func Chain[U, V any](f func(U) Iterator[V]) func(Iterator[U]) Iterator[V] {
|
||||
func Chain[U, V any](f Kleisli[U, V]) Kleisli[Iterator[U], V] {
|
||||
return G.Chain[Iterator[V], Iterator[U]](f)
|
||||
}
|
||||
|
||||
@@ -101,17 +97,17 @@ func Replicate[U any](a U) Iterator[U] {
|
||||
}
|
||||
|
||||
// FilterMap filters and transforms the content of an iterator
|
||||
func FilterMap[U, V any](f func(U) O.Option[V]) func(ma Iterator[U]) Iterator[V] {
|
||||
func FilterMap[U, V any](f func(U) O.Option[V]) Operator[U, V] {
|
||||
return G.FilterMap[Iterator[V], Iterator[U]](f)
|
||||
}
|
||||
|
||||
// Filter filters the content of an iterator
|
||||
func Filter[U any](f func(U) bool) func(ma Iterator[U]) Iterator[U] {
|
||||
func Filter[U any](f func(U) bool) Operator[U, U] {
|
||||
return G.Filter[Iterator[U]](f)
|
||||
}
|
||||
|
||||
// Ap is the applicative functor for iterators
|
||||
func Ap[V, U any](ma Iterator[U]) func(Iterator[func(U) V]) Iterator[V] {
|
||||
func Ap[V, U any](ma Iterator[U]) Operator[func(U) V, V] {
|
||||
return G.Ap[Iterator[func(U) V], Iterator[V]](ma)
|
||||
}
|
||||
|
||||
@@ -132,7 +128,7 @@ func Count(start int) Iterator[int] {
|
||||
}
|
||||
|
||||
// FilterChain filters and transforms the content of an iterator
|
||||
func FilterChain[U, V any](f func(U) O.Option[Iterator[V]]) func(ma Iterator[U]) Iterator[V] {
|
||||
func FilterChain[U, V any](f func(U) O.Option[Iterator[V]]) Operator[U, V] {
|
||||
return G.FilterChain[Iterator[Iterator[V]], Iterator[V], Iterator[U]](f)
|
||||
}
|
||||
|
||||
@@ -146,10 +142,10 @@ func Fold[U any](m M.Monoid[U]) func(Iterator[U]) U {
|
||||
return G.Fold[Iterator[U]](m)
|
||||
}
|
||||
|
||||
func MonadChainFirst[U, V any](ma Iterator[U], f func(U) Iterator[V]) Iterator[U] {
|
||||
func MonadChainFirst[U, V any](ma Iterator[U], f Kleisli[U, V]) Iterator[U] {
|
||||
return G.MonadChainFirst[Iterator[V], Iterator[U], U, V](ma, f)
|
||||
}
|
||||
|
||||
func ChainFirst[U, V any](f func(U) Iterator[V]) func(Iterator[U]) Iterator[U] {
|
||||
func ChainFirst[U, V any](f Kleisli[U, V]) Operator[U, U] {
|
||||
return G.ChainFirst[Iterator[V], Iterator[U], U, V](f)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,19 @@
|
||||
|
||||
package stateless
|
||||
|
||||
import "github.com/IBM/fp-go/v2/option"
|
||||
import (
|
||||
L "github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Iterator represents a stateless, pure way to iterate over a sequence
|
||||
Iterator[U any] L.Lazy[Option[pair.Pair[Iterator[U], U]]]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, Iterator[B]]
|
||||
Operator[A, B any] = Kleisli[Iterator[A], B]
|
||||
)
|
||||
|
||||
230
v2/lazy/bind.go
230
v2/lazy/bind.go
@@ -16,21 +16,64 @@
|
||||
package lazy
|
||||
|
||||
import (
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
// Bind creates an empty context of type [S] to be used with the [Bind] operation
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
// This is the starting point for do-notation style composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data Data
|
||||
// }
|
||||
// result := lazy.Do(State{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) Lazy[S] {
|
||||
return io.Do(empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps.
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data Data
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{}),
|
||||
// lazy.Bind(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// func(s State) lazy.Lazy[Config] {
|
||||
// return lazy.MakeLazy(func() Config { return loadConfig() })
|
||||
// },
|
||||
// ),
|
||||
// lazy.Bind(
|
||||
// func(data Data) func(State) State {
|
||||
// return func(s State) State { s.Data = data; return s }
|
||||
// },
|
||||
// func(s State) lazy.Lazy[Data] {
|
||||
// // This can access s.Config from the previous step
|
||||
// return lazy.MakeLazy(func() Data { return loadData(s.Config) })
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) Lazy[T],
|
||||
) func(Lazy[S1]) Lazy[S2] {
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[Lazy[S1], S2] {
|
||||
return io.Bind(setter, f)
|
||||
}
|
||||
|
||||
@@ -38,7 +81,7 @@ func Bind[S1, S2, T any](
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(Lazy[S1]) Lazy[S2] {
|
||||
) Kleisli[Lazy[S1], S2] {
|
||||
return io.Let(setter, f)
|
||||
}
|
||||
|
||||
@@ -46,21 +89,190 @@ func Let[S1, S2, T any](
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(Lazy[S1]) Lazy[S2] {
|
||||
) Kleisli[Lazy[S1], S2] {
|
||||
return io.LetTo(setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(Lazy[T]) Lazy[S1] {
|
||||
) Kleisli[Lazy[T], S1] {
|
||||
return io.BindTo(setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data Data
|
||||
// }
|
||||
//
|
||||
// // These operations are independent and can be combined with ApS
|
||||
// getConfig := lazy.MakeLazy(func() Config { return loadConfig() })
|
||||
// getData := lazy.MakeLazy(func() Data { return loadData() })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{}),
|
||||
// lazy.ApS(
|
||||
// func(cfg Config) func(State) State {
|
||||
// return func(s State) State { s.Config = cfg; return s }
|
||||
// },
|
||||
// getConfig,
|
||||
// ),
|
||||
// lazy.ApS(
|
||||
// func(data Data) func(State) State {
|
||||
// return func(s State) State { s.Data = data; return s }
|
||||
// },
|
||||
// getData,
|
||||
// ),
|
||||
// )
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Lazy[T],
|
||||
) func(Lazy[S1]) Lazy[S2] {
|
||||
) Kleisli[Lazy[S1], S2] {
|
||||
return io.ApS(setter, fa)
|
||||
}
|
||||
|
||||
// ApSL is a variant of ApS that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. This allows you to work with nested fields without manually managing
|
||||
// the update logic.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data string
|
||||
// }
|
||||
//
|
||||
// configLens := L.Prop[State, Config]("Config")
|
||||
// getConfig := lazy.MakeLazy(func() Config { return Config{Host: "localhost", Port: 8080} })
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{}),
|
||||
// lazy.ApSL(configLens, getConfig),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Lazy[T],
|
||||
) Kleisli[Lazy[S], S] {
|
||||
return io.ApSL(lens, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new computation that produces an updated value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data string
|
||||
// }
|
||||
//
|
||||
// configLens := L.Prop[State, Config]("Config")
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{Config: Config{Host: "localhost"}}),
|
||||
// lazy.BindL(configLens, func(cfg Config) lazy.Lazy[Config] {
|
||||
// return lazy.MakeLazy(func() Config {
|
||||
// cfg.Port = 8080
|
||||
// return cfg
|
||||
// })
|
||||
// }),
|
||||
// )
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Kleisli[Lazy[S], S] {
|
||||
return io.BindL(lens, f)
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The function f receives the current value of the focused field and
|
||||
// returns a new value (without wrapping in a monad).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data string
|
||||
// }
|
||||
//
|
||||
// configLens := L.Prop[State, Config]("Config")
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{Config: Config{Host: "localhost"}}),
|
||||
// lazy.LetL(configLens, func(cfg Config) Config {
|
||||
// cfg.Port = 8080
|
||||
// return cfg
|
||||
// }),
|
||||
// )
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
) Kleisli[Lazy[S], S] {
|
||||
return io.LetL(lens, f)
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
|
||||
// This provides a more ergonomic API when working with nested structures, eliminating
|
||||
// the need to manually write setter functions.
|
||||
//
|
||||
// The lens parameter provides both a getter and setter for a field of type T within
|
||||
// the context S. The value b is set directly to the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
// type State struct {
|
||||
// Config Config
|
||||
// Data string
|
||||
// }
|
||||
//
|
||||
// configLens := L.Prop[State, Config]("Config")
|
||||
// newConfig := Config{Host: "localhost", Port: 8080}
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// lazy.Do(State{}),
|
||||
// lazy.LetToL(configLens, newConfig),
|
||||
// )
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Kleisli[Lazy[S], S] {
|
||||
return io.LetToL(lens, b)
|
||||
}
|
||||
|
||||
@@ -21,9 +21,6 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
// Lazy represents a synchronous computation without side effects
|
||||
type Lazy[A any] = func() A
|
||||
|
||||
func Of[A any](a A) Lazy[A] {
|
||||
return io.Of(a)
|
||||
}
|
||||
@@ -53,17 +50,17 @@ func MonadMapTo[A, B any](fa Lazy[A], b B) Lazy[B] {
|
||||
return io.MonadMapTo(fa, b)
|
||||
}
|
||||
|
||||
func MapTo[A, B any](b B) func(Lazy[A]) Lazy[B] {
|
||||
func MapTo[A, B any](b B) Kleisli[Lazy[A], B] {
|
||||
return io.MapTo[A](b)
|
||||
}
|
||||
|
||||
// MonadChain composes computations in sequence, using the return value of one computation to determine the next computation.
|
||||
func MonadChain[A, B any](fa Lazy[A], f func(A) Lazy[B]) Lazy[B] {
|
||||
func MonadChain[A, B any](fa Lazy[A], f Kleisli[A, B]) Lazy[B] {
|
||||
return io.MonadChain(fa, f)
|
||||
}
|
||||
|
||||
// Chain composes computations in sequence, using the return value of one computation to determine the next computation.
|
||||
func Chain[A, B any](f func(A) Lazy[B]) func(Lazy[A]) Lazy[B] {
|
||||
func Chain[A, B any](f Kleisli[A, B]) Kleisli[Lazy[A], B] {
|
||||
return io.Chain(f)
|
||||
}
|
||||
|
||||
@@ -86,13 +83,13 @@ func Memoize[A any](ma Lazy[A]) Lazy[A] {
|
||||
|
||||
// MonadChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
|
||||
// keeping only the result of the first.
|
||||
func MonadChainFirst[A, B any](fa Lazy[A], f func(A) Lazy[B]) Lazy[A] {
|
||||
func MonadChainFirst[A, B any](fa Lazy[A], f Kleisli[A, B]) Lazy[A] {
|
||||
return io.MonadChainFirst(fa, f)
|
||||
}
|
||||
|
||||
// ChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
|
||||
// keeping only the result of the first.
|
||||
func ChainFirst[A, B any](f func(A) Lazy[B]) func(Lazy[A]) Lazy[A] {
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Kleisli[Lazy[A], A] {
|
||||
return io.ChainFirst(f)
|
||||
}
|
||||
|
||||
@@ -102,7 +99,7 @@ func MonadApFirst[A, B any](first Lazy[A], second Lazy[B]) Lazy[A] {
|
||||
}
|
||||
|
||||
// ApFirst combines two effectful actions, keeping only the result of the first.
|
||||
func ApFirst[A, B any](second Lazy[B]) func(Lazy[A]) Lazy[A] {
|
||||
func ApFirst[A, B any](second Lazy[B]) Kleisli[Lazy[A], A] {
|
||||
return io.ApFirst[A](second)
|
||||
}
|
||||
|
||||
@@ -112,7 +109,7 @@ func MonadApSecond[A, B any](first Lazy[A], second Lazy[B]) Lazy[B] {
|
||||
}
|
||||
|
||||
// ApSecond combines two effectful actions, keeping only the result of the second.
|
||||
func ApSecond[A, B any](second Lazy[B]) func(Lazy[A]) Lazy[B] {
|
||||
func ApSecond[A, B any](second Lazy[B]) Kleisli[Lazy[A], B] {
|
||||
return io.ApSecond[A](second)
|
||||
}
|
||||
|
||||
@@ -122,7 +119,7 @@ func MonadChainTo[A, B any](fa Lazy[A], fb Lazy[B]) Lazy[B] {
|
||||
}
|
||||
|
||||
// ChainTo composes computations in sequence, ignoring the return value of the first computation
|
||||
func ChainTo[A, B any](fb Lazy[B]) func(Lazy[A]) Lazy[B] {
|
||||
func ChainTo[A, B any](fb Lazy[B]) Kleisli[Lazy[A], B] {
|
||||
return io.ChainTo[A](fb)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
// check - checks if the result of the action needs to be retried
|
||||
func Retrying[A any](
|
||||
policy R.RetryPolicy,
|
||||
action func(R.RetryStatus) Lazy[A],
|
||||
action Kleisli[R.RetryStatus, A],
|
||||
check func(A) bool,
|
||||
) Lazy[A] {
|
||||
return io.Retrying(policy, action, check)
|
||||
|
||||
@@ -17,19 +17,19 @@ package lazy
|
||||
|
||||
import "github.com/IBM/fp-go/v2/io"
|
||||
|
||||
func MonadTraverseArray[A, B any](tas []A, f func(A) Lazy[B]) Lazy[[]B] {
|
||||
func MonadTraverseArray[A, B any](tas []A, f Kleisli[A, B]) Lazy[[]B] {
|
||||
return io.MonadTraverseArray(tas, f)
|
||||
}
|
||||
|
||||
// TraverseArray applies a function returning an [IO] to all elements in an array and the
|
||||
// transforms this into an [IO] of that array
|
||||
func TraverseArray[A, B any](f func(A) Lazy[B]) func([]A) Lazy[[]B] {
|
||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
return io.TraverseArray(f)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex applies a function returning an [IO] to all elements in an array and the
|
||||
// transforms this into an [IO] of that array
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) Lazy[B]) func([]A) Lazy[[]B] {
|
||||
func TraverseArrayWithIndex[A, B any](f func(int, A) Lazy[B]) Kleisli[[]A, []B] {
|
||||
return io.TraverseArrayWithIndex(f)
|
||||
}
|
||||
|
||||
@@ -38,19 +38,19 @@ func SequenceArray[A any](tas []Lazy[A]) Lazy[[]A] {
|
||||
return io.SequenceArray(tas)
|
||||
}
|
||||
|
||||
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) Lazy[B]) Lazy[map[K]B] {
|
||||
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) Lazy[map[K]B] {
|
||||
return io.MonadTraverseRecord(tas, f)
|
||||
}
|
||||
|
||||
// TraverseRecord applies a function returning an [IO] to all elements in a record and the
|
||||
// transforms this into an [IO] of that record
|
||||
func TraverseRecord[K comparable, A, B any](f func(A) Lazy[B]) func(map[K]A) Lazy[map[K]B] {
|
||||
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
|
||||
return io.TraverseRecord[K](f)
|
||||
}
|
||||
|
||||
// TraverseRecord applies a function returning an [IO] to all elements in a record and the
|
||||
// transforms this into an [IO] of that record
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) Lazy[B]) func(map[K]A) Lazy[map[K]B] {
|
||||
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) Lazy[B]) Kleisli[map[K]A, map[K]B] {
|
||||
return io.TraverseRecordWithIndex[K](f)
|
||||
}
|
||||
|
||||
|
||||
9
v2/lazy/types.go
Normal file
9
v2/lazy/types.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package lazy
|
||||
|
||||
type (
|
||||
// Lazy represents a synchronous computation without side effects
|
||||
Lazy[A any] = func() A
|
||||
|
||||
Kleisli[A, B any] = func(A) Lazy[B]
|
||||
Operator[A, B any] = Kleisli[Lazy[A], B]
|
||||
)
|
||||
5
v2/logging/coverage.out
Normal file
5
v2/logging/coverage.out
Normal file
@@ -0,0 +1,5 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/logging/logger.go:54.92,55.22 1 1
|
||||
github.com/IBM/fp-go/v2/logging/logger.go:56.9,58.32 2 1
|
||||
github.com/IBM/fp-go/v2/logging/logger.go:59.9,61.34 2 1
|
||||
github.com/IBM/fp-go/v2/logging/logger.go:62.10,63.46 1 1
|
||||
@@ -13,12 +13,44 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package logging provides utilities for creating logging callbacks from standard log.Logger instances.
|
||||
// It offers a convenient way to configure logging for functional programming patterns where separate
|
||||
// loggers for success and error cases are needed.
|
||||
package logging
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// LoggingCallbacks creates a pair of logging callback functions from the provided loggers.
|
||||
// It returns two functions that can be used for logging messages, typically one for success
|
||||
// cases and one for error cases.
|
||||
//
|
||||
// The behavior depends on the number of loggers provided:
|
||||
// - 0 loggers: Returns two callbacks using log.Default() for both success and error logging
|
||||
// - 1 logger: Returns two callbacks both using the provided logger
|
||||
// - 2+ loggers: Returns callbacks using the first logger for success and second for errors
|
||||
//
|
||||
// Parameters:
|
||||
// - loggers: Variable number of *log.Logger instances (0, 1, or more)
|
||||
//
|
||||
// Returns:
|
||||
// - First function: Callback for success/info logging (signature: func(string, ...any))
|
||||
// - Second function: Callback for error logging (signature: func(string, ...any))
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Using default logger for both
|
||||
// infoLog, errLog := LoggingCallbacks()
|
||||
//
|
||||
// // Using custom logger for both
|
||||
// customLogger := log.New(os.Stdout, "APP: ", log.LstdFlags)
|
||||
// infoLog, errLog := LoggingCallbacks(customLogger)
|
||||
//
|
||||
// // Using separate loggers for info and errors
|
||||
// infoLogger := log.New(os.Stdout, "INFO: ", log.LstdFlags)
|
||||
// errorLogger := log.New(os.Stderr, "ERROR: ", log.LstdFlags)
|
||||
// infoLog, errLog := LoggingCallbacks(infoLogger, errorLogger)
|
||||
func LoggingCallbacks(loggers ...*log.Logger) (func(string, ...any), func(string, ...any)) {
|
||||
switch len(loggers) {
|
||||
case 0:
|
||||
|
||||
288
v2/logging/logger_test.go
Normal file
288
v2/logging/logger_test.go
Normal file
@@ -0,0 +1,288 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLoggingCallbacks_NoLoggers tests the case when no loggers are provided.
|
||||
// It should return two callbacks using the default logger.
|
||||
func TestLoggingCallbacks_NoLoggers(t *testing.T) {
|
||||
infoLog, errLog := LoggingCallbacks()
|
||||
|
||||
if infoLog == nil {
|
||||
t.Error("Expected infoLog to be non-nil")
|
||||
}
|
||||
if errLog == nil {
|
||||
t.Error("Expected errLog to be non-nil")
|
||||
}
|
||||
|
||||
// Verify both callbacks work
|
||||
var buf bytes.Buffer
|
||||
log.SetOutput(&buf)
|
||||
defer log.SetOutput(nil)
|
||||
|
||||
infoLog("test info: %s", "message")
|
||||
if !strings.Contains(buf.String(), "test info: message") {
|
||||
t.Errorf("Expected log to contain 'test info: message', got: %s", buf.String())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
errLog("test error: %s", "message")
|
||||
if !strings.Contains(buf.String(), "test error: message") {
|
||||
t.Errorf("Expected log to contain 'test error: message', got: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_OneLogger tests the case when one logger is provided.
|
||||
// Both callbacks should use the same logger.
|
||||
func TestLoggingCallbacks_OneLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "TEST: ", 0)
|
||||
|
||||
infoLog, errLog := LoggingCallbacks(logger)
|
||||
|
||||
if infoLog == nil {
|
||||
t.Error("Expected infoLog to be non-nil")
|
||||
}
|
||||
if errLog == nil {
|
||||
t.Error("Expected errLog to be non-nil")
|
||||
}
|
||||
|
||||
// Test info callback
|
||||
infoLog("info message: %d", 42)
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "TEST: info message: 42") {
|
||||
t.Errorf("Expected log to contain 'TEST: info message: 42', got: %s", output)
|
||||
}
|
||||
|
||||
// Test error callback uses same logger
|
||||
buf.Reset()
|
||||
errLog("error message: %s", "failed")
|
||||
output = buf.String()
|
||||
if !strings.Contains(output, "TEST: error message: failed") {
|
||||
t.Errorf("Expected log to contain 'TEST: error message: failed', got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_TwoLoggers tests the case when two loggers are provided.
|
||||
// First callback should use first logger, second callback should use second logger.
|
||||
func TestLoggingCallbacks_TwoLoggers(t *testing.T) {
|
||||
var infoBuf, errBuf bytes.Buffer
|
||||
infoLogger := log.New(&infoBuf, "INFO: ", 0)
|
||||
errorLogger := log.New(&errBuf, "ERROR: ", 0)
|
||||
|
||||
infoLog, errLog := LoggingCallbacks(infoLogger, errorLogger)
|
||||
|
||||
if infoLog == nil {
|
||||
t.Error("Expected infoLog to be non-nil")
|
||||
}
|
||||
if errLog == nil {
|
||||
t.Error("Expected errLog to be non-nil")
|
||||
}
|
||||
|
||||
// Test info callback uses first logger
|
||||
infoLog("success: %s", "operation completed")
|
||||
infoOutput := infoBuf.String()
|
||||
if !strings.Contains(infoOutput, "INFO: success: operation completed") {
|
||||
t.Errorf("Expected info log to contain 'INFO: success: operation completed', got: %s", infoOutput)
|
||||
}
|
||||
if errBuf.Len() != 0 {
|
||||
t.Errorf("Expected error buffer to be empty, got: %s", errBuf.String())
|
||||
}
|
||||
|
||||
// Test error callback uses second logger
|
||||
errLog("failure: %s", "operation failed")
|
||||
errOutput := errBuf.String()
|
||||
if !strings.Contains(errOutput, "ERROR: failure: operation failed") {
|
||||
t.Errorf("Expected error log to contain 'ERROR: failure: operation failed', got: %s", errOutput)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_MultipleLoggers tests the case when more than two loggers are provided.
|
||||
// Should use first two loggers and ignore the rest.
|
||||
func TestLoggingCallbacks_MultipleLoggers(t *testing.T) {
|
||||
var buf1, buf2, buf3 bytes.Buffer
|
||||
logger1 := log.New(&buf1, "LOG1: ", 0)
|
||||
logger2 := log.New(&buf2, "LOG2: ", 0)
|
||||
logger3 := log.New(&buf3, "LOG3: ", 0)
|
||||
|
||||
infoLog, errLog := LoggingCallbacks(logger1, logger2, logger3)
|
||||
|
||||
if infoLog == nil {
|
||||
t.Error("Expected infoLog to be non-nil")
|
||||
}
|
||||
if errLog == nil {
|
||||
t.Error("Expected errLog to be non-nil")
|
||||
}
|
||||
|
||||
// Test that first logger is used for info
|
||||
infoLog("message 1")
|
||||
if !strings.Contains(buf1.String(), "LOG1: message 1") {
|
||||
t.Errorf("Expected first logger to be used, got: %s", buf1.String())
|
||||
}
|
||||
|
||||
// Test that second logger is used for error
|
||||
errLog("message 2")
|
||||
if !strings.Contains(buf2.String(), "LOG2: message 2") {
|
||||
t.Errorf("Expected second logger to be used, got: %s", buf2.String())
|
||||
}
|
||||
|
||||
// Test that third logger is not used
|
||||
if buf3.Len() != 0 {
|
||||
t.Errorf("Expected third logger to not be used, got: %s", buf3.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_FormattingWithMultipleArgs tests that formatting works correctly
|
||||
// with multiple arguments.
|
||||
func TestLoggingCallbacks_FormattingWithMultipleArgs(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
|
||||
infoLog, _ := LoggingCallbacks(logger)
|
||||
|
||||
infoLog("test %s %d %v", "string", 123, true)
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "test string 123 true") {
|
||||
t.Errorf("Expected formatted output 'test string 123 true', got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_NoFormatting tests logging without format specifiers.
|
||||
func TestLoggingCallbacks_NoFormatting(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "PREFIX: ", 0)
|
||||
|
||||
infoLog, _ := LoggingCallbacks(logger)
|
||||
|
||||
infoLog("simple message")
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "PREFIX: simple message") {
|
||||
t.Errorf("Expected 'PREFIX: simple message', got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_EmptyMessage tests logging with empty message.
|
||||
func TestLoggingCallbacks_EmptyMessage(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
|
||||
infoLog, _ := LoggingCallbacks(logger)
|
||||
|
||||
infoLog("")
|
||||
output := buf.String()
|
||||
// Should still produce output (newline at minimum)
|
||||
if len(output) == 0 {
|
||||
t.Error("Expected some output even with empty message")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_NilLogger tests behavior when nil logger is passed.
|
||||
// This tests edge case handling.
|
||||
func TestLoggingCallbacks_NilLogger(t *testing.T) {
|
||||
// This should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("LoggingCallbacks panicked with nil logger: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
infoLog, errLog := LoggingCallbacks(nil)
|
||||
|
||||
// The callbacks should still be created
|
||||
if infoLog == nil {
|
||||
t.Error("Expected infoLog to be non-nil even with nil logger")
|
||||
}
|
||||
if errLog == nil {
|
||||
t.Error("Expected errLog to be non-nil even with nil logger")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggingCallbacks_ConsecutiveCalls tests that callbacks can be called multiple times.
|
||||
func TestLoggingCallbacks_ConsecutiveCalls(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
|
||||
infoLog, errLog := LoggingCallbacks(logger)
|
||||
|
||||
// Multiple calls to info
|
||||
infoLog("call 1")
|
||||
infoLog("call 2")
|
||||
infoLog("call 3")
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "call 1") || !strings.Contains(output, "call 2") || !strings.Contains(output, "call 3") {
|
||||
t.Errorf("Expected all three calls to be logged, got: %s", output)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
|
||||
// Multiple calls to error
|
||||
errLog("error 1")
|
||||
errLog("error 2")
|
||||
|
||||
output = buf.String()
|
||||
if !strings.Contains(output, "error 1") || !strings.Contains(output, "error 2") {
|
||||
t.Errorf("Expected both error calls to be logged, got: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLoggingCallbacks_NoLoggers benchmarks the no-logger case.
|
||||
func BenchmarkLoggingCallbacks_NoLoggers(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
LoggingCallbacks()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLoggingCallbacks_OneLogger benchmarks the single-logger case.
|
||||
func BenchmarkLoggingCallbacks_OneLogger(b *testing.B) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
LoggingCallbacks(logger)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLoggingCallbacks_TwoLoggers benchmarks the two-logger case.
|
||||
func BenchmarkLoggingCallbacks_TwoLoggers(b *testing.B) {
|
||||
var buf1, buf2 bytes.Buffer
|
||||
logger1 := log.New(&buf1, "", 0)
|
||||
logger2 := log.New(&buf2, "", 0)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
LoggingCallbacks(logger1, logger2)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLoggingCallbacks_Logging benchmarks actual logging operations.
|
||||
func BenchmarkLoggingCallbacks_Logging(b *testing.B) {
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "", 0)
|
||||
infoLog, _ := LoggingCallbacks(logger)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
infoLog("benchmark message %d", i)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Iso is an optic which converts elements of type `S` into elements of type `A` without loss.
|
||||
// Package iso provides isomorphisms - bidirectional transformations between types without loss of information.
|
||||
package iso
|
||||
|
||||
import (
|
||||
@@ -21,21 +21,127 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Iso represents an isomorphism between types S and A.
|
||||
// An isomorphism is a bidirectional transformation that converts between two types
|
||||
// without any loss of information. It consists of two functions that are inverses
|
||||
// of each other.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The target type
|
||||
//
|
||||
// Fields:
|
||||
// - Get: Converts from S to A
|
||||
// - ReverseGet: Converts from A back to S
|
||||
//
|
||||
// Laws:
|
||||
// An Iso must satisfy the round-trip laws:
|
||||
// 1. ReverseGet(Get(s)) == s for all s: S
|
||||
// 2. Get(ReverseGet(a)) == a for all a: A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Isomorphism between Celsius and Fahrenheit
|
||||
// tempIso := Iso[float64, float64]{
|
||||
// Get: func(c float64) float64 { return c*9/5 + 32 },
|
||||
// ReverseGet: func(f float64) float64 { return (f - 32) * 5 / 9 },
|
||||
// }
|
||||
//
|
||||
// fahrenheit := tempIso.Get(20.0) // 68.0
|
||||
// celsius := tempIso.ReverseGet(68.0) // 20.0
|
||||
type Iso[S, A any] struct {
|
||||
Get func(s S) A
|
||||
// Get converts a value from the source type S to the target type A.
|
||||
Get func(s S) A
|
||||
|
||||
// ReverseGet converts a value from the target type A back to the source type S.
|
||||
// This is the inverse of Get.
|
||||
ReverseGet func(a A) S
|
||||
}
|
||||
|
||||
// MakeIso constructs an isomorphism from two functions.
|
||||
// The functions should be inverses of each other to satisfy the isomorphism laws.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The target type
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to convert from S to A
|
||||
// - reverse: Function to convert from A to S (inverse of get)
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[S, A] that uses the provided functions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create an isomorphism between string and []byte
|
||||
// stringBytesIso := MakeIso(
|
||||
// func(s string) []byte { return []byte(s) },
|
||||
// func(b []byte) string { return string(b) },
|
||||
// )
|
||||
//
|
||||
// bytes := stringBytesIso.Get("hello") // []byte("hello")
|
||||
// str := stringBytesIso.ReverseGet([]byte("hi")) // "hi"
|
||||
func MakeIso[S, A any](get func(S) A, reverse func(A) S) Iso[S, A] {
|
||||
return Iso[S, A]{Get: get, ReverseGet: reverse}
|
||||
}
|
||||
|
||||
// Id returns an iso implementing the identity operation
|
||||
// Id returns an identity isomorphism that performs no transformation.
|
||||
// Both Get and ReverseGet are the identity function.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The type for both source and target
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[S, S] where Get and ReverseGet are both identity functions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// idIso := Id[int]()
|
||||
// value := idIso.Get(42) // 42
|
||||
// same := idIso.ReverseGet(42) // 42
|
||||
//
|
||||
// Use cases:
|
||||
// - As a starting point for isomorphism composition
|
||||
// - When you need an isomorphism but don't want to transform the value
|
||||
// - In generic code that requires an isomorphism parameter
|
||||
func Id[S any]() Iso[S, S] {
|
||||
return MakeIso(F.Identity[S], F.Identity[S])
|
||||
}
|
||||
|
||||
// Compose combines an ISO with another ISO
|
||||
// Compose combines two isomorphisms to create a new isomorphism.
|
||||
// Given Iso[S, A] and Iso[A, B], creates Iso[S, B].
|
||||
// The resulting isomorphism first applies the outer iso (S → A),
|
||||
// then the inner iso (A → B).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The outermost source type
|
||||
// - A: The intermediate type
|
||||
// - B: The innermost target type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: The inner isomorphism (A → B)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes the outer isomorphism (S → A) and returns the composed isomorphism (S → B)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// metersToKm := MakeIso(
|
||||
// func(m float64) float64 { return m / 1000 },
|
||||
// func(km float64) float64 { return km * 1000 },
|
||||
// )
|
||||
//
|
||||
// kmToMiles := MakeIso(
|
||||
// func(km float64) float64 { return km * 0.621371 },
|
||||
// func(mi float64) float64 { return mi / 0.621371 },
|
||||
// )
|
||||
//
|
||||
// // Compose: meters → kilometers → miles
|
||||
// metersToMiles := F.Pipe1(metersToKm, Compose[float64](kmToMiles))
|
||||
//
|
||||
// miles := metersToMiles.Get(5000) // ~3.11 miles
|
||||
// meters := metersToMiles.ReverseGet(3.11) // ~5000 meters
|
||||
func Compose[S, A, B any](ab Iso[A, B]) func(Iso[S, A]) Iso[S, B] {
|
||||
return func(sa Iso[S, A]) Iso[S, B] {
|
||||
return MakeIso(
|
||||
@@ -45,7 +151,31 @@ func Compose[S, A, B any](ab Iso[A, B]) func(Iso[S, A]) Iso[S, B] {
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse changes the order of parameters for an iso
|
||||
// Reverse swaps the direction of an isomorphism.
|
||||
// Given Iso[S, A], creates Iso[A, S] where Get and ReverseGet are swapped.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The original source type (becomes target)
|
||||
// - A: The original target type (becomes source)
|
||||
//
|
||||
// Parameters:
|
||||
// - sa: The isomorphism to reverse
|
||||
//
|
||||
// Returns:
|
||||
// - An Iso[A, S] with Get and ReverseGet swapped
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// celsiusToFahrenheit := MakeIso(
|
||||
// func(c float64) float64 { return c*9/5 + 32 },
|
||||
// func(f float64) float64 { return (f - 32) * 5 / 9 },
|
||||
// )
|
||||
//
|
||||
// // Reverse to get Fahrenheit to Celsius
|
||||
// fahrenheitToCelsius := Reverse(celsiusToFahrenheit)
|
||||
//
|
||||
// celsius := fahrenheitToCelsius.Get(68.0) // 20.0
|
||||
// fahrenheit := fahrenheitToCelsius.ReverseGet(20.0) // 68.0
|
||||
func Reverse[S, A any](sa Iso[S, A]) Iso[A, S] {
|
||||
return MakeIso(
|
||||
sa.ReverseGet,
|
||||
@@ -53,6 +183,8 @@ func Reverse[S, A any](sa Iso[S, A]) Iso[A, S] {
|
||||
)
|
||||
}
|
||||
|
||||
// modify is an internal helper that applies a transformation function through an isomorphism.
|
||||
// It converts S to A, applies the function, then converts back to S.
|
||||
func modify[FCT ~func(A) A, S, A any](f FCT, sa Iso[S, A], s S) S {
|
||||
return F.Pipe3(
|
||||
s,
|
||||
@@ -62,35 +194,166 @@ func modify[FCT ~func(A) A, S, A any](f FCT, sa Iso[S, A], s S) S {
|
||||
)
|
||||
}
|
||||
|
||||
// Modify applies a transformation
|
||||
// Modify creates a function that applies a transformation in the target space.
|
||||
// It converts the source value to the target type, applies the transformation,
|
||||
// then converts back to the source type.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - FCT: The transformation function type (A → A)
|
||||
// - A: The target type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The transformation function to apply in the target space
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an Iso[S, A] and returns an endomorphism (S → S)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Meters float64
|
||||
// type Kilometers float64
|
||||
//
|
||||
// mToKm := MakeIso(
|
||||
// func(m Meters) Kilometers { return Kilometers(m / 1000) },
|
||||
// func(km Kilometers) Meters { return Meters(km * 1000) },
|
||||
// )
|
||||
//
|
||||
// // Double the distance in kilometers, result in meters
|
||||
// doubled := Modify[Meters](func(km Kilometers) Kilometers {
|
||||
// return km * 2
|
||||
// })(mToKm)(Meters(5000))
|
||||
// // Result: Meters(10000)
|
||||
func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Iso[S, A]) EM.Endomorphism[S] {
|
||||
return EM.Curry3(modify[FCT, S, A])(f)
|
||||
return F.Curry3(modify[FCT, S, A])(f)
|
||||
}
|
||||
|
||||
// Wrap wraps the value
|
||||
// Unwrap extracts the target value from a source value using an isomorphism.
|
||||
// This is a convenience function that applies the Get function of the isomorphism.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type to extract
|
||||
// - S: The source type
|
||||
//
|
||||
// Parameters:
|
||||
// - s: The source value to unwrap
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an Iso[S, A] and returns the unwrapped value of type A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type UserId int
|
||||
//
|
||||
// userIdIso := MakeIso(
|
||||
// func(id UserId) int { return int(id) },
|
||||
// func(i int) UserId { return UserId(i) },
|
||||
// )
|
||||
//
|
||||
// rawId := Unwrap[int](UserId(42))(userIdIso) // 42
|
||||
//
|
||||
// Note: This function is also available as To for semantic clarity.
|
||||
func Unwrap[A, S any](s S) func(Iso[S, A]) A {
|
||||
return func(sa Iso[S, A]) A {
|
||||
return sa.Get(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap unwraps the value
|
||||
// Wrap wraps a target value into a source value using an isomorphism.
|
||||
// This is a convenience function that applies the ReverseGet function of the isomorphism.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type to wrap into
|
||||
// - A: The target type
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The target value to wrap
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an Iso[S, A] and returns the wrapped value of type S
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type UserId int
|
||||
//
|
||||
// userIdIso := MakeIso(
|
||||
// func(id UserId) int { return int(id) },
|
||||
// func(i int) UserId { return UserId(i) },
|
||||
// )
|
||||
//
|
||||
// userId := Wrap[UserId](42)(userIdIso) // UserId(42)
|
||||
//
|
||||
// Note: This function is also available as From for semantic clarity.
|
||||
func Wrap[S, A any](a A) func(Iso[S, A]) S {
|
||||
return func(sa Iso[S, A]) S {
|
||||
return sa.ReverseGet(a)
|
||||
}
|
||||
}
|
||||
|
||||
// From wraps the value
|
||||
// To extracts the target value from a source value using an isomorphism.
|
||||
// This is an alias for Unwrap, provided for semantic clarity when the
|
||||
// direction of conversion is important.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type to convert to
|
||||
// - S: The source type
|
||||
//
|
||||
// Parameters:
|
||||
// - s: The source value to convert
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an Iso[S, A] and returns the converted value of type A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Email string
|
||||
// type ValidatedEmail struct{ value Email }
|
||||
//
|
||||
// emailIso := MakeIso(
|
||||
// func(ve ValidatedEmail) Email { return ve.value },
|
||||
// func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
|
||||
// )
|
||||
//
|
||||
// // Convert to Email
|
||||
// email := To[Email](ValidatedEmail{value: "user@example.com"})(emailIso)
|
||||
// // "user@example.com"
|
||||
func To[A, S any](s S) func(Iso[S, A]) A {
|
||||
return Unwrap[A, S](s)
|
||||
}
|
||||
|
||||
// To unwraps the value
|
||||
// From wraps a target value into a source value using an isomorphism.
|
||||
// This is an alias for Wrap, provided for semantic clarity when the
|
||||
// direction of conversion is important.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type to convert from
|
||||
// - A: The target type
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The target value to convert
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an Iso[S, A] and returns the converted value of type S
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Email string
|
||||
// type ValidatedEmail struct{ value Email }
|
||||
//
|
||||
// emailIso := MakeIso(
|
||||
// func(ve ValidatedEmail) Email { return ve.value },
|
||||
// func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
|
||||
// )
|
||||
//
|
||||
// // Convert from Email
|
||||
// validated := From[ValidatedEmail](Email("admin@example.com"))(emailIso)
|
||||
// // ValidatedEmail{value: "admin@example.com"}
|
||||
func From[S, A any](a A) func(Iso[S, A]) S {
|
||||
return Wrap[S](a)
|
||||
}
|
||||
|
||||
// imap is an internal helper that bidirectionally maps an isomorphism.
|
||||
// It transforms both directions of the isomorphism using the provided functions.
|
||||
func imap[S, A, B any](sa Iso[S, A], ab func(A) B, ba func(B) A) Iso[S, B] {
|
||||
return MakeIso(
|
||||
F.Flow2(sa.Get, ab),
|
||||
@@ -98,7 +361,43 @@ func imap[S, A, B any](sa Iso[S, A], ab func(A) B, ba func(B) A) Iso[S, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// IMap implements a bidirectional mapping of the transform
|
||||
// IMap bidirectionally maps the target type of an isomorphism.
|
||||
// Given Iso[S, A] and functions A → B and B → A, creates Iso[S, B].
|
||||
// This allows you to transform both directions of an isomorphism.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type (unchanged)
|
||||
// - A: The original target type
|
||||
// - B: The new target type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: Function to map from A to B
|
||||
// - ba: Function to map from B to A (inverse of ab)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms Iso[S, A] to Iso[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Celsius float64
|
||||
// type Kelvin float64
|
||||
//
|
||||
// celsiusIso := Id[Celsius]()
|
||||
//
|
||||
// // Create isomorphism to Kelvin
|
||||
// celsiusToKelvin := F.Pipe1(
|
||||
// celsiusIso,
|
||||
// IMap(
|
||||
// func(c Celsius) Kelvin { return Kelvin(c + 273.15) },
|
||||
// func(k Kelvin) Celsius { return Celsius(k - 273.15) },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// kelvin := celsiusToKelvin.Get(Celsius(20)) // 293.15 K
|
||||
// celsius := celsiusToKelvin.ReverseGet(Kelvin(293.15)) // 20°C
|
||||
//
|
||||
// Note: The functions ab and ba must be inverses of each other to maintain
|
||||
// the isomorphism laws.
|
||||
func IMap[S, A, B any](ab func(A) B, ba func(B) A) func(Iso[S, A]) Iso[S, B] {
|
||||
return func(sa Iso[S, A]) Iso[S, B] {
|
||||
return imap(sa, ab, ba)
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
package lens
|
||||
|
||||
import (
|
||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
|
||||
@@ -44,32 +41,156 @@ func setCopyCurried[SET ~func(A) Endomorphism[*S], S, A any](setter SET) func(a
|
||||
}
|
||||
}
|
||||
|
||||
// MakeLens creates a [Lens] based on a getter and a setter function. Make sure that the setter creates a (shallow) copy of the
|
||||
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeLensRef`
|
||||
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
|
||||
// MakeLens creates a [Lens] based on a getter and a setter F.
|
||||
//
|
||||
// The setter must create a (shallow) copy of the data structure. This happens automatically
|
||||
// when the data is passed by value. For pointer-based structures, use [MakeLensRef] instead.
|
||||
// For other reference types (slices, maps), ensure the setter creates a copy.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GET: Getter function type (S → A)
|
||||
// - SET: Setter function type (S, A → S)
|
||||
// - S: Source structure type
|
||||
// - A: Focus/field type
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to extract value A from structure S
|
||||
// - set: Function to update value A in structure S, returning a new S
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[S, A] that can get and set values immutably
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// nameLens := lens.MakeLens(
|
||||
// func(p Person) string { return p.Name },
|
||||
// func(p Person, name string) Person {
|
||||
// p.Name = name
|
||||
// return p
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// person := Person{Name: "Alice", Age: 30}
|
||||
// name := nameLens.Get(person) // "Alice"
|
||||
// updated := nameLens.Set("Bob")(person) // Person{Name: "Bob", Age: 30}
|
||||
func MakeLens[GET ~func(S) A, SET ~func(S, A) S, S, A any](get GET, set SET) Lens[S, A] {
|
||||
return MakeLensCurried(get, function.Curry2(F.Swap(set)))
|
||||
return MakeLensCurried(get, F.Curry2(F.Swap(set)))
|
||||
}
|
||||
|
||||
// MakeLensCurried creates a [Lens] based on a getter and a setter function. Make sure that the setter creates a (shallow) copy of the
|
||||
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeLensRef`
|
||||
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
|
||||
// MakeLensCurried creates a [Lens] with a curried setter F.
|
||||
//
|
||||
// This is similar to [MakeLens] but accepts a curried setter (A → S → S) instead of
|
||||
// an uncurried one (S, A → S). The curried form is more composable in functional pipelines.
|
||||
//
|
||||
// The setter must create a (shallow) copy of the data structure. This happens automatically
|
||||
// when the data is passed by value. For pointer-based structures, use [MakeLensRefCurried].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GET: Getter function type (S → A)
|
||||
// - SET: Curried setter function type (A → S → S)
|
||||
// - S: Source structure type
|
||||
// - A: Focus/field type
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to extract value A from structure S
|
||||
// - set: Curried function to update value A in structure S
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[S, A] that can get and set values immutably
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nameLens := lens.MakeLensCurried(
|
||||
// func(p Person) string { return p.Name },
|
||||
// func(name string) func(Person) Person {
|
||||
// return func(p Person) Person {
|
||||
// p.Name = name
|
||||
// return p
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
func MakeLensCurried[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](get GET, set SET) Lens[S, A] {
|
||||
return Lens[S, A]{Get: get, Set: set}
|
||||
}
|
||||
|
||||
// MakeLensRef creates a [Lens] based on a getter and a setter function. The setter passed in does not have to create a shallow
|
||||
// copy, the implementation wraps the setter into one that copies the pointer before modifying it
|
||||
// MakeLensRef creates a [Lens] for pointer-based structures.
|
||||
//
|
||||
// Such a [Lens] assumes that property A of S always exists
|
||||
// Unlike [MakeLens], the setter does not need to create a copy manually. This function
|
||||
// automatically wraps the setter to create a shallow copy of the pointed-to value before
|
||||
// modification, ensuring immutability.
|
||||
//
|
||||
// This lens assumes that property A always exists in structure S (i.e., it's not optional).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GET: Getter function type (*S → A)
|
||||
// - SET: Setter function type (*S, A → *S)
|
||||
// - S: Source structure type (will be used as *S)
|
||||
// - A: Focus/field type
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to extract value A from pointer *S
|
||||
// - set: Function to update value A in pointer *S (copying handled automatically)
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[*S, A] that can get and set values immutably on pointers
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// nameLens := lens.MakeLensRef(
|
||||
// func(p *Person) string { return p.Name },
|
||||
// func(p *Person, name string) *Person {
|
||||
// p.Name = name // No manual copy needed
|
||||
// return p
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// person := &Person{Name: "Alice", Age: 30}
|
||||
// updated := nameLens.Set("Bob")(person)
|
||||
// // person.Name is still "Alice", updated is a new pointer with Name "Bob"
|
||||
func MakeLensRef[GET ~func(*S) A, SET func(*S, A) *S, S, A any](get GET, set SET) Lens[*S, A] {
|
||||
return MakeLens(get, setCopy(set))
|
||||
}
|
||||
|
||||
// MakeLensRefCurried creates a [Lens] based on a getter and a setter function. The setter passed in does not have to create a shallow
|
||||
// copy, the implementation wraps the setter into one that copies the pointer before modifying it
|
||||
// MakeLensRefCurried creates a [Lens] for pointer-based structures with a curried setter.
|
||||
//
|
||||
// Such a [Lens] assumes that property A of S always exists
|
||||
// This combines the benefits of [MakeLensRef] (automatic copying) with [MakeLensCurried]
|
||||
// (curried setter for better composition). The setter does not need to create a copy manually;
|
||||
// this function automatically wraps it to ensure immutability.
|
||||
//
|
||||
// This lens assumes that property A always exists in structure S (i.e., it's not optional).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Source structure type (will be used as *S)
|
||||
// - A: Focus/field type
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to extract value A from pointer *S
|
||||
// - set: Curried function to update value A in pointer *S (copying handled automatically)
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[*S, A] that can get and set values immutably on pointers
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nameLens := lens.MakeLensRefCurried(
|
||||
// func(p *Person) string { return p.Name },
|
||||
// func(name string) func(*Person) *Person {
|
||||
// return func(p *Person) *Person {
|
||||
// p.Name = name // No manual copy needed
|
||||
// return p
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
func MakeLensRefCurried[S, A any](get func(*S) A, set func(A) Endomorphism[*S]) Lens[*S, A] {
|
||||
return MakeLensCurried(get, setCopyCurried(set))
|
||||
}
|
||||
@@ -79,12 +200,54 @@ func id[GET ~func(S) S, SET ~func(S, S) S, S any](creator func(get GET, set SET)
|
||||
return creator(F.Identity[S], F.Second[S, S])
|
||||
}
|
||||
|
||||
// Id returns a [Lens] implementing the identity operation
|
||||
// Id returns an identity [Lens] that focuses on the entire structure.
|
||||
//
|
||||
// The identity lens is useful as a starting point for lens composition or when you need
|
||||
// a lens that doesn't actually focus on a subpart. Get returns the structure unchanged,
|
||||
// and Set replaces the entire structure.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The structure type
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[S, S] where both source and focus are the same type
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// idLens := lens.Id[Person]()
|
||||
// person := Person{Name: "Alice", Age: 30}
|
||||
//
|
||||
// same := idLens.Get(person) // Returns person unchanged
|
||||
// replaced := idLens.Set(Person{Name: "Bob", Age: 25})(person)
|
||||
// // replaced is Person{Name: "Bob", Age: 25}
|
||||
func Id[S any]() Lens[S, S] {
|
||||
return id(MakeLens[Endomorphism[S], func(S, S) S])
|
||||
}
|
||||
|
||||
// IdRef returns a [Lens] implementing the identity operation
|
||||
// IdRef returns an identity [Lens] for pointer-based structures.
|
||||
//
|
||||
// This is the pointer version of [Id]. It focuses on the entire pointer structure,
|
||||
// with automatic copying to ensure immutability.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The structure type (will be used as *S)
|
||||
//
|
||||
// Returns:
|
||||
// - A Lens[*S, *S] where both source and focus are pointers to the same type
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// idLens := lens.IdRef[Person]()
|
||||
// person := &Person{Name: "Alice", Age: 30}
|
||||
//
|
||||
// same := idLens.Get(person) // Returns person pointer
|
||||
// replaced := idLens.Set(&Person{Name: "Bob", Age: 25})(person)
|
||||
// // person.Name is still "Alice", replaced is a new pointer
|
||||
func IdRef[S any]() Lens[*S, *S] {
|
||||
return id(MakeLensRef[Endomorphism[*S], func(*S, *S) *S])
|
||||
}
|
||||
@@ -105,111 +268,94 @@ func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GE
|
||||
}
|
||||
}
|
||||
|
||||
// Compose combines two lenses and allows to narrow down the focus to a sub-lens
|
||||
// Compose combines two lenses to focus on a deeply nested field.
|
||||
//
|
||||
// Given a lens from S to A and a lens from A to B, Compose creates a lens from S to B.
|
||||
// This allows you to navigate through nested structures in a composable way.
|
||||
//
|
||||
// The composition follows the mathematical property: (sa ∘ ab).Get = ab.Get ∘ sa.Get
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Outer structure type
|
||||
// - A: Intermediate structure type
|
||||
// - B: Inner focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: Lens from A to B (inner lens)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns a Lens[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Address struct {
|
||||
// Street string
|
||||
// City string
|
||||
// }
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Address Address
|
||||
// }
|
||||
//
|
||||
// addressLens := lens.MakeLens(
|
||||
// func(p Person) Address { return p.Address },
|
||||
// func(p Person, a Address) Person { p.Address = a; return p },
|
||||
// )
|
||||
//
|
||||
// streetLens := lens.MakeLens(
|
||||
// func(a Address) string { return a.Street },
|
||||
// func(a Address, s string) Address { a.Street = s; return a },
|
||||
// )
|
||||
//
|
||||
// // Compose to access street directly from person
|
||||
// personStreetLens := F.Pipe1(addressLens, lens.Compose[Person](streetLens))
|
||||
//
|
||||
// person := Person{Name: "Alice", Address: Address{Street: "Main St"}}
|
||||
// street := personStreetLens.Get(person) // "Main St"
|
||||
// updated := personStreetLens.Set("Oak Ave")(person)
|
||||
func Compose[S, A, B any](ab Lens[A, B]) func(Lens[S, A]) Lens[S, B] {
|
||||
return compose(MakeLens[func(S) B, func(S, B) S], ab)
|
||||
}
|
||||
|
||||
// ComposeOption combines a `Lens` that returns an optional value with a `Lens` that returns a definite value
|
||||
// the getter returns an `Option[B]` because the container `A` could already be an option
|
||||
// if the setter is invoked with `Some[B]` then the value of `B` will be set, potentially on a default value of `A` if `A` did not exist
|
||||
// if the setter is invoked with `None[B]` then the container `A` is reset to `None[A]` because this is the only way to remove `B`
|
||||
func ComposeOption[S, B, A any](defaultA A) func(ab Lens[A, B]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
defa := F.Constant(defaultA)
|
||||
return func(ab Lens[A, B]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
foldab := O.Fold(O.None[B], F.Flow2(ab.Get, O.Some[B]))
|
||||
return func(sa Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
// set A on S
|
||||
seta := F.Flow2(
|
||||
O.Some[A],
|
||||
sa.Set,
|
||||
)
|
||||
// remove A from S
|
||||
unseta := F.Nullary2(
|
||||
O.None[A],
|
||||
sa.Set,
|
||||
)
|
||||
return MakeLens(
|
||||
F.Flow2(sa.Get, foldab),
|
||||
func(s S, ob O.Option[B]) S {
|
||||
return F.Pipe2(
|
||||
ob,
|
||||
O.Fold(unseta, func(b B) Endomorphism[S] {
|
||||
setbona := F.Flow2(
|
||||
ab.Set(b),
|
||||
seta,
|
||||
)
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.Get,
|
||||
O.Fold(
|
||||
F.Nullary2(
|
||||
defa,
|
||||
setbona,
|
||||
),
|
||||
setbona,
|
||||
),
|
||||
)
|
||||
}),
|
||||
EM.Ap(s),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeOptions combines a `Lens` that returns an optional value with a `Lens` that returns another optional value
|
||||
// the getter returns `None[B]` if either `A` or `B` is `None`
|
||||
// if the setter is called with `Some[B]` and `A` exists, 'A' is updated with `B`
|
||||
// if the setter is called with `Some[B]` and `A` does not exist, the default of 'A' is updated with `B`
|
||||
// if the setter is called with `None[B]` and `A` does not exist this is the identity operation on 'S'
|
||||
// if the setter is called with `None[B]` and `A` does exist, 'B' is removed from 'A'
|
||||
func ComposeOptions[S, B, A any](defaultA A) func(ab Lens[A, O.Option[B]]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
defa := F.Constant(defaultA)
|
||||
noops := EM.Identity[S]
|
||||
noneb := O.None[B]()
|
||||
return func(ab Lens[A, O.Option[B]]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
unsetb := ab.Set(noneb)
|
||||
return func(sa Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
|
||||
// sets an A onto S
|
||||
seta := F.Flow2(
|
||||
O.Some[A],
|
||||
sa.Set,
|
||||
)
|
||||
return MakeLensCurried(
|
||||
F.Flow2(
|
||||
sa.Get,
|
||||
O.Chain(ab.Get),
|
||||
),
|
||||
func(b O.Option[B]) Endomorphism[S] {
|
||||
return func(s S) S {
|
||||
return O.MonadFold(b, func() Endomorphism[S] {
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.Get,
|
||||
O.Fold(noops, F.Flow2(unsetb, seta)),
|
||||
)
|
||||
}, func(b B) Endomorphism[S] {
|
||||
// sets a B onto an A
|
||||
setb := F.Flow2(
|
||||
ab.Set(O.Some(b)),
|
||||
seta,
|
||||
)
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.Get,
|
||||
O.Fold(F.Nullary2(defa, setb), setb),
|
||||
)
|
||||
})(s)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose combines two lenses and allows to narrow down the focus to a sub-lens
|
||||
// ComposeRef combines two lenses for pointer-based structures.
|
||||
//
|
||||
// This is the pointer version of [Compose], automatically handling copying to ensure immutability.
|
||||
// It allows you to navigate through nested pointer structures in a composable way.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Outer structure type (will be used as *S)
|
||||
// - A: Intermediate structure type
|
||||
// - B: Inner focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: Lens from A to B (inner lens)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[*S, A] and returns a Lens[*S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Address struct {
|
||||
// Street string
|
||||
// }
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Address Address
|
||||
// }
|
||||
//
|
||||
// addressLens := lens.MakeLensRef(
|
||||
// func(p *Person) Address { return p.Address },
|
||||
// func(p *Person, a Address) *Person { p.Address = a; return p },
|
||||
// )
|
||||
//
|
||||
// streetLens := lens.MakeLens(
|
||||
// func(a Address) string { return a.Street },
|
||||
// func(a Address, s string) Address { a.Street = s; return a },
|
||||
// )
|
||||
//
|
||||
// personStreetLens := F.Pipe1(addressLens, lens.ComposeRef[Person](streetLens))
|
||||
func ComposeRef[S, A, B any](ab Lens[A, B]) func(Lens[*S, A]) Lens[*S, B] {
|
||||
return compose(MakeLensRef[func(*S) B, func(*S, B) *S], ab)
|
||||
}
|
||||
@@ -218,101 +364,108 @@ func modify[FCT ~func(A) A, S, A any](f FCT, sa Lens[S, A], s S) S {
|
||||
return sa.Set(f(sa.Get(s)))(s)
|
||||
}
|
||||
|
||||
// Modify changes a property of a [Lens] by invoking a transformation function
|
||||
// if the transformed property has not changes, the method returns the original state
|
||||
// Modify transforms a value through a lens using a transformation F.
|
||||
//
|
||||
// Instead of setting a specific value, Modify applies a function to the current value.
|
||||
// This is useful for updates like incrementing a counter, appending to a string, etc.
|
||||
// If the transformation doesn't change the value, the original structure is returned.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Structure type
|
||||
// - FCT: Transformation function type (A → A)
|
||||
// - A: Focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Transformation function to apply to the focused value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns an Endomorphism[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// counter := Counter{Value: 5}
|
||||
//
|
||||
// // Increment the counter
|
||||
// incremented := F.Pipe2(
|
||||
// valueLens,
|
||||
// lens.Modify[Counter](func(v int) int { return v + 1 }),
|
||||
// F.Ap(counter),
|
||||
// )
|
||||
// // incremented.Value == 6
|
||||
//
|
||||
// // Double the counter
|
||||
// doubled := F.Pipe2(
|
||||
// valueLens,
|
||||
// lens.Modify[Counter](func(v int) int { return v * 2 }),
|
||||
// F.Ap(counter),
|
||||
// )
|
||||
// // doubled.Value == 10
|
||||
func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S] {
|
||||
return function.Curry3(modify[FCT, S, A])(f)
|
||||
return F.Curry3(modify[FCT, S, A])(f)
|
||||
}
|
||||
|
||||
// IMap transforms the focus type of a lens using an isomorphism.
|
||||
//
|
||||
// An isomorphism is a pair of functions (A → B, B → A) that are inverses of each other.
|
||||
// IMap allows you to work with a lens in a different but equivalent type. This is useful
|
||||
// for unit conversions, encoding/decoding, or any bidirectional transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: Structure type
|
||||
// - AB: Forward transformation function type (A → B)
|
||||
// - BA: Backward transformation function type (B → A)
|
||||
// - A: Original focus type
|
||||
// - B: Transformed focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: Forward transformation (A → B)
|
||||
// - ba: Backward transformation (B → A)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[E, A] and returns a Lens[E, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Celsius float64
|
||||
// type Fahrenheit float64
|
||||
//
|
||||
// celsiusToFahrenheit := func(c Celsius) Fahrenheit {
|
||||
// return Fahrenheit(c*9/5 + 32)
|
||||
// }
|
||||
//
|
||||
// fahrenheitToCelsius := func(f Fahrenheit) Celsius {
|
||||
// return Celsius((f - 32) * 5 / 9)
|
||||
// }
|
||||
//
|
||||
// type Weather struct {
|
||||
// Temperature Celsius
|
||||
// }
|
||||
//
|
||||
// tempCelsiusLens := lens.MakeLens(
|
||||
// func(w Weather) Celsius { return w.Temperature },
|
||||
// func(w Weather, t Celsius) Weather { w.Temperature = t; return w },
|
||||
// )
|
||||
//
|
||||
// // Create a lens that works with Fahrenheit
|
||||
// tempFahrenheitLens := F.Pipe1(
|
||||
// tempCelsiusLens,
|
||||
// lens.IMap[Weather](celsiusToFahrenheit, fahrenheitToCelsius),
|
||||
// )
|
||||
//
|
||||
// weather := Weather{Temperature: 20} // 20°C
|
||||
// tempF := tempFahrenheitLens.Get(weather) // 68°F
|
||||
// updated := tempFahrenheitLens.Set(86)(weather) // Set to 86°F (30°C)
|
||||
func IMap[E any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Lens[E, A]) Lens[E, B] {
|
||||
return func(ea Lens[E, A]) Lens[E, B] {
|
||||
return Lens[E, B]{Get: F.Flow2(ea.Get, ab), Set: F.Flow2(ba, ea.Set)}
|
||||
}
|
||||
}
|
||||
|
||||
// fromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func fromPredicate[GET ~func(S) O.Option[A], SET ~func(S, O.Option[A]) S, S, A any](creator func(get GET, set SET) Lens[S, O.Option[A]], pred func(A) bool, nilValue A) func(sa Lens[S, A]) Lens[S, O.Option[A]] {
|
||||
fromPred := O.FromPredicate(pred)
|
||||
return func(sa Lens[S, A]) Lens[S, O.Option[A]] {
|
||||
fold := O.Fold(F.Bind1of1(sa.Set)(nilValue), sa.Set)
|
||||
return creator(F.Flow2(sa.Get, fromPred), func(s S, a O.Option[A]) S {
|
||||
return F.Pipe2(
|
||||
a,
|
||||
fold,
|
||||
EM.Ap(s),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func FromPredicate[S, A any](pred func(A) bool, nilValue A) func(sa Lens[S, A]) Lens[S, O.Option[A]] {
|
||||
return fromPredicate(MakeLens[func(S) O.Option[A], func(S, O.Option[A]) S], pred, nilValue)
|
||||
}
|
||||
|
||||
// FromPredicateRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func FromPredicateRef[S, A any](pred func(A) bool, nilValue A) func(sa Lens[*S, A]) Lens[*S, O.Option[A]] {
|
||||
return fromPredicate(MakeLensRef[func(*S) O.Option[A], func(*S, O.Option[A]) *S], pred, nilValue)
|
||||
}
|
||||
|
||||
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the `nil` value will be set instead
|
||||
func FromNillable[S, A any](sa Lens[S, *A]) Lens[S, O.Option[*A]] {
|
||||
return FromPredicate[S](F.IsNonNil[A], nil)(sa)
|
||||
}
|
||||
|
||||
// FromNillableRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the `nil` value will be set instead
|
||||
func FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, O.Option[*A]] {
|
||||
return FromPredicateRef[S](F.IsNonNil[A], nil)(sa)
|
||||
}
|
||||
|
||||
// fromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func fromNullableProp[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], isNullable func(A) O.Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
return func(sa Lens[S, A]) Lens[S, A] {
|
||||
return creator(F.Flow3(
|
||||
sa.Get,
|
||||
isNullable,
|
||||
O.GetOrElse(F.Constant(defaultValue)),
|
||||
), func(s S, a A) S {
|
||||
return sa.Set(a)(s)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func FromNullableProp[S, A any](isNullable func(A) O.Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
return fromNullableProp(MakeLens[func(S) A, func(S, A) S], isNullable, defaultValue)
|
||||
}
|
||||
|
||||
// FromNullablePropRef returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func FromNullablePropRef[S, A any](isNullable func(A) O.Option[A], defaultValue A) func(sa Lens[*S, A]) Lens[*S, A] {
|
||||
return fromNullableProp(MakeLensRef[func(*S) A, func(*S, A) *S], isNullable, defaultValue)
|
||||
}
|
||||
|
||||
// fromFromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func fromOption[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(sa Lens[S, O.Option[A]]) Lens[S, A] {
|
||||
return func(sa Lens[S, O.Option[A]]) Lens[S, A] {
|
||||
return creator(F.Flow2(
|
||||
sa.Get,
|
||||
O.GetOrElse(F.Constant(defaultValue)),
|
||||
), func(s S, a A) S {
|
||||
return sa.Set(O.Some(a))(s)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FromFromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func FromOption[S, A any](defaultValue A) func(sa Lens[S, O.Option[A]]) Lens[S, A] {
|
||||
return fromOption(MakeLens[func(S) A, func(S, A) S], defaultValue)
|
||||
}
|
||||
|
||||
// FromFromOptionRef returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, O.Option[A]]) Lens[*S, A] {
|
||||
return fromOption(MakeLensRef[func(*S) A, func(*S, A) *S], defaultValue)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -172,83 +171,6 @@ func TestPassByValue(t *testing.T) {
|
||||
assert.Equal(t, "value2", s2.name)
|
||||
}
|
||||
|
||||
func TestFromNullableProp(t *testing.T) {
|
||||
// default inner object
|
||||
defaultInner := &Inner{
|
||||
Value: 0,
|
||||
Foo: "foo",
|
||||
}
|
||||
// access to the value
|
||||
value := MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
// access to inner
|
||||
inner := FromNullableProp[Outer](O.FromNillable[Inner], defaultInner)(MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
// compose
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
Compose[Outer](value),
|
||||
)
|
||||
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
|
||||
// the checks
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{}))
|
||||
assert.Equal(t, 0, lens.Get(Outer{}))
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
|
||||
assert.Equal(t, 1, lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
|
||||
assert.Equal(t, outer1, Modify[Outer](F.Identity[int])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestComposeOption(t *testing.T) {
|
||||
// default inner object
|
||||
defaultInner := &Inner{
|
||||
Value: 0,
|
||||
Foo: "foo",
|
||||
}
|
||||
// access to the value
|
||||
value := MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
// access to inner
|
||||
inner := FromNillable(MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
// compose lenses
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
ComposeOption[Outer, int](defaultInner)(value),
|
||||
)
|
||||
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
|
||||
// the checks
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{}))
|
||||
assert.Equal(t, O.None[int](), lens.Get(Outer{}))
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
|
||||
assert.Equal(t, O.Some(1), lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
|
||||
assert.Equal(t, outer1, Modify[Outer](F.Identity[O.Option[int]])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestComposeOptions(t *testing.T) {
|
||||
// default inner object
|
||||
defaultValue1 := 1
|
||||
defaultFoo1 := "foo1"
|
||||
defaultInner := &InnerOpt{
|
||||
Value: &defaultValue1,
|
||||
Foo: &defaultFoo1,
|
||||
}
|
||||
// access to the value
|
||||
value := FromNillable(MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
|
||||
// access to inner
|
||||
inner := FromNillable(MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
|
||||
// compose lenses
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
ComposeOptions[OuterOpt, *int](defaultInner)(value),
|
||||
)
|
||||
// additional settings
|
||||
defaultValue2 := 2
|
||||
defaultFoo2 := "foo2"
|
||||
outer1 := OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}
|
||||
// the checks
|
||||
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{}))
|
||||
assert.Equal(t, O.None[*int](), lens.Get(OuterOpt{}))
|
||||
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo2}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}))
|
||||
assert.Equal(t, O.Some(&defaultValue1), lens.Get(OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}))
|
||||
assert.Equal(t, outer1, Modify[OuterOpt](F.Identity[O.Option[*int]])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestIdRef(t *testing.T) {
|
||||
idLens := IdRef[Street]()
|
||||
street := &Street{num: 1, name: "Main"}
|
||||
@@ -272,93 +194,6 @@ func TestComposeRef(t *testing.T) {
|
||||
assert.Equal(t, sampleStreet.name, sampleAddress.street.name) // Original unchanged
|
||||
}
|
||||
|
||||
func TestFromPredicateRef(t *testing.T) {
|
||||
type Person struct {
|
||||
age int
|
||||
}
|
||||
|
||||
ageLens := MakeLensRef(
|
||||
func(p *Person) int { return p.age },
|
||||
func(p *Person, age int) *Person {
|
||||
p.age = age
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
adultLens := FromPredicateRef[Person](func(age int) bool { return age >= 18 }, 0)(ageLens)
|
||||
|
||||
adult := &Person{age: 25}
|
||||
assert.Equal(t, O.Some(25), adultLens.Get(adult))
|
||||
|
||||
minor := &Person{age: 15}
|
||||
assert.Equal(t, O.None[int](), adultLens.Get(minor))
|
||||
}
|
||||
|
||||
func TestFromNillableRef(t *testing.T) {
|
||||
type Config struct {
|
||||
timeout *int
|
||||
}
|
||||
|
||||
timeoutLens := MakeLensRef(
|
||||
func(c *Config) *int { return c.timeout },
|
||||
func(c *Config, t *int) *Config {
|
||||
c.timeout = t
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
optLens := FromNillableRef(timeoutLens)
|
||||
|
||||
config := &Config{timeout: nil}
|
||||
assert.Equal(t, O.None[*int](), optLens.Get(config))
|
||||
|
||||
timeout := 30
|
||||
configWithTimeout := &Config{timeout: &timeout}
|
||||
assert.True(t, O.IsSome(optLens.Get(configWithTimeout)))
|
||||
}
|
||||
|
||||
func TestFromNullablePropRef(t *testing.T) {
|
||||
type Config struct {
|
||||
timeout *int
|
||||
}
|
||||
|
||||
timeoutLens := MakeLensRef(
|
||||
func(c *Config) *int { return c.timeout },
|
||||
func(c *Config, t *int) *Config {
|
||||
c.timeout = t
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
defaultTimeout := 30
|
||||
safeLens := FromNullablePropRef[Config](O.FromNillable[int], &defaultTimeout)(timeoutLens)
|
||||
|
||||
config := &Config{timeout: nil}
|
||||
assert.Equal(t, &defaultTimeout, safeLens.Get(config))
|
||||
}
|
||||
|
||||
func TestFromOptionRef(t *testing.T) {
|
||||
type Settings struct {
|
||||
retries O.Option[int]
|
||||
}
|
||||
|
||||
retriesLens := MakeLensRef(
|
||||
func(s *Settings) O.Option[int] { return s.retries },
|
||||
func(s *Settings, r O.Option[int]) *Settings {
|
||||
s.retries = r
|
||||
return s
|
||||
},
|
||||
)
|
||||
|
||||
safeLens := FromOptionRef[Settings](3)(retriesLens)
|
||||
|
||||
settings := &Settings{retries: O.None[int]()}
|
||||
assert.Equal(t, 3, safeLens.Get(settings))
|
||||
|
||||
settingsWithRetries := &Settings{retries: O.Some(5)}
|
||||
assert.Equal(t, 5, safeLens.Get(settingsWithRetries))
|
||||
}
|
||||
|
||||
func TestMakeLensCurried(t *testing.T) {
|
||||
nameLens := MakeLensCurried(
|
||||
func(s Street) string { return s.name },
|
||||
|
||||
192
v2/optics/lens/option/compose.go
Normal file
192
v2/optics/lens/option/compose.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Compose composes two lenses that both return optional values.
|
||||
//
|
||||
// This handles the case where both the intermediate structure A and the inner focus B are optional.
|
||||
// The getter returns None[B] if either A or B is None. The setter behavior is:
|
||||
// - Set(Some[B]) when A exists: Updates B in A
|
||||
// - Set(Some[B]) when A doesn't exist: Creates A with defaultA and sets B
|
||||
// - Set(None[B]) when A doesn't exist: Identity operation (no change)
|
||||
// - Set(None[B]) when A exists: Removes B from A (sets it to None)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Outer structure type
|
||||
// - B: Inner focus type (optional)
|
||||
// - A: Intermediate structure type (optional)
|
||||
//
|
||||
// Parameters:
|
||||
// - defaultA: Default value for A when it doesn't exist but B needs to be set
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a LensO[A, B] and returns a function that takes a
|
||||
// LensO[S, A] and returns a LensO[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Settings struct {
|
||||
// MaxRetries *int
|
||||
// }
|
||||
//
|
||||
// type Config struct {
|
||||
// Settings *Settings
|
||||
// }
|
||||
//
|
||||
// settingsLens := lens.FromNillable(lens.MakeLens(
|
||||
// func(c Config) *Settings { return c.Settings },
|
||||
// func(c Config, s *Settings) Config { c.Settings = s; return c },
|
||||
// ))
|
||||
//
|
||||
// retriesLens := lens.FromNillable(lens.MakeLensRef(
|
||||
// func(s *Settings) *int { return s.MaxRetries },
|
||||
// func(s *Settings, r *int) *Settings { s.MaxRetries = r; return s },
|
||||
// ))
|
||||
//
|
||||
// defaultSettings := &Settings{}
|
||||
// configRetriesLens := F.Pipe1(settingsLens,
|
||||
// lens.Compose[Config, *int](defaultSettings)(retriesLens))
|
||||
func Compose[S, B, A any](defaultA A) func(ab LensO[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
noneb := O.None[B]()
|
||||
return func(ab LensO[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
abGet := ab.Get
|
||||
abSetNone := ab.Set(noneb)
|
||||
return func(sa LensO[S, A]) LensO[S, B] {
|
||||
saGet := sa.Get
|
||||
// Pre-compute setter for Some[A]
|
||||
setSomeA := F.Flow2(O.Some[A], sa.Set)
|
||||
return lens.MakeLensCurried(
|
||||
F.Flow2(saGet, O.Chain(abGet)),
|
||||
func(optB Option[B]) Endomorphism[S] {
|
||||
return func(s S) S {
|
||||
optA := saGet(s)
|
||||
return O.MonadFold(
|
||||
optB,
|
||||
// optB is None
|
||||
func() S {
|
||||
return O.MonadFold(
|
||||
optA,
|
||||
// optA is None - no-op
|
||||
F.Constant(s),
|
||||
// optA is Some - unset B in A
|
||||
func(a A) S {
|
||||
return setSomeA(abSetNone(a))(s)
|
||||
},
|
||||
)
|
||||
},
|
||||
// optB is Some
|
||||
func(b B) S {
|
||||
setB := ab.Set(O.Some(b))
|
||||
return O.MonadFold(
|
||||
optA,
|
||||
// optA is None - create with defaultA
|
||||
func() S {
|
||||
return setSomeA(setB(defaultA))(s)
|
||||
},
|
||||
// optA is Some - update B in A
|
||||
func(a A) S {
|
||||
return setSomeA(setB(a))(s)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeOption composes a lens returning an optional value with a lens returning a definite value.
|
||||
//
|
||||
// This is useful when you have an optional intermediate structure and want to focus on a field
|
||||
// within it. The getter returns Option[B] because the container A might not exist. The setter
|
||||
// behavior depends on the input:
|
||||
// - Set(Some[B]): Updates B in A, creating A with defaultA if it doesn't exist
|
||||
// - Set(None[B]): Removes A entirely (sets it to None[A])
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Outer structure type
|
||||
// - B: Inner focus type (definite value)
|
||||
// - A: Intermediate structure type (optional)
|
||||
//
|
||||
// Parameters:
|
||||
// - defaultA: Default value for A when it doesn't exist but B needs to be set
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[A, B] and returns a function that takes a
|
||||
// LensO[S, A] and returns a LensO[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Database struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// type Config struct {
|
||||
// Database *Database
|
||||
// }
|
||||
//
|
||||
// dbLens := lens.FromNillable(lens.MakeLens(
|
||||
// func(c Config) *Database { return c.Database },
|
||||
// func(c Config, db *Database) Config { c.Database = db; return c },
|
||||
// ))
|
||||
//
|
||||
// portLens := lens.MakeLensRef(
|
||||
// func(db *Database) int { return db.Port },
|
||||
// func(db *Database, port int) *Database { db.Port = port; return db },
|
||||
// )
|
||||
//
|
||||
// defaultDB := &Database{Host: "localhost", Port: 5432}
|
||||
// configPortLens := F.Pipe1(dbLens, lens.ComposeOption[Config, int](defaultDB)(portLens))
|
||||
//
|
||||
// config := Config{Database: nil}
|
||||
// port := configPortLens.Get(config) // None[int]
|
||||
// updated := configPortLens.Set(O.Some(3306))(config)
|
||||
// // updated.Database.Port == 3306, Host == "localhost" (from default)
|
||||
func ComposeOption[S, B, A any](defaultA A) func(ab Lens[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
return func(ab Lens[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
abGet := ab.Get
|
||||
abSet := ab.Set
|
||||
return func(sa LensO[S, A]) LensO[S, B] {
|
||||
saGet := sa.Get
|
||||
saSet := sa.Set
|
||||
// Pre-compute setters
|
||||
setNoneA := saSet(O.None[A]())
|
||||
setSomeA := func(a A) Endomorphism[S] {
|
||||
return saSet(O.Some(a))
|
||||
}
|
||||
return lens.MakeLens(
|
||||
func(s S) Option[B] {
|
||||
return O.Map(abGet)(saGet(s))
|
||||
},
|
||||
func(s S, optB Option[B]) S {
|
||||
return O.Fold(
|
||||
// optB is None - remove A entirely
|
||||
F.Constant(setNoneA(s)),
|
||||
// optB is Some - set B
|
||||
func(b B) S {
|
||||
optA := saGet(s)
|
||||
return O.Fold(
|
||||
// optA is None - create with defaultA
|
||||
func() S {
|
||||
return setSomeA(abSet(b)(defaultA))(s)
|
||||
},
|
||||
// optA is Some - update B in A
|
||||
func(a A) S {
|
||||
return setSomeA(abSet(b)(a))(s)
|
||||
},
|
||||
)(optA)
|
||||
},
|
||||
)(optB)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
31
v2/optics/lens/option/coverage.out
Normal file
31
v2/optics/lens/option/coverage.out
Normal file
@@ -0,0 +1,31 @@
|
||||
mode: count
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:55.97,59.60 4 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:59.60,61.43 2 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:61.43,72.39 2 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:72.39,73.25 1 13
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:73.25,74.52 1 13
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:74.52,80.8 1 6
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:80.36,91.8 2 7
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:147.95,149.59 2 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:149.59,151.43 2 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:151.43,164.31 3 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:164.31,167.48 1 12
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:167.48,183.8 2 7
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:12.188,14.41 2 15
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:14.41,16.70 2 15
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:16.70,22.4 1 60
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:28.93,30.2 1 12
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:34.105,36.2 1 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:40.65,42.2 1 10
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:46.70,48.2 1 2
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:51.188,52.40 1 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:52.40,57.23 1 3
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:57.23,59.4 1 4
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:65.110,67.2 1 2
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:70.115,72.2 1 1
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:75.153,76.41 1 2
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:76.41,80.23 1 2
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:80.23,82.4 1 2
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:88.75,90.2 1 1
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/from.go:107.87,109.2 1 1
|
||||
github.com/IBM/fp-go/v2/optics/lens/option/option.go:63.67,65.2 1 1
|
||||
138
v2/optics/lens/option/doc.go
Normal file
138
v2/optics/lens/option/doc.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package option provides utilities for working with lenses that focus on optional values.
|
||||
//
|
||||
// This package extends the lens optics pattern to handle Option types, enabling safe
|
||||
// manipulation of potentially absent values in nested data structures. It provides
|
||||
// functions for creating, composing, and transforming lenses that work with optional
|
||||
// fields.
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// A LensO[S, A] is a Lens[S, Option[A]] - a lens that focuses on an optional value A
|
||||
// within a structure S. This is particularly useful when dealing with nullable pointers,
|
||||
// optional fields, or values that may not always be present.
|
||||
//
|
||||
// # Key Functions
|
||||
//
|
||||
// Creating Lenses from Optional Values:
|
||||
// - FromNillable: Creates a lens from a nullable pointer field
|
||||
// - FromNillableRef: Pointer-based version of FromNillable
|
||||
// - FromPredicate: Creates a lens based on a predicate function
|
||||
// - FromPredicateRef: Pointer-based version of FromPredicate
|
||||
// - FromOption: Converts an optional lens to a definite lens with a default value
|
||||
// - FromOptionRef: Pointer-based version of FromOption
|
||||
// - FromNullableProp: Creates a lens with a default value for nullable properties
|
||||
// - FromNullablePropRef: Pointer-based version of FromNullableProp
|
||||
//
|
||||
// Composing Lenses:
|
||||
// - ComposeOption: Composes a lens returning Option[A] with a lens returning B
|
||||
// - ComposeOptions: Composes two lenses that both return optional values
|
||||
//
|
||||
// Conversions:
|
||||
// - AsTraversal: Converts a lens to a traversal for use with traversal operations
|
||||
//
|
||||
// # Usage Examples
|
||||
//
|
||||
// Working with nullable pointers:
|
||||
//
|
||||
// type Config struct {
|
||||
// Database *DatabaseConfig
|
||||
// }
|
||||
//
|
||||
// type DatabaseConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Create a lens for the optional database config
|
||||
// dbLens := lens.FromNillable(lens.MakeLens(
|
||||
// func(c Config) *DatabaseConfig { return c.Database },
|
||||
// func(c Config, db *DatabaseConfig) Config { c.Database = db; return c },
|
||||
// ))
|
||||
//
|
||||
// // Access the optional value
|
||||
// config := Config{Database: nil}
|
||||
// dbOpt := dbLens.Get(config) // Returns None[*DatabaseConfig]
|
||||
//
|
||||
// // Set a value
|
||||
// newDB := &DatabaseConfig{Host: "localhost", Port: 5432}
|
||||
// updated := dbLens.Set(O.Some(newDB))(config)
|
||||
//
|
||||
// Composing optional lenses:
|
||||
//
|
||||
// // Lens to access port through optional database
|
||||
// portLens := lens.MakeLensRef(
|
||||
// func(db *DatabaseConfig) int { return db.Port },
|
||||
// func(db *DatabaseConfig, port int) *DatabaseConfig { db.Port = port; return db },
|
||||
// )
|
||||
//
|
||||
// defaultDB := &DatabaseConfig{Host: "localhost", Port: 5432}
|
||||
// configPortLens := F.Pipe1(dbLens,
|
||||
// lens.ComposeOption[Config, int](defaultDB)(portLens))
|
||||
//
|
||||
// // Get returns None if database is not set
|
||||
// port := configPortLens.Get(config) // None[int]
|
||||
//
|
||||
// // Set creates the database with default values if needed
|
||||
// withPort := configPortLens.Set(O.Some(3306))(config)
|
||||
// // withPort.Database.Port == 3306, Host == "localhost"
|
||||
//
|
||||
// Working with predicates:
|
||||
//
|
||||
// type Person struct {
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLensRef(
|
||||
// func(p *Person) int { return p.Age },
|
||||
// func(p *Person, age int) *Person { p.Age = age; return p },
|
||||
// )
|
||||
//
|
||||
// // Only consider adults (age >= 18)
|
||||
// adultLens := lens.FromPredicateRef[Person](
|
||||
// func(age int) bool { return age >= 18 },
|
||||
// 0, // nil value for non-adults
|
||||
// )(ageLens)
|
||||
//
|
||||
// adult := &Person{Age: 25}
|
||||
// adultLens.Get(adult) // Some(25)
|
||||
//
|
||||
// minor := &Person{Age: 15}
|
||||
// adultLens.Get(minor) // None[int]
|
||||
//
|
||||
// # Design Patterns
|
||||
//
|
||||
// The package follows functional programming principles:
|
||||
// - Immutability: All operations return new values rather than modifying in place
|
||||
// - Composition: Lenses can be composed to access deeply nested optional values
|
||||
// - Type Safety: The type system ensures correct usage at compile time
|
||||
// - Lawful: All lenses satisfy the lens laws (get-put, put-get, put-put)
|
||||
//
|
||||
// # Performance Considerations
|
||||
//
|
||||
// Lens operations are generally efficient, but composing many lenses can create
|
||||
// function call overhead. For performance-critical code, consider:
|
||||
// - Caching composed lenses rather than recreating them
|
||||
// - Using direct field access for simple cases
|
||||
// - Profiling to identify bottlenecks
|
||||
//
|
||||
// # Related Packages
|
||||
//
|
||||
// - github.com/IBM/fp-go/v2/optics/lens: Core lens functionality
|
||||
// - github.com/IBM/fp-go/v2/option: Option type and operations
|
||||
// - github.com/IBM/fp-go/v2/optics/traversal/option: Traversals for optional values
|
||||
package option
|
||||
109
v2/optics/lens/option/from.go
Normal file
109
v2/optics/lens/option/from.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// fromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func fromPredicate[GET ~func(S) Option[A], SET ~func(S, Option[A]) S, S, A any](creator func(get GET, set SET) LensO[S, A], pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
|
||||
fromPred := O.FromPredicate(pred)
|
||||
return func(sa Lens[S, A]) LensO[S, A] {
|
||||
fold := O.Fold(F.Bind1of1(sa.Set)(nilValue), sa.Set)
|
||||
return creator(F.Flow2(sa.Get, fromPred), func(s S, a Option[A]) S {
|
||||
return F.Pipe2(
|
||||
a,
|
||||
fold,
|
||||
EM.Ap(s),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func FromPredicate[S, A any](pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
|
||||
return fromPredicate(lens.MakeLens[func(S) Option[A], func(S, Option[A]) S], pred, nilValue)
|
||||
}
|
||||
|
||||
// FromPredicateRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func FromPredicateRef[S, A any](pred func(A) bool, nilValue A) func(sa Lens[*S, A]) Lens[*S, Option[A]] {
|
||||
return fromPredicate(lens.MakeLensRef[func(*S) Option[A], func(*S, Option[A]) *S], pred, nilValue)
|
||||
}
|
||||
|
||||
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the `nil` value will be set instead
|
||||
func FromNillable[S, A any](sa Lens[S, *A]) Lens[S, Option[*A]] {
|
||||
return FromPredicate[S](F.IsNonNil[A], nil)(sa)
|
||||
}
|
||||
|
||||
// FromNillableRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the `nil` value will be set instead
|
||||
func FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, Option[*A]] {
|
||||
return FromPredicateRef[S](F.IsNonNil[A], nil)(sa)
|
||||
}
|
||||
|
||||
// fromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func fromNullableProp[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
return func(sa Lens[S, A]) Lens[S, A] {
|
||||
return creator(F.Flow3(
|
||||
sa.Get,
|
||||
isNullable,
|
||||
O.GetOrElse(F.Constant(defaultValue)),
|
||||
), func(s S, a A) S {
|
||||
return sa.Set(a)(s)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func FromNullableProp[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
return fromNullableProp(lens.MakeLens[func(S) A, func(S, A) S], isNullable, defaultValue)
|
||||
}
|
||||
|
||||
// FromNullablePropRef returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func FromNullablePropRef[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[*S, A]) Lens[*S, A] {
|
||||
return fromNullableProp(lens.MakeLensRef[func(*S) A, func(*S, A) *S], isNullable, defaultValue)
|
||||
}
|
||||
|
||||
// fromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func fromOption[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
|
||||
return func(sa LensO[S, A]) Lens[S, A] {
|
||||
return creator(F.Flow2(
|
||||
sa.Get,
|
||||
O.GetOrElse(F.Constant(defaultValue)),
|
||||
), func(s S, a A) S {
|
||||
return sa.Set(O.Some(a))(s)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func FromOption[S, A any](defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
|
||||
return fromOption(lens.MakeLens[func(S) A, func(S, A) S], defaultValue)
|
||||
}
|
||||
|
||||
// FromOptionRef creates a lens from an Option property with a default value for pointer structures.
|
||||
//
|
||||
// This is the pointer version of [FromOption], with automatic copying to ensure immutability.
|
||||
// The getter returns the value inside Some[A], or the defaultValue if it's None[A].
|
||||
// The setter always wraps the value in Some[A].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: Structure type (will be used as *S)
|
||||
// - A: Focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - defaultValue: Value to return when the Option is None
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[*S, Option[A]] and returns a Lens[*S, A]
|
||||
func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, Option[A]]) Lens[*S, A] {
|
||||
return fromOption(lens.MakeLensRef[func(*S) A, func(*S, A) *S], defaultValue)
|
||||
}
|
||||
759
v2/optics/lens/option/lens_test.go
Normal file
759
v2/optics/lens/option/lens_test.go
Normal file
@@ -0,0 +1,759 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
EQT "github.com/IBM/fp-go/v2/eq/testing"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type (
|
||||
Street struct {
|
||||
name string
|
||||
}
|
||||
|
||||
Address struct {
|
||||
street *Street
|
||||
}
|
||||
|
||||
Inner struct {
|
||||
Value int
|
||||
Foo string
|
||||
}
|
||||
|
||||
InnerOpt struct {
|
||||
Value *int
|
||||
Foo *string
|
||||
}
|
||||
|
||||
Outer struct {
|
||||
inner *Inner
|
||||
}
|
||||
|
||||
OuterOpt struct {
|
||||
inner *InnerOpt
|
||||
}
|
||||
)
|
||||
|
||||
func (outer Outer) GetInner() *Inner {
|
||||
return outer.inner
|
||||
}
|
||||
|
||||
func (outer Outer) SetInner(inner *Inner) Outer {
|
||||
outer.inner = inner
|
||||
return outer
|
||||
}
|
||||
|
||||
func (outer OuterOpt) GetInnerOpt() *InnerOpt {
|
||||
return outer.inner
|
||||
}
|
||||
|
||||
func (outer OuterOpt) SetInnerOpt(inner *InnerOpt) OuterOpt {
|
||||
outer.inner = inner
|
||||
return outer
|
||||
}
|
||||
|
||||
func (inner *Inner) GetValue() int {
|
||||
return inner.Value
|
||||
}
|
||||
|
||||
func (inner *Inner) SetValue(value int) *Inner {
|
||||
inner.Value = value
|
||||
return inner
|
||||
}
|
||||
|
||||
func (inner *InnerOpt) GetValue() *int {
|
||||
return inner.Value
|
||||
}
|
||||
|
||||
func (inner *InnerOpt) SetValue(value *int) *InnerOpt {
|
||||
inner.Value = value
|
||||
return inner
|
||||
}
|
||||
|
||||
func (street *Street) GetName() string {
|
||||
return street.name
|
||||
}
|
||||
|
||||
func (street *Street) SetName(name string) *Street {
|
||||
street.name = name
|
||||
return street
|
||||
}
|
||||
|
||||
func (addr *Address) GetStreet() *Street {
|
||||
return addr.street
|
||||
}
|
||||
|
||||
func (addr *Address) SetStreet(s *Street) *Address {
|
||||
addr.street = s
|
||||
return addr
|
||||
}
|
||||
|
||||
var (
|
||||
streetLens = L.MakeLensRef((*Street).GetName, (*Street).SetName)
|
||||
addrLens = L.MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
|
||||
|
||||
sampleStreet = Street{name: "Schönaicherstr"}
|
||||
sampleAddress = Address{street: &sampleStreet}
|
||||
)
|
||||
|
||||
func TestComposeOption(t *testing.T) {
|
||||
// default inner object
|
||||
defaultInner := &Inner{
|
||||
Value: 0,
|
||||
Foo: "foo",
|
||||
}
|
||||
// access to the value
|
||||
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
// access to inner
|
||||
inner := FromNillable(L.MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
// compose lenses
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
ComposeOption[Outer, int](defaultInner)(value),
|
||||
)
|
||||
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
|
||||
// the checks
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{}))
|
||||
assert.Equal(t, O.None[int](), lens.Get(Outer{}))
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
|
||||
assert.Equal(t, O.Some(1), lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
|
||||
assert.Equal(t, outer1, L.Modify[Outer](F.Identity[Option[int]])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestComposeOptions(t *testing.T) {
|
||||
// default inner object
|
||||
defaultValue1 := 1
|
||||
defaultFoo1 := "foo1"
|
||||
defaultInner := &InnerOpt{
|
||||
Value: &defaultValue1,
|
||||
Foo: &defaultFoo1,
|
||||
}
|
||||
// access to the value
|
||||
value := FromNillable(L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
|
||||
// access to inner
|
||||
inner := FromNillable(L.MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
|
||||
// compose lenses
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
Compose[OuterOpt, *int](defaultInner)(value),
|
||||
)
|
||||
// additional settings
|
||||
defaultValue2 := 2
|
||||
defaultFoo2 := "foo2"
|
||||
outer1 := OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}
|
||||
// the checks
|
||||
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{}))
|
||||
assert.Equal(t, O.None[*int](), lens.Get(OuterOpt{}))
|
||||
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo2}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}))
|
||||
assert.Equal(t, O.Some(&defaultValue1), lens.Get(OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}))
|
||||
assert.Equal(t, outer1, L.Modify[OuterOpt](F.Identity[Option[*int]])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestFromNullableProp(t *testing.T) {
|
||||
// default inner object
|
||||
defaultInner := &Inner{
|
||||
Value: 0,
|
||||
Foo: "foo",
|
||||
}
|
||||
// access to the value
|
||||
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
// access to inner
|
||||
inner := FromNullableProp[Outer](O.FromNillable[Inner], defaultInner)(L.MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
// compose
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
L.Compose[Outer](value),
|
||||
)
|
||||
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
|
||||
// the checks
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{}))
|
||||
assert.Equal(t, 0, lens.Get(Outer{}))
|
||||
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
|
||||
assert.Equal(t, 1, lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
|
||||
assert.Equal(t, outer1, L.Modify[Outer](F.Identity[int])(lens)(outer1))
|
||||
}
|
||||
|
||||
func TestFromPredicateRef(t *testing.T) {
|
||||
type Person struct {
|
||||
age int
|
||||
}
|
||||
|
||||
ageLens := L.MakeLensRef(
|
||||
func(p *Person) int { return p.age },
|
||||
func(p *Person, age int) *Person {
|
||||
p.age = age
|
||||
return p
|
||||
},
|
||||
)
|
||||
|
||||
adultLens := FromPredicateRef[Person](func(age int) bool { return age >= 18 }, 0)(ageLens)
|
||||
|
||||
adult := &Person{age: 25}
|
||||
assert.Equal(t, O.Some(25), adultLens.Get(adult))
|
||||
|
||||
minor := &Person{age: 15}
|
||||
assert.Equal(t, O.None[int](), adultLens.Get(minor))
|
||||
}
|
||||
|
||||
func TestFromNillableRef(t *testing.T) {
|
||||
type Config struct {
|
||||
timeout *int
|
||||
}
|
||||
|
||||
timeoutLens := L.MakeLensRef(
|
||||
func(c *Config) *int { return c.timeout },
|
||||
func(c *Config, t *int) *Config {
|
||||
c.timeout = t
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
optLens := FromNillableRef(timeoutLens)
|
||||
|
||||
config := &Config{timeout: nil}
|
||||
assert.Equal(t, O.None[*int](), optLens.Get(config))
|
||||
|
||||
timeout := 30
|
||||
configWithTimeout := &Config{timeout: &timeout}
|
||||
assert.True(t, O.IsSome(optLens.Get(configWithTimeout)))
|
||||
}
|
||||
|
||||
func TestFromNullablePropRef(t *testing.T) {
|
||||
type Config struct {
|
||||
timeout *int
|
||||
}
|
||||
|
||||
timeoutLens := L.MakeLensRef(
|
||||
func(c *Config) *int { return c.timeout },
|
||||
func(c *Config, t *int) *Config {
|
||||
c.timeout = t
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
defaultTimeout := 30
|
||||
safeLens := FromNullablePropRef[Config](O.FromNillable[int], &defaultTimeout)(timeoutLens)
|
||||
|
||||
config := &Config{timeout: nil}
|
||||
assert.Equal(t, &defaultTimeout, safeLens.Get(config))
|
||||
}
|
||||
|
||||
func TestFromOptionRef(t *testing.T) {
|
||||
type Settings struct {
|
||||
retries Option[int]
|
||||
}
|
||||
|
||||
retriesLens := L.MakeLensRef(
|
||||
func(s *Settings) Option[int] { return s.retries },
|
||||
func(s *Settings, r Option[int]) *Settings {
|
||||
s.retries = r
|
||||
return s
|
||||
},
|
||||
)
|
||||
|
||||
safeLens := FromOptionRef[Settings](3)(retriesLens)
|
||||
|
||||
settings := &Settings{retries: O.None[int]()}
|
||||
assert.Equal(t, 3, safeLens.Get(settings))
|
||||
|
||||
settingsWithRetries := &Settings{retries: O.Some(5)}
|
||||
assert.Equal(t, 5, safeLens.Get(settingsWithRetries))
|
||||
}
|
||||
|
||||
func TestFromOption(t *testing.T) {
|
||||
type Config struct {
|
||||
retries Option[int]
|
||||
}
|
||||
|
||||
retriesLens := L.MakeLens(
|
||||
func(c Config) Option[int] { return c.retries },
|
||||
func(c Config, r Option[int]) Config { c.retries = r; return c },
|
||||
)
|
||||
|
||||
defaultRetries := 3
|
||||
safeLens := FromOption[Config](defaultRetries)(retriesLens)
|
||||
|
||||
// Test with None - should return default
|
||||
config := Config{retries: O.None[int]()}
|
||||
assert.Equal(t, defaultRetries, safeLens.Get(config))
|
||||
|
||||
// Test with Some - should return the value
|
||||
configWithRetries := Config{retries: O.Some(5)}
|
||||
assert.Equal(t, 5, safeLens.Get(configWithRetries))
|
||||
|
||||
// Test setter - should always set Some
|
||||
updated := safeLens.Set(10)(config)
|
||||
assert.Equal(t, O.Some(10), updated.retries)
|
||||
|
||||
// Test setter on existing Some - should replace
|
||||
updated2 := safeLens.Set(7)(configWithRetries)
|
||||
assert.Equal(t, O.Some(7), updated2.retries)
|
||||
}
|
||||
|
||||
func TestAsTraversal(t *testing.T) {
|
||||
type Data struct {
|
||||
value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(d Data) int { return d.value },
|
||||
func(d Data, v int) Data { d.value = v; return d },
|
||||
)
|
||||
|
||||
// Convert lens to traversal
|
||||
traversal := AsTraversal[Data, int]()(valueLens)
|
||||
|
||||
// Test that traversal is created (basic smoke test)
|
||||
assert.NotNil(t, traversal)
|
||||
|
||||
// The traversal should work with the data
|
||||
data := Data{value: 42}
|
||||
|
||||
// Verify the traversal can be used (it's a function that takes a functor)
|
||||
// This is a basic smoke test to ensure the conversion works
|
||||
assert.NotNil(t, data)
|
||||
assert.Equal(t, 42, valueLens.Get(data))
|
||||
}
|
||||
|
||||
func TestComposeOptionsEdgeCases(t *testing.T) {
|
||||
// Test setting None when inner doesn't exist
|
||||
defaultValue1 := 1
|
||||
defaultFoo1 := "foo1"
|
||||
defaultInner := &InnerOpt{
|
||||
Value: &defaultValue1,
|
||||
Foo: &defaultFoo1,
|
||||
}
|
||||
|
||||
value := FromNillable(L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
|
||||
inner := FromNillable(L.MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
Compose[OuterOpt, *int](defaultInner)(value),
|
||||
)
|
||||
|
||||
// Setting None when inner doesn't exist should be a no-op
|
||||
emptyOuter := OuterOpt{}
|
||||
result := lens.Set(O.None[*int]())(emptyOuter)
|
||||
assert.Equal(t, O.None[*InnerOpt](), inner.Get(result))
|
||||
|
||||
// Setting None when inner exists should unset the value
|
||||
defaultValue2 := 2
|
||||
defaultFoo2 := "foo2"
|
||||
outerWithInner := OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}
|
||||
result2 := lens.Set(O.None[*int]())(outerWithInner)
|
||||
assert.NotNil(t, result2.inner)
|
||||
assert.Nil(t, result2.inner.Value)
|
||||
assert.Equal(t, &defaultFoo2, result2.inner.Foo)
|
||||
}
|
||||
|
||||
func TestComposeOptionEdgeCases(t *testing.T) {
|
||||
defaultInner := &Inner{
|
||||
Value: 0,
|
||||
Foo: "foo",
|
||||
}
|
||||
|
||||
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
inner := FromNillable(L.MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
lens := F.Pipe1(
|
||||
inner,
|
||||
ComposeOption[Outer, int](defaultInner)(value),
|
||||
)
|
||||
|
||||
// Setting None should remove the inner entirely
|
||||
outerWithInner := Outer{inner: &Inner{Value: 42, Foo: "bar"}}
|
||||
result := lens.Set(O.None[int]())(outerWithInner)
|
||||
assert.Nil(t, result.inner)
|
||||
|
||||
// Getting from empty should return None
|
||||
emptyOuter := Outer{}
|
||||
assert.Equal(t, O.None[int](), lens.Get(emptyOuter))
|
||||
}
|
||||
|
||||
func TestFromPredicateEdgeCases(t *testing.T) {
|
||||
type Score struct {
|
||||
points int
|
||||
}
|
||||
|
||||
pointsLens := L.MakeLens(
|
||||
func(s Score) int { return s.points },
|
||||
func(s Score, p int) Score { s.points = p; return s },
|
||||
)
|
||||
|
||||
// Only positive scores are valid
|
||||
validLens := FromPredicate[Score](func(p int) bool { return p > 0 }, 0)(pointsLens)
|
||||
|
||||
// Test with valid score
|
||||
validScore := Score{points: 100}
|
||||
assert.Equal(t, O.Some(100), validLens.Get(validScore))
|
||||
|
||||
// Test with invalid score (zero)
|
||||
zeroScore := Score{points: 0}
|
||||
assert.Equal(t, O.None[int](), validLens.Get(zeroScore))
|
||||
|
||||
// Test with invalid score (negative)
|
||||
negativeScore := Score{points: -10}
|
||||
assert.Equal(t, O.None[int](), validLens.Get(negativeScore))
|
||||
|
||||
// Test setting None sets the nil value
|
||||
result := validLens.Set(O.None[int]())(validScore)
|
||||
assert.Equal(t, 0, result.points)
|
||||
|
||||
// Test setting Some sets the value
|
||||
result2 := validLens.Set(O.Some(50))(zeroScore)
|
||||
assert.Equal(t, 50, result2.points)
|
||||
}
|
||||
|
||||
func TestFromNullablePropEdgeCases(t *testing.T) {
|
||||
type Container struct {
|
||||
item *string
|
||||
}
|
||||
|
||||
itemLens := L.MakeLens(
|
||||
func(c Container) *string { return c.item },
|
||||
func(c Container, i *string) Container { c.item = i; return c },
|
||||
)
|
||||
|
||||
defaultItem := "default"
|
||||
safeLens := FromNullableProp[Container](O.FromNillable[string], &defaultItem)(itemLens)
|
||||
|
||||
// Test with nil - should return default
|
||||
emptyContainer := Container{item: nil}
|
||||
assert.Equal(t, &defaultItem, safeLens.Get(emptyContainer))
|
||||
|
||||
// Test with value - should return the value
|
||||
value := "actual"
|
||||
containerWithItem := Container{item: &value}
|
||||
assert.Equal(t, &value, safeLens.Get(containerWithItem))
|
||||
|
||||
// Test setter
|
||||
newValue := "new"
|
||||
updated := safeLens.Set(&newValue)(emptyContainer)
|
||||
assert.Equal(t, &newValue, updated.item)
|
||||
}
|
||||
|
||||
// Lens Law Tests for LensO types
|
||||
|
||||
func TestFromNillableLensLaws(t *testing.T) {
|
||||
type Config struct {
|
||||
timeout *int
|
||||
}
|
||||
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) *int { return c.timeout },
|
||||
func(c Config, t *int) Config { c.timeout = t; return c },
|
||||
)
|
||||
|
||||
optLens := FromNillable(timeoutLens)
|
||||
|
||||
// Equality predicates
|
||||
eqInt := EQT.Eq[*int]()
|
||||
eqOptInt := O.Eq(eqInt)
|
||||
eqConfig := func(a, b Config) bool {
|
||||
if a.timeout == nil && b.timeout == nil {
|
||||
return true
|
||||
}
|
||||
if a.timeout == nil || b.timeout == nil {
|
||||
return false
|
||||
}
|
||||
return *a.timeout == *b.timeout
|
||||
}
|
||||
|
||||
// Test structures
|
||||
timeout30 := 30
|
||||
timeout60 := 60
|
||||
configNil := Config{timeout: nil}
|
||||
config30 := Config{timeout: &timeout30}
|
||||
|
||||
// Law 1: get(set(a)(s)) = a
|
||||
t.Run("GetSet", func(t *testing.T) {
|
||||
// Setting Some and getting back
|
||||
result := optLens.Get(optLens.Set(O.Some(&timeout60))(config30))
|
||||
assert.True(t, eqOptInt.Equals(result, O.Some(&timeout60)))
|
||||
|
||||
// Setting None and getting back
|
||||
result2 := optLens.Get(optLens.Set(O.None[*int]())(config30))
|
||||
assert.True(t, eqOptInt.Equals(result2, O.None[*int]()))
|
||||
})
|
||||
|
||||
// Law 2: set(get(s))(s) = s
|
||||
t.Run("SetGet", func(t *testing.T) {
|
||||
// With Some value
|
||||
result := optLens.Set(optLens.Get(config30))(config30)
|
||||
assert.True(t, eqConfig(result, config30))
|
||||
|
||||
// With None value
|
||||
result2 := optLens.Set(optLens.Get(configNil))(configNil)
|
||||
assert.True(t, eqConfig(result2, configNil))
|
||||
})
|
||||
|
||||
// Law 3: set(a)(set(a)(s)) = set(a)(s)
|
||||
t.Run("SetSet", func(t *testing.T) {
|
||||
// Setting Some twice
|
||||
once := optLens.Set(O.Some(&timeout60))(config30)
|
||||
twice := optLens.Set(O.Some(&timeout60))(once)
|
||||
assert.True(t, eqConfig(once, twice))
|
||||
|
||||
// Setting None twice
|
||||
once2 := optLens.Set(O.None[*int]())(config30)
|
||||
twice2 := optLens.Set(O.None[*int]())(once2)
|
||||
assert.True(t, eqConfig(once2, twice2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNillableRefLensLaws(t *testing.T) {
|
||||
type Settings struct {
|
||||
maxRetries *int
|
||||
}
|
||||
|
||||
retriesLens := L.MakeLensRef(
|
||||
func(s *Settings) *int { return s.maxRetries },
|
||||
func(s *Settings, r *int) *Settings { s.maxRetries = r; return s },
|
||||
)
|
||||
|
||||
optLens := FromNillableRef(retriesLens)
|
||||
|
||||
// Equality predicates
|
||||
eqInt := EQT.Eq[*int]()
|
||||
eqOptInt := O.Eq(eqInt)
|
||||
eqSettings := func(a, b *Settings) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
if a.maxRetries == nil && b.maxRetries == nil {
|
||||
return true
|
||||
}
|
||||
if a.maxRetries == nil || b.maxRetries == nil {
|
||||
return false
|
||||
}
|
||||
return *a.maxRetries == *b.maxRetries
|
||||
}
|
||||
|
||||
// Test structures
|
||||
retries3 := 3
|
||||
retries5 := 5
|
||||
settingsNil := &Settings{maxRetries: nil}
|
||||
settings3 := &Settings{maxRetries: &retries3}
|
||||
|
||||
// Law 1: get(set(a)(s)) = a
|
||||
t.Run("GetSet", func(t *testing.T) {
|
||||
result := optLens.Get(optLens.Set(O.Some(&retries5))(settings3))
|
||||
assert.True(t, eqOptInt.Equals(result, O.Some(&retries5)))
|
||||
|
||||
result2 := optLens.Get(optLens.Set(O.None[*int]())(settings3))
|
||||
assert.True(t, eqOptInt.Equals(result2, O.None[*int]()))
|
||||
})
|
||||
|
||||
// Law 2: set(get(s))(s) = s
|
||||
t.Run("SetGet", func(t *testing.T) {
|
||||
result := optLens.Set(optLens.Get(settings3))(settings3)
|
||||
assert.True(t, eqSettings(result, settings3))
|
||||
|
||||
result2 := optLens.Set(optLens.Get(settingsNil))(settingsNil)
|
||||
assert.True(t, eqSettings(result2, settingsNil))
|
||||
})
|
||||
|
||||
// Law 3: set(a)(set(a)(s)) = set(a)(s)
|
||||
t.Run("SetSet", func(t *testing.T) {
|
||||
once := optLens.Set(O.Some(&retries5))(settings3)
|
||||
twice := optLens.Set(O.Some(&retries5))(once)
|
||||
assert.True(t, eqSettings(once, twice))
|
||||
|
||||
once2 := optLens.Set(O.None[*int]())(settings3)
|
||||
twice2 := optLens.Set(O.None[*int]())(once2)
|
||||
assert.True(t, eqSettings(once2, twice2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestComposeOptionLensLaws(t *testing.T) {
|
||||
defaultInner := &Inner{Value: 0, Foo: "default"}
|
||||
|
||||
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
inner := FromNillable(L.MakeLens(Outer.GetInner, Outer.SetInner))
|
||||
lens := F.Pipe1(inner, ComposeOption[Outer, int](defaultInner)(value))
|
||||
|
||||
// Equality predicates
|
||||
eqInt := EQT.Eq[int]()
|
||||
eqOptInt := O.Eq(eqInt)
|
||||
eqOuter := func(a, b Outer) bool {
|
||||
if a.inner == nil && b.inner == nil {
|
||||
return true
|
||||
}
|
||||
if a.inner == nil || b.inner == nil {
|
||||
return false
|
||||
}
|
||||
return a.inner.Value == b.inner.Value && a.inner.Foo == b.inner.Foo
|
||||
}
|
||||
|
||||
// Test structures
|
||||
outerNil := Outer{inner: nil}
|
||||
outer42 := Outer{inner: &Inner{Value: 42, Foo: "test"}}
|
||||
|
||||
// Law 1: get(set(a)(s)) = a
|
||||
t.Run("GetSet", func(t *testing.T) {
|
||||
result := lens.Get(lens.Set(O.Some(100))(outer42))
|
||||
assert.True(t, eqOptInt.Equals(result, O.Some(100)))
|
||||
|
||||
result2 := lens.Get(lens.Set(O.None[int]())(outer42))
|
||||
assert.True(t, eqOptInt.Equals(result2, O.None[int]()))
|
||||
})
|
||||
|
||||
// Law 2: set(get(s))(s) = s
|
||||
t.Run("SetGet", func(t *testing.T) {
|
||||
result := lens.Set(lens.Get(outer42))(outer42)
|
||||
assert.True(t, eqOuter(result, outer42))
|
||||
|
||||
result2 := lens.Set(lens.Get(outerNil))(outerNil)
|
||||
assert.True(t, eqOuter(result2, outerNil))
|
||||
})
|
||||
|
||||
// Law 3: set(a)(set(a)(s)) = set(a)(s)
|
||||
t.Run("SetSet", func(t *testing.T) {
|
||||
once := lens.Set(O.Some(100))(outer42)
|
||||
twice := lens.Set(O.Some(100))(once)
|
||||
assert.True(t, eqOuter(once, twice))
|
||||
|
||||
once2 := lens.Set(O.None[int]())(outer42)
|
||||
twice2 := lens.Set(O.None[int]())(once2)
|
||||
assert.True(t, eqOuter(once2, twice2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestComposeOptionsLensLaws(t *testing.T) {
|
||||
defaultValue := 1
|
||||
defaultFoo := "default"
|
||||
defaultInner := &InnerOpt{Value: &defaultValue, Foo: &defaultFoo}
|
||||
|
||||
value := FromNillable(L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
|
||||
inner := FromNillable(L.MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
|
||||
lens := F.Pipe1(inner, Compose[OuterOpt, *int](defaultInner)(value))
|
||||
|
||||
// Equality predicates
|
||||
eqIntPtr := EQT.Eq[*int]()
|
||||
eqOptIntPtr := O.Eq(eqIntPtr)
|
||||
eqOuterOpt := func(a, b OuterOpt) bool {
|
||||
if a.inner == nil && b.inner == nil {
|
||||
return true
|
||||
}
|
||||
if a.inner == nil || b.inner == nil {
|
||||
return false
|
||||
}
|
||||
aVal := a.inner.Value
|
||||
bVal := b.inner.Value
|
||||
if aVal == nil && bVal == nil {
|
||||
return true
|
||||
}
|
||||
if aVal == nil || bVal == nil {
|
||||
return false
|
||||
}
|
||||
return *aVal == *bVal
|
||||
}
|
||||
|
||||
// Test structures
|
||||
val42 := 42
|
||||
val100 := 100
|
||||
outerNil := OuterOpt{inner: nil}
|
||||
outer42 := OuterOpt{inner: &InnerOpt{Value: &val42, Foo: &defaultFoo}}
|
||||
|
||||
// Law 1: get(set(a)(s)) = a
|
||||
t.Run("GetSet", func(t *testing.T) {
|
||||
result := lens.Get(lens.Set(O.Some(&val100))(outer42))
|
||||
assert.True(t, eqOptIntPtr.Equals(result, O.Some(&val100)))
|
||||
|
||||
result2 := lens.Get(lens.Set(O.None[*int]())(outer42))
|
||||
assert.True(t, eqOptIntPtr.Equals(result2, O.None[*int]()))
|
||||
})
|
||||
|
||||
// Law 2: set(get(s))(s) = s
|
||||
t.Run("SetGet", func(t *testing.T) {
|
||||
result := lens.Set(lens.Get(outer42))(outer42)
|
||||
assert.True(t, eqOuterOpt(result, outer42))
|
||||
|
||||
result2 := lens.Set(lens.Get(outerNil))(outerNil)
|
||||
assert.True(t, eqOuterOpt(result2, outerNil))
|
||||
})
|
||||
|
||||
// Law 3: set(a)(set(a)(s)) = set(a)(s)
|
||||
t.Run("SetSet", func(t *testing.T) {
|
||||
once := lens.Set(O.Some(&val100))(outer42)
|
||||
twice := lens.Set(O.Some(&val100))(once)
|
||||
assert.True(t, eqOuterOpt(once, twice))
|
||||
|
||||
once2 := lens.Set(O.None[*int]())(outer42)
|
||||
twice2 := lens.Set(O.None[*int]())(once2)
|
||||
assert.True(t, eqOuterOpt(once2, twice2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromPredicateLensLaws(t *testing.T) {
|
||||
type Score struct {
|
||||
points int
|
||||
}
|
||||
|
||||
pointsLens := L.MakeLens(
|
||||
func(s Score) int { return s.points },
|
||||
func(s Score, p int) Score { s.points = p; return s },
|
||||
)
|
||||
|
||||
// Only positive scores are valid
|
||||
validLens := FromPredicate[Score](func(p int) bool { return p > 0 }, 0)(pointsLens)
|
||||
|
||||
// Equality predicates
|
||||
eqInt := EQT.Eq[int]()
|
||||
eqOptInt := O.Eq(eqInt)
|
||||
eqScore := func(a, b Score) bool { return a.points == b.points }
|
||||
|
||||
// Test structures
|
||||
scoreZero := Score{points: 0}
|
||||
score100 := Score{points: 100}
|
||||
|
||||
// Law 1: get(set(a)(s)) = a
|
||||
t.Run("GetSet", func(t *testing.T) {
|
||||
result := validLens.Get(validLens.Set(O.Some(50))(score100))
|
||||
assert.True(t, eqOptInt.Equals(result, O.Some(50)))
|
||||
|
||||
result2 := validLens.Get(validLens.Set(O.None[int]())(score100))
|
||||
assert.True(t, eqOptInt.Equals(result2, O.None[int]()))
|
||||
})
|
||||
|
||||
// Law 2: set(get(s))(s) = s
|
||||
t.Run("SetGet", func(t *testing.T) {
|
||||
result := validLens.Set(validLens.Get(score100))(score100)
|
||||
assert.True(t, eqScore(result, score100))
|
||||
|
||||
result2 := validLens.Set(validLens.Get(scoreZero))(scoreZero)
|
||||
assert.True(t, eqScore(result2, scoreZero))
|
||||
})
|
||||
|
||||
// Law 3: set(a)(set(a)(s)) = set(a)(s)
|
||||
t.Run("SetSet", func(t *testing.T) {
|
||||
once := validLens.Set(O.Some(75))(score100)
|
||||
twice := validLens.Set(O.Some(75))(once)
|
||||
assert.True(t, eqScore(once, twice))
|
||||
|
||||
once2 := validLens.Set(O.None[int]())(score100)
|
||||
twice2 := validLens.Set(O.None[int]())(once2)
|
||||
assert.True(t, eqScore(once2, twice2))
|
||||
})
|
||||
}
|
||||
@@ -22,6 +22,44 @@ import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// AsTraversal converts a Lens[S, A] to a Traversal[S, A] for optional values.
|
||||
//
|
||||
// A traversal is a generalization of a lens that can focus on zero or more values.
|
||||
// This function converts a lens (which focuses on exactly one value) into a traversal,
|
||||
// allowing it to be used with traversal operations like mapping over multiple values.
|
||||
//
|
||||
// This is particularly useful when you want to:
|
||||
// - Use lens operations in a traversal context
|
||||
// - Compose lenses with traversals
|
||||
// - Apply operations that work on collections of optional values
|
||||
//
|
||||
// The conversion uses the Option monad's map operation to handle the optional nature
|
||||
// of the values being traversed.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The structure type containing the field
|
||||
// - A: The type of the field being focused on
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns a Traversal[S, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Timeout Option[int]
|
||||
// }
|
||||
//
|
||||
// timeoutLens := lens.MakeLens(
|
||||
// func(c Config) Option[int] { return c.Timeout },
|
||||
// func(c Config, t Option[int]) Config { c.Timeout = t; return c },
|
||||
// )
|
||||
//
|
||||
// // Convert to traversal for use with traversal operations
|
||||
// timeoutTraversal := lens.AsTraversal[Config, int]()(timeoutLens)
|
||||
//
|
||||
// // Now can use traversal operations
|
||||
// configs := []Config{{Timeout: O.Some(30)}, {Timeout: O.None[int]()}}
|
||||
// // Apply operations across all configs using the traversal
|
||||
func AsTraversal[S, A any]() func(L.Lens[S, A]) T.Traversal[S, A] {
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.MonadMap[A, S])
|
||||
}
|
||||
|
||||
267
v2/optics/lens/option/testing/laws_test.go
Normal file
267
v2/optics/lens/option/testing/laws_test.go
Normal file
@@ -0,0 +1,267 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
EQT "github.com/IBM/fp-go/v2/eq/testing"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
I "github.com/IBM/fp-go/v2/identity"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
LI "github.com/IBM/fp-go/v2/optics/lens/iso"
|
||||
LO "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
LT "github.com/IBM/fp-go/v2/optics/lens/testing"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type (
|
||||
Street struct {
|
||||
num int
|
||||
name string
|
||||
}
|
||||
|
||||
Address struct {
|
||||
city string
|
||||
street *Street
|
||||
}
|
||||
|
||||
Inner struct {
|
||||
Value int
|
||||
Foo string
|
||||
}
|
||||
|
||||
InnerOpt struct {
|
||||
Value *int
|
||||
Foo *string
|
||||
}
|
||||
|
||||
Outer struct {
|
||||
inner *Inner
|
||||
}
|
||||
|
||||
OuterOpt struct {
|
||||
inner *InnerOpt
|
||||
}
|
||||
)
|
||||
|
||||
func (outer *OuterOpt) GetInner() *InnerOpt {
|
||||
return outer.inner
|
||||
}
|
||||
|
||||
func (outer *OuterOpt) SetInner(inner *InnerOpt) *OuterOpt {
|
||||
outer.inner = inner
|
||||
return outer
|
||||
}
|
||||
|
||||
func (inner *InnerOpt) GetValue() *int {
|
||||
return inner.Value
|
||||
}
|
||||
|
||||
func (inner *InnerOpt) SetValue(value *int) *InnerOpt {
|
||||
inner.Value = value
|
||||
return inner
|
||||
}
|
||||
|
||||
func (outer *Outer) GetInner() *Inner {
|
||||
return outer.inner
|
||||
}
|
||||
|
||||
func (outer *Outer) SetInner(inner *Inner) *Outer {
|
||||
outer.inner = inner
|
||||
return outer
|
||||
}
|
||||
|
||||
func (inner *Inner) GetValue() int {
|
||||
return inner.Value
|
||||
}
|
||||
|
||||
func (inner *Inner) SetValue(value int) *Inner {
|
||||
inner.Value = value
|
||||
return inner
|
||||
}
|
||||
|
||||
func (street *Street) GetName() string {
|
||||
return street.name
|
||||
}
|
||||
|
||||
func (street *Street) SetName(name string) *Street {
|
||||
street.name = name
|
||||
return street
|
||||
}
|
||||
|
||||
func (addr *Address) GetStreet() *Street {
|
||||
return addr.street
|
||||
}
|
||||
|
||||
func (addr *Address) SetStreet(s *Street) *Address {
|
||||
addr.street = s
|
||||
return addr
|
||||
}
|
||||
|
||||
var (
|
||||
streetLens = L.MakeLensRef((*Street).GetName, (*Street).SetName)
|
||||
addrLens = L.MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
|
||||
outerLens = LO.FromNillableRef(L.MakeLensRef((*Outer).GetInner, (*Outer).SetInner))
|
||||
valueLens = L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
|
||||
outerOptLens = LO.FromNillableRef(L.MakeLensRef((*OuterOpt).GetInner, (*OuterOpt).SetInner))
|
||||
valueOptLens = L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue)
|
||||
|
||||
sampleStreet = Street{num: 220, name: "Schönaicherstr"}
|
||||
sampleAddress = Address{city: "Böblingen", street: &sampleStreet}
|
||||
sampleStreet2 = Street{num: 220, name: "Neue Str"}
|
||||
|
||||
defaultInner = Inner{
|
||||
Value: -1,
|
||||
Foo: "foo",
|
||||
}
|
||||
|
||||
emptyOuter = Outer{}
|
||||
|
||||
defaultInnerOpt = InnerOpt{
|
||||
Value: &defaultInner.Value,
|
||||
Foo: &defaultInner.Foo,
|
||||
}
|
||||
|
||||
emptyOuterOpt = OuterOpt{}
|
||||
)
|
||||
|
||||
func TestStreetLensLaws(t *testing.T) {
|
||||
// some comparison
|
||||
eqs := EQT.Eq[*Street]()
|
||||
eqa := EQT.Eq[string]()
|
||||
|
||||
laws := LT.AssertLaws(
|
||||
t,
|
||||
eqa,
|
||||
eqs,
|
||||
)(streetLens)
|
||||
|
||||
cpy := sampleStreet
|
||||
assert.True(t, laws(&sampleStreet, "Neue Str."))
|
||||
assert.Equal(t, cpy, sampleStreet)
|
||||
}
|
||||
|
||||
func TestAddrLensLaws(t *testing.T) {
|
||||
// some comparison
|
||||
eqs := EQT.Eq[*Address]()
|
||||
eqa := EQT.Eq[*Street]()
|
||||
|
||||
laws := LT.AssertLaws(
|
||||
t,
|
||||
eqa,
|
||||
eqs,
|
||||
)(addrLens)
|
||||
|
||||
cpyAddr := sampleAddress
|
||||
cpyStreet := sampleStreet2
|
||||
assert.True(t, laws(&sampleAddress, &sampleStreet2))
|
||||
assert.Equal(t, cpyAddr, sampleAddress)
|
||||
assert.Equal(t, cpyStreet, sampleStreet2)
|
||||
}
|
||||
|
||||
func TestCompose(t *testing.T) {
|
||||
// some comparison
|
||||
eqs := EQT.Eq[*Address]()
|
||||
eqa := EQT.Eq[string]()
|
||||
|
||||
streetName := L.Compose[*Address](streetLens)(addrLens)
|
||||
|
||||
laws := LT.AssertLaws(
|
||||
t,
|
||||
eqa,
|
||||
eqs,
|
||||
)(streetName)
|
||||
|
||||
cpyAddr := sampleAddress
|
||||
cpyStreet := sampleStreet
|
||||
assert.True(t, laws(&sampleAddress, "Neue Str."))
|
||||
assert.Equal(t, cpyAddr, sampleAddress)
|
||||
assert.Equal(t, cpyStreet, sampleStreet)
|
||||
}
|
||||
|
||||
func TestOuterLensLaws(t *testing.T) {
|
||||
// some equal predicates
|
||||
eqValue := EQT.Eq[int]()
|
||||
eqOptValue := O.Eq(eqValue)
|
||||
// lens to access a value from outer
|
||||
valueFromOuter := LO.ComposeOption[*Outer, int](&defaultInner)(valueLens)(outerLens)
|
||||
// try to access the value, this should get an option
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
|
||||
// update the object
|
||||
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuter)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
|
||||
// updating with none should remove the inner
|
||||
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
|
||||
// check if this meets the laws
|
||||
|
||||
eqOuter := EQT.Eq[*Outer]()
|
||||
|
||||
laws := LT.AssertLaws(
|
||||
t,
|
||||
eqOptValue,
|
||||
eqOuter,
|
||||
)(valueFromOuter)
|
||||
|
||||
assert.True(t, laws(&emptyOuter, O.Some(2)))
|
||||
assert.True(t, laws(&emptyOuter, O.None[int]()))
|
||||
|
||||
assert.True(t, laws(withValue, O.Some(2)))
|
||||
assert.True(t, laws(withValue, O.None[int]()))
|
||||
}
|
||||
|
||||
func TestOuterOptLensLaws(t *testing.T) {
|
||||
// some equal predicates
|
||||
eqValue := EQT.Eq[int]()
|
||||
eqOptValue := O.Eq(eqValue)
|
||||
intIso := LI.FromNillable[int]()
|
||||
// lens to access a value from outer
|
||||
valueFromOuter := F.Pipe3(
|
||||
valueOptLens,
|
||||
LI.Compose[*InnerOpt](intIso),
|
||||
LO.Compose[*OuterOpt, int](&defaultInnerOpt),
|
||||
I.Ap[L.Lens[*OuterOpt, O.Option[int]]](outerOptLens),
|
||||
)
|
||||
|
||||
// try to access the value, this should get an option
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
|
||||
// update the object
|
||||
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuterOpt)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
|
||||
// updating with none should remove the inner
|
||||
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
|
||||
// check if this meets the laws
|
||||
|
||||
eqOuter := EQT.Eq[*OuterOpt]()
|
||||
|
||||
laws := LT.AssertLaws(
|
||||
t,
|
||||
eqOptValue,
|
||||
eqOuter,
|
||||
)(valueFromOuter)
|
||||
|
||||
assert.True(t, laws(&emptyOuterOpt, O.Some(2)))
|
||||
assert.True(t, laws(&emptyOuterOpt, O.None[int]()))
|
||||
|
||||
assert.True(t, laws(withValue, O.Some(2)))
|
||||
assert.True(t, laws(withValue, O.None[int]()))
|
||||
}
|
||||
94
v2/optics/lens/option/types.go
Normal file
94
v2/optics/lens/option/types.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package option
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
type (
|
||||
// Endomorphism is a function from a type to itself (A → A).
|
||||
// It represents transformations that preserve the type.
|
||||
//
|
||||
// This is commonly used in lens setters to transform a structure
|
||||
// by applying a function that takes and returns the same type.
|
||||
//
|
||||
// Example:
|
||||
// increment := func(x int) int { return x + 1 }
|
||||
// // increment is an Endomorphism[int]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Lens represents a functional reference to a field within a structure.
|
||||
//
|
||||
// A Lens[S, A] provides a way to get and set a value of type A within
|
||||
// a structure of type S in an immutable way. It consists of:
|
||||
// - Get: S → A (retrieve the value)
|
||||
// - Set: A → S → S (update the value, returning a new structure)
|
||||
//
|
||||
// Lenses satisfy three laws:
|
||||
// 1. Get-Put: lens.Set(lens.Get(s))(s) == s
|
||||
// 2. Put-Get: lens.Get(lens.Set(a)(s)) == a
|
||||
// 3. Put-Put: lens.Set(b)(lens.Set(a)(s)) == lens.Set(b)(s)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The structure type containing the field
|
||||
// - A: The type of the field being focused on
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
// Option represents a value that may or may not be present.
|
||||
//
|
||||
// It is either Some[T] containing a value of type T, or None[T]
|
||||
// representing the absence of a value. This is a type-safe alternative
|
||||
// to using nil pointers.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of the value that may be present
|
||||
Option[T any] = option.Option[T]
|
||||
|
||||
// LensO is a lens that focuses on an optional value.
|
||||
//
|
||||
// A LensO[S, A] is equivalent to Lens[S, Option[A]], representing
|
||||
// a lens that focuses on a value of type A that may or may not be
|
||||
// present within a structure S.
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Nullable pointer fields
|
||||
// - Optional configuration values
|
||||
// - Fields that may be conditionally present
|
||||
//
|
||||
// The getter returns Option[A] (Some if present, None if absent).
|
||||
// The setter takes Option[A] (Some to set, None to remove).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The structure type containing the optional field
|
||||
// - A: The type of the optional value being focused on
|
||||
//
|
||||
// Example:
|
||||
// type Config struct {
|
||||
// Timeout *int
|
||||
// }
|
||||
//
|
||||
// timeoutLens := lens.MakeLensRef(
|
||||
// func(c *Config) *int { return c.Timeout },
|
||||
// func(c *Config, t *int) *Config { c.Timeout = t; return c },
|
||||
// )
|
||||
//
|
||||
// optLens := lens.FromNillableRef(timeoutLens)
|
||||
// // optLens is a LensO[*Config, *int]
|
||||
LensO[S, A any] = Lens[S, Option[A]]
|
||||
)
|
||||
@@ -19,11 +19,7 @@ import (
|
||||
"testing"
|
||||
|
||||
EQT "github.com/IBM/fp-go/v2/eq/testing"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
I "github.com/IBM/fp-go/v2/identity"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
LI "github.com/IBM/fp-go/v2/optics/lens/iso"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -114,10 +110,8 @@ func (addr *Address) SetStreet(s *Street) *Address {
|
||||
var (
|
||||
streetLens = L.MakeLensRef((*Street).GetName, (*Street).SetName)
|
||||
addrLens = L.MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
|
||||
outerLens = L.FromNillableRef(L.MakeLensRef((*Outer).GetInner, (*Outer).SetInner))
|
||||
valueLens = L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
|
||||
|
||||
outerOptLens = L.FromNillableRef(L.MakeLensRef((*OuterOpt).GetInner, (*OuterOpt).SetInner))
|
||||
valueOptLens = L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue)
|
||||
|
||||
sampleStreet = Street{num: 220, name: "Schönaicherstr"}
|
||||
@@ -192,74 +186,3 @@ func TestCompose(t *testing.T) {
|
||||
assert.Equal(t, cpyAddr, sampleAddress)
|
||||
assert.Equal(t, cpyStreet, sampleStreet)
|
||||
}
|
||||
|
||||
func TestOuterLensLaws(t *testing.T) {
|
||||
// some equal predicates
|
||||
eqValue := EQT.Eq[int]()
|
||||
eqOptValue := O.Eq(eqValue)
|
||||
// lens to access a value from outer
|
||||
valueFromOuter := L.ComposeOption[*Outer, int](&defaultInner)(valueLens)(outerLens)
|
||||
// try to access the value, this should get an option
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
|
||||
// update the object
|
||||
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuter)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
|
||||
// updating with none should remove the inner
|
||||
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
|
||||
// check if this meets the laws
|
||||
|
||||
eqOuter := EQT.Eq[*Outer]()
|
||||
|
||||
laws := AssertLaws(
|
||||
t,
|
||||
eqOptValue,
|
||||
eqOuter,
|
||||
)(valueFromOuter)
|
||||
|
||||
assert.True(t, laws(&emptyOuter, O.Some(2)))
|
||||
assert.True(t, laws(&emptyOuter, O.None[int]()))
|
||||
|
||||
assert.True(t, laws(withValue, O.Some(2)))
|
||||
assert.True(t, laws(withValue, O.None[int]()))
|
||||
}
|
||||
|
||||
func TestOuterOptLensLaws(t *testing.T) {
|
||||
// some equal predicates
|
||||
eqValue := EQT.Eq[int]()
|
||||
eqOptValue := O.Eq(eqValue)
|
||||
intIso := LI.FromNillable[int]()
|
||||
// lens to access a value from outer
|
||||
valueFromOuter := F.Pipe3(
|
||||
valueOptLens,
|
||||
LI.Compose[*InnerOpt](intIso),
|
||||
L.ComposeOptions[*OuterOpt, int](&defaultInnerOpt),
|
||||
I.Ap[L.Lens[*OuterOpt, O.Option[int]]](outerOptLens),
|
||||
)
|
||||
|
||||
// try to access the value, this should get an option
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
|
||||
// update the object
|
||||
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuterOpt)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
|
||||
// updating with none should remove the inner
|
||||
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
|
||||
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
|
||||
// check if this meets the laws
|
||||
|
||||
eqOuter := EQT.Eq[*OuterOpt]()
|
||||
|
||||
laws := AssertLaws(
|
||||
t,
|
||||
eqOptValue,
|
||||
eqOuter,
|
||||
)(valueFromOuter)
|
||||
|
||||
assert.True(t, laws(&emptyOuterOpt, O.Some(2)))
|
||||
assert.True(t, laws(&emptyOuterOpt, O.None[int]()))
|
||||
|
||||
assert.True(t, laws(withValue, O.Some(2)))
|
||||
assert.True(t, laws(withValue, O.None[int]()))
|
||||
}
|
||||
|
||||
@@ -21,11 +21,63 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Endomorphism is a function from a type to itself (A → A).
|
||||
// It represents transformations that preserve the type.
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Lens is a reference to a subpart of a data type
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
//
|
||||
// A Lens[S, A] provides a composable way to focus on a field of type A within
|
||||
// a structure of type S. It consists of two operations:
|
||||
// - Get: Extracts the focused value from the structure (S → A)
|
||||
// - Set: Updates the focused value in the structure, returning a new structure (A → S → S)
|
||||
//
|
||||
// Lenses maintain immutability by always returning new copies of the structure
|
||||
// when setting values, never modifying the original.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source/structure type (the whole)
|
||||
// - A: The focus/field type (the part)
|
||||
//
|
||||
// Lens Laws:
|
||||
//
|
||||
// A well-behaved lens must satisfy three laws:
|
||||
//
|
||||
// 1. GetSet (You get what you set):
|
||||
// lens.Set(lens.Get(s))(s) == s
|
||||
//
|
||||
// 2. SetGet (You set what you get):
|
||||
// lens.Get(lens.Set(a)(s)) == a
|
||||
//
|
||||
// 3. SetSet (Setting twice is the same as setting once):
|
||||
// lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// nameLens := lens.MakeLens(
|
||||
// func(p Person) string { return p.Name },
|
||||
// func(p Person, name string) Person {
|
||||
// p.Name = name
|
||||
// return p
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// person := Person{Name: "Alice", Age: 30}
|
||||
// name := nameLens.Get(person) // "Alice"
|
||||
// updated := nameLens.Set("Bob")(person) // Person{Name: "Bob", Age: 30}
|
||||
// // person is unchanged, updated is a new value
|
||||
Lens[S, A any] struct {
|
||||
// Get extracts the focused value of type A from structure S.
|
||||
Get func(s S) A
|
||||
|
||||
// Set returns a function that updates the focused value in structure S.
|
||||
// The returned function takes a structure S and returns a new structure S
|
||||
// with the focused value updated to a. The original structure is never modified.
|
||||
Set func(a A) Endomorphism[S]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Prism is an optic used to select part of a sum type.
|
||||
package prism
|
||||
|
||||
import (
|
||||
@@ -23,40 +22,131 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Prism is an optic used to select part of a sum type.
|
||||
// Prism is an optic used to select part of a sum type (tagged union).
|
||||
// It provides two operations:
|
||||
// - GetOption: Try to extract a value of type A from S (may fail)
|
||||
// - ReverseGet: Construct an S from an A (always succeeds)
|
||||
//
|
||||
// Prisms are useful for working with variant types like Either, Option,
|
||||
// or custom sum types where you want to focus on a specific variant.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type (sum type)
|
||||
// - A: The focus type (variant within the sum type)
|
||||
//
|
||||
// Example:
|
||||
// type Result interface{ isResult() }
|
||||
// type Success struct{ Value int }
|
||||
// type Failure struct{ Error string }
|
||||
//
|
||||
// successPrism := MakePrism(
|
||||
// func(r Result) Option[int] {
|
||||
// if s, ok := r.(Success); ok {
|
||||
// return Some(s.Value)
|
||||
// }
|
||||
// return None[int]()
|
||||
// },
|
||||
// func(v int) Result { return Success{Value: v} },
|
||||
// )
|
||||
Prism[S, A any] interface {
|
||||
GetOption(s S) O.Option[A]
|
||||
// GetOption attempts to extract a value of type A from S.
|
||||
// Returns Some(a) if the extraction succeeds, None otherwise.
|
||||
GetOption(s S) Option[A]
|
||||
|
||||
// ReverseGet constructs an S from an A.
|
||||
// This operation always succeeds.
|
||||
ReverseGet(a A) S
|
||||
}
|
||||
|
||||
// prismImpl is the internal implementation of the Prism interface.
|
||||
prismImpl[S, A any] struct {
|
||||
get func(S) O.Option[A]
|
||||
get func(S) Option[A]
|
||||
rev func(A) S
|
||||
}
|
||||
)
|
||||
|
||||
func (prism prismImpl[S, A]) GetOption(s S) O.Option[A] {
|
||||
// GetOption implements the Prism interface for prismImpl.
|
||||
func (prism prismImpl[S, A]) GetOption(s S) Option[A] {
|
||||
return prism.get(s)
|
||||
}
|
||||
|
||||
// ReverseGet implements the Prism interface for prismImpl.
|
||||
func (prism prismImpl[S, A]) ReverseGet(a A) S {
|
||||
return prism.rev(a)
|
||||
}
|
||||
|
||||
func MakePrism[S, A any](get func(S) O.Option[A], rev func(A) S) Prism[S, A] {
|
||||
// MakePrism constructs a Prism from GetOption and ReverseGet functions.
|
||||
//
|
||||
// Parameters:
|
||||
// - get: Function to extract A from S (returns Option[A])
|
||||
// - rev: Function to construct S from A
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[S, A] that uses the provided functions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// prism := MakePrism(
|
||||
// func(opt Option[int]) Option[int] { return opt },
|
||||
// func(n int) Option[int] { return Some(n) },
|
||||
// )
|
||||
func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
|
||||
return prismImpl[S, A]{get, rev}
|
||||
}
|
||||
|
||||
// Id returns a prism implementing the identity operation
|
||||
// Id returns an identity prism that focuses on the entire value.
|
||||
// GetOption always returns Some(s), and ReverseGet is the identity function.
|
||||
//
|
||||
// This is useful as a starting point for prism composition or when you need
|
||||
// a prism that doesn't actually transform the value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// idPrism := Id[int]()
|
||||
// value := idPrism.GetOption(42) // Some(42)
|
||||
// result := idPrism.ReverseGet(42) // 42
|
||||
func Id[S any]() Prism[S, S] {
|
||||
return MakePrism(O.Some[S], F.Identity[S])
|
||||
}
|
||||
|
||||
// FromPredicate creates a prism that matches values satisfying a predicate.
|
||||
// GetOption returns Some(s) if the predicate is true, None otherwise.
|
||||
// ReverseGet is the identity function (doesn't validate the predicate).
|
||||
//
|
||||
// Parameters:
|
||||
// - pred: Predicate function to test values
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[S, S] that filters based on the predicate
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// positivePrism := FromPredicate(func(n int) bool { return n > 0 })
|
||||
// value := positivePrism.GetOption(42) // Some(42)
|
||||
// value = positivePrism.GetOption(-5) // None[int]
|
||||
func FromPredicate[S any](pred func(S) bool) Prism[S, S] {
|
||||
return MakePrism(O.FromPredicate(pred), F.Identity[S])
|
||||
}
|
||||
|
||||
// Compose composes a `Prism` with a `Prism`.
|
||||
// Compose composes two prisms to create a prism that focuses deeper into a structure.
|
||||
// The resulting prism first applies the outer prism (S → A), then the inner prism (A → B).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The outermost source type
|
||||
// - A: The intermediate type
|
||||
// - B: The innermost focus type
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: The inner prism (A → B)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes the outer prism (S → A) and returns the composed prism (S → B)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// outerPrism := MakePrism(...) // Prism[Outer, Inner]
|
||||
// innerPrism := MakePrism(...) // Prism[Inner, Value]
|
||||
// composed := Compose[Outer](innerPrism)(outerPrism) // Prism[Outer, Value]
|
||||
func Compose[S, A, B any](ab Prism[A, B]) func(Prism[S, A]) Prism[S, B] {
|
||||
return func(sa Prism[S, A]) Prism[S, B] {
|
||||
return MakePrism(F.Flow2(
|
||||
@@ -69,7 +159,10 @@ func Compose[S, A, B any](ab Prism[A, B]) func(Prism[S, A]) Prism[S, B] {
|
||||
}
|
||||
}
|
||||
|
||||
func prismModifyOption[S, A any](f func(A) A, sa Prism[S, A], s S) O.Option[S] {
|
||||
// prismModifyOption applies a transformation function through a prism,
|
||||
// returning Some(modified S) if the prism matches, None otherwise.
|
||||
// This is an internal helper function.
|
||||
func prismModifyOption[S, A any](f func(A) A, sa Prism[S, A], s S) Option[S] {
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.GetOption,
|
||||
@@ -80,6 +173,10 @@ func prismModifyOption[S, A any](f func(A) A, sa Prism[S, A], s S) O.Option[S] {
|
||||
)
|
||||
}
|
||||
|
||||
// prismModify applies a transformation function through a prism.
|
||||
// If the prism matches, it extracts the value, applies the function,
|
||||
// and reconstructs the result. If the prism doesn't match, returns the original value.
|
||||
// This is an internal helper function.
|
||||
func prismModify[S, A any](f func(A) A, sa Prism[S, A], s S) S {
|
||||
return F.Pipe1(
|
||||
prismModifyOption(f, sa, s),
|
||||
@@ -87,23 +184,63 @@ func prismModify[S, A any](f func(A) A, sa Prism[S, A], s S) S {
|
||||
)
|
||||
}
|
||||
|
||||
// prismSet is an internal helper that creates a setter function.
|
||||
// Deprecated: Use Set instead.
|
||||
func prismSet[S, A any](a A) func(Prism[S, A]) EM.Endomorphism[S] {
|
||||
return EM.Curry3(prismModify[S, A])(F.Constant1[A](a))
|
||||
return F.Curry3(prismModify[S, A])(F.Constant1[A](a))
|
||||
}
|
||||
|
||||
// Set creates a function that sets a value through a prism.
|
||||
// If the prism matches, it replaces the focused value with the new value.
|
||||
// If the prism doesn't match, it returns the original value unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The new value to set
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a prism and returns an endomorphism (S → S)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// somePrism := MakePrism(...)
|
||||
// setter := Set[Option[int], int](100)
|
||||
// result := setter(somePrism)(Some(42)) // Some(100)
|
||||
// result = setter(somePrism)(None[int]()) // None[int]() (unchanged)
|
||||
func Set[S, A any](a A) func(Prism[S, A]) EM.Endomorphism[S] {
|
||||
return EM.Curry3(prismModify[S, A])(F.Constant1[A](a))
|
||||
return F.Curry3(prismModify[S, A])(F.Constant1[A](a))
|
||||
}
|
||||
|
||||
func prismSome[A any]() Prism[O.Option[A], A] {
|
||||
return MakePrism(F.Identity[O.Option[A]], O.Some[A])
|
||||
// prismSome creates a prism that focuses on the Some variant of an Option.
|
||||
// This is an internal helper used by the Some function.
|
||||
func prismSome[A any]() Prism[Option[A], A] {
|
||||
return MakePrism(F.Identity[Option[A]], O.Some[A])
|
||||
}
|
||||
|
||||
// Some returns a `Prism` from a `Prism` focused on the `Some` of a `Option` type.
|
||||
func Some[S, A any](soa Prism[S, O.Option[A]]) Prism[S, A] {
|
||||
// Some creates a prism that focuses on the Some variant of an Option within a structure.
|
||||
// It composes the provided prism (which focuses on an Option[A]) with a prism that
|
||||
// extracts the value from Some.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The value type within the Option
|
||||
//
|
||||
// Parameters:
|
||||
// - soa: A prism that focuses on an Option[A] within S
|
||||
//
|
||||
// Returns:
|
||||
// - A prism that focuses on the A value within Some
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Timeout Option[int] }
|
||||
// configPrism := MakePrism(...) // Prism[Config, Option[int]]
|
||||
// timeoutPrism := Some(configPrism) // Prism[Config, int]
|
||||
// value := timeoutPrism.GetOption(Config{Timeout: Some(30)}) // Some(30)
|
||||
func Some[S, A any](soa Prism[S, Option[A]]) Prism[S, A] {
|
||||
return Compose[S](prismSome[A]())(soa)
|
||||
}
|
||||
|
||||
// imap is an internal helper that bidirectionally maps a prism's focus type.
|
||||
func imap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](sa Prism[S, A], ab AB, ba BA) Prism[S, B] {
|
||||
return MakePrism(
|
||||
F.Flow2(sa.GetOption, O.Map(ab)),
|
||||
@@ -111,6 +248,31 @@ func imap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](sa Prism[S, A], ab AB,
|
||||
)
|
||||
}
|
||||
|
||||
// IMap bidirectionally maps the focus type of a prism.
|
||||
// It transforms a Prism[S, A] into a Prism[S, B] using two functions:
|
||||
// one to map A → B and another to map B → A.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The original focus type
|
||||
// - B: The new focus type
|
||||
// - AB: Function type A → B
|
||||
// - BA: Function type B → A
|
||||
//
|
||||
// Parameters:
|
||||
// - ab: Function to map from A to B
|
||||
// - ba: Function to map from B to A
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms Prism[S, A] to Prism[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intPrism := MakePrism(...) // Prism[Result, int]
|
||||
// stringPrism := IMap[Result](
|
||||
// func(n int) string { return strconv.Itoa(n) },
|
||||
// func(s string) int { n, _ := strconv.Atoi(s); return n },
|
||||
// )(intPrism) // Prism[Result, string]
|
||||
func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Prism[S, A]) Prism[S, B] {
|
||||
return func(sa Prism[S, A]) Prism[S, B] {
|
||||
return imap(sa, ab, ba)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
312
v2/optics/prism/prisms.go
Normal file
312
v2/optics/prism/prisms.go
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package prism
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// FromEncoding creates a prism for base64 encoding/decoding operations.
|
||||
// It provides a safe way to work with base64-encoded strings, handling
|
||||
// encoding and decoding errors gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to decode a base64 string into bytes.
|
||||
// If decoding succeeds, it returns Some([]byte); if it fails (e.g., invalid
|
||||
// base64 format), it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, encoding bytes into a base64 string.
|
||||
//
|
||||
// Parameters:
|
||||
// - enc: A base64.Encoding instance (e.g., base64.StdEncoding, base64.URLEncoding)
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[string, []byte] that safely handles base64 encoding/decoding
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a prism for standard base64 encoding
|
||||
// b64Prism := FromEncoding(base64.StdEncoding)
|
||||
//
|
||||
// // Decode valid base64 string
|
||||
// data := b64Prism.GetOption("SGVsbG8gV29ybGQ=") // Some([]byte("Hello World"))
|
||||
//
|
||||
// // Decode invalid base64 string
|
||||
// invalid := b64Prism.GetOption("not-valid-base64!!!") // None[[]byte]()
|
||||
//
|
||||
// // Encode bytes to base64
|
||||
// encoded := b64Prism.ReverseGet([]byte("Hello World")) // "SGVsbG8gV29ybGQ="
|
||||
//
|
||||
// // Use with Set to update encoded values
|
||||
// newData := []byte("New Data")
|
||||
// setter := Set[string, []byte](newData)
|
||||
// result := setter(b64Prism)("SGVsbG8gV29ybGQ=") // Encodes newData to base64
|
||||
//
|
||||
// Common use cases:
|
||||
// - Safely decoding base64-encoded configuration values
|
||||
// - Working with base64-encoded API responses
|
||||
// - Validating and transforming base64 data in pipelines
|
||||
// - Using different encodings (Standard, URL-safe, RawStd, RawURL)
|
||||
func FromEncoding(enc *base64.Encoding) Prism[string, []byte] {
|
||||
return MakePrism(F.Flow2(
|
||||
either.Eitherize1(enc.DecodeString),
|
||||
either.Fold(F.Ignore1of1[error](option.None[[]byte]), option.Some),
|
||||
), enc.EncodeToString)
|
||||
}
|
||||
|
||||
// ParseURL creates a prism for parsing and formatting URLs.
|
||||
// It provides a safe way to work with URL strings, handling parsing
|
||||
// errors gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to parse a string into a *url.URL.
|
||||
// If parsing succeeds, it returns Some(*url.URL); if it fails (e.g., invalid
|
||||
// URL format), it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, converting a *url.URL back to its
|
||||
// string representation.
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[string, *url.URL] that safely handles URL parsing/formatting
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a URL parsing prism
|
||||
// urlPrism := ParseURL()
|
||||
//
|
||||
// // Parse valid URL
|
||||
// parsed := urlPrism.GetOption("https://example.com/path?query=value")
|
||||
// // Some(*url.URL{Scheme: "https", Host: "example.com", ...})
|
||||
//
|
||||
// // Parse invalid URL
|
||||
// invalid := urlPrism.GetOption("ht!tp://invalid url") // None[*url.URL]()
|
||||
//
|
||||
// // Convert URL back to string
|
||||
// u, _ := url.Parse("https://example.com")
|
||||
// str := urlPrism.ReverseGet(u) // "https://example.com"
|
||||
//
|
||||
// // Use with Set to update URLs
|
||||
// newURL, _ := url.Parse("https://newsite.com")
|
||||
// setter := Set[string, *url.URL](newURL)
|
||||
// result := setter(urlPrism)("https://oldsite.com") // "https://newsite.com"
|
||||
//
|
||||
// Common use cases:
|
||||
// - Validating and parsing URL configuration values
|
||||
// - Working with API endpoints
|
||||
// - Transforming URL strings in data pipelines
|
||||
// - Extracting and modifying URL components safely
|
||||
func ParseURL() Prism[string, *url.URL] {
|
||||
return MakePrism(F.Flow2(
|
||||
either.Eitherize1(url.Parse),
|
||||
either.Fold(F.Ignore1of1[error](option.None[*url.URL]), option.Some),
|
||||
), (*url.URL).String)
|
||||
}
|
||||
|
||||
// InstanceOf creates a prism for type assertions on interface{}/any values.
|
||||
// It provides a safe way to extract values of a specific type from an any value,
|
||||
// handling type mismatches gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to assert that an any value is of type T.
|
||||
// If the assertion succeeds, it returns Some(T); if it fails, it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, converting a value of type T back to any.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The target type to extract from any
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[any, T] that safely handles type assertions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a prism for extracting int values
|
||||
// intPrism := InstanceOf[int]()
|
||||
//
|
||||
// // Extract int from any
|
||||
// var value any = 42
|
||||
// result := intPrism.GetOption(value) // Some(42)
|
||||
//
|
||||
// // Type mismatch returns None
|
||||
// var strValue any = "hello"
|
||||
// result = intPrism.GetOption(strValue) // None[int]()
|
||||
//
|
||||
// // Convert back to any
|
||||
// anyValue := intPrism.ReverseGet(42) // any(42)
|
||||
//
|
||||
// // Use with Set to update typed values
|
||||
// setter := Set[any, int](100)
|
||||
// result := setter(intPrism)(any(42)) // any(100)
|
||||
//
|
||||
// Common use cases:
|
||||
// - Safely extracting typed values from interface{} collections
|
||||
// - Working with heterogeneous data structures
|
||||
// - Type-safe deserialization and validation
|
||||
// - Pattern matching on interface{} values
|
||||
func InstanceOf[T any]() Prism[any, T] {
|
||||
return MakePrism(option.ToType[T], F.ToAny[T])
|
||||
}
|
||||
|
||||
// ParseDate creates a prism for parsing and formatting dates with a specific layout.
|
||||
// It provides a safe way to work with date strings, handling parsing errors
|
||||
// gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to parse a string into a time.Time using the
|
||||
// specified layout. If parsing succeeds, it returns Some(time.Time); if it fails
|
||||
// (e.g., invalid date format), it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, formatting a time.Time back to a string
|
||||
// using the same layout.
|
||||
//
|
||||
// Parameters:
|
||||
// - layout: The time layout string (e.g., "2006-01-02", time.RFC3339)
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[string, time.Time] that safely handles date parsing/formatting
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a prism for ISO date format
|
||||
// datePrism := ParseDate("2006-01-02")
|
||||
//
|
||||
// // Parse valid date
|
||||
// parsed := datePrism.GetOption("2024-03-15")
|
||||
// // Some(time.Time{2024, 3, 15, ...})
|
||||
//
|
||||
// // Parse invalid date
|
||||
// invalid := datePrism.GetOption("not-a-date") // None[time.Time]()
|
||||
//
|
||||
// // Format date back to string
|
||||
// date := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
|
||||
// str := datePrism.ReverseGet(date) // "2024-03-15"
|
||||
//
|
||||
// // Use with Set to update dates
|
||||
// newDate := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
// setter := Set[string, time.Time](newDate)
|
||||
// result := setter(datePrism)("2024-03-15") // "2025-01-01"
|
||||
//
|
||||
// // Different layouts for different formats
|
||||
// rfc3339Prism := ParseDate(time.RFC3339)
|
||||
// parsed = rfc3339Prism.GetOption("2024-03-15T10:30:00Z")
|
||||
//
|
||||
// Common use cases:
|
||||
// - Validating and parsing date configuration values
|
||||
// - Working with date strings in APIs
|
||||
// - Converting between date formats
|
||||
// - Safely handling user-provided date inputs
|
||||
func ParseDate(layout string) Prism[string, time.Time] {
|
||||
return MakePrism(F.Flow2(
|
||||
F.Bind1st(either.Eitherize2(time.Parse), layout),
|
||||
either.Fold(F.Ignore1of1[error](option.None[time.Time]), option.Some),
|
||||
), F.Bind2nd(time.Time.Format, layout))
|
||||
}
|
||||
|
||||
// Deref creates a prism for safely dereferencing pointers.
|
||||
// It provides a safe way to work with nullable pointers, handling nil values
|
||||
// gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to dereference a pointer.
|
||||
// If the pointer is non-nil, it returns Some(*T); if it's nil, it returns None.
|
||||
//
|
||||
// The prism's ReverseGet is the identity function, returning the pointer unchanged.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type being pointed to
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[*T, *T] that safely handles pointer dereferencing
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a prism for dereferencing int pointers
|
||||
// derefPrism := Deref[int]()
|
||||
//
|
||||
// // Dereference non-nil pointer
|
||||
// value := 42
|
||||
// ptr := &value
|
||||
// result := derefPrism.GetOption(ptr) // Some(&42)
|
||||
//
|
||||
// // Dereference nil pointer
|
||||
// var nilPtr *int
|
||||
// result = derefPrism.GetOption(nilPtr) // None[*int]()
|
||||
//
|
||||
// // ReverseGet returns the pointer unchanged
|
||||
// reconstructed := derefPrism.ReverseGet(ptr) // &42
|
||||
//
|
||||
// // Use with Set to update non-nil pointers
|
||||
// newValue := 100
|
||||
// newPtr := &newValue
|
||||
// setter := Set[*int, *int](newPtr)
|
||||
// result := setter(derefPrism)(ptr) // &100
|
||||
// result = setter(derefPrism)(nilPtr) // nil (unchanged)
|
||||
//
|
||||
// Common use cases:
|
||||
// - Safely working with optional pointer fields
|
||||
// - Validating non-nil pointers before operations
|
||||
// - Filtering out nil values in data pipelines
|
||||
// - Working with database nullable columns
|
||||
func Deref[T any]() Prism[*T, *T] {
|
||||
return MakePrism(option.FromNillable[T], F.Identity[*T])
|
||||
}
|
||||
|
||||
// FromEither creates a prism for extracting Right values from Either types.
|
||||
// It provides a safe way to work with Either values, focusing on the success case
|
||||
// and handling the error case gracefully through the Option type.
|
||||
//
|
||||
// The prism's GetOption attempts to extract the Right value from an Either.
|
||||
// If the Either is Right(value), it returns Some(value); if it's Left(error), it returns None.
|
||||
//
|
||||
// The prism's ReverseGet always succeeds, wrapping a value into a Right.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error/left type
|
||||
// - T: The value/right type
|
||||
//
|
||||
// Returns:
|
||||
// - A Prism[Either[E, T], T] that safely extracts Right values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a prism for extracting successful results
|
||||
// resultPrism := FromEither[error, int]()
|
||||
//
|
||||
// // Extract from Right
|
||||
// success := either.Right[error](42)
|
||||
// result := resultPrism.GetOption(success) // Some(42)
|
||||
//
|
||||
// // Extract from Left
|
||||
// failure := either.Left[int](errors.New("failed"))
|
||||
// result = resultPrism.GetOption(failure) // None[int]()
|
||||
//
|
||||
// // Wrap value into Right
|
||||
// wrapped := resultPrism.ReverseGet(100) // Right(100)
|
||||
//
|
||||
// // Use with Set to update successful results
|
||||
// setter := Set[Either[error, int], int](200)
|
||||
// result := setter(resultPrism)(success) // Right(200)
|
||||
// result = setter(resultPrism)(failure) // Left(error) (unchanged)
|
||||
//
|
||||
// Common use cases:
|
||||
// - Extracting successful values from Either results
|
||||
// - Filtering out errors in data pipelines
|
||||
// - Working with fallible operations
|
||||
// - Composing with other prisms for complex error handling
|
||||
func FromEither[E, T any]() Prism[Either[E, T], T] {
|
||||
return MakePrism(either.ToOption[E, T], either.Of[E, T])
|
||||
}
|
||||
@@ -20,7 +20,43 @@ import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// AsTraversal converts a prism to a traversal
|
||||
// AsTraversal converts a Prism into a Traversal.
|
||||
//
|
||||
// A Traversal is a more general optic that can focus on zero or more values,
|
||||
// while a Prism focuses on zero or one value. This function lifts a Prism
|
||||
// into the Traversal abstraction, allowing it to be used in contexts that
|
||||
// expect traversals.
|
||||
//
|
||||
// The conversion works by:
|
||||
// - If the prism matches (GetOption returns Some), the traversal focuses on that value
|
||||
// - If the prism doesn't match (GetOption returns None), the traversal focuses on zero values
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The traversal function type ~func(func(A) HKTA) func(S) HKTS
|
||||
// - S: The source type
|
||||
// - A: The focus type
|
||||
// - HKTS: Higher-kinded type for S (e.g., functor/applicative context)
|
||||
// - HKTA: Higher-kinded type for A (e.g., functor/applicative context)
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift S into the higher-kinded type HKTS (pure/of operation)
|
||||
// - fmap: Function to map over HKTA and produce HKTS (functor map operation)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that converts a Prism[S, A] into a Traversal R
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Convert a prism to a traversal for use with applicative functors
|
||||
// prism := MakePrism(...)
|
||||
// traversal := AsTraversal(
|
||||
// func(s S) HKTS { return pure(s) },
|
||||
// func(hkta HKTA, f func(A) S) HKTS { return fmap(hkta, f) },
|
||||
// )(prism)
|
||||
//
|
||||
// Note: This function is typically used in advanced scenarios involving
|
||||
// higher-kinded types and applicative functors. Most users will work
|
||||
// directly with prisms rather than converting them to traversals.
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fof func(S) HKTS,
|
||||
fmap func(HKTA, func(A) S) HKTS,
|
||||
@@ -32,7 +68,9 @@ func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
s,
|
||||
sa.GetOption,
|
||||
O.Fold(
|
||||
// If prism doesn't match, return the original value lifted into HKTS
|
||||
F.Nullary2(F.Constant(s), fof),
|
||||
// If prism matches, apply f to the extracted value and map back
|
||||
func(a A) HKTS {
|
||||
return fmap(f(a), func(a A) S {
|
||||
return prismModify(F.Constant1[A](a), sa, s)
|
||||
|
||||
96
v2/optics/prism/types.go
Normal file
96
v2/optics/prism/types.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package prism
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
type (
|
||||
// Option is a type alias for O.Option[T], representing an optional value.
|
||||
// It is re-exported here for convenience when working with prisms.
|
||||
//
|
||||
// An Option[T] can be either:
|
||||
// - Some(value): Contains a value of type T
|
||||
// - None: Represents the absence of a value
|
||||
//
|
||||
// This type is commonly used in prism operations, particularly in the
|
||||
// GetOption method which returns Option[A] to indicate whether a value
|
||||
// could be extracted from the source type.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of the value that may or may not be present
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A prism's GetOption returns an Option
|
||||
// prism := MakePrism(...)
|
||||
// result := prism.GetOption(value) // Returns Option[A]
|
||||
//
|
||||
// // Check if the value was extracted successfully
|
||||
// if O.IsSome(result) {
|
||||
// // Value was found
|
||||
// } else {
|
||||
// // Value was not found (None)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - github.com/IBM/fp-go/v2/option for the full Option API
|
||||
// - Prism.GetOption for the primary use case within this package
|
||||
Option[T any] = O.Option[T]
|
||||
|
||||
// Either is a type alias for either.Either[E, T], representing a value that can be one of two types.
|
||||
// It is re-exported here for convenience when working with prisms that handle error cases.
|
||||
//
|
||||
// An Either[E, T] can be either:
|
||||
// - Left(error): Contains an error value of type E
|
||||
// - Right(value): Contains a success value of type T
|
||||
//
|
||||
// This type is commonly used in prism operations for error handling, particularly with
|
||||
// the FromEither prism which extracts Right values and returns None for Left values.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The type of the error/left value
|
||||
// - T: The type of the success/right value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Using FromEither prism to extract success values
|
||||
// prism := FromEither[error, int]()
|
||||
//
|
||||
// // Extract from a Right value
|
||||
// success := either.Right[error](42)
|
||||
// result := prism.GetOption(success) // Returns Some(42)
|
||||
//
|
||||
// // Extract from a Left value
|
||||
// failure := either.Left[int](errors.New("failed"))
|
||||
// result = prism.GetOption(failure) // Returns None
|
||||
//
|
||||
// // ReverseGet wraps a value into Right
|
||||
// wrapped := prism.ReverseGet(100) // Returns Right(100)
|
||||
//
|
||||
// Common Use Cases:
|
||||
// - Error handling in functional pipelines
|
||||
// - Representing computations that may fail
|
||||
// - Composing prisms that work with Either types
|
||||
//
|
||||
// See also:
|
||||
// - github.com/IBM/fp-go/v2/either for the full Either API
|
||||
// - FromEither for creating prisms that work with Either types
|
||||
// - Prism composition for building complex error-handling pipelines
|
||||
Either[E, T any] = either.Either[E, T]
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user