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:
@@ -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
|
||||
|
||||
@@ -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
187
v2/optics/iso/isos.go
Normal 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
432
v2/optics/iso/isos_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
83
v2/optics/iso/option/isos.go
Normal file
83
v2/optics/iso/option/isos.go
Normal 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 }),
|
||||
)
|
||||
}
|
||||
366
v2/optics/iso/option/isos_test.go
Normal file
366
v2/optics/iso/option/isos_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
534
v2/optics/prism/prisms_test.go
Normal file
534
v2/optics/prism/prisms_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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 },
|
||||
),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user