Skip to content
17 changes: 17 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ Dalec has adopted the CNCF Code of Conduct. Refer to our [Community Code of Cond

When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.

## Development

### Code Generation

Some files in this project are automatically generated. If you modify struct definitions (especially `Spec` or `Source`), you may need to regenerate the corresponding code:

```bash
# Regenerate all generated files
go generate ./...

# Or regenerate specific files
go generate ./spec.go # Regenerates spec_resolve_generated.go
go generate ./source.go # Regenerates source_generated.go
```

See [docs/code-generation.md](docs/code-generation.md) for more details about the code generation system.

## Issue and pull request management

Anyone can comment on issues and submit reviews for pull requests. In order to be assigned an issue or pull request, you can leave a `/assign <your Github ID>` comment on the issue or pull request.
272 changes: 272 additions & 0 deletions cmd/gen-resolve/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
package main
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This generator needs tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comprehensive test suite for the generator covering field type parsing, struct field extraction, and code generation logic. The tests ensure the generator works correctly with the actual Spec and Target structs. See e9b42a8.


import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"sort"
)

func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s <output-file>\n", os.Args[0])
os.Exit(1)
}

outputFile := os.Args[1]

// Parse the source files to extract struct information
specFields, targetFields, err := extractStructFields()
if err != nil {
fmt.Fprintf(os.Stderr, "Error extracting struct fields: %v\n", err)
os.Exit(1)
}

// Generate the code
code, err := generateResolveMethod(specFields, targetFields)
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating code: %v\n", err)
os.Exit(1)
}

// Write to output file
err = os.WriteFile(outputFile, code, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "Error writing output file: %v\n", err)
os.Exit(1)
}

fmt.Printf("Generated Resolve method in %s\n", outputFile)
}

type FieldInfo struct {
Name string // Field name (e.g. "Name")
TypeName string // Type name (e.g. "string")
IsSlice bool // Is it a slice type
IsMap bool // Is it a map type
IsPtr bool // Is it a pointer type
}

func extractStructFields() ([]FieldInfo, []FieldInfo, error) {
// Parse spec.go for Spec struct
specFields, err := parseStructFromFile("spec.go", "Spec")
if err != nil {
return nil, nil, fmt.Errorf("failed to extract Spec fields: %w", err)
}

// Parse target.go for Target struct
targetFields, err := parseStructFromFile("target.go", "Target")
if err != nil {
return nil, nil, fmt.Errorf("failed to extract Target fields: %w", err)
}

return specFields, targetFields, nil
}

func parseStructFromFile(filename, structName string) ([]FieldInfo, error) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", filename, err)
}

var fields []FieldInfo

// Find the target struct
ast.Inspect(node, func(n ast.Node) bool {
if ts, ok := n.(*ast.TypeSpec); ok && ts.Name.Name == structName {
if st, ok := ts.Type.(*ast.StructType); ok {
for _, field := range st.Fields.List {
if len(field.Names) == 0 {
continue // Skip embedded fields
}

fieldName := field.Names[0].Name

// Skip unexported fields and internal fields
if !ast.IsExported(fieldName) || fieldName == "extensions" || fieldName == "decodeOpts" {
continue
}

typeName, isSlice, isMap, isPtr := parseFieldType(field.Type)

fields = append(fields, FieldInfo{
Name: fieldName,
TypeName: typeName,
IsSlice: isSlice,
IsMap: isMap,
IsPtr: isPtr,
})
}
}
}
return true
})

// Sort fields for consistent output
sort.Slice(fields, func(i, j int) bool {
return fields[i].Name < fields[j].Name
})

return fields, nil
}

func parseFieldType(expr ast.Expr) (typeName string, isSlice, isMap, isPtr bool) {
switch t := expr.(type) {
case *ast.Ident:
return t.Name, false, false, false
case *ast.StarExpr:
name, slice, mp, _ := parseFieldType(t.X)
return name, slice, mp, true
case *ast.ArrayType:
if t.Len == nil { // slice
name, _, mp, ptr := parseFieldType(t.Elt)
return name, true, mp, ptr
}
return "array", false, false, false // fixed-size array
case *ast.MapType:
return "map", false, true, false
case *ast.SelectorExpr:
// Handle qualified identifiers like pkg.Type
if ident, ok := t.X.(*ast.Ident); ok {
return fmt.Sprintf("%s.%s", ident.Name, t.Sel.Name), false, false, false
}
return "selector", false, false, false
default:
return "unknown", false, false, false
}
}

