ADR-003: Generator Architecture - Adopting Nx Patterns

Status

Accepted (2025-11-29)

Context

Our current CLI generates files directly to disk using Qute templates. After analyzing Nx's generator architecture, we identified several patterns that could improve our code generation capabilities:

  1. Virtual File System - Nx stages all changes in memory before applying
  2. Schema-Driven Validation - Declarative JSON schemas define options and validation
  3. Composable Generators - Generators can invoke other generators
  4. AST Manipulation - Safe modification of existing code files
  5. Interactive Prompts - Better UX with guided input

Decision

Adopt Nx patterns incrementally while maintaining our Java/Quarkus foundation and iDempiere-specific features.

Implementation Plan

Phase 1: Virtual File System (Tree API)

Create a GeneratorTree that stages changes before applying:

public class GeneratorTree {
    private final Path workingDir;
    private final Map<Path, FileChange> changes = new LinkedHashMap<>();

    public enum ChangeType { CREATE, UPDATE, DELETE }

    public record FileChange(ChangeType type, String content, String original) {}

    // Stage a new file
    public void create(Path path, String content) {
        changes.put(path, new FileChange(ChangeType.CREATE, content, null));
    }

    // Stage file modification
    public void update(Path path, String content) {
        String original = readExisting(path);
        changes.put(path, new FileChange(ChangeType.UPDATE, content, original));
    }

    // Preview all staged changes
    public List<FileChange> preview() {
        return List.copyOf(changes.values());
    }

    // Apply all changes atomically
    public void apply() throws IOException {
        for (var entry : changes.entrySet()) {
            applyChange(entry.getKey(), entry.getValue());
        }
    }

    // Rollback on failure
    public void rollback() {
        // Restore original files from FileChange.original
    }
}

Benefits:

Phase 2: Schema-Driven Generators

Define generator options in JSON schema files:

src/main/resources/generators/
├── init/
│   ├── schema.json
│   └── templates/
├── callout/
│   ├── schema.json
│   └── templates/
└── process/
    ├── schema.json
    └── templates/

schema.json example:

{
  "$schema": "http://json-schema.org/schema",
  "id": "callout",
  "title": "Add Callout",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Callout class name",
      "pattern": "^[A-Z][a-zA-Z0-9]*$",
      "x-prompt": "What is the callout name?"
    },
    "table": {
      "type": "string",
      "description": "Target table name",
      "x-prompt": "Which table should trigger this callout?"
    },
    "column": {
      "type": "string",
      "description": "Target column (optional)"
    },
    "idempiereVersion": {
      "type": "string",
      "enum": ["10", "11", "12", "13", "custom"],
      "default": "12"
    }
  },
  "required": ["name", "table"]
}

Java integration:

public class GeneratorSchema {
    private final Map<String, PropertySchema> properties;

    public record PropertySchema(
        String type,
        String description,
        String pattern,
        List<String> enumValues,
        Object defaultValue,
        String prompt,
        boolean required
    ) {}

    // Load from JSON
    public static GeneratorSchema load(String generatorName) {
        // Load from classpath: generators/{name}/schema.json
    }

    // Validate options against schema
    public ValidationResult validate(Map<String, Object> options) {
        // Check required, patterns, enums
    }

    // Generate interactive prompts for missing options
    public Map<String, Object> promptMissing(Map<String, Object> provided) {
        // Use x-prompt for interactive input
    }
}

Phase 3: Composable Generators

Allow generators to invoke other generators:

public interface Generator {
    String getName();
    GeneratorSchema getSchema();
    void generate(GeneratorTree tree, GeneratorContext context);
}

public class GeneratorContext {
    private final Map<String, Object> options;
    private final GeneratorRegistry registry;

    // Invoke another generator
    public void invoke(String generatorName, Map<String, Object> options) {
        Generator generator = registry.get(generatorName);
        generator.generate(tree, new GeneratorContext(options, registry));
    }
}

