mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
fix: improve lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
137
v2/cli/lens.go
137
v2/cli/lens.go
@@ -60,10 +60,11 @@ type structInfo struct {
|
|||||||
|
|
||||||
// fieldInfo holds information about a struct field
|
// fieldInfo holds information about a struct field
|
||||||
type fieldInfo struct {
|
type fieldInfo struct {
|
||||||
Name string
|
Name string
|
||||||
TypeName string
|
TypeName string
|
||||||
BaseType string // TypeName without leading * for pointer types
|
BaseType string // TypeName without leading * for pointer types
|
||||||
IsOptional bool // true if field is a pointer or has json omitempty tag
|
IsOptional bool // true if field is a pointer or has json omitempty tag
|
||||||
|
IsComparable bool // true if the type is comparable (can use ==)
|
||||||
}
|
}
|
||||||
|
|
||||||
// templateData holds data for template rendering
|
// templateData holds data for template rendering
|
||||||
@@ -127,12 +128,19 @@ func Make{{.Name}}RefLenses() {{.Name}}RefLenses {
|
|||||||
func(s *{{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
|
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 },
|
func(s *{{$.Name}}, v O.Option[{{.TypeName}}]) *{{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
|
||||||
),
|
),
|
||||||
|
{{- else}}
|
||||||
|
{{- if .IsComparable}}
|
||||||
|
{{.Name}}: L.MakeLensStrict(
|
||||||
|
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
|
||||||
|
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
|
||||||
|
),
|
||||||
{{- else}}
|
{{- else}}
|
||||||
{{.Name}}: L.MakeLensRef(
|
{{.Name}}: L.MakeLensRef(
|
||||||
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
|
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
|
||||||
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
|
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
|
||||||
),
|
),
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,6 +265,111 @@ func isPointerType(expr ast.Expr) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isComparableType checks if a type expression represents a comparable type.
|
||||||
|
// Comparable types in Go include:
|
||||||
|
// - Basic types (bool, numeric types, string)
|
||||||
|
// - Pointer types
|
||||||
|
// - Channel types
|
||||||
|
// - Interface types
|
||||||
|
// - Structs where all fields are comparable
|
||||||
|
// - Arrays where the element type is comparable
|
||||||
|
//
|
||||||
|
// Non-comparable types include:
|
||||||
|
// - Slices
|
||||||
|
// - Maps
|
||||||
|
// - Functions
|
||||||
|
func isComparableType(expr ast.Expr) bool {
|
||||||
|
switch t := expr.(type) {
|
||||||
|
case *ast.Ident:
|
||||||
|
// Basic types and named types
|
||||||
|
// We assume named types are comparable unless they're known non-comparable types
|
||||||
|
name := t.Name
|
||||||
|
// Known non-comparable built-in types
|
||||||
|
if name == "error" {
|
||||||
|
// error is an interface, which is comparable
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Most basic types and named types are comparable
|
||||||
|
// We can't determine if a custom type is comparable without type checking,
|
||||||
|
// so we assume it is (conservative approach)
|
||||||
|
return true
|
||||||
|
case *ast.StarExpr:
|
||||||
|
// Pointer types are always comparable
|
||||||
|
return true
|
||||||
|
case *ast.ArrayType:
|
||||||
|
// Arrays are comparable if their element type is comparable
|
||||||
|
if t.Len == nil {
|
||||||
|
// This is a slice (no length), slices are not comparable
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Fixed-size array, check element type
|
||||||
|
return isComparableType(t.Elt)
|
||||||
|
case *ast.MapType:
|
||||||
|
// Maps are not comparable
|
||||||
|
return false
|
||||||
|
case *ast.FuncType:
|
||||||
|
// Functions are not comparable
|
||||||
|
return false
|
||||||
|
case *ast.InterfaceType:
|
||||||
|
// Interface types are comparable
|
||||||
|
return true
|
||||||
|
case *ast.StructType:
|
||||||
|
// Structs are comparable if all fields are comparable
|
||||||
|
// We can't easily determine this without full type information,
|
||||||
|
// so we conservatively return false for struct literals
|
||||||
|
return false
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
// Qualified identifier (e.g., pkg.Type)
|
||||||
|
// We can't determine comparability without type information
|
||||||
|
// Check for known non-comparable types from standard library
|
||||||
|
if ident, ok := t.X.(*ast.Ident); ok {
|
||||||
|
pkgName := ident.Name
|
||||||
|
typeName := t.Sel.Name
|
||||||
|
// Check for known non-comparable types
|
||||||
|
if pkgName == "context" && typeName == "Context" {
|
||||||
|
// context.Context is an interface, which is comparable
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// For other qualified types, we assume they're comparable
|
||||||
|
// This is a conservative approach
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case *ast.IndexExpr, *ast.IndexListExpr:
|
||||||
|
// Generic types - we can't determine comparability without type information
|
||||||
|
// For common generic types, we can make educated guesses
|
||||||
|
var baseExpr ast.Expr
|
||||||
|
if idx, ok := t.(*ast.IndexExpr); ok {
|
||||||
|
baseExpr = idx.X
|
||||||
|
} else if idxList, ok := t.(*ast.IndexListExpr); ok {
|
||||||
|
baseExpr = idxList.X
|
||||||
|
}
|
||||||
|
|
||||||
|
if sel, ok := baseExpr.(*ast.SelectorExpr); ok {
|
||||||
|
if ident, ok := sel.X.(*ast.Ident); ok {
|
||||||
|
pkgName := ident.Name
|
||||||
|
typeName := sel.Sel.Name
|
||||||
|
// Check for known non-comparable generic types
|
||||||
|
if pkgName == "option" && typeName == "Option" {
|
||||||
|
// Option types are not comparable (they contain a slice internally)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if pkgName == "either" && typeName == "Either" {
|
||||||
|
// Either types are not comparable
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For other generic types, conservatively assume not comparable
|
||||||
|
return false
|
||||||
|
case *ast.ChanType:
|
||||||
|
// Channel types are comparable
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
// Unknown type, conservatively assume not comparable
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parseFile parses a Go file and extracts structs with lens annotations
|
// parseFile parses a Go file and extracts structs with lens annotations
|
||||||
func parseFile(filename string) ([]structInfo, string, error) {
|
func parseFile(filename string) ([]structInfo, string, error) {
|
||||||
fset := token.NewFileSet()
|
fset := token.NewFileSet()
|
||||||
@@ -331,6 +444,7 @@ func parseFile(filename string) ([]structInfo, string, error) {
|
|||||||
typeName := getTypeName(field.Type)
|
typeName := getTypeName(field.Type)
|
||||||
isOptional := false
|
isOptional := false
|
||||||
baseType := typeName
|
baseType := typeName
|
||||||
|
isComparable := false
|
||||||
|
|
||||||
// Check if field is optional:
|
// Check if field is optional:
|
||||||
// 1. Pointer types are always optional
|
// 1. Pointer types are always optional
|
||||||
@@ -344,6 +458,12 @@ func parseFile(filename string) ([]structInfo, string, error) {
|
|||||||
isOptional = true
|
isOptional = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the type is comparable (for non-optional fields)
|
||||||
|
// For optional fields, we don't need to check since they use LensO
|
||||||
|
if !isOptional {
|
||||||
|
isComparable = isComparableType(field.Type)
|
||||||
|
}
|
||||||
|
|
||||||
// Extract imports from this field's type
|
// Extract imports from this field's type
|
||||||
fieldImports := make(map[string]string)
|
fieldImports := make(map[string]string)
|
||||||
extractImports(field.Type, fieldImports)
|
extractImports(field.Type, fieldImports)
|
||||||
@@ -356,10 +476,11 @@ func parseFile(filename string) ([]structInfo, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fields = append(fields, fieldInfo{
|
fields = append(fields, fieldInfo{
|
||||||
Name: name.Name,
|
Name: name.Name,
|
||||||
TypeName: typeName,
|
TypeName: typeName,
|
||||||
BaseType: baseType,
|
BaseType: baseType,
|
||||||
IsOptional: isOptional,
|
IsOptional: isOptional,
|
||||||
|
IsComparable: isComparable,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,91 @@ func TestIsPointerType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsComparableType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
code string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic type - string",
|
||||||
|
code: "type T struct { F string }",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic type - int",
|
||||||
|
code: "type T struct { F int }",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basic type - bool",
|
||||||
|
code: "type T struct { F bool }",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pointer type",
|
||||||
|
code: "type T struct { F *string }",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "slice type - not comparable",
|
||||||
|
code: "type T struct { F []string }",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map type - not comparable",
|
||||||
|
code: "type T struct { F map[string]int }",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "array type - comparable if element is",
|
||||||
|
code: "type T struct { F [5]int }",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "interface type",
|
||||||
|
code: "type T struct { F interface{} }",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "channel type",
|
||||||
|
code: "type T struct { F chan int }",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "function type - not comparable",
|
||||||
|
code: "type T struct { F func() }",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "struct literal - conservatively not comparable",
|
||||||
|
code: "type T struct { F struct{ X int } }",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var fieldType ast.Expr
|
||||||
|
ast.Inspect(file, func(n ast.Node) bool {
|
||||||
|
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
|
||||||
|
fieldType = field.Type
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NotNil(t, fieldType)
|
||||||
|
result := isComparableType(fieldType)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHasOmitEmpty(t *testing.T) {
|
func TestHasOmitEmpty(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -337,6 +422,171 @@ type Config struct {
|
|||||||
assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional")
|
assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseFileWithComparableTypes(t *testing.T) {
|
||||||
|
// Create a temporary test file
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testFile := filepath.Join(tmpDir, "test.go")
|
||||||
|
|
||||||
|
testCode := `package testpkg
|
||||||
|
|
||||||
|
// fp-go:Lens
|
||||||
|
type TypeTest struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
Pointer *string
|
||||||
|
Slice []string
|
||||||
|
Map map[string]int
|
||||||
|
Channel chan int
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
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 TypeTest struct
|
||||||
|
typeTest := structs[0]
|
||||||
|
assert.Equal(t, "TypeTest", typeTest.Name)
|
||||||
|
assert.Len(t, typeTest.Fields, 6)
|
||||||
|
|
||||||
|
// Name - string is comparable
|
||||||
|
assert.Equal(t, "Name", typeTest.Fields[0].Name)
|
||||||
|
assert.Equal(t, "string", typeTest.Fields[0].TypeName)
|
||||||
|
assert.False(t, typeTest.Fields[0].IsOptional)
|
||||||
|
assert.True(t, typeTest.Fields[0].IsComparable, "string should be comparable")
|
||||||
|
|
||||||
|
// Age - int is comparable
|
||||||
|
assert.Equal(t, "Age", typeTest.Fields[1].Name)
|
||||||
|
assert.Equal(t, "int", typeTest.Fields[1].TypeName)
|
||||||
|
assert.False(t, typeTest.Fields[1].IsOptional)
|
||||||
|
assert.True(t, typeTest.Fields[1].IsComparable, "int should be comparable")
|
||||||
|
|
||||||
|
// Pointer - pointer is optional, IsComparable not checked for optional fields
|
||||||
|
assert.Equal(t, "Pointer", typeTest.Fields[2].Name)
|
||||||
|
assert.Equal(t, "*string", typeTest.Fields[2].TypeName)
|
||||||
|
assert.True(t, typeTest.Fields[2].IsOptional)
|
||||||
|
assert.False(t, typeTest.Fields[2].IsComparable, "IsComparable not set for optional fields")
|
||||||
|
|
||||||
|
// Slice - not comparable
|
||||||
|
assert.Equal(t, "Slice", typeTest.Fields[3].Name)
|
||||||
|
assert.Equal(t, "[]string", typeTest.Fields[3].TypeName)
|
||||||
|
assert.False(t, typeTest.Fields[3].IsOptional)
|
||||||
|
assert.False(t, typeTest.Fields[3].IsComparable, "slice should not be comparable")
|
||||||
|
|
||||||
|
// Map - not comparable
|
||||||
|
assert.Equal(t, "Map", typeTest.Fields[4].Name)
|
||||||
|
assert.Equal(t, "map[string]int", typeTest.Fields[4].TypeName)
|
||||||
|
assert.False(t, typeTest.Fields[4].IsOptional)
|
||||||
|
assert.False(t, typeTest.Fields[4].IsComparable, "map should not be comparable")
|
||||||
|
|
||||||
|
// Channel - comparable (note: getTypeName returns "any" for channel types, but isComparableType correctly identifies them)
|
||||||
|
assert.Equal(t, "Channel", typeTest.Fields[5].Name)
|
||||||
|
assert.Equal(t, "any", typeTest.Fields[5].TypeName) // getTypeName doesn't handle chan types specifically
|
||||||
|
assert.False(t, typeTest.Fields[5].IsOptional)
|
||||||
|
assert.True(t, typeTest.Fields[5].IsComparable, "channel should be comparable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLensRefTemplatesWithComparable(t *testing.T) {
|
||||||
|
s := structInfo{
|
||||||
|
Name: "TestStruct",
|
||||||
|
Fields: []fieldInfo{
|
||||||
|
{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
|
||||||
|
{Name: "Age", TypeName: "int", IsOptional: false, IsComparable: true},
|
||||||
|
{Name: "Data", TypeName: "[]byte", IsOptional: false, IsComparable: false},
|
||||||
|
{Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test constructor template for RefLenses
|
||||||
|
var constructorBuf bytes.Buffer
|
||||||
|
err := constructorTmpl.Execute(&constructorBuf, s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
constructorStr := constructorBuf.String()
|
||||||
|
|
||||||
|
// Check that MakeLensStrict is used for comparable types in RefLenses
|
||||||
|
assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses")
|
||||||
|
|
||||||
|
// Name field - comparable, should use MakeLensStrict
|
||||||
|
assert.Contains(t, constructorStr, "Name: L.MakeLensStrict(",
|
||||||
|
"comparable field Name should use MakeLensStrict in RefLenses")
|
||||||
|
|
||||||
|
// Age field - comparable, should use MakeLensStrict
|
||||||
|
assert.Contains(t, constructorStr, "Age: L.MakeLensStrict(",
|
||||||
|
"comparable field Age should use MakeLensStrict in RefLenses")
|
||||||
|
|
||||||
|
// Data field - not comparable, should use MakeLensRef
|
||||||
|
assert.Contains(t, constructorStr, "Data: L.MakeLensRef(",
|
||||||
|
"non-comparable field Data should use MakeLensRef in RefLenses")
|
||||||
|
|
||||||
|
// Pointer field - optional, should use MakeLensRef
|
||||||
|
assert.Contains(t, constructorStr, "Pointer: L.MakeLensRef(",
|
||||||
|
"optional field Pointer should use MakeLensRef in RefLenses")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateLensHelpersWithComparable(t *testing.T) {
|
||||||
|
// Create a temporary directory with test files
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
testCode := `package testpkg
|
||||||
|
|
||||||
|
// fp-go:Lens
|
||||||
|
type TestStruct struct {
|
||||||
|
Name string
|
||||||
|
Count int
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
testFile := filepath.Join(tmpDir, "test.go")
|
||||||
|
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Generate lens code
|
||||||
|
outputFile := "gen.go"
|
||||||
|
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify the generated file exists
|
||||||
|
genPath := filepath.Join(tmpDir, outputFile)
|
||||||
|
_, err = os.Stat(genPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Read and verify the generated content
|
||||||
|
content, err := os.ReadFile(genPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
contentStr := string(content)
|
||||||
|
|
||||||
|
// Check for expected content in RefLenses
|
||||||
|
assert.Contains(t, contentStr, "MakeTestStructRefLenses")
|
||||||
|
|
||||||
|
// Name and Count are comparable, should use MakeLensStrict
|
||||||
|
assert.Contains(t, contentStr, "L.MakeLensStrict",
|
||||||
|
"comparable fields should use MakeLensStrict in RefLenses")
|
||||||
|
|
||||||
|
// Data is not comparable (slice), should use MakeLensRef
|
||||||
|
assert.Contains(t, contentStr, "L.MakeLensRef",
|
||||||
|
"non-comparable fields should use MakeLensRef in RefLenses")
|
||||||
|
|
||||||
|
// Verify the pattern appears for Name field (comparable)
|
||||||
|
namePattern := "Name: L.MakeLensStrict("
|
||||||
|
assert.Contains(t, contentStr, namePattern,
|
||||||
|
"Name field should use MakeLensStrict")
|
||||||
|
|
||||||
|
// Verify the pattern appears for Data field (not comparable)
|
||||||
|
dataPattern := "Data: L.MakeLensRef("
|
||||||
|
assert.Contains(t, contentStr, dataPattern,
|
||||||
|
"Data field should use MakeLensRef")
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerateLensHelpers(t *testing.T) {
|
func TestGenerateLensHelpers(t *testing.T) {
|
||||||
// Create a temporary directory with test files
|
// Create a temporary directory with test files
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ package lens
|
|||||||
|
|
||||||
import "github.com/IBM/fp-go/v2/optics/lens/option"
|
import "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||||
|
|
||||||
|
//go:generate go run ../../main.go lens --dir . --filename gen_lens.go
|
||||||
|
|
||||||
// fp-go:Lens
|
// fp-go:Lens
|
||||||
type Person struct {
|
type Person struct {
|
||||||
Name string
|
Name string
|
||||||
|
|||||||
@@ -153,3 +153,46 @@ func TestLensComposition(t *testing.T) {
|
|||||||
assert.Equal(t, 55, olderCEO.CEO.Age)
|
assert.Equal(t, 55, olderCEO.CEO.Age)
|
||||||
assert.Equal(t, 50, company.CEO.Age) // Original unchanged
|
assert.Equal(t, 50, company.CEO.Age) // Original unchanged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPersonRefLensesIdempotent(t *testing.T) {
|
||||||
|
// Create a person pointer
|
||||||
|
person := &Person{
|
||||||
|
Name: "Alice",
|
||||||
|
Age: 30,
|
||||||
|
Email: "alice@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ref lenses
|
||||||
|
refLenses := MakePersonRefLenses()
|
||||||
|
|
||||||
|
// Test that setting the same value returns the identical pointer (idempotent)
|
||||||
|
// This works because Name, Age, and Email use MakeLensStrict which has equality optimization
|
||||||
|
|
||||||
|
// Test Name field - setting same value should return same pointer
|
||||||
|
sameName := refLenses.Name.Set("Alice")(person)
|
||||||
|
assert.Same(t, person, sameName, "Setting Name to same value should return identical pointer")
|
||||||
|
|
||||||
|
// Test Age field - setting same value should return same pointer
|
||||||
|
sameAge := refLenses.Age.Set(30)(person)
|
||||||
|
assert.Same(t, person, sameAge, "Setting Age to same value should return identical pointer")
|
||||||
|
|
||||||
|
// Test Email field - setting same value should return same pointer
|
||||||
|
sameEmail := refLenses.Email.Set("alice@example.com")(person)
|
||||||
|
assert.Same(t, person, sameEmail, "Setting Email to same value should return identical pointer")
|
||||||
|
|
||||||
|
// Test that setting a different value creates a new pointer
|
||||||
|
differentName := refLenses.Name.Set("Bob")(person)
|
||||||
|
assert.NotSame(t, person, differentName, "Setting Name to different value should return new pointer")
|
||||||
|
assert.Equal(t, "Bob", differentName.Name)
|
||||||
|
assert.Equal(t, "Alice", person.Name, "Original should be unchanged")
|
||||||
|
|
||||||
|
differentAge := refLenses.Age.Set(31)(person)
|
||||||
|
assert.NotSame(t, person, differentAge, "Setting Age to different value should return new pointer")
|
||||||
|
assert.Equal(t, 31, differentAge.Age)
|
||||||
|
assert.Equal(t, 30, person.Age, "Original should be unchanged")
|
||||||
|
|
||||||
|
differentEmail := refLenses.Email.Set("bob@example.com")(person)
|
||||||
|
assert.NotSame(t, person, differentEmail, "Setting Email to different value should return new pointer")
|
||||||
|
assert.Equal(t, "bob@example.com", differentEmail.Email)
|
||||||
|
assert.Equal(t, "alice@example.com", person.Email, "Original should be unchanged")
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package lens
|
|||||||
|
|
||||||
// Code generated by go generate; DO NOT EDIT.
|
// Code generated by go generate; DO NOT EDIT.
|
||||||
// This file was generated by robots at
|
// This file was generated by robots at
|
||||||
// 2025-11-07 16:52:17.4935733 +0100 CET m=+0.003883901
|
// 2025-11-12 17:16:40.1431921 +0100 CET m=+0.003694701
|
||||||
|
|
||||||
import (
|
import (
|
||||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||||
@@ -55,15 +55,15 @@ func MakePersonLenses() PersonLenses {
|
|||||||
func MakePersonRefLenses() PersonRefLenses {
|
func MakePersonRefLenses() PersonRefLenses {
|
||||||
isoPhone := I.FromZero[*string]()
|
isoPhone := I.FromZero[*string]()
|
||||||
return PersonRefLenses{
|
return PersonRefLenses{
|
||||||
Name: L.MakeLensRef(
|
Name: L.MakeLensStrict(
|
||||||
func(s *Person) string { return s.Name },
|
func(s *Person) string { return s.Name },
|
||||||
func(s *Person, v string) *Person { s.Name = v; return s },
|
func(s *Person, v string) *Person { s.Name = v; return s },
|
||||||
),
|
),
|
||||||
Age: L.MakeLensRef(
|
Age: L.MakeLensStrict(
|
||||||
func(s *Person) int { return s.Age },
|
func(s *Person) int { return s.Age },
|
||||||
func(s *Person, v int) *Person { s.Age = v; return s },
|
func(s *Person, v int) *Person { s.Age = v; return s },
|
||||||
),
|
),
|
||||||
Email: L.MakeLensRef(
|
Email: L.MakeLensStrict(
|
||||||
func(s *Person) string { return s.Email },
|
func(s *Person) string { return s.Email },
|
||||||
func(s *Person, v string) *Person { s.Email = v; return s },
|
func(s *Person, v string) *Person { s.Email = v; return s },
|
||||||
),
|
),
|
||||||
@@ -123,19 +123,19 @@ func MakeAddressLenses() AddressLenses {
|
|||||||
func MakeAddressRefLenses() AddressRefLenses {
|
func MakeAddressRefLenses() AddressRefLenses {
|
||||||
isoState := I.FromZero[*string]()
|
isoState := I.FromZero[*string]()
|
||||||
return AddressRefLenses{
|
return AddressRefLenses{
|
||||||
Street: L.MakeLensRef(
|
Street: L.MakeLensStrict(
|
||||||
func(s *Address) string { return s.Street },
|
func(s *Address) string { return s.Street },
|
||||||
func(s *Address, v string) *Address { s.Street = v; return s },
|
func(s *Address, v string) *Address { s.Street = v; return s },
|
||||||
),
|
),
|
||||||
City: L.MakeLensRef(
|
City: L.MakeLensStrict(
|
||||||
func(s *Address) string { return s.City },
|
func(s *Address) string { return s.City },
|
||||||
func(s *Address, v string) *Address { s.City = v; return s },
|
func(s *Address, v string) *Address { s.City = v; return s },
|
||||||
),
|
),
|
||||||
ZipCode: L.MakeLensRef(
|
ZipCode: L.MakeLensStrict(
|
||||||
func(s *Address) string { return s.ZipCode },
|
func(s *Address) string { return s.ZipCode },
|
||||||
func(s *Address, v string) *Address { s.ZipCode = v; return s },
|
func(s *Address, v string) *Address { s.ZipCode = v; return s },
|
||||||
),
|
),
|
||||||
Country: L.MakeLensRef(
|
Country: L.MakeLensStrict(
|
||||||
func(s *Address) string { return s.Country },
|
func(s *Address) string { return s.Country },
|
||||||
func(s *Address, v string) *Address { s.Country = v; return s },
|
func(s *Address, v string) *Address { s.Country = v; return s },
|
||||||
),
|
),
|
||||||
@@ -189,15 +189,15 @@ func MakeCompanyLenses() CompanyLenses {
|
|||||||
func MakeCompanyRefLenses() CompanyRefLenses {
|
func MakeCompanyRefLenses() CompanyRefLenses {
|
||||||
isoWebsite := I.FromZero[*string]()
|
isoWebsite := I.FromZero[*string]()
|
||||||
return CompanyRefLenses{
|
return CompanyRefLenses{
|
||||||
Name: L.MakeLensRef(
|
Name: L.MakeLensStrict(
|
||||||
func(s *Company) string { return s.Name },
|
func(s *Company) string { return s.Name },
|
||||||
func(s *Company, v string) *Company { s.Name = v; return s },
|
func(s *Company, v string) *Company { s.Name = v; return s },
|
||||||
),
|
),
|
||||||
Address: L.MakeLensRef(
|
Address: L.MakeLensStrict(
|
||||||
func(s *Company) Address { return s.Address },
|
func(s *Company) Address { return s.Address },
|
||||||
func(s *Company, v Address) *Company { s.Address = v; return s },
|
func(s *Company, v Address) *Company { s.Address = v; return s },
|
||||||
),
|
),
|
||||||
CEO: L.MakeLensRef(
|
CEO: L.MakeLensStrict(
|
||||||
func(s *Company) Person { return s.CEO },
|
func(s *Company) Person { return s.CEO },
|
||||||
func(s *Company, v Person) *Company { s.CEO = v; return s },
|
func(s *Company, v Person) *Company { s.CEO = v; return s },
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user