feat: Add a template-optimizer pipeline for REST-connector templates#7359
Conversation
vringar
left a comment
There was a problem hiding this comment.
@claude[agent] Left comments focussed on the overall structure. Don't assume the rest is good but focus on getting the architecture right first. Also how do we integrate this with the congen CLI. Note the PR you are likely to be blocked by.
|
#7354 is required for the CLI to be runable again. I'd like this pipeline to be a postprocessing step before the file gets written out to disk, so that for generated templates we only expose the compacted form. In this context none of the disable flags are relevant. |
7cb1ef0 to
110dd18
Compare
044bc94 to
b3edb39
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new element-template-optimizer Maven module under element-template-generator/ that compacts conditional properties in generated Camunda element templates via four sequential passes (merge-by-identity, totalize, strength-reduce, reorder) operating on the typed ElementTemplate IR. The optimizer is exposed both as a library entrypoint (Optimizer.defaultPipeline()), as a standalone CLI binary (element-template-optimizer), and is wired unconditionally into congen-cli's generate command before serialization. Strict polymorphic Jackson deserializers for the sealed Property / PropertyBinding / PropertyCondition hierarchies are added because the core DSL ships serializers only.
Changes:
- New
element-template-optimizermodule: four passes,Optimizerentrypoint,PropertyUtilsbuilder-based rebuilder, strict polymorphic deserializers, and a standalone CLI (OptimizerCommand+ListPassesCommand). congen-cli generaterunsOptimizer.defaultPipeline()after generation and before JSON-schema validation, with a newOPTIMIZATION_FAILED(4)return code.- Test coverage: per-pass unit tests, deserializer round-trip and rejection tests, optimizer-level pipeline tests, and a CI-gated end-to-end BPMN-equivalence suite using the npm
@camunda/element-templates-cli.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| element-template-generator/pom.xml | Register new optimizer submodule. |
| element-template-generator/optimizer/pom.xml | New module POM: Java 21, picocli + appassembler, slf4j-nop, xmlunit (test). |
| element-template-generator/optimizer/README.md | Documents passes, reorder side effects, CLI usage, and equivalence-test gating. |
| optimizer/core/Pass.java | Pass interface: id, apply, description. |
| optimizer/core/Optimizer.java | Library entrypoint with default pipeline and defaultPipelineExcept that hard-fails on unknown ids. |
| optimizer/core/PropertyUtils.java | Builder-based copy helpers (withId, withCondition, withValue, withoutCondition) with exhaustive sealed switch. |
| optimizer/core/TemplateLoader.java | ObjectMapper that registers ElementTemplateModule + strict deserializers, plus load/save/toString helpers. |
| optimizer/core/PropertyDeserializer.java | Discriminates on type, dispatches to concrete DSL builders, validates feel/optional/generatedValue/choices. |
| optimizer/core/PropertyConditionDeserializer.java | Requires exactly one of equals/oneOf/isActive/allMatch; validates value types. |
| optimizer/core/PropertyBindingDeserializer.java | Dispatches on binding type; rejects missing/empty identifier fields. |
| optimizer/pass/MergeByIdentityPass.java | Groups by binding+value+presentation; merges equals/oneOf on same discriminator; refuses non-string equals; chooses neutral id via longest common suffix with collision disambiguation. |
| optimizer/pass/TotalizePass.java | Drops a top-level equals/oneOf condition when its values cover every choice of its Dropdown discriminator. |
| optimizer/pass/StrengthReducePass.java | Rewrites singleton oneOf as equals. |
| optimizer/pass/ReorderPass.java | Sorts properties by group, then Hidden-first, then id. |
| optimizer/Main.java | picocli bootstrap. |
| optimizer/command/OptimizerCommand.java | CLI: positional input, -o, --dry-run, --skip-passes; distinct exit codes 1/2/3/4; list-passes subcommand. |
| optimizer/test/.../TestTemplates.java | Test DSL helper factories for templates/properties/conditions/bindings. |
| optimizer/test/.../MergeByIdentityPassTest.java | Coverage incl. collision and non-string-equals refusal. |
| optimizer/test/.../TotalizePassTest.java | Coverage for full-coverage drop, partial keep, equals, unknown discriminator. |
| optimizer/test/.../StrengthReducePassTest.java | Singleton vs. multi vs. no condition. |
| optimizer/test/.../ReorderPassTest.java | Group→visibility→id ordering. |
| optimizer/test/.../core/OptimizerTest.java | Default pipeline collapse, skip behavior, unknown-id rejection. |
| optimizer/test/.../core/TemplateLoaderTest.java | Round-trip + strict rejection of malformed JSON. |
| optimizer/test/.../OptimizerPropertyTest.java | CI-gated end-to-end equivalence via element-templates-cli over 36 parametric template shapes. |
| congen-cli/pom.xml | Add element-template-optimizer dependency. |
| congen-cli/.../command/Generate.java | Run Optimizer.defaultPipeline() after generation; map failures to OPTIMIZATION_FAILED. |
| congen-cli/.../ReturnCodes.java | New OPTIMIZATION_FAILED(4). |
b3edb39 to
def6ff0
Compare
|
|
ztefanie
left a comment
There was a problem hiding this comment.
Hi @vringar
Thanks for your contribution.
Some comments:
- We want to have this optimizer as a separate module and do not want to apply it to the OOTB connectors for now
- The optimizer is changing the ids of properties. Can you verify if this breaks migration to higher version of the element templates? In the team call we were assumed the bindings are used for this, but it would be good if you can verify changing ids is safe.
- Can you please revert the reordering as this breaks conditions. Properties need to be declared before they can be used.
A new sibling to the element-template validator that compacts conditional
properties in Camunda element templates into smaller, semantically-
equivalent forms. REST-based templates typically emit one identical
property per operation gated by a condition; collapsing those is a 30-45%
property-count reduction on real specs without any change in applied
behaviour.
The pipeline runs four passes in order:
- merge-by-identity: groups properties by everything except condition
and id, then collapses each group whose members differ only in their
discriminator value into one property with a oneOf condition over
the union.
- totalize: drops conditions that cover every value of their
discriminator (currently scoped to Dropdown discriminators).
- strength-reduce: rewrites singleton oneOf as equals.
- reorder: emits properties in a stable canonical order (group ->
visibility -> id) so regenerations produce review-friendly diffs.
Public surface:
- Optimizer.defaultPipeline() / defaultPipelineExcept(List<String>) —
the library entrypoint for downstream consumers. Unknown skip ids
are rejected loudly so a typo can't silently disable a pass.
- Pass interface — id(), apply(template), description().
- PropertyUtils — produces modified copies of Property objects via
their public builders. The switch over the sealed Property
hierarchy is exhaustive at compile time so adding a new subtype to
core surfaces here as a compile error.
- TemplateLoader — round-trips templates as JSON. The core DSL is
sealed without Jackson polymorphism annotations so this module
ships polymorphism-aware deserializers for Property,
PropertyBinding, and PropertyCondition that fail loud on missing
discriminators, conflicting discriminators, unknown enum values,
and empty required identifiers.
Tests:
- Per-pass unit tests cover the obvious positive and negative cases
plus the id-collision and discriminator-value-coercion edge cases.
- OptimizerTest covers the library entrypoint, including the
unknown-pass-id validation.
- TemplateLoaderTest covers the strict-deserializer paths
(missing-type, empty binding field, multiple discriminators, etc.).
- OptimizerPropertyTest is a JUnit ParameterizedTest enumerating 36
multi-operation template shapes and asserting XMLUnit-level BPMN
equivalence per discriminator value via the npm-published
element-templates-cli. Gated by @EnabledIf; in CI a missing CLI
binary is a hard failure so the equivalence gate cannot vanish
silently behind a green build.
Adds a picocli-based command (element-template-optimizer) that loads a
single template file, runs the Optimizer pipeline over it, and either
rewrites the file in place or writes to the path passed via --output.
Flags:
--output, -o path to write the optimized template to (default:
rewrite the input file)
--dry-run report whether changes would be made, write nothing
--skip-passes comma-separated pass ids to leave out of the
pipeline. Unknown ids are rejected at parse time.
Subcommands:
list-passes prints the pass ids and one-line descriptions
Errors from TemplateLoader.load and Optimizer.optimize are reported as
single-line messages with dedicated exit codes (3, 4) instead of letting
Jackson stacks leak through picocli.
Packaged via appassembler-maven-plugin into target/appassembler/bin/
element-template-optimizer.
congen-cli's generate subcommand now runs every emitted template through Optimizer.defaultPipeline() before serializing to disk. Generated artifacts are always shipped in their compacted form; there is no flag. The optimizer call is wrapped with try/catch and surfaces failures via the new OPTIMIZATION_FAILED return code with a single-line error message, matching the existing INPUT_PREPARATION_FAILED / GENERATION_FAILED / JSON_SCHEMA_VALIDATION_FAILED pattern in this command rather than letting an unexpected RuntimeException leak out as a raw stack trace through picocli.
def6ff0 to
904bd86
Compare
|
@vringar have you seen my comments above?
Half-addressed. The optimizer was correctly extracted into its own Maven module (element-template-generator/optimizer/). But the second half — "do not apply it to OOTB connectors" — was not addressed.
Not addressed at all. There is no mention of migration anywhere in the optimizer module — not in the README, not in tests, not in comments. The reviewer's specific concern (changing merged property IDs and whether Some more comments:
|
Adds an
element-template-optimizermodule that compacts conditional properties in generated Camunda element templates. REST-based templates frequently emit dozens of near-identical conditional properties per multi-operation API; collapsing them is a ~45% property-count reduction on real specs without any change in applied behaviour.Closes #7357.
What the optimizer does
Four passes run in sequence, each taking an
ElementTemplateand returning a new one:merge-by-identity— groups properties by(binding, value, presentation)and collapses each group whose members differ only in their condition's discriminator value into one property with aoneOfcondition over the union.totalize— drops conditions that cover every value of their discriminator (currently scoped toDropdowndiscriminators).strength-reduce— rewrites singletononeOfasequals.reorder— emits properties in a stable canonical order (group → visibility → id) so regenerations produce review-friendly diffs.How it's integrated
congen-cli'sgeneratecommand runs the default pipeline before serializing each template, so generated templates always ship in their compacted form.A standalone
element-template-optimizerbinary is also provided for one-shot cleanup of hand-maintained templates that bypass the generator (with--skip-passes,--dry-run, and alist-passessubcommand).Branch layout
Three commits:
Optimizerlibrary entrypoint, thePropertyUtilsrebuilder, polymorphism-aware deserializers for the sealedProperty/PropertyBinding/PropertyConditionhierarchies, and the test suite.Main+OptimizerCommand.OPTIMIZATION_FAILEDreturn code.Behavioural equivalence
The core DSL is sealed without Jackson polymorphism annotations, so this module ships strict deserializers for
Property,PropertyBinding, andPropertyCondition. They fail loud on missing or conflicting discriminators, non-stringequalsvalues, empty binding identifiers, unknownfeelmodes, and unknowngeneratedValue.type.OptimizerPropertyTestenumerates 36 multi-operation template shapes (operations × shared params × unshared params × unconditional) and asserts XMLUnit-level BPMN equivalence per discriminator value by applying both the original and optimized templates via the npm-published@camunda/element-templates-cli. The test is gated by@EnabledIf; whenCI=trueorOPTIMIZER_REQUIRE_CLI=trueis set and the CLI binary is missing, the gate is a hard failure rather than a silent skip.Out of scope / follow-ups
TotalizePasscurrently considers onlyDropdownproperties as discriminators; a constant-valued hidden property would also be a tautological discriminator. Documented in the module README; can be widened in a follow-up.