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

fix: better package import

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
Dr. Carsten Leue
2025-11-07 16:15:16 +01:00
parent aa5e908810
commit 51adce0c95
7 changed files with 1583 additions and 0 deletions

View File

@@ -35,5 +35,6 @@ func Commands() []*C.Command {
IOCommand(),
IOOptionCommand(),
DICommand(),
LensCommand(),
}
}

521
v2/cli/lens.go Normal file
View File

@@ -0,0 +1,521 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"bytes"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"text/template"
C "github.com/urfave/cli/v2"
)
const (
keyLensDir = "dir"
keyVerbose = "verbose"
lensAnnotation = "fp-go:Lens"
)
var (
flagLensDir = &C.StringFlag{
Name: keyLensDir,
Value: ".",
Usage: "Directory to scan for Go files",
}
flagVerbose = &C.BoolFlag{
Name: keyVerbose,
Aliases: []string{"v"},
Value: false,
Usage: "Enable verbose output",
}
)
// structInfo holds information about a struct that needs lens generation
type structInfo struct {
Name string
Fields []fieldInfo
Imports map[string]string // package path -> alias
}
// fieldInfo holds information about a struct field
type fieldInfo struct {
Name string
TypeName string
BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if json tag has omitempty or field is a pointer
}
// templateData holds data for template rendering
type templateData struct {
PackageName string
Structs []structInfo
}
const lensStructTemplate = `
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
type {{.Name}}Lenses struct {
{{- range .Fields}}
{{.Name}} {{if .IsOptional}}LO.LensO[{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[{{$.Name}}, {{.TypeName}}]{{end}}
{{- end}}
}
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
type {{.Name}}RefLenses struct {
{{- range .Fields}}
{{.Name}} {{if .IsOptional}}LO.LensO[*{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[*{{$.Name}}, {{.TypeName}}]{{end}}
{{- end}}
}
`
const lensConstructorTemplate = `
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
func Make{{.Name}}Lenses() {{.Name}}Lenses {
{{- range .Fields}}
{{- if .IsOptional}}
getOrElse{{.Name}} := O.GetOrElse(F.ConstNil[{{.BaseType}}])
{{- end}}
{{- end}}
return {{.Name}}Lenses{
{{- range .Fields}}
{{- if .IsOptional}}
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) O.Option[{{.TypeName}}] { return O.FromNillable(s.{{.Name}}) },
func(s {{$.Name}}, v O.Option[{{.TypeName}}]) {{$.Name}} { s.{{.Name}} = getOrElse{{.Name}}(v); return s },
),
{{- else}}
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}, v {{.TypeName}}) {{$.Name}} { s.{{.Name}} = v; return s },
),
{{- end}}
{{- end}}
}
}
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
func Make{{.Name}}RefLenses() {{.Name}}RefLenses {
{{- range .Fields}}
{{- if .IsOptional}}
getOrElse{{.Name}} := O.GetOrElse(F.ConstNil[{{.BaseType}}])
{{- end}}
{{- end}}
return {{.Name}}RefLenses{
{{- range .Fields}}
{{- if .IsOptional}}
{{.Name}}: L.MakeLensRef(
func(s *{{$.Name}}) O.Option[{{.TypeName}}] { return O.FromNillable(s.{{.Name}}) },
func(s *{{$.Name}}, v O.Option[{{.TypeName}}]) *{{$.Name}} { s.{{.Name}} = getOrElse{{.Name}}(v); return s },
),
{{- else}}
{{.Name}}: L.MakeLensRef(
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
),
{{- end}}
{{- end}}
}
}
`
var (
structTmpl *template.Template
constructorTmpl *template.Template
)
func init() {
var err error
structTmpl, err = template.New("struct").Parse(lensStructTemplate)
if err != nil {
panic(err)
}
constructorTmpl, err = template.New("constructor").Parse(lensConstructorTemplate)
if err != nil {
panic(err)
}
}
// hasLensAnnotation checks if a comment group contains the lens annotation
func hasLensAnnotation(doc *ast.CommentGroup) bool {
if doc == nil {
return false
}
for _, comment := range doc.List {
if strings.Contains(comment.Text, lensAnnotation) {
return true
}
}
return false
}
// getTypeName extracts the type name from a field type expression
func getTypeName(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
return "*" + getTypeName(t.X)
case *ast.ArrayType:
return "[]" + getTypeName(t.Elt)
case *ast.MapType:
return "map[" + getTypeName(t.Key) + "]" + getTypeName(t.Value)
case *ast.SelectorExpr:
return getTypeName(t.X) + "." + t.Sel.Name
case *ast.InterfaceType:
return "interface{}"
case *ast.IndexExpr:
// Generic type with single type parameter (Go 1.18+)
// e.g., Option[string]
return getTypeName(t.X) + "[" + getTypeName(t.Index) + "]"
case *ast.IndexListExpr:
// Generic type with multiple type parameters (Go 1.18+)
// e.g., Map[string, int]
var params []string
for _, index := range t.Indices {
params = append(params, getTypeName(index))
}
return getTypeName(t.X) + "[" + strings.Join(params, ", ") + "]"
default:
return "any"
}
}
// extractImports extracts package imports from a type expression
// Returns a map of package path -> package name
func extractImports(expr ast.Expr, imports map[string]string) {
switch t := expr.(type) {
case *ast.StarExpr:
extractImports(t.X, imports)
case *ast.ArrayType:
extractImports(t.Elt, imports)
case *ast.MapType:
extractImports(t.Key, imports)
extractImports(t.Value, imports)
case *ast.SelectorExpr:
// This is a qualified identifier like "option.Option"
if ident, ok := t.X.(*ast.Ident); ok {
// ident.Name is the package name (e.g., "option")
// We need to track this for import resolution
imports[ident.Name] = ident.Name
}
case *ast.IndexExpr:
// Generic type with single type parameter
extractImports(t.X, imports)
extractImports(t.Index, imports)
case *ast.IndexListExpr:
// Generic type with multiple type parameters
extractImports(t.X, imports)
for _, index := range t.Indices {
extractImports(index, imports)
}
}
}
// hasOmitEmpty checks if a struct tag contains json omitempty
func hasOmitEmpty(tag *ast.BasicLit) bool {
if tag == nil {
return false
}
// Parse the struct tag
tagValue := strings.Trim(tag.Value, "`")
structTag := reflect.StructTag(tagValue)
jsonTag := structTag.Get("json")
// Check if omitempty is present
parts := strings.Split(jsonTag, ",")
for _, part := range parts {
if strings.TrimSpace(part) == "omitempty" {
return true
}
}
return false
}
// isPointerType checks if a type expression is a pointer
func isPointerType(expr ast.Expr) bool {
_, ok := expr.(*ast.StarExpr)
return ok
}
// parseFile parses a Go file and extracts structs with lens annotations
func parseFile(filename string) ([]structInfo, string, error) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
return nil, "", err
}
var structs []structInfo
packageName := node.Name.Name
// Build import map: package name -> import path
fileImports := make(map[string]string)
for _, imp := range node.Imports {
path := strings.Trim(imp.Path.Value, `"`)
var name string
if imp.Name != nil {
name = imp.Name.Name
} else {
// Extract package name from path (last component)
parts := strings.Split(path, "/")
name = parts[len(parts)-1]
}
fileImports[name] = path
}
// First pass: collect all GenDecls with their doc comments
declMap := make(map[*ast.TypeSpec]*ast.CommentGroup)
ast.Inspect(node, func(n ast.Node) bool {
if gd, ok := n.(*ast.GenDecl); ok {
for _, spec := range gd.Specs {
if ts, ok := spec.(*ast.TypeSpec); ok {
declMap[ts] = gd.Doc
}
}
}
return true
})
// Second pass: process type specs
ast.Inspect(node, func(n ast.Node) bool {
// Look for type declarations
typeSpec, ok := n.(*ast.TypeSpec)
if !ok {
return true
}
// Check if it's a struct type
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
return true
}
// Get the doc comment from our map
doc := declMap[typeSpec]
if !hasLensAnnotation(doc) {
return true
}
// Extract field information and collect imports
var fields []fieldInfo
structImports := make(map[string]string)
for _, field := range structType.Fields.List {
if len(field.Names) == 0 {
// Embedded field, skip for now
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
typeName := getTypeName(field.Type)
isOptional := false
baseType := typeName
// Only pointer types can be optional
if isPointerType(field.Type) {
isOptional = true
// Strip leading * for base type
baseType = strings.TrimPrefix(typeName, "*")
}
// Extract imports from this field's type
fieldImports := make(map[string]string)
extractImports(field.Type, fieldImports)
// Resolve package names to full import paths
for pkgName := range fieldImports {
if importPath, ok := fileImports[pkgName]; ok {
structImports[importPath] = pkgName
}
}
fields = append(fields, fieldInfo{
Name: name.Name,
TypeName: typeName,
BaseType: baseType,
IsOptional: isOptional,
})
}
}
}
if len(fields) > 0 {
structs = append(structs, structInfo{
Name: typeSpec.Name.Name,
Fields: fields,
Imports: structImports,
})
}
return true
})
return structs, packageName, nil
}
// generateLensHelpers scans a directory for Go files and generates lens code
func generateLensHelpers(dir, filename string, verbose bool) error {
// Get absolute path
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
if verbose {
log.Printf("Scanning directory: %s", absDir)
}
// Find all Go files in the directory
files, err := filepath.Glob(filepath.Join(absDir, "*.go"))
if err != nil {
return err
}
if verbose {
log.Printf("Found %d Go files", len(files))
}
// Parse all files and collect structs
var allStructs []structInfo
var packageName string
for _, file := range files {
// Skip generated files and test files
if strings.HasSuffix(file, "_test.go") || strings.Contains(file, "gen.go") {
if verbose {
log.Printf("Skipping file: %s", filepath.Base(file))
}
continue
}
if verbose {
log.Printf("Parsing file: %s", filepath.Base(file))
}
structs, pkg, err := parseFile(file)
if err != nil {
log.Printf("Warning: failed to parse %s: %v", file, err)
continue
}
if verbose && len(structs) > 0 {
log.Printf("Found %d annotated struct(s) in %s", len(structs), filepath.Base(file))
for _, s := range structs {
log.Printf(" - %s (%d fields)", s.Name, len(s.Fields))
}
}
if packageName == "" {
packageName = pkg
}
allStructs = append(allStructs, structs...)
}
if len(allStructs) == 0 {
log.Printf("No structs with %s annotation found in %s", lensAnnotation, absDir)
return nil
}
// Collect all unique imports from all structs
allImports := make(map[string]string) // import path -> alias
for _, s := range allStructs {
for importPath, alias := range s.Imports {
allImports[importPath] = alias
}
}
// Create output file
outPath := filepath.Join(absDir, filename)
f, err := os.Create(filepath.Clean(outPath))
if err != nil {
return err
}
defer f.Close()
log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(allStructs))
// Write header
writePackage(f, packageName)
// Write imports
f.WriteString("import (\n")
// Standard fp-go imports always needed
f.WriteString("\tF \"github.com/IBM/fp-go/v2/function\"\n")
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
// Add additional imports collected from field types
for importPath, alias := range allImports {
f.WriteString("\t" + alias + " \"" + importPath + "\"\n")
}
f.WriteString(")\n")
// Generate lens code for each struct using templates
for _, s := range allStructs {
var buf bytes.Buffer
// Generate struct type
if err := structTmpl.Execute(&buf, s); err != nil {
return err
}
// Generate constructor
if err := constructorTmpl.Execute(&buf, s); err != nil {
return err
}
// Write to file
if _, err := f.Write(buf.Bytes()); err != nil {
return err
}
}
return nil
}
// LensCommand creates the CLI command for lens generation
func LensCommand() *C.Command {
return &C.Command{
Name: "lens",
Usage: "generate lens code for annotated structs",
Description: "Scans Go files for structs annotated with 'fp-go:Lens' and generates lens types. Fields with json omitempty tag or pointer types generate LensO (optional lens).",
Flags: []C.Flag{
flagLensDir,
flagFilename,
flagVerbose,
},
Action: func(ctx *C.Context) error {
return generateLensHelpers(
ctx.String(keyLensDir),
ctx.String(keyFilename),
ctx.Bool(keyVerbose),
)
},
}
}
// Made with Bob

