// Copyright (c) 2023 - 2025 IBM Corp. // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cli import ( "bytes" "go/ast" "go/parser" "go/token" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHasLensAnnotation(t *testing.T) { tests := []struct { name string comment string expected bool }{ { name: "has annotation", comment: "// fp-go:Lens", expected: true, }, { name: "has annotation with other text", comment: "// This is a struct with fp-go:Lens annotation", expected: true, }, { name: "no annotation", comment: "// This is just a regular comment", expected: false, }, { name: "nil comment", comment: "", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var doc *ast.CommentGroup if tt.comment != "" { doc = &ast.CommentGroup{ List: []*ast.Comment{ {Text: tt.comment}, }, } } result := hasLensAnnotation(doc) assert.Equal(t, tt.expected, result) }) } } func TestGetTypeName(t *testing.T) { tests := []struct { name string code string expected string }{ { name: "simple type", code: "type T struct { F string }", expected: "string", }, { name: "pointer type", code: "type T struct { F *string }", expected: "*string", }, { name: "slice type", code: "type T struct { F []int }", expected: "[]int", }, { name: "map type", code: "type T struct { F map[string]int }", expected: "map[string]int", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0) require.NoError(t, err) var fieldType ast.Expr ast.Inspect(file, func(n ast.Node) bool { if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 { fieldType = field.Type return false } return true }) require.NotNil(t, fieldType) result := getTypeName(fieldType) assert.Equal(t, tt.expected, result) }) } } func TestIsPointerType(t *testing.T) { tests := []struct { name string code string expected bool }{ { name: "pointer type", code: "type T struct { F *string }", expected: true, }, { name: "non-pointer type", code: "type T struct { F string }", expected: false, }, { name: "slice type", code: "type T struct { F []string }", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0) require.NoError(t, err) var fieldType ast.Expr ast.Inspect(file, func(n ast.Node) bool { if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 { fieldType = field.Type return false } return true }) require.NotNil(t, fieldType) result := isPointerType(fieldType) assert.Equal(t, tt.expected, result) }) } } func 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, map[string]string{}) assert.Equal(t, tt.expected, result) }) } } func TestHasOmitEmpty(t *testing.T) { tests := []struct { name string tag string expected bool }{ { name: "has omitempty", tag: "`json:\"field,omitempty\"`", expected: true, }, { name: "has omitempty with other options", tag: "`json:\"field,omitempty,string\"`", expected: true, }, { name: "no omitempty", tag: "`json:\"field\"`", expected: false, }, { name: "no tag", tag: "", expected: false, }, { name: "different tag", tag: "`xml:\"field\"`", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var tag *ast.BasicLit if tt.tag != "" { tag = &ast.BasicLit{ Value: tt.tag, } } result := hasOmitEmpty(tag) assert.Equal(t, tt.expected, result) }) } } func TestParseFile(t *testing.T) { // Create a temporary test file tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "test.go") testCode := `package testpkg // fp-go:Lens type Person struct { Name string Age int Phone *string } // fp-go:Lens type Address struct { Street string City string } // Not annotated type Other struct { Field string } ` err := os.WriteFile(testFile, []byte(testCode), 0644) require.NoError(t, err) // Parse the file structs, pkg, err := parseFile(testFile) require.NoError(t, err) // Verify results assert.Equal(t, "testpkg", pkg) assert.Len(t, structs, 2) // Check Person struct person := structs[0] assert.Equal(t, "Person", person.Name) assert.Len(t, person.Fields, 3) assert.Equal(t, "Name", person.Fields[0].Name) assert.Equal(t, "string", person.Fields[0].TypeName) assert.False(t, person.Fields[0].IsOptional) assert.Equal(t, "Age", person.Fields[1].Name) assert.Equal(t, "int", person.Fields[1].TypeName) assert.False(t, person.Fields[1].IsOptional) assert.Equal(t, "Phone", person.Fields[2].Name) assert.Equal(t, "*string", person.Fields[2].TypeName) assert.True(t, person.Fields[2].IsOptional) // Check Address struct address := structs[1] assert.Equal(t, "Address", address.Name) assert.Len(t, address.Fields, 2) assert.Equal(t, "Street", address.Fields[0].Name) assert.Equal(t, "City", address.Fields[1].Name) } func 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 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) // 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, "lensName := L.MakeLensStrict(", "comparable field Name should use MakeLensStrict in RefLenses") // Age field - comparable, should use MakeLensStrict assert.Contains(t, constructorStr, "lensAge := L.MakeLensStrict(", "comparable field Age should use MakeLensStrict in RefLenses") // Data field - not comparable, should use MakeLensRef assert.Contains(t, constructorStr, "lensData := L.MakeLensRef(", "non-comparable field Data 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 := "lensName := L.MakeLensStrict(" assert.Contains(t, contentStr, namePattern, "Name field should use MakeLensStrict") // Verify the pattern appears for Data field (not comparable) dataPattern := "lensData := L.MakeLensRef(" assert.Contains(t, contentStr, dataPattern, "Data field should use MakeLensRef") } func TestGenerateLensHelpers(t *testing.T) { // Create a temporary directory with test files tmpDir := t.TempDir() testCode := `package testpkg // fp-go:Lens type TestStruct struct { Name string Value *int } ` testFile := filepath.Join(tmpDir, "test.go") err := os.WriteFile(testFile, []byte(testCode), 0644) require.NoError(t, err) // Generate lens code outputFile := "gen.go" err = generateLensHelpers(tmpDir, outputFile, false) require.NoError(t, err) // Verify the generated file exists genPath := filepath.Join(tmpDir, outputFile) _, err = os.Stat(genPath) require.NoError(t, err) // Read and verify the generated content content, err := os.ReadFile(genPath) require.NoError(t, err) contentStr := string(content) // Check for expected content assert.Contains(t, contentStr, "package testpkg") assert.Contains(t, contentStr, "Code generated by go generate") assert.Contains(t, contentStr, "TestStructLenses") assert.Contains(t, contentStr, "MakeTestStructLenses") assert.Contains(t, contentStr, "L.Lens[TestStruct, string]") assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]") assert.Contains(t, contentStr, "IO.FromZero") } func TestGenerateLensHelpersNoAnnotations(t *testing.T) { // Create a temporary directory with test files tmpDir := t.TempDir() testCode := `package testpkg // No annotation type TestStruct struct { Name string } ` testFile := filepath.Join(tmpDir, "test.go") err := os.WriteFile(testFile, []byte(testCode), 0644) require.NoError(t, err) // Generate lens code (should not create file) outputFile := "gen.go" err = generateLensHelpers(tmpDir, outputFile, false) require.NoError(t, err) // Verify the generated file does not exist genPath := filepath.Join(tmpDir, outputFile) _, err = os.Stat(genPath) assert.True(t, os.IsNotExist(err)) } func TestLensTemplates(t *testing.T) { s := structInfo{ Name: "TestStruct", Fields: []fieldInfo{ {Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true}, {Name: "Value", TypeName: "*int", IsOptional: true, IsComparable: true}, }, } // Test struct template var structBuf bytes.Buffer err := structTmpl.Execute(&structBuf, s) require.NoError(t, err) structStr := structBuf.String() assert.Contains(t, structStr, "type TestStructLenses struct") assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]") assert.Contains(t, structStr, "NameO LO.LensO[TestStruct, string]") assert.Contains(t, structStr, "Value L.Lens[TestStruct, *int]") assert.Contains(t, structStr, "ValueO LO.LensO[TestStruct, *int]") // Test constructor template var constructorBuf bytes.Buffer err = constructorTmpl.Execute(&constructorBuf, s) require.NoError(t, err) constructorStr := constructorBuf.String() assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses") assert.Contains(t, constructorStr, "return TestStructLenses{") assert.Contains(t, constructorStr, "Name: lensName,") assert.Contains(t, constructorStr, "NameO: lensNameO,") assert.Contains(t, constructorStr, "Value: lensValue,") assert.Contains(t, constructorStr, "ValueO: lensValueO,") assert.Contains(t, constructorStr, "IO.FromZero") } func TestLensTemplatesWithOmitEmpty(t *testing.T) { s := structInfo{ Name: "ConfigStruct", Fields: []fieldInfo{ {Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true}, {Name: "Value", TypeName: "string", IsOptional: true, IsComparable: true}, // non-pointer with omitempty {Name: "Count", TypeName: "int", IsOptional: true, IsComparable: true}, // non-pointer with omitempty {Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: 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, "NameO LO.LensO[ConfigStruct, string]") assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]") assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens") assert.Contains(t, structStr, "Count L.Lens[ConfigStruct, int]") assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens") assert.Contains(t, structStr, "Pointer L.Lens[ConfigStruct, *string]") assert.Contains(t, structStr, "PointerO 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, "IO.FromZero[string]()") assert.Contains(t, constructorStr, "IO.FromZero[int]()") assert.Contains(t, constructorStr, "IO.FromZero[*string]()") } func TestLensCommandFlags(t *testing.T) { cmd := LensCommand() assert.Equal(t, "lens", cmd.Name) assert.Equal(t, "generate lens code for annotated structs", cmd.Usage) assert.Contains(t, strings.ToLower(cmd.Description), "fp-go:lens") assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses") // Check flags assert.Len(t, cmd.Flags, 3) var hasDir, hasFilename, hasVerbose bool for _, flag := range cmd.Flags { switch flag.Names()[0] { case "dir": hasDir = true case "filename": hasFilename = true case "verbose": hasVerbose = true } } assert.True(t, hasDir, "should have dir flag") assert.True(t, hasFilename, "should have filename flag") assert.True(t, hasVerbose, "should have verbose flag") } func TestParseFileWithEmbeddedStruct(t *testing.T) { // Create a temporary test file tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "test.go") testCode := `package testpkg // Base struct to be embedded type Base struct { ID int Name string } // fp-go:Lens type Extended struct { Base Extra string } ` err := os.WriteFile(testFile, []byte(testCode), 0644) require.NoError(t, err) // Parse the file structs, pkg, err := parseFile(testFile) require.NoError(t, err) // Verify results assert.Equal(t, "testpkg", pkg) assert.Len(t, structs, 1) // Check Extended struct extended := structs[0] assert.Equal(t, "Extended", extended.Name) assert.Len(t, extended.Fields, 3, "Should have 3 fields: ID, Name (from Base), and Extra") // Check that embedded fields are promoted fieldNames := make(map[string]bool) for _, field := range extended.Fields { fieldNames[field.Name] = true } assert.True(t, fieldNames["ID"], "Should have promoted ID field from Base") assert.True(t, fieldNames["Name"], "Should have promoted Name field from Base") assert.True(t, fieldNames["Extra"], "Should have Extra field") } func TestGenerateLensHelpersWithEmbeddedStruct(t *testing.T) { // Create a temporary directory with test files tmpDir := t.TempDir() testCode := `package testpkg // Base struct to be embedded type Address struct { Street string City string } // fp-go:Lens type Person struct { Address Name string Age int } ` testFile := filepath.Join(tmpDir, "test.go") err := os.WriteFile(testFile, []byte(testCode), 0644) require.NoError(t, err) // Generate lens code outputFile := "gen.go" err = generateLensHelpers(tmpDir, outputFile, false) require.NoError(t, err) // Verify the generated file exists genPath := filepath.Join(tmpDir, outputFile) _, err = os.Stat(genPath) require.NoError(t, err) // Read and verify the generated content content, err := os.ReadFile(genPath) require.NoError(t, err) contentStr := string(content) // Check for expected content assert.Contains(t, contentStr, "package testpkg") assert.Contains(t, contentStr, "PersonLenses") assert.Contains(t, contentStr, "MakePersonLenses") // Check that embedded fields are included assert.Contains(t, contentStr, "Street L.Lens[Person, string]", "Should have lens for embedded Street field") assert.Contains(t, contentStr, "City L.Lens[Person, string]", "Should have lens for embedded City field") assert.Contains(t, contentStr, "Name L.Lens[Person, string]", "Should have lens for Name field") assert.Contains(t, contentStr, "Age L.Lens[Person, int]", "Should have lens for Age field") // Check that optional lenses are also generated for embedded fields assert.Contains(t, contentStr, "StreetO LO.LensO[Person, string]") assert.Contains(t, contentStr, "CityO LO.LensO[Person, string]") } func TestParseFileWithPointerEmbeddedStruct(t *testing.T) { // Create a temporary test file tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "test.go") testCode := `package testpkg // Base struct to be embedded type Metadata struct { CreatedAt string UpdatedAt string } // fp-go:Lens type Document struct { *Metadata Title string Content string } ` err := os.WriteFile(testFile, []byte(testCode), 0644) require.NoError(t, err) // Parse the file structs, pkg, err := parseFile(testFile) require.NoError(t, err) // Verify results assert.Equal(t, "testpkg", pkg) assert.Len(t, structs, 1) // Check Document struct doc := structs[0] assert.Equal(t, "Document", doc.Name) assert.Len(t, doc.Fields, 4, "Should have 4 fields: CreatedAt, UpdatedAt (from *Metadata), Title, and Content") // Check that embedded fields are promoted fieldNames := make(map[string]bool) for _, field := range doc.Fields { fieldNames[field.Name] = true } assert.True(t, fieldNames["CreatedAt"], "Should have promoted CreatedAt field from *Metadata") assert.True(t, fieldNames["UpdatedAt"], "Should have promoted UpdatedAt field from *Metadata") assert.True(t, fieldNames["Title"], "Should have Title field") assert.True(t, fieldNames["Content"], "Should have Content field") } func TestParseFileWithGenericStruct(t *testing.T) { // Create a temporary test file tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "test.go") testCode := `package testpkg // fp-go:Lens type Container[T any] struct { Value T Count 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 Container struct container := structs[0] assert.Equal(t, "Container", container.Name) assert.Equal(t, "[T any]", container.TypeParams, "Should have type parameter [T any]") assert.Len(t, container.Fields, 2) assert.Equal(t, "Value", container.Fields[0].Name) assert.Equal(t, "T", container.Fields[0].TypeName) assert.Equal(t, "Count", container.Fields[1].Name) assert.Equal(t, "int", container.Fields[1].TypeName) } func TestParseFileWithMultipleTypeParams(t *testing.T) { // Create a temporary test file tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "test.go") testCode := `package testpkg // fp-go:Lens type Pair[K comparable, V any] struct { Key K Value V } ` 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 Pair struct pair := structs[0] assert.Equal(t, "Pair", pair.Name) assert.Equal(t, "[K comparable, V any]", pair.TypeParams, "Should have type parameters [K comparable, V any]") assert.Len(t, pair.Fields, 2) assert.Equal(t, "Key", pair.Fields[0].Name) assert.Equal(t, "K", pair.Fields[0].TypeName) assert.Equal(t, "Value", pair.Fields[1].Name) assert.Equal(t, "V", pair.Fields[1].TypeName) } func TestGenerateLensHelpersWithGenericStruct(t *testing.T) { // Create a temporary directory with test files tmpDir := t.TempDir() testCode := `package testpkg // fp-go:Lens type Box[T any] struct { Content T Label string } ` 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 with type parameters assert.Contains(t, contentStr, "package testpkg") assert.Contains(t, contentStr, "type BoxLenses[T any] struct", "Should have generic BoxLenses type") assert.Contains(t, contentStr, "type BoxRefLenses[T any] struct", "Should have generic BoxRefLenses type") assert.Contains(t, contentStr, "func MakeBoxLenses[T any]() BoxLenses[T]", "Should have generic constructor") assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor") // Check that fields use the generic type parameter assert.Contains(t, contentStr, "Content L.Lens[Box[T], T]", "Should have lens for generic Content field") assert.Contains(t, contentStr, "Label L.Lens[Box[T], string]", "Should have lens for Label field") // Check optional lenses - only for comparable types // T any is not comparable, so ContentO should NOT be generated assert.NotContains(t, contentStr, "ContentO LO.LensO[Box[T], T]", "T any is not comparable, should not have optional lens") // string is comparable, so LabelO should be generated assert.Contains(t, contentStr, "LabelO LO.LensO[Box[T], string]", "string is comparable, should have optional lens") } func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) { // Create a temporary directory with test files tmpDir := t.TempDir() testCode := `package testpkg // fp-go:Lens type ComparableBox[T comparable] struct { Key T Value string } ` 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 with type parameters assert.Contains(t, contentStr, "package testpkg") assert.Contains(t, contentStr, "type ComparableBoxLenses[T comparable] struct", "Should have generic ComparableBoxLenses type") assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type") // Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses assert.Contains(t, contentStr, "lensKey := L.MakeLensStrict(", "Key field with comparable constraint should use MakeLensStrict") // Check that Value field (string, always comparable) also uses MakeLensStrict assert.Contains(t, contentStr, "lensValue := L.MakeLensStrict(", "Value field (string) should use MakeLensStrict") // Verify that MakeLensRef is NOT used (since both fields are comparable) assert.NotContains(t, contentStr, "L.MakeLensRef(", "Should not use MakeLensRef when all fields are comparable") }