func generateResolveMethod(specFields []FieldInfo, targetFields []FieldInfo) ([]byte, error) {
var buf bytes.Buffer

// Generate file header
buf.WriteString(`// Code generated by cmd/gen-resolve. DO NOT EDIT.

package dalec

`)

// Determine which fields exist in both Spec and Target structs
targetFieldNames := make(map[string]bool)
for _, field := range targetFields {
targetFieldNames[field.Name] = true
}

// Fields that need special merge logic (exist in both structs)
var targetOverrideFields []FieldInfo
var regularFields []FieldInfo

for _, field := range specFields {
// Skip Targets field itself
if field.Name == "Targets" {
continue
}

if targetFieldNames[field.Name] {
targetOverrideFields = append(targetOverrideFields, field)
} else {
regularFields = append(regularFields, field)
}
}

// Start the Resolve method
buf.WriteString("// Resolve creates a new Spec with target-specific configuration merged in.\n")
buf.WriteString("// This eliminates the need to pass targetKey parameters around by pre-resolving\n")
buf.WriteString("// all target-specific configuration into a single Spec.\n")
buf.WriteString("func (s *Spec) Resolve(targetKey string) *Spec {\n")
buf.WriteString("\t// Create a deep copy of the current spec\n")
buf.WriteString("\tresolved := &Spec{\n")

// Copy regular fields (no target overrides)
for _, field := range regularFields {
buf.WriteString(fmt.Sprintf("\t\t%s: s.%s,\n", field.Name, field.Name))
}

buf.WriteString("\t}\n\n")

// Copy extension fields
buf.WriteString("\t// Copy extension fields\n")
buf.WriteString("\tif s.extensions != nil {\n")
buf.WriteString("\t\tresolved.extensions = make(extensionFields)\n")
buf.WriteString("\t\tfor k, v := range s.extensions {\n")
buf.WriteString("\t\t\tresolved.extensions[k] = v\n")
buf.WriteString("\t\t}\n")
buf.WriteString("\t}\n\n")

// Handle target override fields dynamically
buf.WriteString("\t// Get target-specific configuration\n")
buf.WriteString("\ttarget, hasTarget := s.Targets[targetKey]\n\n")

for _, field := range targetOverrideFields {
generateFieldMergeLogic(&buf, field)
buf.WriteString("\n")
}

buf.WriteString("\t// Clear targets as this is now a resolved spec for a specific target\n")
buf.WriteString("\tresolved.Targets = nil\n\n")

buf.WriteString("\treturn resolved\n")
buf.WriteString("}\n")

// Format the generated code
formatted, err := format.Source(buf.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to format generated code: %w", err)
}

return formatted, nil
}

func generateFieldMergeLogic(buf *bytes.Buffer, field FieldInfo) {
switch field.Name {
case "Tests":
// Tests are appended (global + target-specific)
buf.WriteString(fmt.Sprintf("\t// Merge %s (global + target-specific)\n", field.Name))
buf.WriteString(fmt.Sprintf("\tresolved.%s = append([]*TestSpec(nil), s.%s...)\n", field.Name, field.Name))
buf.WriteString("\tif hasTarget && target.Tests != nil {\n")
buf.WriteString(fmt.Sprintf("\t\tresolved.%s = append(resolved.%s, target.%s...)\n", field.Name, field.Name, field.Name))
buf.WriteString("\t}")

case "Dependencies":
// Dependencies use special GetPackageDeps logic
buf.WriteString(fmt.Sprintf("\t// Resolve %s using existing merge logic\n", field.Name))
buf.WriteString(fmt.Sprintf("\tresolved.%s = s.GetPackageDeps(targetKey)", field.Name))

case "Image":
// Image uses special MergeSpecImage logic
buf.WriteString(fmt.Sprintf("\t// Resolve %s using existing merge logic\n", field.Name))
buf.WriteString(fmt.Sprintf("\tresolved.%s = MergeSpecImage(s, targetKey)", field.Name))

case "Artifacts":
// Artifacts use GetArtifacts logic
buf.WriteString(fmt.Sprintf("\t// Resolve %s using existing logic\n", field.Name))
buf.WriteString(fmt.Sprintf("\tresolved.%s = s.GetArtifacts(targetKey)", field.Name))

case "Provides", "Replaces", "Conflicts":
// These use the existing Get* methods
methodName := "Get" + field.Name
buf.WriteString(fmt.Sprintf("\t// Resolve %s using existing logic\n", field.Name))
buf.WriteString(fmt.Sprintf("\tresolved.%s = s.%s(targetKey)", field.Name, methodName))

case "PackageConfig":
// PackageConfig: target overrides global
buf.WriteString(fmt.Sprintf("\t// Resolve %s (target overrides global)\n", field.Name))
buf.WriteString(fmt.Sprintf("\tresolved.%s = s.%s\n", field.Name, field.Name))
buf.WriteString(fmt.Sprintf("\tif hasTarget && target.%s != nil {\n", field.Name))
buf.WriteString(fmt.Sprintf("\t\tresolved.%s = target.%s\n", field.Name, field.Name))
buf.WriteString("\t}")

default:
// Default: target overrides global (for future fields)
buf.WriteString(fmt.Sprintf("\t// Resolve %s (target overrides global)\n", field.Name))
buf.WriteString(fmt.Sprintf("\tresolved.%s = s.%s\n", field.Name, field.Name))
buf.WriteString(fmt.Sprintf("\tif hasTarget && target.%s != nil {\n", field.Name))
buf.WriteString(fmt.Sprintf("\t\tresolved.%s = target.%s\n", field.Name, field.Name))
buf.WriteString("\t}")
}
}
Loading