// Example: init generator composes multiple generators
public class InitGenerator implements Generator {
    @Override
    public void generate(GeneratorTree tree, GeneratorContext ctx) {
        // Generate base plugin structure
        generatePluginStructure(tree, ctx);

        // Compose with extension generators based on options
        if (ctx.getBoolean("withCallout")) {
            ctx.invoke("callout", Map.of(
                "name", "Sample" + ctx.getString("name") + "Callout",
                "table", "C_Order"
            ));
        }

        if (ctx.getBoolean("withProcess")) {
            ctx.invoke("process", Map.of(
                "name", "Sample" + ctx.getString("name") + "Process"
            ));
        }
    }
}

Phase 4: AST Manipulation

For modifying existing Java files safely:

public class JavaAstModifier {
    private final CompilationUnit cu;

    // Add import if not exists
    public void addImport(String importName) {
        if (!hasImport(importName)) {
            cu.addImport(importName);
        }
    }

    // Add annotation to class
    public void addClassAnnotation(String annotation, Map<String, String> params) {
        // Parse and add annotation
    }

    // Add method to class
    public void addMethod(String methodCode) {
        // Parse and insert method
    }

    // Register component in existing factory
    public void registerInFactory(String factoryClass, String registration) {
        // Find factory, add registration call
    }
}

Use cases:

Phase 5: Enhanced CLI UX

Interactive mode with prompts:

$ idempiere-cli add callout

? What is the callout name? OrderValidation
? Which table should trigger this callout? C_Order
? Which column (optional)? GrandTotal
? Target iDempiere version? (Use arrow keys)
  ❯ 12 (current stable)
    11
    13 (Jakarta EE)
    custom

Creating callout...

Changes to be applied:
  CREATE src/main/java/.../OrderValidationCallout.java
  UPDATE plugin.xml (add callout registration)

? Apply changes? (Y/n)

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                         CLI Commands                                 │
│              (Picocli - validates args, invokes generators)          │
├─────────────────────────────────────────────────────────────────────┤
│                       Generator Registry                             │
│         (discovers and manages available generators)                 │
├──────────────┬──────────────┬──────────────┬───────────────────────┤
│ InitGenerator│CalloutGenerator│ProcessGenerator│ ... more generators │
├──────────────┴──────────────┴──────────────┴───────────────────────┤
│                        Generator Context                             │
│    (options, tree, composition API, iDempiere version info)         │
├─────────────────────────────────────────────────────────────────────┤
│                       GeneratorTree (Virtual FS)                     │
│         (stages changes: create, update, delete files)               │
├───────────────────┬─────────────────────────────────────────────────┤
│  Qute Templates   │           AST Modifier                          │
│  (new files)      │      (existing file changes)                    │
├───────────────────┴─────────────────────────────────────────────────┤
│                        Schema Validation                             │
│              (schema.json per generator, prompts)                    │
├─────────────────────────────────────────────────────────────────────┤
│                         File System                                  │
│              (actual writes after preview/confirm)                   │
└─────────────────────────────────────────────────────────────────────┘

Implementation Priority

Phase Feature Effort Impact Status
1 Virtual Tree Medium High ✅ Done
2 Schema validation Medium Medium ✅ Done
3 Composable generators Low High ✅ Done
4 AST manipulation High Medium ✅ Done
5 Interactive prompts Low Medium ✅ Done

Recommended order: Phase 1 → Phase 3 → Phase 2 → Phase 5 → Phase 4

Completed: All phases complete - Phase 1 (GeneratorTree), Phase 2 (Schema Validation), Phase 3 (Composable Generators), Phase 4 (AST Manipulation), Phase 5 (Interactive Prompts)

What We Keep (iDempiere-Specific)

These features differentiate us from generic tools like Nx:

Feature Rationale
REST API integration Direct AD manipulation, not just files
Version-aware generation Different code for v10 vs v13
2Pack workflow iDempiere distribution mechanism
OSGi templates Plugin structure specific to iDempiere
AI code generation Domain-specific prompts for iDempiere patterns

Consequences

Positive

Negative

Risks

References

Path: /docs/developers/architecture/idempiere-hub/003-generator-architecture-nx-patterns