Migrating an Xtend code generator to the fluent API
The reference migration is DeepPathUtilGenerator (PR #1255): compare
DeepPathUtilGenerator.java on the current branch with
DeepPathUtilGenerator.xtend on the last commit before the migration to see
every pattern below applied to a real generator.
Architecture
- Extend
FluentJavaClassGenerator<T, C> (or FluentRObjectJavaClassGenerator
when T is an RObject) instead of XtendJavaClassGenerator /
RObjectJavaClassGenerator.
generateClass returns a CodeRenderer instead of a StringConcatenationClient.
- The renderer is rendered once into a
RecordingCodeWriter, which claims
imports and identifiers in the file scope while recording; the recording is
then replayed with all identifiers resolved. Identifiers may therefore be
created while rendering (just like Xtend templates do mid-template).
- Even so, prefer separating model building (compute methods, scopes,
identifiers; capture in small records) from rendering (pure writes) when it
doesn't contort the code — it makes generators easier to unit-test and read.
Porting steps
streamObjects / createTypeRepresentation / getSource: port 1:1.
- Transcribe the
'''…''' template into render* methods using
out.write / out.writeln / out.indented(() -> …) / out.join(items, ", ", item -> …).
- Do not hand-render expression logic (getters, null checks, exists
checks, coercions). Keep delegating to
ExpressionGenerator,
TypeCoercionService, and the JavaStatementBuilder API exactly as the
Xtend code did — the resulting JavaStatement/JavaExpression objects
implement CodeRenderer and can be written directly to the writer. A
hand-rendered reimplementation silently changes the generated output style
and duplicates coercion semantics.
- Wrap ad-hoc expression fragments with
JavaExpression.from(CodeRenderer, JavaType)
(not the legacy StringConcatenationClient overload).
- Delete the
.xtend file in the same commit.
- If a piece of Xtend infrastructure loses its last user, delete it; if it
still has users, mark it
@Deprecated pointing at the fluent replacement.
Xtend → Java translation patterns
| Xtend |
Java |
@Inject extension JavaTypeTranslator + attr.toMetaJavaType |
@Inject private JavaTypeTranslator typeTranslator; + typeTranslator.toMetaJavaType(attr) |
name.toFirstLower / name.toFirstUpper |
statically imported StringUtils.uncapitalize(name) / capitalize(name) (commons-lang3) |
«FOR x : xs SEPARATOR ', '»…«ENDFOR» |
out.join(xs, ", ", x -> …) |
«IF cond»…«ENDIF» around lines |
plain if around out.writeln(…) calls |
| template indentation |
out.indented(() -> …) |
val t = if (x instanceof RChoiceType) x.asRDataType else x |
a small normalizeChoiceType(RType) helper |
new HashSet<>() for collected output |
LinkedHashSet / LinkedHashMap — deterministic iteration order |
xs.reverseView |
com.google.common.collect.Lists.reverse(xs) |
Expect roughly +35 lines of fixed boilerplate per generator (license header,
explicit imports, @Inject fields on two lines) plus 20–40% body growth from
Java verbosity. The algorithm itself should port mechanically — if it doesn't,
you are probably reimplementing instead of transcribing.
Output conventions
- Newline is always
"\n"; indentation is 4 spaces (CodeWriterConfig
defaults). Legacy Xtend output used tabs and platform line separators, so
whitespace-only diffs in downstream generated code are expected and accepted.
- Blank lines are truly empty (the writer never emits trailing whitespace);
Xtend used to leak the template's indentation into blank lines.
- New files (main and test) carry the Apache license header.
Verification
- Regression test: create
rune-integration-tests/src/test/resources/generation-regression-tests/<name>/model/*.rosetta
covering the generator's interesting cases, plus a test class extending
AbstractJavaGeneratorRegressionTest. Generate the expected/ files by
temporarily flipping UPDATE_EXPECTATIONS to true in
AbstractJavaGeneratorRegressionTest, running the test, and flipping it
back. Review the generated expectations before committing them.
- Parity check against the old generator: check out the commit that still
has the
.xtend version (e.g. in a second worktree), copy the same model
and test class there, generate expectations with the old generator, and
diff against the new output. Aim for token-identical output; only
whitespace (tabs vs spaces, trailing whitespace, line endings) should
differ. Investigate any non-whitespace diff before proceeding.
- Run the functional tests that execute generated code for the feature (e.g.
the relevant tests in
FunctionGeneratorTest), plus all existing
*RegressionTest classes — a migration must not change other generators'
output.
Pitfalls
GeneratorScope.getActualName closes the scope on first call. Never
resolve identifier names while the file is still being built — write
GeneratedIdentifier objects to the writer and let replay resolve them.
- The line-ending normalization in
AbstractJavaGeneratorRegressionTest is
temporary scaffolding for the migration period (legacy generators emit
platform separators on Windows); remove it when the last Xtend generator is
gone.
- The migration-only bridges —
CodeWriterTargetStringConcatenation and
TargetStringConcatenationCodeWriter (both standalone classes under
generator/java/util/, extracted out of JavaExpression in PR #1293),
JavaExpression's legacy StringConcatenationClient overload of from,
and the legacy com.regnosys.rosetta.generator.TargetLanguageRepresentation
interface — exist only to let not-yet-migrated Xtend templates interop with
the fluent CodeWriter API. The final cleanup deletes them together with
the Xtend machinery and adds a fluent debug writer for
JavaExpression.toString().
1---2name: migrate-xtend-generator3description: Migrate an Xtend code generator (rune-lang) to the fluent Java CodeRenderer/CodeWriter API. Use when converting a *.xtend generator class to Java, when asked to "migrate a generator", or when reviewing such a migration. Covers the porting steps, Xtend-to-Java translation patterns, output conventions, verification via regression tests, and parity checking against the old generator.4---56# Migrating an Xtend code generator to the fluent API78The reference migration is `DeepPathUtilGenerator` (PR #1255): compare9`DeepPathUtilGenerator.java` on the current branch with10`DeepPathUtilGenerator.xtend` on the last commit before the migration to see11every pattern below applied to a real generator.1213## Architecture1415- Extend `FluentJavaClassGenerator<T, C>` (or `FluentRObjectJavaClassGenerator`16 when `T` is an `RObject`) instead of `XtendJavaClassGenerator` /17 `RObjectJavaClassGenerator`.18- `generateClass` returns a `CodeRenderer` instead of a `StringConcatenationClient`.19- The renderer is rendered **once** into a `RecordingCodeWriter`, which claims20 imports and identifiers in the file scope while recording; the recording is21 then replayed with all identifiers resolved. Identifiers may therefore be22 created while rendering (just like Xtend templates do mid-template).23- Even so, prefer separating *model building* (compute methods, scopes,24 identifiers; capture in small records) from *rendering* (pure writes) when it25 doesn't contort the code — it makes generators easier to unit-test and read.2627## Porting steps28291. `streamObjects` / `createTypeRepresentation` / `getSource`: port 1:1.302. Transcribe the `'''…'''` template into `render*` methods using31 `out.write` / `out.writeln` / `out.indented(() -> …)` / `out.join(items, ", ", item -> …)`.323. **Do not hand-render expression logic** (getters, null checks, exists33 checks, coercions). Keep delegating to `ExpressionGenerator`,34 `TypeCoercionService`, and the `JavaStatementBuilder` API exactly as the35 Xtend code did — the resulting `JavaStatement`/`JavaExpression` objects36 implement `CodeRenderer` and can be written directly to the writer. A37 hand-rendered reimplementation silently changes the generated output style38 and duplicates coercion semantics.394. Wrap ad-hoc expression fragments with `JavaExpression.from(CodeRenderer, JavaType)`40 (not the legacy `StringConcatenationClient` overload).415. Delete the `.xtend` file in the same commit.426. If a piece of Xtend infrastructure loses its last user, delete it; if it43 still has users, mark it `@Deprecated` pointing at the fluent replacement.4445## Xtend → Java translation patterns4647| Xtend | Java |48|---|---|49| `@Inject extension JavaTypeTranslator` + `attr.toMetaJavaType` | `@Inject private JavaTypeTranslator typeTranslator;` + `typeTranslator.toMetaJavaType(attr)` |50| `name.toFirstLower` / `name.toFirstUpper` | statically imported `StringUtils.uncapitalize(name)` / `capitalize(name)` (commons-lang3) |51| `«FOR x : xs SEPARATOR ', '»…«ENDFOR»` | `out.join(xs, ", ", x -> …)` |52| `«IF cond»…«ENDIF»` around lines | plain `if` around `out.writeln(…)` calls |53| template indentation | `out.indented(() -> …)` |54| `val t = if (x instanceof RChoiceType) x.asRDataType else x` | a small `normalizeChoiceType(RType)` helper |55| `new HashSet<>()` for collected output | `LinkedHashSet` / `LinkedHashMap` — deterministic iteration order |56| `xs.reverseView` | `com.google.common.collect.Lists.reverse(xs)` |5758Expect roughly +35 lines of fixed boilerplate per generator (license header,59explicit imports, `@Inject` fields on two lines) plus 20–40% body growth from60Java verbosity. The algorithm itself should port mechanically — if it doesn't,61you are probably reimplementing instead of transcribing.6263## Output conventions6465- Newline is always `"\n"`; indentation is 4 spaces (`CodeWriterConfig`66 defaults). Legacy Xtend output used tabs and platform line separators, so67 whitespace-only diffs in downstream generated code are expected and accepted.68- Blank lines are truly empty (the writer never emits trailing whitespace);69 Xtend used to leak the template's indentation into blank lines.70- New files (main and test) carry the Apache license header.7172## Verification73741. **Regression test**: create75 `rune-integration-tests/src/test/resources/generation-regression-tests/<name>/model/*.rosetta`76 covering the generator's interesting cases, plus a test class extending77 `AbstractJavaGeneratorRegressionTest`. Generate the `expected/` files by78 temporarily flipping `UPDATE_EXPECTATIONS` to `true` in79 `AbstractJavaGeneratorRegressionTest`, running the test, and flipping it80 back. Review the generated expectations before committing them.812. **Parity check against the old generator**: check out the commit that still82 has the `.xtend` version (e.g. in a second worktree), copy the same model83 and test class there, generate expectations with the old generator, and84 diff against the new output. Aim for token-identical output; only85 whitespace (tabs vs spaces, trailing whitespace, line endings) should86 differ. Investigate any non-whitespace diff before proceeding.873. Run the functional tests that execute generated code for the feature (e.g.88 the relevant tests in `FunctionGeneratorTest`), plus all existing89 `*RegressionTest` classes — a migration must not change other generators'90 output.9192## Pitfalls9394- `GeneratorScope.getActualName` **closes the scope** on first call. Never95 resolve identifier names while the file is still being built — write96 `GeneratedIdentifier` objects to the writer and let replay resolve them.97- The line-ending normalization in `AbstractJavaGeneratorRegressionTest` is98 temporary scaffolding for the migration period (legacy generators emit99 platform separators on Windows); remove it when the last Xtend generator is100 gone.101- The migration-only bridges — `CodeWriterTargetStringConcatenation` and102 `TargetStringConcatenationCodeWriter` (both standalone classes under103 `generator/java/util/`, extracted out of `JavaExpression` in PR #1293),104 `JavaExpression`'s legacy `StringConcatenationClient` overload of `from`,105 and the legacy `com.regnosys.rosetta.generator.TargetLanguageRepresentation`106 interface — exist only to let not-yet-migrated Xtend templates interop with107 the fluent `CodeWriter` API. The final cleanup deletes them together with108 the Xtend machinery and adds a fluent debug writer for109 `JavaExpression.toString()`.