411
v2/cli/lens_test.go Normal file
View File

@@ -0,0 +1,411 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"bytes"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHasLensAnnotation(t *testing.T) {
tests := []struct {
name string
comment string
expected bool
}{
{
name: "has annotation",
comment: "// fp-go:Lens",
expected: true,
},
{
name: "has annotation with other text",
comment: "// This is a struct with fp-go:Lens annotation",
expected: true,
},
{
name: "no annotation",
comment: "// This is just a regular comment",
expected: false,
},
{
name: "nil comment",
comment: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var doc *ast.CommentGroup
if tt.comment != "" {
doc = &ast.CommentGroup{
List: []*ast.Comment{
{Text: tt.comment},
},
}
}
result := hasLensAnnotation(doc)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetTypeName(t *testing.T) {
tests := []struct {
name string
code string
expected string
}{
{
name: "simple type",
code: "type T struct { F string }",
expected: "string",
},
{
name: "pointer type",
code: "type T struct { F *string }",
expected: "*string",
},
{
name: "slice type",
code: "type T struct { F []int }",
expected: "[]int",
},
{
name: "map type",
code: "type T struct { F map[string]int }",
expected: "map[string]int",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
require.NoError(t, err)
var fieldType ast.Expr
ast.Inspect(file, func(n ast.Node) bool {
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
fieldType = field.Type
return false
}
return true
})
require.NotNil(t, fieldType)
result := getTypeName(fieldType)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsPointerType(t *testing.T) {
tests := []struct {
name string
code string
expected bool
}{
{
name: "pointer type",
code: "type T struct { F *string }",
expected: true,
},
{
name: "non-pointer type",
code: "type T struct { F string }",
expected: false,
},
{
name: "slice type",
code: "type T struct { F []string }",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
require.NoError(t, err)
var fieldType ast.Expr
ast.Inspect(file, func(n ast.Node) bool {
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
fieldType = field.Type
return false
}
return true
})
require.NotNil(t, fieldType)
result := isPointerType(fieldType)
assert.Equal(t, tt.expected, result)
})
}
}
func TestHasOmitEmpty(t *testing.T) {
tests := []struct {
name string
tag string
expected bool
}{
{
name: "has omitempty",
tag: "`json:\"field,omitempty\"`",
expected: true,
},
{
name: "has omitempty with other options",
tag: "`json:\"field,omitempty,string\"`",
expected: true,
},
{
name: "no omitempty",
tag: "`json:\"field\"`",
expected: false,
},
{
name: "no tag",
tag: "",
expected: false,
},
{
name: "different tag",
tag: "`xml:\"field\"`",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var tag *ast.BasicLit
if tt.tag != "" {
tag = &ast.BasicLit{
Value: tt.tag,
}
}
result := hasOmitEmpty(tag)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseFile(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Person struct {
Name string
Age int
Phone *string
}
// fp-go:Lens
type Address struct {
Street string
City string
}
// Not annotated
type Other struct {
Field string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 2)
// Check Person struct
person := structs[0]
assert.Equal(t, "Person", person.Name)
assert.Len(t, person.Fields, 3)
assert.Equal(t, "Name", person.Fields[0].Name)
assert.Equal(t, "string", person.Fields[0].TypeName)
assert.False(t, person.Fields[0].IsOptional)
assert.Equal(t, "Age", person.Fields[1].Name)
assert.Equal(t, "int", person.Fields[1].TypeName)
assert.False(t, person.Fields[1].IsOptional)
assert.Equal(t, "Phone", person.Fields[2].Name)
assert.Equal(t, "*string", person.Fields[2].TypeName)
assert.True(t, person.Fields[2].IsOptional)
// Check Address struct
address := structs[1]
assert.Equal(t, "Address", address.Name)
assert.Len(t, address.Fields, 2)
assert.Equal(t, "Street", address.Fields[0].Name)
assert.Equal(t, "City", address.Fields[1].Name)
}
func TestGenerateLensHelpers(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type TestStruct struct {
Name string
Value *int
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "Code generated by go generate")
assert.Contains(t, contentStr, "TestStructLens")
assert.Contains(t, contentStr, "MakeTestStructLens")
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
assert.Contains(t, contentStr, "O.FromNillable")
assert.Contains(t, contentStr, "O.GetOrElse")
}
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// No annotation
type TestStruct struct {
Name string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code (should not create file)
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file does not exist
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
assert.True(t, os.IsNotExist(err))
}
func TestLensTemplates(t *testing.T) {
s := structInfo{
Name: "TestStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "*int", IsOptional: true},
},
}
// Test struct template
var structBuf bytes.Buffer
err := structTmpl.Execute(&structBuf, s)
require.NoError(t, err)
structStr := structBuf.String()
assert.Contains(t, structStr, "type TestStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[TestStruct, *int]")
// Test constructor template
var constructorBuf bytes.Buffer
err = constructorTmpl.Execute(&constructorBuf, s)
require.NoError(t, err)
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses")
assert.Contains(t, constructorStr, "return TestStructLenses{")
assert.Contains(t, constructorStr, "Name: L.MakeLens(")
assert.Contains(t, constructorStr, "Value: L.MakeLens(")
assert.Contains(t, constructorStr, "O.FromNillable")
assert.Contains(t, constructorStr, "O.GetOrElse")
}
func TestLensCommandFlags(t *testing.T) {
cmd := LensCommand()
assert.Equal(t, "lens", cmd.Name)
assert.Equal(t, "generate lens code for annotated structs", cmd.Usage)
assert.Contains(t, strings.ToLower(cmd.Description), "fp-go:lens")
assert.Contains(t, strings.ToLower(cmd.Description), "lenso")
// Check flags
assert.Len(t, cmd.Flags, 3)
var hasDir, hasFilename, hasVerbose bool
for _, flag := range cmd.Flags {
switch flag.Names()[0] {
case "dir":
hasDir = true
case "filename":
hasFilename = true
case "verbose":
hasVerbose = true
}
}
assert.True(t, hasDir, "should have dir flag")
assert.True(t, hasFilename, "should have filename flag")
assert.True(t, hasVerbose, "should have verbose flag")
}

194
v2/samples/lens/README.md Normal file
View File

@@ -0,0 +1,194 @@
# Lens Generator Example
This example demonstrates the lens code generator for Go structs.
## Overview
The lens generator automatically creates lens types for Go structs annotated with `fp-go:Lens`. Lenses provide a functional way to access and update nested immutable data structures.
## Usage
### 1. Annotate Your Structs
Add the `fp-go:Lens` annotation in a comment above your struct declaration:
```go
// fp-go:Lens
type Person struct {
Name string
Age int
Email string
Phone *string // Pointer fields generate LensO (optional lens)
}
```
### 2. Generate Lens Code
Run the generator command:
```bash
go run ../../main.go lens --dir . --filename gen.go
```
Or use it as a go generate directive:
```go
//go:generate go run ../../main.go lens --dir . --filename gen.go
```
### 3. Use the Generated Lenses
The generator creates:
- A `<TypeName>Lens` struct with a lens for each exported field
- A `Make<TypeName>Lens()` constructor function
```go
// Create lenses
lenses := MakePersonLens()
// Get a field value
name := lenses.Name.Get(person)
// Set a field value (returns a new instance)
updated := lenses.Name.Set("Bob")(person)
// Modify a field value
incremented := F.Pipe1(
lenses.Age,
L.Modify[Person](func(age int) int { return age + 1 }),
)(person)
```
## Features
### Optional Fields (LensO)
Pointer fields automatically generate `LensO` (optional lenses) that work with `Option[*T]`:
```go
// fp-go:Lens
type Person struct {
Name string
Phone *string // Generates LensO[Person, *string]
}
lenses := MakePersonLens()
person := Person{Name: "Alice", Phone: nil}
// Get returns Option[*string]
phoneOpt := lenses.Phone.Get(person) // None
// Set with Some
phone := "555-1234"
updated := lenses.Phone.Set(O.Some(&phone))(person)
// Set with None (clears the field)
cleared := lenses.Phone.Set(O.None[*string]())(person)
```
### Immutable Updates
All lens operations return new instances, leaving the original unchanged:
```go
person := Person{Name: "Alice", Age: 30}
updated := lenses.Name.Set("Bob")(person)
// person.Name is still "Alice"
// updated.Name is "Bob"
```
### Lens Composition
Compose lenses to access deeply nested fields:
```go
// Access company.CEO.Name
ceoNameLens := F.Pipe1(
companyLenses.CEO,
L.Compose[Company](personLenses.Name),
)
name := ceoNameLens.Get(company)
updated := ceoNameLens.Set("Jane")(company)
```
### Type Safety
All operations are type-safe at compile time:
```go
// Compile error: type mismatch
lenses.Age.Set("not a number")(person)
```
## Generated Code Structure
For each annotated struct, the generator creates:
```go
// Lens struct with a lens for each field
type PersonLens struct {
Name L.Lens[Person, string]
Age L.Lens[Person, int]
Email L.Lens[Person, string]
}
// Constructor function
func MakePersonLens() PersonLens {
return PersonLens{
Name: L.MakeLens(
func(s Person) string { return s.Name },
func(s Person, v string) Person { s.Name = v; return s },
),
// ... other fields
}
}
```
## Generated Code Structure
For each annotated struct, the generator creates:
```go
// Regular field generates Lens
type PersonLens struct {
Name L.Lens[Person, string]
Phone LO.LensO[Person, *string] // Pointer field generates LensO
}
// Constructor function
func MakePersonLens() PersonLens {
return PersonLens{
Name: L.MakeLens(
func(s Person) string { return s.Name },
func(s Person, v string) Person { s.Name = 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 = O.GetOrElse(func() *string { return nil })(v)
return s
},
),
}
}
```
## Command Options
- `--dir`: Directory to scan for Go files (default: ".")
- `--filename`: Name of the generated file (default: "gen.go")
## Notes
- Only pointer fields (`*T`) generate `LensO` (optional lenses)
- The `json:"...,omitempty"` tag alone does not make a field optional in the lens generator
- Pointer fields work with `Option[*T]` using `FromNillable` and `GetOrElse`
## Examples
See `example_test.go` for comprehensive examples including:
- Basic lens operations (Get, Set, Modify)
- Nested struct access
- Lens composition
- Complex data structure manipulation

View File

@@ -0,0 +1,52 @@
// 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 lens
import "github.com/IBM/fp-go/v2/optics/lens/option"
// fp-go:Lens
type Person struct {
Name string
Age int
Email string
// Optional field with pointer
Phone *string
}
// fp-go:Lens
type Address struct {
Street string
City string
ZipCode string
Country string
// Optional field
State *string `json:"state,omitempty"`
}
// fp-go:Lens
type Company struct {
Name string
Address Address
CEO Person
// Optional field
Website *string
}
// fp-go:Lens
type CheckOption struct {
Name option.Option[string]
Value string `json:",omitempty"`
}

View File

@@ -0,0 +1,155 @@
// 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 lens
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
func TestPersonLens(t *testing.T) {
// Create a person
person := Person{
Name: "Alice",
Age: 30,
Email: "alice@example.com",
}
// Create lenses
lenses := MakePersonLenses()
// Test Get
assert.Equal(t, "Alice", lenses.Name.Get(person))
assert.Equal(t, 30, lenses.Age.Get(person))
assert.Equal(t, "alice@example.com", lenses.Email.Get(person))
// Test Set
updated := lenses.Name.Set("Bob")(person)
assert.Equal(t, "Bob", updated.Name)
assert.Equal(t, 30, updated.Age) // Other fields unchanged
assert.Equal(t, "Alice", person.Name) // Original unchanged
// Test Modify
incrementAge := F.Pipe1(
lenses.Age,
L.Modify[Person](func(age int) int { return age + 1 }),
)
incremented := incrementAge(person)
assert.Equal(t, 31, incremented.Age)
assert.Equal(t, 30, person.Age) // Original unchanged
}
func TestCompanyLens(t *testing.T) {
// Create a company with nested structures
company := Company{
Name: "Acme Corp",
Address: Address{
Street: "123 Main St",
City: "Springfield",
ZipCode: "12345",
Country: "USA",
},
CEO: Person{
Name: "John Doe",
Age: 45,
Email: "john@acme.com",
},
}
// Create lenses
companyLenses := MakeCompanyLenses()
addressLenses := MakeAddressLenses()
personLenses := MakePersonLenses()
// Test simple field access
assert.Equal(t, "Acme Corp", companyLenses.Name.Get(company))
// Test nested field access using composition
cityLens := F.Pipe1(
companyLenses.Address,
L.Compose[Company](addressLenses.City),
)
assert.Equal(t, "Springfield", cityLens.Get(company))
// Test nested field update
updatedCompany := cityLens.Set("New York")(company)
assert.Equal(t, "New York", updatedCompany.Address.City)
assert.Equal(t, "Springfield", company.Address.City) // Original unchanged
// Test deeply nested field access
ceoNameLens := F.Pipe1(
companyLenses.CEO,
L.Compose[Company](personLenses.Name),
)
assert.Equal(t, "John Doe", ceoNameLens.Get(company))
// Test deeply nested field update
updatedCompany2 := ceoNameLens.Set("Jane Smith")(company)
assert.Equal(t, "Jane Smith", updatedCompany2.CEO.Name)
assert.Equal(t, "John Doe", company.CEO.Name) // Original unchanged
}
func TestLensComposition(t *testing.T) {
company := Company{
Name: "Tech Inc",
Address: Address{
Street: "456 Oak Ave",
City: "Boston",
ZipCode: "02101",
Country: "USA",
},
CEO: Person{
Name: "Alice Johnson",
Age: 50,
Email: "alice@techinc.com",
},
}
companyLenses := MakeCompanyLenses()
personLenses := MakePersonLenses()
// Compose lenses to access CEO's email
ceoEmailLens := F.Pipe1(
companyLenses.CEO,
L.Compose[Company](personLenses.Email),
)
// Get the CEO's email
email := ceoEmailLens.Get(company)
assert.Equal(t, "alice@techinc.com", email)
// Update the CEO's email
updated := ceoEmailLens.Set("alice.johnson@techinc.com")(company)
assert.Equal(t, "alice.johnson@techinc.com", updated.CEO.Email)
assert.Equal(t, "alice@techinc.com", company.CEO.Email) // Original unchanged
// Modify the CEO's age
ceoAgeLens := F.Pipe1(
companyLenses.CEO,
L.Compose[Company](personLenses.Age),
)
modifyAge := F.Pipe1(
ceoAgeLens,
L.Modify[Company](func(age int) int { return age + 5 }),
)
olderCEO := modifyAge(company)
assert.Equal(t, 55, olderCEO.CEO.Age)
assert.Equal(t, 50, company.CEO.Age) // Original unchanged
}

249
v2/samples/lens/gen.go Normal file
View File

@@ -0,0 +1,249 @@
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
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"
option "github.com/IBM/fp-go/v2/optics/lens/option"
)
// PersonLenses provides lenses for accessing fields of Person
type PersonLenses struct {
Name L.Lens[Person, string]
Age L.Lens[Person, int]
Email L.Lens[Person, string]
Phone LO.LensO[Person, *string]
}
// PersonRefLenses provides lenses for accessing fields of Person via a reference to Person
type PersonRefLenses struct {
Name L.Lens[*Person, string]
Age L.Lens[*Person, int]
Email L.Lens[*Person, string]
Phone LO.LensO[*Person, *string]
}
// MakePersonLenses creates a new PersonLenses with lenses for all fields
func MakePersonLenses() PersonLenses {
getOrElsePhone := O.GetOrElse(F.ConstNil[string])
return PersonLenses{
Name: L.MakeLens(
func(s Person) string { return s.Name },
func(s Person, v string) Person { s.Name = v; return s },
),
Age: L.MakeLens(
func(s Person) int { return s.Age },
func(s Person, v int) Person { s.Age = v; return s },
),
Email: L.MakeLens(
func(s Person) string { return s.Email },
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 },
),
}
}
// MakePersonRefLenses creates a new PersonRefLenses with lenses for all fields
func MakePersonRefLenses() PersonRefLenses {
getOrElsePhone := O.GetOrElse(F.ConstNil[string])
return PersonRefLenses{
Name: L.MakeLensRef(
func(s *Person) string { return s.Name },
func(s *Person, v string) *Person { s.Name = v; return s },
),
Age: L.MakeLensRef(
func(s *Person) int { return s.Age },
func(s *Person, v int) *Person { s.Age = v; return s },
),
Email: L.MakeLensRef(
func(s *Person) string { return s.Email },
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 },
),
}
}
// AddressLenses provides lenses for accessing fields of Address
type AddressLenses struct {
Street L.Lens[Address, string]
City L.Lens[Address, string]
ZipCode L.Lens[Address, string]
Country L.Lens[Address, string]
State LO.LensO[Address, *string]
}
// AddressRefLenses provides lenses for accessing fields of Address via a reference to Address
type AddressRefLenses struct {
Street L.Lens[*Address, string]
City L.Lens[*Address, string]
ZipCode L.Lens[*Address, string]
Country L.Lens[*Address, string]
State LO.LensO[*Address, *string]
}
// MakeAddressLenses creates a new AddressLenses with lenses for all fields
func MakeAddressLenses() AddressLenses {
getOrElseState := O.GetOrElse(F.ConstNil[string])
return AddressLenses{
Street: L.MakeLens(
func(s Address) string { return s.Street },
func(s Address, v string) Address { s.Street = v; return s },
),
City: L.MakeLens(
func(s Address) string { return s.City },
func(s Address, v string) Address { s.City = v; return s },
),
ZipCode: L.MakeLens(
func(s Address) string { return s.ZipCode },
func(s Address, v string) Address { s.ZipCode = v; return s },
),
Country: L.MakeLens(
func(s Address) string { return s.Country },
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 },
),
}
}
// MakeAddressRefLenses creates a new AddressRefLenses with lenses for all fields
func MakeAddressRefLenses() AddressRefLenses {
getOrElseState := O.GetOrElse(F.ConstNil[string])
return AddressRefLenses{
Street: L.MakeLensRef(
func(s *Address) string { return s.Street },
func(s *Address, v string) *Address { s.Street = v; return s },
),
City: L.MakeLensRef(
func(s *Address) string { return s.City },
func(s *Address, v string) *Address { s.City = v; return s },
),
ZipCode: L.MakeLensRef(
func(s *Address) string { return s.ZipCode },
func(s *Address, v string) *Address { s.ZipCode = v; return s },
),
Country: L.MakeLensRef(
func(s *Address) string { return s.Country },
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 },
),
}
}
// CompanyLenses provides lenses for accessing fields of Company
type CompanyLenses struct {
Name L.Lens[Company, string]
Address L.Lens[Company, Address]
CEO L.Lens[Company, Person]
Website LO.LensO[Company, *string]
}
// CompanyRefLenses provides lenses for accessing fields of Company via a reference to Company
type CompanyRefLenses struct {
Name L.Lens[*Company, string]
Address L.Lens[*Company, Address]
CEO L.Lens[*Company, Person]
Website LO.LensO[*Company, *string]
}
// MakeCompanyLenses creates a new CompanyLenses with lenses for all fields
func MakeCompanyLenses() CompanyLenses {
getOrElseWebsite := O.GetOrElse(F.ConstNil[string])
return CompanyLenses{
Name: L.MakeLens(
func(s Company) string { return s.Name },
func(s Company, v string) Company { s.Name = v; return s },
),
Address: L.MakeLens(
func(s Company) Address { return s.Address },
func(s Company, v Address) Company { s.Address = v; return s },
),
CEO: L.MakeLens(
func(s Company) Person { return s.CEO },
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 },
),
}
}
// MakeCompanyRefLenses creates a new CompanyRefLenses with lenses for all fields
func MakeCompanyRefLenses() CompanyRefLenses {
getOrElseWebsite := O.GetOrElse(F.ConstNil[string])
return CompanyRefLenses{
Name: L.MakeLensRef(
func(s *Company) string { return s.Name },
func(s *Company, v string) *Company { s.Name = v; return s },
),
Address: L.MakeLensRef(
func(s *Company) Address { return s.Address },
func(s *Company, v Address) *Company { s.Address = v; return s },
),
CEO: L.MakeLensRef(
func(s *Company) Person { return s.CEO },
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 },
),
}
}
// CheckOptionLenses provides lenses for accessing fields of CheckOption
type CheckOptionLenses struct {
Name L.Lens[CheckOption, option.Option[string]]
Value L.Lens[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]
}
// MakeCheckOptionLenses creates a new CheckOptionLenses with lenses for all fields
func MakeCheckOptionLenses() CheckOptionLenses {
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 },
),
}
}
// MakeCheckOptionRefLenses creates a new CheckOptionRefLenses with lenses for all fields
func MakeCheckOptionRefLenses() CheckOptionRefLenses {
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 },
),
}
}