Skip to content

feat(ide-template): execute model generation in Java - #6628

Merged
delchev merged 1 commit into
masterfrom
feat/generation-utils-java
Aug 9, 2026
Merged

feat(ide-template): execute model generation in Java#6628
delchev merged 1 commit into
masterfrom
feat/generation-utils-java

Conversation

@delchev

@delchev delchev commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What

Ports the template-generation "generation utils" from JavaScript to Java — the first of three stacked
PRs described in GENERATION_UTILS_JAVA_PLAN.md (added here).

Ported, all into components/ide/ide-template/.../service/model/:

  • generateUtils.jsModelGenerator + GlueGenerator: the 47-case dispatch, the ~28 entity
    partitions, per-collection rendering, the administration surface, the whole-model fallback, and both
    translate actions. GlueGenerator carries the 22 glue collections, including the roll-up coalescing
    and the four aggregate variants.
  • parameterUtils.jsModelParameterProcessor: the per-entity and per-property derivation pass,
    the dropdown lookup URLs, the personal and partner surfaces, label parts, perspectives and roles.
  • generate.mjsModelGenerationService: model read, cross-project entity-extension folding,
    the targeted gen/<sub> scrub derived from the actual output paths, the .gen descriptor, publish.

Naming collapses into one implementation each: a new NamingHelper in commons-helpers, which both
IntentNaming and the generation pipeline delegate to.

What this does not do

Nothing is switched over. The JavaScript endpoint is untouched and is still the only one any consumer
calls; the Java pipeline is a service and is not yet exposed over HTTP. Consumer switch-over and the
in-process intent execution are PR 2; deleting the JavaScript is PR 3.

Why it can be trusted

GenerationParityIT (HTTP-level, no browser) runs both pipelines over 11 fixture/template pairs
and asserts every produced path and every byte of content matches — the .gen descriptor included,
because its formatting is content-compared across regenerations:

Fixture Templates
a rich model (47 entities, depends-on, master/detail, projections) schema, DAO-Java, REST-Java, full stack
a simple model schema, full stack
the simple model + a sibling project contributing an entity extension schema, full stack
process glue (triggers, notifications, roll-ups incl. coalescing, keyed aggregates, setters) glue-Java
a form Harmonia form builder
a report Harmonia report

The extension case additionally asserts the merged column reaches the output, so "both pipelines
dropped it" cannot pass as parity. Plus 45 new unit tests over the naming rules, the type mapping, the
JSON writer and the derivation pass.

Four things the parity harness turned up

  1. MustacheGenerationEngine mutates the parameter map it is handed, adding an indexed twin of
    every collection under a _-suffixed key, recursively. No caller had noticed, because they all
    reached it through a fresh map — the JS facade deserializes per call. Handing it a live graph left
    those artefacts in the parameters and from there in the .gen descriptor, where they would
    accumulate on every regeneration. Worked around by copying the context per invocation; the engine
    not mutating its input would be the better fix, but that changes shared platform behaviour and
    belongs in its own change.
  2. Every number in the parameter graph must be a Double — the graph reaches a template engine as
    the result of parsing JSON with Gson, so that is what the templates have always seen.
  3. Absent is not null — assigning undefined drops the key from the serialized parameters, while a
    Java null keeps it.
  4. IntentNaming.humanize is not the JS humanizeName: it additionally splits on - and _.
    The plan assumed they were the same function and said to verify the override maps; the maps match,
    the algorithms do not. They decompose cleanly instead — the JS function is the per-segment core —
    so both forms now live in NamingHelper as one implementation each, with no behaviour change on
    either side. Whether generation should adopt the separator-aware form is a separate decision: it
    would relabel generated artefacts and needs its own regeneration sweep.

Scoped out, deliberately

template-mapping-java is not handled by the Java pipeline — its preparation is a mapping compiler
rather than marshalling. It stays on the JavaScript path and must be ported before PR 2 switches the
.mapping consumers. The Java pipeline rejects it by name rather than generating something partial.

Verification

  • GenerationParityIT green (11/11 cases byte-identical)
  • 52 unit tests green (45 new, plus the pre-existing IntentNamingTest, which guards the delegation)
  • mvn formatter:validate clean on every touched module (cache wiped first)
  • javadoc clean under -P release on the touched modules
  • full reactor compile green (-pl tests/tests-integrations -am, 305 modules)

Not run locally: the full integration suite, and the PostgreSQL leg. This change touches no SQL, and
the new IT is untagged so it lands in the PR smoke set.

