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:
- Virtual File System - Nx stages all changes in memory before applying
- Schema-Driven Validation - Declarative JSON schemas define options and validation
- Composable Generators - Generators can invoke other generators
- AST Manipulation - Safe modification of existing code files
- 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:
- Dry-run shows exact changes without temp files
- Atomic commits - all or nothing
- Rollback on partial failure
- Enables diff preview before apply
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:
- Add
@Componentannotation to existing class - Register callout in factory class
- Add imports to generated model classes
- Update
plugin.xmlprogrammatically
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
- Safer file operations with preview and rollback
- Better validation with schema-driven approach
- Improved UX with interactive prompts
- More maintainable with composable generators
- Can modify existing code safely with AST
Negative
- Increased complexity
- More code to maintain
- Learning curve for contributors
Risks
- Over-engineering for current needs
- AST manipulation complexity in Java (vs TypeScript in Nx)
References
- Nx Local Generators
- Nx Devkit API
- JavaParser - Java AST manipulation library
- JSON Schema - Schema specification