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(),
|
IOCommand(),
|
||||||
IOOptionCommand(),
|
IOOptionCommand(),
|
||||||
DICommand(),
|
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