🤖 Generated with Claude Code

Port the generation utils to Java: generateUtils.js (the 47-case dispatch,
the entity partitions, the collection and glue rendering, the translation
catalogs), parameterUtils.js (the per-entity and per-property derivation
pass) and the generate.mjs orchestration (model read, cross-project entity
extension folding, the targeted gen/<sub> scrub, the .gen descriptor,
publish).

Nothing is switched over. The JavaScript endpoint is untouched and remains
the only one any consumer calls; the Java pipeline is exposed as a service
and not yet over HTTP. What this change buys is the safety net for doing the
switch: GenerationParityIT runs both pipelines over eleven fixture and
template pairs - a rich model and a simple one through the schema, DAO, REST
and full-stack templates, plus process glue, a form, a report, and the simple
model again with a sibling project contributing an entity extension - and
asserts every produced path and every byte of content matches, the .gen
descriptor included. The extension case also asserts the merged column
reaches the output, so two pipelines that both dropped it cannot pass.

Getting there turned up four things worth recording:

- The Mustache engine writes into the parameter map it is handed, adding an
  indexed twin of every collection under a _-suffixed key, recursively. No
  caller noticed because they all reached it through a fresh map. Rendering
  now copies the context per invocation, which is what the engines have
  effectively always received.
- Every number in the parameter graph has to be a Double. The graph reaches
  a template engine as the result of parsing JSON with Gson, so that is what
  the templates have always seen, and a derived int would render differently.
- Absent is not null. Assigning undefined in JavaScript drops the key from
  the serialized parameters, while a Java null keeps it - so where the
  original assigned an absent value, the port removes the key.
- IntentNaming.humanize is not the JavaScript humanizeName: it additionally
  splits on - and _. They decompose instead, so NamingHelper now carries both
  forms as one implementation each and both callers delegate, with no change
  to either behaviour.

