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

fix: better package import

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

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

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