1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-11-23 22:14:53 +02:00

fix: more tests for iso and prism

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
Dr. Carsten Leue
2025-11-07 17:31:27 +01:00
parent 51adce0c95
commit 54d5dbd04a
9 changed files with 2088 additions and 42 deletions

View File

@@ -63,7 +63,7 @@ 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
IsOptional bool // true if field is a pointer or has json omitempty tag
}
// templateData holds data for template rendering
@@ -93,15 +93,15 @@ const lensConstructorTemplate = `
func Make{{.Name}}Lenses() {{.Name}}Lenses {
{{- range .Fields}}
{{- if .IsOptional}}
getOrElse{{.Name}} := O.GetOrElse(F.ConstNil[{{.BaseType}}])
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
{{- 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 },
func(s {{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
func(s {{$.Name}}, v O.Option[{{.TypeName}}]) {{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
),
{{- else}}
{{.Name}}: L.MakeLens(
@@ -117,15 +117,15 @@ func Make{{.Name}}Lenses() {{.Name}}Lenses {
func Make{{.Name}}RefLenses() {{.Name}}RefLenses {
{{- range .Fields}}
{{- if .IsOptional}}
getOrElse{{.Name}} := O.GetOrElse(F.ConstNil[{{.BaseType}}])
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
{{- 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 },
func(s *{{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
func(s *{{$.Name}}, v O.Option[{{.TypeName}}]) *{{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
),
{{- else}}
{{.Name}}: L.MakeLensRef(
@@ -332,11 +332,16 @@ func parseFile(filename string) ([]structInfo, string, error) {
isOptional := false
baseType := typeName
// Only pointer types can be optional
// Check if field is optional:
// 1. Pointer types are always optional
// 2. Non-pointer types with json omitempty tag are optional
if isPointerType(field.Type) {
isOptional = true
// Strip leading * for base type
baseType = strings.TrimPrefix(typeName, "*")
} else if hasOmitEmpty(field.Tag) {
// Non-pointer type with omitempty is also optional
isOptional = true
}
// Extract imports from this field's type
@@ -462,10 +467,10 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
// 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")
f.WriteString("\tI \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
// Add additional imports collected from field types
for importPath, alias := range allImports {
@@ -502,7 +507,7 @@ 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).",
Description: "Scans Go files for structs annotated with 'fp-go:Lens' and generates lens types. Pointer types and non-pointer types with json omitempty tag generate LensO (optional lens).",
Flags: []C.Flag{
flagLensDir,
flagFilename,
@@ -517,5 +522,3 @@ func LensCommand() *C.Command {
},
}
}
// Made with Bob

View File

@@ -278,6 +278,65 @@ type Other struct {
assert.Equal(t, "City", address.Fields[1].Name)
}
func TestParseFileWithOmitEmpty(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Config struct {
Name string
Value string ` + "`json:\"value,omitempty\"`" + `
Count int ` + "`json:\",omitempty\"`" + `
Optional *string ` + "`json:\"optional,omitempty\"`" + `
Required int ` + "`json:\"required\"`" + `
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Config struct
config := structs[0]
assert.Equal(t, "Config", config.Name)
assert.Len(t, config.Fields, 5)
// Name - no tag, not optional
assert.Equal(t, "Name", config.Fields[0].Name)
assert.Equal(t, "string", config.Fields[0].TypeName)
assert.False(t, config.Fields[0].IsOptional)
// Value - has omitempty, should be optional
assert.Equal(t, "Value", config.Fields[1].Name)
assert.Equal(t, "string", config.Fields[1].TypeName)
assert.True(t, config.Fields[1].IsOptional, "Value field with omitempty should be optional")
// Count - has omitempty (no field name in tag), should be optional
assert.Equal(t, "Count", config.Fields[2].Name)
assert.Equal(t, "int", config.Fields[2].TypeName)
assert.True(t, config.Fields[2].IsOptional, "Count field with omitempty should be optional")
// Optional - pointer with omitempty, should be optional
assert.Equal(t, "Optional", config.Fields[3].Name)
assert.Equal(t, "*string", config.Fields[3].TypeName)
assert.True(t, config.Fields[3].IsOptional)
// Required - has json tag but no omitempty, not optional
assert.Equal(t, "Required", config.Fields[4].Name)
assert.Equal(t, "int", config.Fields[4].TypeName)
assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional")
}
func TestGenerateLensHelpers(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
@@ -318,8 +377,7 @@ type TestStruct struct {
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")
assert.Contains(t, contentStr, "I.FromZero")
}
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
@@ -378,8 +436,42 @@ func TestLensTemplates(t *testing.T) {
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")
assert.Contains(t, constructorStr, "I.FromZero")
}
func TestLensTemplatesWithOmitEmpty(t *testing.T) {
s := structInfo{
Name: "ConfigStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "string", IsOptional: true}, // non-pointer with omitempty
{Name: "Count", TypeName: "int", IsOptional: true}, // non-pointer with omitempty
{Name: "Pointer", TypeName: "*string", IsOptional: true}, // pointer
},
}
// Test struct template
var structBuf bytes.Buffer
err := structTmpl.Execute(&structBuf, s)
require.NoError(t, err)
structStr := structBuf.String()
assert.Contains(t, structStr, "type ConfigStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[ConfigStruct, string]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Count LO.LensO[ConfigStruct, int]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Pointer LO.LensO[ConfigStruct, *string]")
// Test constructor template
var constructorBuf bytes.Buffer
err = constructorTmpl.Execute(&constructorBuf, s)
require.NoError(t, err)
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
assert.Contains(t, constructorStr, "isoValue := I.FromZero[string]()")
assert.Contains(t, constructorStr, "isoCount := I.FromZero[int]()")
assert.Contains(t, constructorStr, "isoPointer := I.FromZero[*string]()")
}
func TestLensCommandFlags(t *testing.T) {

187
v2/optics/iso/isos.go Normal file
View File

@@ -0,0 +1,187 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package iso
import (
"strings"
"time"
B "github.com/IBM/fp-go/v2/bytes"
F "github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/string"
)
// UTF8String creates an isomorphism between byte slices and UTF-8 strings.
// This isomorphism provides bidirectional conversion between []byte and string,
// treating the byte slice as UTF-8 encoded text.
//
// Returns:
// - An Iso[[]byte, string] where:
// - Get: Converts []byte to string using UTF-8 encoding
// - ReverseGet: Converts string to []byte using UTF-8 encoding
//
// Behavior:
// - Get direction: Interprets the byte slice as UTF-8 and returns the corresponding string
// - ReverseGet direction: Encodes the string as UTF-8 bytes
//
// Example:
//
// iso := UTF8String()
//
// // Convert bytes to string
// str := iso.Get([]byte("hello")) // "hello"
//
// // Convert string to bytes
// bytes := iso.ReverseGet("world") // []byte("world")
//
// // Round-trip conversion
// original := []byte("test")
// result := iso.ReverseGet(iso.Get(original)) // []byte("test")
//
// Use cases:
// - Converting between string and byte representations
// - Working with APIs that use different text representations
// - File I/O operations where you need to switch between strings and bytes
// - Network protocols that work with byte streams
//
// Note: This isomorphism assumes valid UTF-8 encoding. Invalid UTF-8 sequences
// in the byte slice will be handled according to Go's string conversion rules
// (typically replaced with the Unicode replacement character U+FFFD).
func UTF8String() Iso[[]byte, string] {
return MakeIso(B.ToString, S.ToBytes)
}
// lines creates an isomorphism between a slice of strings and a single string
// with lines separated by the specified separator.
// This is an internal helper function used by Lines.
//
// Parameters:
// - sep: The separator string to use for joining/splitting lines
//
// Returns:
// - An Iso[[]string, string] that joins/splits strings using the separator
//
// Behavior:
// - Get direction: Joins the string slice into a single string with separators
// - ReverseGet direction: Splits the string by the separator into a slice
func lines(sep string) Iso[[]string, string] {
return MakeIso(S.Join(sep), F.Bind2nd(strings.Split, sep))
}
// Lines creates an isomorphism between a slice of strings and a single string
// with newline-separated lines.
// This is useful for working with multi-line text where you need to convert
// between a single string and individual lines.
//
// Returns:
// - An Iso[[]string, string] where:
// - Get: Joins string slice with newline characters ("\n")
// - ReverseGet: Splits string by newline characters into a slice
//
// Behavior:
// - Get direction: Joins each string in the slice with "\n" separator
// - ReverseGet direction: Splits the string at each "\n" into a slice
//
// Example:
//
// iso := Lines()
//
// // Convert lines to single string
// lines := []string{"line1", "line2", "line3"}
// text := iso.Get(lines) // "line1\nline2\nline3"
//
// // Convert string to lines
// text := "hello\nworld"
// lines := iso.ReverseGet(text) // []string{"hello", "world"}
//
// // Round-trip conversion
// original := []string{"a", "b", "c"}
// result := iso.ReverseGet(iso.Get(original)) // []string{"a", "b", "c"}
//
// Use cases:
// - Processing multi-line text files
// - Converting between text editor representations (array of lines vs single string)
// - Working with configuration files that have line-based structure
// - Parsing or generating multi-line output
//
// Note: Empty strings in the slice will result in consecutive newlines in the output.
// Splitting a string with trailing newlines will include an empty string at the end.
//
// Example with edge cases:
//
// iso := Lines()
// lines := []string{"a", "", "b"}
// text := iso.Get(lines) // "a\n\nb"
// result := iso.ReverseGet(text) // []string{"a", "", "b"}
//
// text := "a\nb\n"
// lines := iso.ReverseGet(text) // []string{"a", "b", ""}
func Lines() Iso[[]string, string] {
return lines("\n")
}
// UnixMilli creates an isomorphism between Unix millisecond timestamps and time.Time values.
// This isomorphism provides bidirectional conversion between int64 milliseconds since
// the Unix epoch (January 1, 1970 UTC) and Go's time.Time type.
//
// Returns:
// - An Iso[int64, time.Time] where:
// - Get: Converts Unix milliseconds (int64) to time.Time
// - ReverseGet: Converts time.Time to Unix milliseconds (int64)
//
// Behavior:
// - Get direction: Creates a time.Time from milliseconds since Unix epoch
// - ReverseGet direction: Extracts milliseconds since Unix epoch from time.Time
//
// Example:
//
// iso := UnixMilli()
//
// // Convert milliseconds to time.Time
// millis := int64(1609459200000) // 2021-01-01 00:00:00 UTC
// t := iso.Get(millis)
//
// // Convert time.Time to milliseconds
// now := time.Now()
// millis := iso.ReverseGet(now)
//
// // Round-trip conversion
// original := int64(1234567890000)
// result := iso.ReverseGet(iso.Get(original)) // 1234567890000
//
// Use cases:
// - Working with APIs that use Unix millisecond timestamps (e.g., JavaScript Date.now())
// - Database storage where timestamps are stored as integers
// - JSON serialization/deserialization of timestamps
// - Converting between different time representations in distributed systems
//
// Precision notes:
// - Millisecond precision is maintained in both directions
// - Sub-millisecond precision in time.Time is lost when converting to int64
// - The conversion is timezone-aware (time.Time includes location information)
//
// Example with precision:
//
// iso := UnixMilli()
// t := time.Date(2021, 1, 1, 12, 30, 45, 123456789, time.UTC)
// millis := iso.ReverseGet(t) // Nanoseconds are truncated to milliseconds
// restored := iso.Get(millis) // Nanoseconds will be 123000000
//
// Note: This isomorphism uses UTC for the time.Time values. If you need to preserve
// timezone information, consider storing it separately or using a different representation.
func UnixMilli() Iso[int64, time.Time] {
return MakeIso(time.UnixMilli, time.Time.UnixMilli)
}

432
v2/optics/iso/isos_test.go Normal file
View File

@@ -0,0 +1,432 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package iso
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestUTF8String tests the UTF8String isomorphism
func TestUTF8String(t *testing.T) {
iso := UTF8String()
t.Run("Get converts bytes to string", func(t *testing.T) {
bytes := []byte("hello world")
result := iso.Get(bytes)
assert.Equal(t, "hello world", result)
})
t.Run("Get handles empty bytes", func(t *testing.T) {
bytes := []byte{}
result := iso.Get(bytes)
assert.Equal(t, "", result)
})
t.Run("Get handles UTF-8 characters", func(t *testing.T) {
bytes := []byte("Hello 世界 🌍")
result := iso.Get(bytes)
assert.Equal(t, "Hello 世界 🌍", result)
})
t.Run("ReverseGet converts string to bytes", func(t *testing.T) {
str := "hello world"
result := iso.ReverseGet(str)
assert.Equal(t, []byte("hello world"), result)
})
t.Run("ReverseGet handles empty string", func(t *testing.T) {
str := ""
result := iso.ReverseGet(str)
assert.Equal(t, []byte{}, result)
})
t.Run("ReverseGet handles UTF-8 characters", func(t *testing.T) {
str := "Hello 世界 🌍"
result := iso.ReverseGet(str)
assert.Equal(t, []byte("Hello 世界 🌍"), result)
})
t.Run("Round-trip bytes to string to bytes", func(t *testing.T) {
original := []byte("test data")
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
})
t.Run("Round-trip string to bytes to string", func(t *testing.T) {
original := "test string"
result := iso.Get(iso.ReverseGet(original))
assert.Equal(t, original, result)
})
t.Run("Handles special characters", func(t *testing.T) {
str := "line1\nline2\ttab\r\nwindows"
bytes := iso.ReverseGet(str)
result := iso.Get(bytes)
assert.Equal(t, str, result)
})
t.Run("Handles binary-like data", func(t *testing.T) {
bytes := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f} // "Hello"
result := iso.Get(bytes)
assert.Equal(t, "Hello", result)
})
}
// TestLines tests the Lines isomorphism
func TestLines(t *testing.T) {
iso := Lines()
t.Run("Get joins lines with newline", func(t *testing.T) {
lines := []string{"line1", "line2", "line3"}
result := iso.Get(lines)
assert.Equal(t, "line1\nline2\nline3", result)
})
t.Run("Get handles single line", func(t *testing.T) {
lines := []string{"single line"}
result := iso.Get(lines)
assert.Equal(t, "single line", result)
})
t.Run("Get handles empty slice", func(t *testing.T) {
lines := []string{}
result := iso.Get(lines)
assert.Equal(t, "", result)
})
t.Run("Get handles empty strings in slice", func(t *testing.T) {
lines := []string{"a", "", "b"}
result := iso.Get(lines)
assert.Equal(t, "a\n\nb", result)
})
t.Run("Get handles slice with only empty strings", func(t *testing.T) {
lines := []string{"", "", ""}
result := iso.Get(lines)
assert.Equal(t, "\n\n", result)
})
t.Run("ReverseGet splits string by newline", func(t *testing.T) {
str := "line1\nline2\nline3"
result := iso.ReverseGet(str)
assert.Equal(t, []string{"line1", "line2", "line3"}, result)
})
t.Run("ReverseGet handles single line", func(t *testing.T) {
str := "single line"
result := iso.ReverseGet(str)
assert.Equal(t, []string{"single line"}, result)
})
t.Run("ReverseGet handles empty string", func(t *testing.T) {
str := ""
result := iso.ReverseGet(str)
assert.Equal(t, []string{""}, result)
})
t.Run("ReverseGet handles consecutive newlines", func(t *testing.T) {
str := "a\n\nb"
result := iso.ReverseGet(str)
assert.Equal(t, []string{"a", "", "b"}, result)
})
t.Run("ReverseGet handles trailing newline", func(t *testing.T) {
str := "a\nb\n"
result := iso.ReverseGet(str)
assert.Equal(t, []string{"a", "b", ""}, result)
})
t.Run("ReverseGet handles leading newline", func(t *testing.T) {
str := "\na\nb"
result := iso.ReverseGet(str)
assert.Equal(t, []string{"", "a", "b"}, result)
})
t.Run("Round-trip lines to string to lines", func(t *testing.T) {
original := []string{"line1", "line2", "line3"}
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
})
t.Run("Round-trip string to lines to string", func(t *testing.T) {
original := "line1\nline2\nline3"
result := iso.Get(iso.ReverseGet(original))
assert.Equal(t, original, result)
})
t.Run("Handles lines with special characters", func(t *testing.T) {
lines := []string{"Hello 世界", "🌍 Earth", "tab\there"}
text := iso.Get(lines)
result := iso.ReverseGet(text)
assert.Equal(t, lines, result)
})
t.Run("Preserves whitespace in lines", func(t *testing.T) {
lines := []string{" indented", "normal", "\ttabbed"}
text := iso.Get(lines)
result := iso.ReverseGet(text)
assert.Equal(t, lines, result)
})
}
// TestUnixMilli tests the UnixMilli isomorphism
func TestUnixMilli(t *testing.T) {
iso := UnixMilli()
t.Run("Get converts milliseconds to time", func(t *testing.T) {
millis := int64(1609459200000) // 2021-01-01 00:00:00 UTC
result := iso.Get(millis)
// Compare Unix timestamps to avoid timezone issues
assert.Equal(t, millis, result.UnixMilli())
})
t.Run("Get handles zero milliseconds (Unix epoch)", func(t *testing.T) {
millis := int64(0)
result := iso.Get(millis)
assert.Equal(t, millis, result.UnixMilli())
})
t.Run("Get handles negative milliseconds (before epoch)", func(t *testing.T) {
millis := int64(-86400000) // 1 day before epoch
result := iso.Get(millis)
assert.Equal(t, millis, result.UnixMilli())
})
t.Run("ReverseGet converts time to milliseconds", func(t *testing.T) {
tm := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
result := iso.ReverseGet(tm)
assert.Equal(t, int64(1609459200000), result)
})
t.Run("ReverseGet handles Unix epoch", func(t *testing.T) {
tm := time.Unix(0, 0).UTC()
result := iso.ReverseGet(tm)
assert.Equal(t, int64(0), result)
})
t.Run("ReverseGet handles time before epoch", func(t *testing.T) {
tm := time.Date(1969, 12, 31, 0, 0, 0, 0, time.UTC)
result := iso.ReverseGet(tm)
assert.Equal(t, int64(-86400000), result)
})
t.Run("Round-trip milliseconds to time to milliseconds", func(t *testing.T) {
original := int64(1234567890000)
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
})
t.Run("Round-trip time to milliseconds to time", func(t *testing.T) {
original := time.Date(2021, 6, 15, 12, 30, 45, 0, time.UTC)
result := iso.Get(iso.ReverseGet(original))
// Compare as Unix timestamps to avoid timezone issues
assert.Equal(t, original.UnixMilli(), result.UnixMilli())
})
t.Run("Truncates sub-millisecond precision", func(t *testing.T) {
// Time with nanoseconds
tm := time.Date(2021, 1, 1, 0, 0, 0, 123456789, time.UTC)
millis := iso.ReverseGet(tm)
result := iso.Get(millis)
// Should have millisecond precision only - compare timestamps
assert.Equal(t, tm.Truncate(time.Millisecond).UnixMilli(), result.UnixMilli())
})
t.Run("Handles current time", func(t *testing.T) {
now := time.Now()
millis := iso.ReverseGet(now)
result := iso.Get(millis)
// Should be equal within millisecond precision
assert.Equal(t, now.Truncate(time.Millisecond), result.Truncate(time.Millisecond))
})
t.Run("Handles far future date", func(t *testing.T) {
future := time.Date(2100, 12, 31, 23, 59, 59, 0, time.UTC)
millis := iso.ReverseGet(future)
result := iso.Get(millis)
assert.Equal(t, future.UnixMilli(), result.UnixMilli())
})
t.Run("Handles far past date", func(t *testing.T) {
past := time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
millis := iso.ReverseGet(past)
result := iso.Get(millis)
assert.Equal(t, past.UnixMilli(), result.UnixMilli())
})
t.Run("Preserves timezone information in round-trip", func(t *testing.T) {
// Create time in different timezone
loc, _ := time.LoadLocation("America/New_York")
tm := time.Date(2021, 6, 15, 12, 0, 0, 0, loc)
// Convert to millis and back
millis := iso.ReverseGet(tm)
result := iso.Get(millis)
// Times should represent the same instant (even if timezone differs)
assert.True(t, tm.Equal(result))
})
}
// TestUTF8StringRoundTripLaws verifies isomorphism laws for UTF8String
func TestUTF8StringRoundTripLaws(t *testing.T) {
iso := UTF8String()
t.Run("Law 1: ReverseGet(Get(bytes)) == bytes", func(t *testing.T) {
testCases := [][]byte{
[]byte("hello"),
[]byte(""),
[]byte("Hello 世界 🌍"),
[]byte{0x48, 0x65, 0x6c, 0x6c, 0x6f},
}
for _, original := range testCases {
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
}
})
t.Run("Law 2: Get(ReverseGet(str)) == str", func(t *testing.T) {
testCases := []string{
"hello",
"",
"Hello 世界 🌍",
"special\nchars\ttab",
}
for _, original := range testCases {
result := iso.Get(iso.ReverseGet(original))
assert.Equal(t, original, result)
}
})
}
// TestLinesRoundTripLaws verifies isomorphism laws for Lines
func TestLinesRoundTripLaws(t *testing.T) {
iso := Lines()
t.Run("Law 1: ReverseGet(Get(lines)) == lines", func(t *testing.T) {
testCases := [][]string{
{"line1", "line2"},
{"single"},
{"a", "", "b"},
{"", "", ""},
}
for _, original := range testCases {
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
}
})
t.Run("Law 1: Empty slice special case", func(t *testing.T) {
// Empty slice becomes "" which splits to [""]
// This is expected behavior of strings.Split
original := []string{}
text := iso.Get(original) // ""
result := iso.ReverseGet(text) // [""]
assert.Equal(t, []string{""}, result)
})
t.Run("Law 2: Get(ReverseGet(str)) == str", func(t *testing.T) {
testCases := []string{
"line1\nline2",
"single",
"",
"a\n\nb",
"\n\n",
}
for _, original := range testCases {
result := iso.Get(iso.ReverseGet(original))
assert.Equal(t, original, result)
}
})
}
// TestUnixMilliRoundTripLaws verifies isomorphism laws for UnixMilli
func TestUnixMilliRoundTripLaws(t *testing.T) {
iso := UnixMilli()
t.Run("Law 1: ReverseGet(Get(millis)) == millis", func(t *testing.T) {
testCases := []int64{
0,
1609459200000,
-86400000,
1234567890000,
time.Now().UnixMilli(),
}
for _, original := range testCases {
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
}
})
t.Run("Law 2: Get(ReverseGet(time)) == time (with millisecond precision)", func(t *testing.T) {
testCases := []time.Time{
time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
time.Unix(0, 0).UTC(),
time.Date(1969, 12, 31, 0, 0, 0, 0, time.UTC),
time.Now().Truncate(time.Millisecond),
}
for _, original := range testCases {
result := iso.Get(iso.ReverseGet(original))
// Compare Unix timestamps to avoid timezone issues
assert.Equal(t, original.UnixMilli(), result.UnixMilli())
}
})
}
// TestIsosComposition tests composing the isos functions
func TestIsosComposition(t *testing.T) {
t.Run("Compose UTF8String with Lines", func(t *testing.T) {
utf8Iso := UTF8String()
linesIso := Lines()
// First convert bytes to string, then string to lines
bytes := []byte("line1\nline2\nline3")
str := utf8Iso.Get(bytes)
lines := linesIso.ReverseGet(str)
assert.Equal(t, []string{"line1", "line2", "line3"}, lines)
// Reverse: lines to string to bytes
originalLines := []string{"a", "b", "c"}
text := linesIso.Get(originalLines)
resultBytes := utf8Iso.ReverseGet(text)
assert.Equal(t, []byte("a\nb\nc"), resultBytes)
})
t.Run("Chain UTF8String and Lines operations", func(t *testing.T) {
utf8Iso := UTF8String()
linesIso := Lines()
// Process: bytes -> string -> lines -> string -> bytes
original := []byte("hello\nworld")
str := utf8Iso.Get(original)
lines := linesIso.ReverseGet(str)
text := linesIso.Get(lines)
result := utf8Iso.ReverseGet(text)
assert.Equal(t, original, result)
})
}

View File

@@ -0,0 +1,83 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package option provides isomorphisms for working with Option types.
// It offers utilities to convert between regular values and Option-wrapped values,
// particularly useful for handling zero values and optional data.
package option
import (
"github.com/IBM/fp-go/v2/optics/iso"
"github.com/IBM/fp-go/v2/option"
)
// FromZero creates an isomorphism between a comparable type T and Option[T].
// The isomorphism treats the zero value of T as None and non-zero values as Some.
//
// This is particularly useful for types where the zero value has special meaning
// (e.g., 0 for numbers, "" for strings, nil for pointers) and you want to represent
// the absence of a meaningful value using Option.
//
// Type Parameters:
// - T: A comparable type (must support == and != operators)
//
// Returns:
// - An Iso[T, Option[T]] where:
// - Get: Converts T to Option[T] (zero value → None, non-zero → Some)
// - ReverseGet: Converts Option[T] to T (None → zero value, Some → unwrapped value)
//
// Behavior:
// - Get direction: If the value equals the zero value of T, returns None; otherwise returns Some(value)
// - ReverseGet direction: If the Option is None, returns the zero value; otherwise returns the unwrapped value
//
// Example with integers:
//
// isoInt := FromZero[int]()
// opt := isoInt.Get(0) // None (0 is the zero value)
// opt = isoInt.Get(42) // Some(42)
// val := isoInt.ReverseGet(option.None[int]()) // 0
// val = isoInt.ReverseGet(option.Some(42)) // 42
//
// Example with strings:
//
// isoStr := FromZero[string]()
// opt := isoStr.Get("") // None ("" is the zero value)
// opt = isoStr.Get("hello") // Some("hello")
// val := isoStr.ReverseGet(option.None[string]()) // ""
// val = isoStr.ReverseGet(option.Some("world")) // "world"
//
// Example with pointers:
//
// isoPtr := FromZero[*int]()
// opt := isoPtr.Get(nil) // None (nil is the zero value)
// num := 42
// opt = isoPtr.Get(&num) // Some(&num)
//
// Use cases:
// - Converting between database nullable columns and Go types
// - Handling optional configuration values with defaults
// - Working with APIs that use zero values to indicate absence
// - Simplifying validation logic for required vs optional fields
//
// Note: This isomorphism satisfies the round-trip laws:
// - ReverseGet(Get(t)) == t for all t: T
// - Get(ReverseGet(opt)) == opt for all opt: Option[T]
func FromZero[T comparable]() iso.Iso[T, option.Option[T]] {
var zero T
return iso.MakeIso(
option.FromPredicate(func(t T) bool { return t != zero }),
option.GetOrElse(func() T { return zero }),
)
}

View File

@@ -0,0 +1,366 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package option
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/iso"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestFromZeroInt tests the FromZero isomorphism with integer type
func TestFromZeroInt(t *testing.T) {
isoInt := FromZero[int]()
t.Run("Get converts zero to None", func(t *testing.T) {
result := isoInt.Get(0)
assert.True(t, O.IsNone(result))
})
t.Run("Get converts non-zero to Some", func(t *testing.T) {
result := isoInt.Get(42)
assert.True(t, O.IsSome(result))
assert.Equal(t, 42, O.MonadGetOrElse(result, func() int { return 0 }))
})
t.Run("Get converts negative to Some", func(t *testing.T) {
result := isoInt.Get(-5)
assert.True(t, O.IsSome(result))
assert.Equal(t, -5, O.MonadGetOrElse(result, func() int { return 0 }))
})
t.Run("ReverseGet converts None to zero", func(t *testing.T) {
result := isoInt.ReverseGet(O.None[int]())
assert.Equal(t, 0, result)
})
t.Run("ReverseGet converts Some to value", func(t *testing.T) {
result := isoInt.ReverseGet(O.Some(42))
assert.Equal(t, 42, result)
})
}
// TestFromZeroString tests the FromZero isomorphism with string type
func TestFromZeroString(t *testing.T) {
isoStr := FromZero[string]()
t.Run("Get converts empty string to None", func(t *testing.T) {
result := isoStr.Get("")
assert.True(t, O.IsNone(result))
})
t.Run("Get converts non-empty string to Some", func(t *testing.T) {
result := isoStr.Get("hello")
assert.True(t, O.IsSome(result))
assert.Equal(t, "hello", O.MonadGetOrElse(result, func() string { return "" }))
})
t.Run("ReverseGet converts None to empty string", func(t *testing.T) {
result := isoStr.ReverseGet(O.None[string]())
assert.Equal(t, "", result)
})
t.Run("ReverseGet converts Some to value", func(t *testing.T) {
result := isoStr.ReverseGet(O.Some("world"))
assert.Equal(t, "world", result)
})
}
// TestFromZeroFloat tests the FromZero isomorphism with float64 type
func TestFromZeroFloat(t *testing.T) {
isoFloat := FromZero[float64]()
t.Run("Get converts 0.0 to None", func(t *testing.T) {
result := isoFloat.Get(0.0)
assert.True(t, O.IsNone(result))
})
t.Run("Get converts non-zero float to Some", func(t *testing.T) {
result := isoFloat.Get(3.14)
assert.True(t, O.IsSome(result))
assert.InDelta(t, 3.14, O.MonadGetOrElse(result, func() float64 { return 0.0 }), 0.001)
})
t.Run("ReverseGet converts None to 0.0", func(t *testing.T) {
result := isoFloat.ReverseGet(O.None[float64]())
assert.Equal(t, 0.0, result)
})
t.Run("ReverseGet converts Some to value", func(t *testing.T) {
result := isoFloat.ReverseGet(O.Some(2.718))
assert.InDelta(t, 2.718, result, 0.001)
})
}
// TestFromZeroPointer tests the FromZero isomorphism with pointer type
func TestFromZeroPointer(t *testing.T) {
isoPtr := FromZero[*int]()
t.Run("Get converts nil to None", func(t *testing.T) {
result := isoPtr.Get(nil)
assert.True(t, O.IsNone(result))
})
t.Run("Get converts non-nil pointer to Some", func(t *testing.T) {
num := 42
result := isoPtr.Get(&num)
assert.True(t, O.IsSome(result))
ptr := O.MonadGetOrElse(result, func() *int { return nil })
assert.NotNil(t, ptr)
assert.Equal(t, 42, *ptr)
})
t.Run("ReverseGet converts None to nil", func(t *testing.T) {
result := isoPtr.ReverseGet(O.None[*int]())
assert.Nil(t, result)
})
t.Run("ReverseGet converts Some to pointer", func(t *testing.T) {
num := 99
result := isoPtr.ReverseGet(O.Some(&num))
assert.NotNil(t, result)
assert.Equal(t, 99, *result)
})
}
// TestFromZeroBool tests the FromZero isomorphism with bool type
func TestFromZeroBool(t *testing.T) {
isoBool := FromZero[bool]()
t.Run("Get converts false to None", func(t *testing.T) {
result := isoBool.Get(false)
assert.True(t, O.IsNone(result))
})
t.Run("Get converts true to Some", func(t *testing.T) {
result := isoBool.Get(true)
assert.True(t, O.IsSome(result))
assert.True(t, O.MonadGetOrElse(result, func() bool { return false }))
})
t.Run("ReverseGet converts None to false", func(t *testing.T) {
result := isoBool.ReverseGet(O.None[bool]())
assert.False(t, result)
})
t.Run("ReverseGet converts Some to true", func(t *testing.T) {
result := isoBool.ReverseGet(O.Some(true))
assert.True(t, result)
})
}
// TestFromZeroRoundTripLaws verifies the isomorphism laws
func TestFromZeroRoundTripLaws(t *testing.T) {
t.Run("Law 1: ReverseGet(Get(t)) == t for integers", func(t *testing.T) {
isoInt := FromZero[int]()
// Test with zero value
assert.Equal(t, 0, isoInt.ReverseGet(isoInt.Get(0)))
// Test with non-zero values
assert.Equal(t, 42, isoInt.ReverseGet(isoInt.Get(42)))
assert.Equal(t, -10, isoInt.ReverseGet(isoInt.Get(-10)))
})
t.Run("Law 1: ReverseGet(Get(t)) == t for strings", func(t *testing.T) {
isoStr := FromZero[string]()
// Test with zero value
assert.Equal(t, "", isoStr.ReverseGet(isoStr.Get("")))
// Test with non-zero values
assert.Equal(t, "hello", isoStr.ReverseGet(isoStr.Get("hello")))
})
t.Run("Law 2: Get(ReverseGet(opt)) == opt for None", func(t *testing.T) {
isoInt := FromZero[int]()
none := O.None[int]()
result := isoInt.Get(isoInt.ReverseGet(none))
assert.Equal(t, none, result)
})
t.Run("Law 2: Get(ReverseGet(opt)) == opt for Some", func(t *testing.T) {
isoInt := FromZero[int]()
some := O.Some(42)
result := isoInt.Get(isoInt.ReverseGet(some))
assert.Equal(t, some, result)
})
}
// TestFromZeroWithModify tests using FromZero with iso.Modify
func TestFromZeroWithModify(t *testing.T) {
isoInt := FromZero[int]()
t.Run("Modify applies transformation to non-zero value", func(t *testing.T) {
double := func(opt O.Option[int]) O.Option[int] {
return O.MonadMap(opt, func(x int) int { return x * 2 })
}
result := iso.Modify[int](double)(isoInt)(5)
assert.Equal(t, 10, result)
})
t.Run("Modify preserves zero value", func(t *testing.T) {
double := func(opt O.Option[int]) O.Option[int] {
return O.MonadMap(opt, func(x int) int { return x * 2 })
}
result := iso.Modify[int](double)(isoInt)(0)
assert.Equal(t, 0, result)
})
}
// TestFromZeroWithCompose tests composing FromZero with other isomorphisms
func TestFromZeroWithCompose(t *testing.T) {
isoInt := FromZero[int]()
// Create an isomorphism that doubles/halves values
doubleIso := iso.MakeIso(
func(opt O.Option[int]) O.Option[int] {
return O.MonadMap(opt, func(x int) int { return x * 2 })
},
func(opt O.Option[int]) O.Option[int] {
return O.MonadMap(opt, func(x int) int { return x / 2 })
},
)
composed := F.Pipe1(isoInt, iso.Compose[int](doubleIso))
t.Run("Composed isomorphism works with non-zero", func(t *testing.T) {
result := composed.Get(5)
assert.True(t, O.IsSome(result))
assert.Equal(t, 10, O.MonadGetOrElse(result, func() int { return 0 }))
})
t.Run("Composed isomorphism works with zero", func(t *testing.T) {
result := composed.Get(0)
assert.True(t, O.IsNone(result))
})
t.Run("Composed isomorphism reverse works", func(t *testing.T) {
result := composed.ReverseGet(O.Some(20))
assert.Equal(t, 10, result)
})
}
// TestFromZeroWithUnwrapWrap tests using Unwrap and Wrap helpers
func TestFromZeroWithUnwrapWrap(t *testing.T) {
isoInt := FromZero[int]()
t.Run("Unwrap extracts Option from value", func(t *testing.T) {
result := iso.Unwrap[O.Option[int]](42)(isoInt)
assert.True(t, O.IsSome(result))
assert.Equal(t, 42, O.MonadGetOrElse(result, func() int { return 0 }))
})
t.Run("Wrap creates value from Option", func(t *testing.T) {
result := iso.Wrap[int](O.Some(99))(isoInt)
assert.Equal(t, 99, result)
})
t.Run("To is alias for Unwrap", func(t *testing.T) {
result := iso.To[O.Option[int]](42)(isoInt)
assert.True(t, O.IsSome(result))
})
t.Run("From is alias for Wrap", func(t *testing.T) {
result := iso.From[int](O.Some(99))(isoInt)
assert.Equal(t, 99, result)
})
}
// TestFromZeroWithReverse tests reversing the isomorphism
func TestFromZeroWithReverse(t *testing.T) {
isoInt := FromZero[int]()
reversed := iso.Reverse(isoInt)
t.Run("Reversed Get is original ReverseGet", func(t *testing.T) {
result := reversed.Get(O.Some(42))
assert.Equal(t, 42, result)
})
t.Run("Reversed ReverseGet is original Get", func(t *testing.T) {
result := reversed.ReverseGet(42)
assert.True(t, O.IsSome(result))
assert.Equal(t, 42, O.MonadGetOrElse(result, func() int { return 0 }))
})
}
// TestFromZeroCustomType tests FromZero with a custom comparable type
func TestFromZeroCustomType(t *testing.T) {
type UserID int
isoUserID := FromZero[UserID]()
t.Run("Get converts zero UserID to None", func(t *testing.T) {
result := isoUserID.Get(UserID(0))
assert.True(t, O.IsNone(result))
})
t.Run("Get converts non-zero UserID to Some", func(t *testing.T) {
result := isoUserID.Get(UserID(123))
assert.True(t, O.IsSome(result))
assert.Equal(t, UserID(123), O.MonadGetOrElse(result, func() UserID { return 0 }))
})
t.Run("ReverseGet converts None to zero UserID", func(t *testing.T) {
result := isoUserID.ReverseGet(O.None[UserID]())
assert.Equal(t, UserID(0), result)
})
t.Run("ReverseGet converts Some to UserID", func(t *testing.T) {
result := isoUserID.ReverseGet(O.Some(UserID(456)))
assert.Equal(t, UserID(456), result)
})
}
// TestFromZeroEdgeCases tests edge cases and boundary conditions
func TestFromZeroEdgeCases(t *testing.T) {
t.Run("Works with maximum int value", func(t *testing.T) {
isoInt := FromZero[int]()
maxInt := int(^uint(0) >> 1)
result := isoInt.Get(maxInt)
assert.True(t, O.IsSome(result))
assert.Equal(t, maxInt, isoInt.ReverseGet(result))
})
t.Run("Works with minimum int value", func(t *testing.T) {
isoInt := FromZero[int]()
minInt := -int(^uint(0)>>1) - 1
result := isoInt.Get(minInt)
assert.True(t, O.IsSome(result))
assert.Equal(t, minInt, isoInt.ReverseGet(result))
})
t.Run("Works with very long strings", func(t *testing.T) {
isoStr := FromZero[string]()
longStr := string(make([]byte, 10000))
for i := range longStr {
longStr = longStr[:i] + "a" + longStr[i+1:]
}
result := isoStr.Get(longStr)
assert.True(t, O.IsSome(result))
assert.Equal(t, longStr, isoStr.ReverseGet(result))
})
}

View File

@@ -18,6 +18,7 @@ package prism
import (
"encoding/base64"
"net/url"
"regexp"
"time"
"github.com/IBM/fp-go/v2/either"
@@ -310,3 +311,349 @@ func Deref[T any]() Prism[*T, *T] {
func FromEither[E, T any]() Prism[Either[E, T], T] {
return MakePrism(either.ToOption[E, T], either.Of[E, T])
}
// FromZero creates a prism that matches zero values of comparable types.
// It provides a safe way to work with zero values, handling non-zero values
// gracefully through the Option type.
//
// The prism's GetOption returns Some(t) if the value equals the zero value
// of type T; otherwise, it returns None.
//
// The prism's ReverseGet is the identity function, returning the value unchanged.
//
// Type Parameters:
// - T: A comparable type (must support == and != operators)
//
// Returns:
// - A Prism[T, T] that matches zero values
//
// Example:
//
// // Create a prism for zero integers
// zeroPrism := FromZero[int]()
//
// // Match zero value
// result := zeroPrism.GetOption(0) // Some(0)
//
// // Non-zero returns None
// result = zeroPrism.GetOption(42) // None[int]()
//
// // ReverseGet is identity
// value := zeroPrism.ReverseGet(0) // 0
//
// // Use with Set to update zero values
// setter := Set[int, int](100)
// result := setter(zeroPrism)(0) // 100
// result = setter(zeroPrism)(42) // 42 (unchanged)
//
// Common use cases:
// - Validating that values are zero/default
// - Filtering zero values in data pipelines
// - Working with optional fields that use zero as "not set"
// - Replacing zero values with defaults
func FromZero[T comparable]() Prism[T, T] {
var zero T
return MakePrism(option.FromPredicate(func(t T) bool { return t == zero }), F.Identity[T])
}
// Match represents a regex match result with full reconstruction capability.
// It contains everything needed to reconstruct the original string, making it
// suitable for use in a prism that maintains bidirectionality.
//
// Fields:
// - Before: Text before the match
// - Groups: Capture groups (index 0 is the full match, 1+ are capture groups)
// - After: Text after the match
//
// Example:
//
// // For string "hello world 123" with regex `\d+`:
// // Match{
// // Before: "hello world ",
// // Groups: []string{"123"},
// // After: "",
// // }
type Match struct {
Before string // Text before the match
Groups []string // Capture groups (index 0 is full match)
After string // Text after the match
}
// Reconstruct builds the original string from a Match.
// This is the inverse operation of regex matching, allowing full round-trip conversion.
//
// Returns:
// - The original string that was matched
//
// Example:
//
// match := Match{
// Before: "hello ",
// Groups: []string{"world"},
// After: "!",
// }
// original := match.Reconstruct() // "hello world!"
func (m Match) Reconstruct() string {
return m.Before + m.Groups[0] + m.After
}
// FullMatch returns the complete matched text (the entire regex match).
// This is equivalent to Groups[0] and represents what the regex matched.
//
// Returns:
// - The full matched text
//
// Example:
//
// match := Match{
// Before: "price: ",
// Groups: []string{"$99.99", "99.99"},
// After: " USD",
// }
// full := match.FullMatch() // "$99.99"
func (m Match) FullMatch() string {
return m.Groups[0]
}
// Group returns the nth capture group from the match (1-indexed).
// Capture group 0 is the full match, groups 1+ are the parenthesized captures.
// Returns an empty string if the group index is out of bounds.
//
// Parameters:
// - n: The capture group index (1-indexed)
//
// Returns:
// - The captured text, or empty string if index is invalid
//
// Example:
//
// // Regex: `(\w+)@(\w+\.\w+)` matching "user@example.com"
// match := Match{
// Groups: []string{"user@example.com", "user", "example.com"},
// }
// username := match.Group(1) // "user"
// domain := match.Group(2) // "example.com"
// invalid := match.Group(5) // ""
func (m Match) Group(n int) string {
if n < len(m.Groups) {
return m.Groups[n]
}
return ""
}
// RegexMatcher creates a prism for regex pattern matching with full reconstruction.
// It provides a safe way to match strings against a regex pattern, extracting
// match information while maintaining the ability to reconstruct the original string.
//
// The prism's GetOption attempts to match the regex against the string.
// If a match is found, it returns Some(Match) with all capture groups and context;
// if no match is found, it returns None.
//
// The prism's ReverseGet reconstructs the original string from a Match.
//
// Parameters:
// - re: A compiled regular expression
//
// Returns:
// - A Prism[string, Match] that safely handles regex matching
//
// Example:
//
// // Create a prism for matching numbers
// numRegex := regexp.MustCompile(`\d+`)
// numPrism := RegexMatcher(numRegex)
//
// // Match a string
// match := numPrism.GetOption("price: 42 dollars")
// // Some(Match{Before: "price: ", Groups: ["42"], After: " dollars"})
//
// // No match returns None
// noMatch := numPrism.GetOption("no numbers here") // None[Match]()
//
// // Reconstruct original string
// if m, ok := option.IsSome(match); ok {
// original := numPrism.ReverseGet(m) // "price: 42 dollars"
// }
//
// // Extract capture groups
// emailRegex := regexp.MustCompile(`(\w+)@(\w+\.\w+)`)
// emailPrism := RegexMatcher(emailRegex)
// match = emailPrism.GetOption("contact: user@example.com")
// // Match.Group(1) = "user", Match.Group(2) = "example.com"
//
// Common use cases:
// - Parsing structured text with regex patterns
// - Extracting and validating data from strings
// - Text transformation pipelines
// - Pattern-based string manipulation with reconstruction
//
// Note: This prism is bijective - you can always reconstruct the original
// string from a Match, making it suitable for round-trip transformations.
func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
noMatch := option.None[Match]()
return MakePrism(
// String -> Option[Match]
func(s string) Option[Match] {
loc := re.FindStringSubmatchIndex(s)
if loc == nil {
return noMatch
}
// Extract all capture groups
groups := make([]string, 0)
for i := 0; i < len(loc); i += 2 {
if loc[i] >= 0 {
groups = append(groups, s[loc[i]:loc[i+1]])
} else {
groups = append(groups, "")
}
}
match := Match{
Before: s[:loc[0]],
Groups: groups,
After: s[loc[1]:],
}
return option.Some(match)
},
Match.Reconstruct,
)
}
// NamedMatch represents a regex match result with named capture groups.
// It provides access to captured text by name rather than by index, making
// regex patterns more readable and maintainable.
//
// Fields:
// - Before: Text before the match
// - Groups: Map of capture group names to their matched text
// - Full: The complete matched text
// - After: Text after the match
//
// Example:
//
// // For regex `(?P<user>\w+)@(?P<domain>\w+\.\w+)` matching "user@example.com":
// // NamedMatch{
// // Before: "",
// // Groups: map[string]string{"user": "user", "domain": "example.com"},
// // Full: "user@example.com",
// // After: "",
// // }
type NamedMatch struct {
Before string
Groups map[string]string
Full string // The full matched text
After string
}
// Reconstruct builds the original string from a NamedMatch.
// This is the inverse operation of regex matching, allowing full round-trip conversion.
//
// Returns:
// - The original string that was matched
//
// Example:
//
// match := NamedMatch{
// Before: "email: ",
// Full: "user@example.com",
// Groups: map[string]string{"user": "user", "domain": "example.com"},
// After: "",
// }
// original := match.Reconstruct() // "email: user@example.com"
func (nm NamedMatch) Reconstruct() string {
return nm.Before + nm.Full + nm.After
}
// RegexNamedMatcher creates a prism for regex pattern matching with named capture groups.
// It provides a safe way to match strings against a regex pattern with named groups,
// making it easier to extract specific parts of the match by name rather than index.
//
// The prism's GetOption attempts to match the regex against the string.
// If a match is found, it returns Some(NamedMatch) with all named capture groups;
// if no match is found, it returns None.
//
// The prism's ReverseGet reconstructs the original string from a NamedMatch.
//
// Parameters:
// - re: A compiled regular expression with named capture groups
//
// Returns:
// - A Prism[string, NamedMatch] that safely handles regex matching with named groups
//
// Example:
//
// // Create a prism for matching email addresses with named groups
// emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
// emailPrism := RegexNamedMatcher(emailRegex)
//
// // Match a string
// match := emailPrism.GetOption("contact: user@example.com")
// // Some(NamedMatch{
// // Before: "contact: ",
// // Groups: {"user": "user", "domain": "example.com"},
// // Full: "user@example.com",
// // After: "",
// // })
//
// // Access named groups
// if m, ok := option.IsSome(match); ok {
// username := m.Groups["user"] // "user"
// domain := m.Groups["domain"] // "example.com"
// }
//
// // No match returns None
// noMatch := emailPrism.GetOption("invalid-email") // None[NamedMatch]()
//
// // Reconstruct original string
// if m, ok := option.IsSome(match); ok {
// original := emailPrism.ReverseGet(m) // "contact: user@example.com"
// }
//
// // More complex example with date parsing
// dateRegex := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
// datePrism := RegexNamedMatcher(dateRegex)
// match = datePrism.GetOption("Date: 2024-03-15")
// // Access: match.Groups["year"], match.Groups["month"], match.Groups["day"]
//
// Common use cases:
// - Parsing structured text with meaningful field names
// - Extracting and validating data from formatted strings
// - Log parsing with named fields
// - Configuration file parsing
// - URL route parameter extraction
//
// Note: Only named capture groups appear in the Groups map. Unnamed groups
// are not included. The Full field always contains the complete matched text.
func RegexNamedMatcher(re *regexp.Regexp) Prism[string, NamedMatch] {
names := re.SubexpNames()
noMatch := option.None[NamedMatch]()
return MakePrism(
func(s string) Option[NamedMatch] {
loc := re.FindStringSubmatchIndex(s)
if loc == nil {
return noMatch
}
groups := make(map[string]string)
for i := 1; i < len(loc)/2; i++ {
if names[i] != "" && loc[2*i] >= 0 {
groups[names[i]] = s[loc[2*i]:loc[2*i+1]]
}
}
match := NamedMatch{
Before: s[:loc[0]],
Groups: groups,
Full: s[loc[0]:loc[1]],
After: s[loc[1]:],
}
return option.Some(match)
},
NamedMatch.Reconstruct,
)
}

View File

@@ -0,0 +1,534 @@
// 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 (
"regexp"
"testing"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestFromZero tests the FromZero prism with various comparable types
func TestFromZero(t *testing.T) {
t.Run("int - match zero", func(t *testing.T) {
prism := FromZero[int]()
result := prism.GetOption(0)
assert.True(t, O.IsSome(result))
assert.Equal(t, 0, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("int - non-zero returns None", func(t *testing.T) {
prism := FromZero[int]()
result := prism.GetOption(42)
assert.True(t, O.IsNone(result))
})
t.Run("string - match empty string", func(t *testing.T) {
prism := FromZero[string]()
result := prism.GetOption("")
assert.True(t, O.IsSome(result))
assert.Equal(t, "", O.GetOrElse(F.Constant("default"))(result))
})
t.Run("string - non-empty returns None", func(t *testing.T) {
prism := FromZero[string]()
result := prism.GetOption("hello")
assert.True(t, O.IsNone(result))
})
t.Run("bool - match false", func(t *testing.T) {
prism := FromZero[bool]()
result := prism.GetOption(false)
assert.True(t, O.IsSome(result))
assert.False(t, O.GetOrElse(F.Constant(true))(result))
})
t.Run("bool - true returns None", func(t *testing.T) {
prism := FromZero[bool]()
result := prism.GetOption(true)
assert.True(t, O.IsNone(result))
})
t.Run("float64 - match 0.0", func(t *testing.T) {
prism := FromZero[float64]()
result := prism.GetOption(0.0)
assert.True(t, O.IsSome(result))
assert.Equal(t, 0.0, O.GetOrElse(F.Constant(-1.0))(result))
})
t.Run("float64 - non-zero returns None", func(t *testing.T) {
prism := FromZero[float64]()
result := prism.GetOption(3.14)
assert.True(t, O.IsNone(result))
})
t.Run("pointer - match nil", func(t *testing.T) {
prism := FromZero[*int]()
var nilPtr *int
result := prism.GetOption(nilPtr)
assert.True(t, O.IsSome(result))
})
t.Run("pointer - non-nil returns None", func(t *testing.T) {
prism := FromZero[*int]()
value := 42
result := prism.GetOption(&value)
assert.True(t, O.IsNone(result))
})
t.Run("reverse get is identity", func(t *testing.T) {
prism := FromZero[int]()
assert.Equal(t, 0, prism.ReverseGet(0))
assert.Equal(t, 42, prism.ReverseGet(42))
})
}
// TestFromZeroWithSet tests using Set with FromZero prism
func TestFromZeroWithSet(t *testing.T) {
t.Run("set on zero value", func(t *testing.T) {
prism := FromZero[int]()
setter := Set[int, int](100)
result := setter(prism)(0)
assert.Equal(t, 100, result)
})
t.Run("set on non-zero returns original", func(t *testing.T) {
prism := FromZero[int]()
setter := Set[int, int](100)
result := setter(prism)(42)
assert.Equal(t, 42, result)
})
}
// TestFromZeroPrismLaws tests that FromZero satisfies prism laws
func TestFromZeroPrismLaws(t *testing.T) {
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a) for zero", func(t *testing.T) {
prism := FromZero[int]()
reversed := prism.ReverseGet(0)
extracted := prism.GetOption(reversed)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 0, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("law 2: if GetOption(s) == Some(a), then ReverseGet(a) == s", func(t *testing.T) {
prism := FromZero[string]()
extracted := prism.GetOption("")
if O.IsSome(extracted) {
value := O.GetOrElse(F.Constant("default"))(extracted)
reconstructed := prism.ReverseGet(value)
assert.Equal(t, "", reconstructed)
}
})
}
// TestRegexMatcher tests the RegexMatcher prism
func TestRegexMatcher(t *testing.T) {
t.Run("simple number match", func(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
result := prism.GetOption("price: 42 dollars")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, "price: ", match.Before)
assert.Equal(t, "42", match.FullMatch())
assert.Equal(t, " dollars", match.After)
})
t.Run("no match returns None", func(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
result := prism.GetOption("no numbers here")
assert.True(t, O.IsNone(result))
})
t.Run("match with capture groups", func(t *testing.T) {
re := regexp.MustCompile(`(\w+)@(\w+\.\w+)`)
prism := RegexMatcher(re)
result := prism.GetOption("contact: user@example.com")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, "contact: ", match.Before)
assert.Equal(t, "user@example.com", match.FullMatch())
assert.Equal(t, "user", match.Group(1))
assert.Equal(t, "example.com", match.Group(2))
assert.Equal(t, "", match.After)
})
t.Run("match at beginning", func(t *testing.T) {
re := regexp.MustCompile(`^\d+`)
prism := RegexMatcher(re)
result := prism.GetOption("123 test")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, "", match.Before)
assert.Equal(t, "123", match.FullMatch())
assert.Equal(t, " test", match.After)
})
t.Run("match at end", func(t *testing.T) {
re := regexp.MustCompile(`\d+$`)
prism := RegexMatcher(re)
result := prism.GetOption("test 123")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, "test ", match.Before)
assert.Equal(t, "123", match.FullMatch())
assert.Equal(t, "", match.After)
})
t.Run("reconstruct original string", func(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
original := "price: 42 dollars"
result := prism.GetOption(original)
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
reconstructed := match.Reconstruct()
assert.Equal(t, original, reconstructed)
})
t.Run("reverse get reconstructs", func(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
match := Match{
Before: "price: ",
Groups: []string{"42"},
After: " dollars",
}
reconstructed := prism.ReverseGet(match)
assert.Equal(t, "price: 42 dollars", reconstructed)
})
t.Run("Group with invalid index returns empty", func(t *testing.T) {
match := Match{
Groups: []string{"full", "group1"},
}
assert.Equal(t, "full", match.Group(0))
assert.Equal(t, "group1", match.Group(1))
assert.Equal(t, "", match.Group(5))
})
t.Run("empty string match", func(t *testing.T) {
re := regexp.MustCompile(`.*`)
prism := RegexMatcher(re)
result := prism.GetOption("")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, "", match.Before)
assert.Equal(t, "", match.FullMatch())
assert.Equal(t, "", match.After)
})
}
// TestRegexMatcherPrismLaws tests that RegexMatcher satisfies prism laws
func TestRegexMatcherPrismLaws(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
t.Run("law 1: GetOption(ReverseGet(match)) reconstructs", func(t *testing.T) {
match := Match{
Before: "test ",
Groups: []string{"123"},
After: " end",
}
str := prism.ReverseGet(match)
result := prism.GetOption(str)
assert.True(t, O.IsSome(result))
reconstructed := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, match.Before, reconstructed.Before)
assert.Equal(t, match.Groups[0], reconstructed.Groups[0])
assert.Equal(t, match.After, reconstructed.After)
})
t.Run("law 2: ReverseGet(GetOption(s)) == s for matching strings", func(t *testing.T) {
original := "value: 42 units"
extracted := prism.GetOption(original)
if O.IsSome(extracted) {
match := O.GetOrElse(F.Constant(Match{}))(extracted)
reconstructed := prism.ReverseGet(match)
assert.Equal(t, original, reconstructed)
}
})
}
// TestRegexNamedMatcher tests the RegexNamedMatcher prism
func TestRegexNamedMatcher(t *testing.T) {
t.Run("email with named groups", func(t *testing.T) {
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("contact: user@example.com")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, "contact: ", match.Before)
assert.Equal(t, "user@example.com", match.Full)
assert.Equal(t, "", match.After)
assert.Equal(t, "user", match.Groups["user"])
assert.Equal(t, "example.com", match.Groups["domain"])
})
t.Run("date with named groups", func(t *testing.T) {
re := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("Date: 2024-03-15")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, "Date: ", match.Before)
assert.Equal(t, "2024-03-15", match.Full)
assert.Equal(t, "2024", match.Groups["year"])
assert.Equal(t, "03", match.Groups["month"])
assert.Equal(t, "15", match.Groups["day"])
})
t.Run("no match returns None", func(t *testing.T) {
re := regexp.MustCompile(`(?P<num>\d+)`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("no numbers")
assert.True(t, O.IsNone(result))
})
t.Run("reconstruct original string", func(t *testing.T) {
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
prism := RegexNamedMatcher(re)
original := "email: admin@site.com here"
result := prism.GetOption(original)
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
reconstructed := match.Reconstruct()
assert.Equal(t, original, reconstructed)
})
t.Run("reverse get reconstructs", func(t *testing.T) {
re := regexp.MustCompile(`(?P<num>\d+)`)
prism := RegexNamedMatcher(re)
match := NamedMatch{
Before: "value: ",
Full: "42",
Groups: map[string]string{"num": "42"},
After: " end",
}
reconstructed := prism.ReverseGet(match)
assert.Equal(t, "value: 42 end", reconstructed)
})
t.Run("unnamed groups not in map", func(t *testing.T) {
// Mix of named and unnamed groups - use non-greedy match for clarity
re := regexp.MustCompile(`(?P<name>[a-z]+)(\d+)`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("user123")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, "user123", match.Full)
assert.Equal(t, "user", match.Groups["name"])
// Only named groups should be in the map, not unnamed ones
assert.Equal(t, 1, len(match.Groups))
})
t.Run("empty string match", func(t *testing.T) {
re := regexp.MustCompile(`(?P<all>.*)`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, "", match.Before)
assert.Equal(t, "", match.Full)
assert.Equal(t, "", match.After)
})
t.Run("multiple matches - only first", func(t *testing.T) {
re := regexp.MustCompile(`(?P<num>\d+)`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("first 123 second 456")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, "first ", match.Before)
assert.Equal(t, "123", match.Full)
assert.Equal(t, " second 456", match.After)
assert.Equal(t, "123", match.Groups["num"])
})
}
// TestRegexNamedMatcherPrismLaws tests that RegexNamedMatcher satisfies prism laws
func TestRegexNamedMatcherPrismLaws(t *testing.T) {
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
prism := RegexNamedMatcher(re)
t.Run("law 1: GetOption(ReverseGet(match)) reconstructs", func(t *testing.T) {
match := NamedMatch{
Before: "email: ",
Full: "user@example.com",
Groups: map[string]string{
"user": "user",
"domain": "example.com",
},
After: "",
}
str := prism.ReverseGet(match)
result := prism.GetOption(str)
assert.True(t, O.IsSome(result))
reconstructed := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, match.Before, reconstructed.Before)
assert.Equal(t, match.Full, reconstructed.Full)
assert.Equal(t, match.After, reconstructed.After)
})
t.Run("law 2: ReverseGet(GetOption(s)) == s for matching strings", func(t *testing.T) {
original := "contact: admin@site.com"
extracted := prism.GetOption(original)
if O.IsSome(extracted) {
match := O.GetOrElse(F.Constant(NamedMatch{}))(extracted)
reconstructed := prism.ReverseGet(match)
assert.Equal(t, original, reconstructed)
}
})
}
// TestRegexMatcherWithSet tests using Set with RegexMatcher
func TestRegexMatcherWithSet(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
t.Run("set on matching string", func(t *testing.T) {
original := "price: 42 dollars"
newMatch := Match{
Before: "price: ",
Groups: []string{"100"},
After: " dollars",
}
setter := Set[string, Match](newMatch)
result := setter(prism)(original)
assert.Equal(t, "price: 100 dollars", result)
})
t.Run("set on non-matching string returns original", func(t *testing.T) {
original := "no numbers"
newMatch := Match{
Before: "",
Groups: []string{"42"},
After: "",
}
setter := Set[string, Match](newMatch)
result := setter(prism)(original)
assert.Equal(t, original, result)
})
}
// TestRegexNamedMatcherWithSet tests using Set with RegexNamedMatcher
func TestRegexNamedMatcherWithSet(t *testing.T) {
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
prism := RegexNamedMatcher(re)
t.Run("set on matching string", func(t *testing.T) {
original := "email: user@example.com"
newMatch := NamedMatch{
Before: "email: ",
Full: "admin@newsite.com",
Groups: map[string]string{
"user": "admin",
"domain": "newsite.com",
},
After: "",
}
setter := Set[string, NamedMatch](newMatch)
result := setter(prism)(original)
assert.Equal(t, "email: admin@newsite.com", result)
})
t.Run("set on non-matching string returns original", func(t *testing.T) {
original := "no email here"
newMatch := NamedMatch{
Before: "",
Full: "test@test.com",
Groups: map[string]string{
"user": "test",
"domain": "test.com",
},
After: "",
}
setter := Set[string, NamedMatch](newMatch)
result := setter(prism)(original)
assert.Equal(t, original, result)
})
}

View File

@@ -2,13 +2,13 @@ package lens
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2025-11-07 16:13:10.2317216 +0100 CET m=+0.005378701
// 2025-11-07 16:52:17.4935733 +0100 CET m=+0.003883901
import (
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
LO "github.com/IBM/fp-go/v2/optics/lens/option"
O "github.com/IBM/fp-go/v2/option"
I "github.com/IBM/fp-go/v2/optics/iso/option"
option "github.com/IBM/fp-go/v2/optics/lens/option"
)
@@ -30,7 +30,7 @@ type PersonRefLenses struct {
// MakePersonLenses creates a new PersonLenses with lenses for all fields
func MakePersonLenses() PersonLenses {
getOrElsePhone := O.GetOrElse(F.ConstNil[string])
isoPhone := I.FromZero[*string]()
return PersonLenses{
Name: L.MakeLens(
func(s Person) string { return s.Name },
@@ -45,15 +45,15 @@ func MakePersonLenses() PersonLenses {
func(s Person, v string) Person { s.Email = v; return s },
),
Phone: L.MakeLens(
func(s Person) O.Option[*string] { return O.FromNillable(s.Phone) },
func(s Person, v O.Option[*string]) Person { s.Phone = getOrElsePhone(v); return s },
func(s Person) O.Option[*string] { return isoPhone.Get(s.Phone) },
func(s Person, v O.Option[*string]) Person { s.Phone = isoPhone.ReverseGet(v); return s },
),
}
}
// MakePersonRefLenses creates a new PersonRefLenses with lenses for all fields
func MakePersonRefLenses() PersonRefLenses {
getOrElsePhone := O.GetOrElse(F.ConstNil[string])
isoPhone := I.FromZero[*string]()
return PersonRefLenses{
Name: L.MakeLensRef(
func(s *Person) string { return s.Name },
@@ -68,8 +68,8 @@ func MakePersonRefLenses() PersonRefLenses {
func(s *Person, v string) *Person { s.Email = v; return s },
),
Phone: L.MakeLensRef(
func(s *Person) O.Option[*string] { return O.FromNillable(s.Phone) },
func(s *Person, v O.Option[*string]) *Person { s.Phone = getOrElsePhone(v); return s },
func(s *Person) O.Option[*string] { return isoPhone.Get(s.Phone) },
func(s *Person, v O.Option[*string]) *Person { s.Phone = isoPhone.ReverseGet(v); return s },
),
}
}
@@ -94,7 +94,7 @@ type AddressRefLenses struct {
// MakeAddressLenses creates a new AddressLenses with lenses for all fields
func MakeAddressLenses() AddressLenses {
getOrElseState := O.GetOrElse(F.ConstNil[string])
isoState := I.FromZero[*string]()
return AddressLenses{
Street: L.MakeLens(
func(s Address) string { return s.Street },
@@ -113,15 +113,15 @@ func MakeAddressLenses() AddressLenses {
func(s Address, v string) Address { s.Country = v; return s },
),
State: L.MakeLens(
func(s Address) O.Option[*string] { return O.FromNillable(s.State) },
func(s Address, v O.Option[*string]) Address { s.State = getOrElseState(v); return s },
func(s Address) O.Option[*string] { return isoState.Get(s.State) },
func(s Address, v O.Option[*string]) Address { s.State = isoState.ReverseGet(v); return s },
),
}
}
// MakeAddressRefLenses creates a new AddressRefLenses with lenses for all fields
func MakeAddressRefLenses() AddressRefLenses {
getOrElseState := O.GetOrElse(F.ConstNil[string])
isoState := I.FromZero[*string]()
return AddressRefLenses{
Street: L.MakeLensRef(
func(s *Address) string { return s.Street },
@@ -140,8 +140,8 @@ func MakeAddressRefLenses() AddressRefLenses {
func(s *Address, v string) *Address { s.Country = v; return s },
),
State: L.MakeLensRef(
func(s *Address) O.Option[*string] { return O.FromNillable(s.State) },
func(s *Address, v O.Option[*string]) *Address { s.State = getOrElseState(v); return s },
func(s *Address) O.Option[*string] { return isoState.Get(s.State) },
func(s *Address, v O.Option[*string]) *Address { s.State = isoState.ReverseGet(v); return s },
),
}
}
@@ -164,7 +164,7 @@ type CompanyRefLenses struct {
// MakeCompanyLenses creates a new CompanyLenses with lenses for all fields
func MakeCompanyLenses() CompanyLenses {
getOrElseWebsite := O.GetOrElse(F.ConstNil[string])
isoWebsite := I.FromZero[*string]()
return CompanyLenses{
Name: L.MakeLens(
func(s Company) string { return s.Name },
@@ -179,15 +179,15 @@ func MakeCompanyLenses() CompanyLenses {
func(s Company, v Person) Company { s.CEO = v; return s },
),
Website: L.MakeLens(
func(s Company) O.Option[*string] { return O.FromNillable(s.Website) },
func(s Company, v O.Option[*string]) Company { s.Website = getOrElseWebsite(v); return s },
func(s Company) O.Option[*string] { return isoWebsite.Get(s.Website) },
func(s Company, v O.Option[*string]) Company { s.Website = isoWebsite.ReverseGet(v); return s },
),
}
}
// MakeCompanyRefLenses creates a new CompanyRefLenses with lenses for all fields
func MakeCompanyRefLenses() CompanyRefLenses {
getOrElseWebsite := O.GetOrElse(F.ConstNil[string])
isoWebsite := I.FromZero[*string]()
return CompanyRefLenses{
Name: L.MakeLensRef(
func(s *Company) string { return s.Name },
@@ -202,8 +202,8 @@ func MakeCompanyRefLenses() CompanyRefLenses {
func(s *Company, v Person) *Company { s.CEO = v; return s },
),
Website: L.MakeLensRef(
func(s *Company) O.Option[*string] { return O.FromNillable(s.Website) },
func(s *Company, v O.Option[*string]) *Company { s.Website = getOrElseWebsite(v); return s },
func(s *Company) O.Option[*string] { return isoWebsite.Get(s.Website) },
func(s *Company, v O.Option[*string]) *Company { s.Website = isoWebsite.ReverseGet(v); return s },
),
}
}
@@ -211,39 +211,41 @@ func MakeCompanyRefLenses() CompanyRefLenses {
// CheckOptionLenses provides lenses for accessing fields of CheckOption
type CheckOptionLenses struct {
Name L.Lens[CheckOption, option.Option[string]]
Value L.Lens[CheckOption, string]
Value LO.LensO[CheckOption, string]
}
// CheckOptionRefLenses provides lenses for accessing fields of CheckOption via a reference to CheckOption
type CheckOptionRefLenses struct {
Name L.Lens[*CheckOption, option.Option[string]]
Value L.Lens[*CheckOption, string]
Value LO.LensO[*CheckOption, string]
}
// MakeCheckOptionLenses creates a new CheckOptionLenses with lenses for all fields
func MakeCheckOptionLenses() CheckOptionLenses {
isoValue := I.FromZero[string]()
return CheckOptionLenses{
Name: L.MakeLens(
func(s CheckOption) option.Option[string] { return s.Name },
func(s CheckOption, v option.Option[string]) CheckOption { s.Name = v; return s },
),
Value: L.MakeLens(
func(s CheckOption) string { return s.Value },
func(s CheckOption, v string) CheckOption { s.Value = v; return s },
func(s CheckOption) O.Option[string] { return isoValue.Get(s.Value) },
func(s CheckOption, v O.Option[string]) CheckOption { s.Value = isoValue.ReverseGet(v); return s },
),
}
}
// MakeCheckOptionRefLenses creates a new CheckOptionRefLenses with lenses for all fields
func MakeCheckOptionRefLenses() CheckOptionRefLenses {
isoValue := I.FromZero[string]()
return CheckOptionRefLenses{
Name: L.MakeLensRef(
func(s *CheckOption) option.Option[string] { return s.Name },
func(s *CheckOption, v option.Option[string]) *CheckOption { s.Name = v; return s },
),
Value: L.MakeLensRef(
func(s *CheckOption) string { return s.Value },
func(s *CheckOption, v string) *CheckOption { s.Value = v; return s },
func(s *CheckOption) O.Option[string] { return isoValue.Get(s.Value) },
func(s *CheckOption, v O.Option[string]) *CheckOption { s.Value = isoValue.ReverseGet(v); return s },
),
}
}