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:
@@ -35,5 +35,6 @@ func Commands() []*C.Command {
|
||||
IOCommand(),
|
||||
IOOptionCommand(),
|
||||
DICommand(),
|
||||
LensCommand(),
|
||||
}
|
||||
}
|
||||
|
||||
521
v2/cli/lens.go
Normal file
521
v2/cli/lens.go
Normal 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
411
v2/cli/lens_test.go
Normal 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
194
v2/samples/lens/README.md
Normal 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
|
||||
52
v2/samples/lens/example.go
Normal file
52
v2/samples/lens/example.go
Normal 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"`
|
||||
}
|
||||
155
v2/samples/lens/example_test.go
Normal file
155
v2/samples/lens/example_test.go
Normal 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
249
v2/samples/lens/gen.go
Normal 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 },
|
||||
),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user