template-mapping-java is deliberately not handled by the Java pipeline: its
preparation is a mapping compiler rather than marshalling, and it stays on
the JavaScript path until ported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
siblingModelRoot = asMap(ModelJson.parseObject(new String(siblingModel.getContent(), StandardCharsets.UTF_8))
.get("model"));
} catch (RuntimeException e) {
logger.debug("Skipping project [{}] while collecting entity extensions: its model does not parse", siblingName, e);
if (digits == end) {
return Double.NaN;
}
return Double.parseDouble(text.substring(0, digits));
String key = entry.getKey();
Object value = entry.getValue();
boolean translatable = "label".equals(key) || "errorMessage".equals(key);
if (translatable && value != null && !"".equals(value)) {
*
* @param fixture the fixture file name
* @param templateId the template's module path
* @param pipeline the pipeline the workspace belongs to
@delchev
delchev merged commit a0d53ae into master Aug 9, 2026
10 checks passed
@delchev
delchev deleted the feat/generation-utils-java branch August 9, 2026 10:32
delchev added a commit that referenced this pull request Aug 9, 2026
…irs the #6624 merge left behind (#6629)

* fix(intent): the slot picker is an additional page too, and a view no longer double-registers its perspective

Follow-up to the calendar change, closing the same hole for `view: slots`.

`view: slots` was left replacing the layout on the argument that a picker is an
authoring surface rather than a second way to browse. That does not survive
contact with a booking document: the picker is how a booking is CREATED, the
document page is how it is worked with afterwards, and an author needs both -
exactly the choice #6547 was about. So the picker gets the calendar's treatment:
`EdmIntentGenerator` emits `slotsView` instead of `layoutType: MANAGE_SLOTS`,
`uiSlotsModels` keys on it, `slots.js` stops emitting the shared manage form (the
layout owns the editor, so slot-click on a document master now lands on the
DOCUMENT create page prefilled with the chosen datetime), the picker takes over
/<Entity> while the layout browses at /<Entity>/list, and the two carry a
Slots/List switch. `MANAGE_SLOTS` is gone from every consumer; like
MANAGE_CALENDAR it was intent-only and never offered by the entity editor.

It also fixes a defect the calendar commit introduced, which extending the rule
to slots made obvious: `navigation.js`'s PERSPECTIVE_COLLECTIONS listed
`uiCalendarModels`/`uiSlotsModels` alongside the LAYOUT collections. Now that a
view entity keeps its layout it is a member of both, so its perspective was
generated TWICE - the same rename path written twice and a duplicate
application-perspectives contribution. The two view collections are dropped from
that list; a view entity is always in exactly one layout collection.

Covered by EdmIntentGeneratorTest (a slots document master keeps MANAGE_DOCUMENT
and carries slotsView + the slot* metadata) and the emission oracle, whose fixture
gained a slots document: the /Visit picker landing route, /Visit/list, /Visit/create
resolving to the DOCUMENT editor, and both toggles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ide-template): the Java generation port keeps parity on the additional-page views

#6628 ported generateUtils.js to Java about an hour after #6624 merged, against the
pre-#6624 logic: uiCalendarModels / uiSlotsModels / personalCalendarModels /
personalListModels still keyed on MANAGE_CALENDAR / MANAGE_SLOTS, which
EdmIntentGenerator no longer emits.

Nothing is broken today - both paths are live and no consumer is switched, so the
JS path executes - but the moment consumers flip, calendar and slots pages would
silently stop being generated: an empty partition writes no files and raises no
error. GenerationParityIT could not catch it either, because no parity fixture
declares a view, so all four partitions can disagree while every fixture matches.

Key the Java collections on calendarView / slotsView (and drop the calendar
exclusion from personalListModels, matching the JS), and add a views.model
fixture - one calendar entity, one slot-picker entity, both keeping their MANAGE
layout - as a parity case. Confirmed it is a real guard: with the ModelGenerator
change reverted the case fails naming the absent AppointmentSlotsPage.js and
Appointment-slots.html; with it, both pipelines agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
delchev added a commit that referenced this pull request Aug 9, 2026
…generation port keeps parity (#6632)

* fix(intent): the slot picker is an additional page too, and a view no longer double-registers its perspective

Follow-up to the calendar change, closing the same hole for `view: slots`.

`view: slots` was left replacing the layout on the argument that a picker is an
authoring surface rather than a second way to browse. That does not survive
contact with a booking document: the picker is how a booking is CREATED, the
document page is how it is worked with afterwards, and an author needs both -
exactly the choice #6547 was about. So the picker gets the calendar's treatment:
`EdmIntentGenerator` emits `slotsView` instead of `layoutType: MANAGE_SLOTS`,
`uiSlotsModels` keys on it, `slots.js` stops emitting the shared manage form (the
layout owns the editor, so slot-click on a document master now lands on the
DOCUMENT create page prefilled with the chosen datetime), the picker takes over
/<Entity> while the layout browses at /<Entity>/list, and the two carry a
Slots/List switch. `MANAGE_SLOTS` is gone from every consumer; like
MANAGE_CALENDAR it was intent-only and never offered by the entity editor.

It also fixes a defect the calendar commit introduced, which extending the rule
to slots made obvious: `navigation.js`'s PERSPECTIVE_COLLECTIONS listed
`uiCalendarModels`/`uiSlotsModels` alongside the LAYOUT collections. Now that a
view entity keeps its layout it is a member of both, so its perspective was
generated TWICE - the same rename path written twice and a duplicate
application-perspectives contribution. The two view collections are dropped from
that list; a view entity is always in exactly one layout collection.

Covered by EdmIntentGeneratorTest (a slots document master keeps MANAGE_DOCUMENT
and carries slotsView + the slot* metadata) and the emission oracle, whose fixture
gained a slots document: the /Visit picker landing route, /Visit/list, /Visit/create
resolving to the DOCUMENT editor, and both toggles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ide-template): the Java generation port keeps parity on the additional-page views

#6628 ported generateUtils.js to Java about an hour after #6624 merged, against the
pre-#6624 logic: uiCalendarModels / uiSlotsModels / personalCalendarModels /
personalListModels still keyed on MANAGE_CALENDAR / MANAGE_SLOTS, which
EdmIntentGenerator no longer emits.

Nothing is broken today - both paths are live and no consumer is switched, so the
JS path executes - but the moment consumers flip, calendar and slots pages would
silently stop being generated: an empty partition writes no files and raises no
error. GenerationParityIT could not catch it either, because no parity fixture
declares a view, so all four partitions can disagree while every fixture matches.

Key the Java collections on calendarView / slotsView (and drop the calendar
exclusion from personalListModels, matching the JS), and add a views.model
fixture - one calendar entity, one slot-picker entity, both keeping their MANAGE
layout - as a parity case. Confirmed it is a real guard: with the ModelGenerator
change reverted the case fails naming the absent AppointmentSlotsPage.js and
Appointment-slots.html; with it, both pipelines agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants