Skip to content

Add a template-optimizer pipeline to compact REST-connector element templates #7357

@vringar

Description

@vringar

Why should we do it?

REST-based element templates are reviewed in PRs, hand-edited, and copy-pasted into Modeler. They are the source of truth, not a build artifact.

The pain point: a generator producing a multi-operation REST template today emits one set of hidden fields (method, headers, operationPath, queryParameters, request body) per operation, each gated by condition: {property: operationId, equals: "<op>"}. For a 5-operation API, that's 30+ conditional properties — and most of them are byte-identical duplicates that could be one property with a wider condition. A PR reviewer has no syntactic signal that the four equals: "search"|"autocomplete"|"feed"|"recommendations" properties are equivalent and could be collapsed; they have to mentally do the merge themselves to verify correctness.
Once a hand edit lands on top of generator output, the boundary between "generated boilerplate" and "human intent" is invisible to the next reviewer.

Two further reasons in favor:

1. Concrete win on real templates. A prototype against a 5-operation real-world REST spec took a generated template from 42 → 23 properties (−45%) by running the merge and totalize passes proposed below. Verified semantically equivalent by applying the template via element-templates-cli and cycling all operations — identical <zeebe:input> and <zeebe:taskHeader> output before and after.

2. Each pass is small. Every pass takes a template and returns a template; inputs are bounded JSON. Testable with static fixtures. The validator (#7152) already provides a Rule / Finding / ReportPrinter framework we can reuse, so the only new code is the pass logic itself.

What should we do?

Add an optimizer module — sibling to the validator from #7152 — that runs a small set of passes over a template, each rewriting it into a smaller semantically-equivalent form. The validator's Rule / Finding / ReportPrinter infrastructure can be reused for the per-pass interfaces and dry-run reporting.

Key concepts

An element template property has, at minimum, an id, a binding (which <zeebe:input> it produces), a value (literal or FEEL expression), a condition (when the property is active), and presentation fields (label, description, feel, constraints) that affect how Modeler renders it.

Two terms used below:

  • discriminator — the property that conditional fields key off (in REST-connector output today, this is operationId).
  • merged property — the single property that results from collapsing several conditional properties whose only difference was their condition's equals / oneOf value.

Proposed passes

Pass What it does
merge-by-identity Group properties by (binding, value, presentation). If a group's members all condition on the same discriminator and differ only in their equals / oneOf value, replace the group with one property whose condition is oneOf: [union of their values]. The merged property's id is also chosen at this point — the source members' ids are merge-artifact names (autocomplete_query_locale, alphabetical first by accident), so strip the per-operation prefix to a neutral form (query_locale) while the merge context is still in scope.
totalize When a oneOf covers every choice of its discriminator, drop the condition entirely — the property becomes unconditional.
strength-reduce Singleton oneOf: [x]equals: x. Cosmetic; matches what the generator emits naturally.
reorder Stable canonical order (group, then hidden-vs-visible, then id). Makes regeneration diffs reviewable.

A lint pass at the end is desirable but not new work — several validator rules from #7152 (condition-value-in-choices, condition-target-exists, empty-group, …) already catch what an optimizer's lint step would. Run them as a post-optimization gate.

Worked example

Before — four near-identical properties from a locale query parameter shared across search, autocomplete, feed, recommendations:

{ "id": "search_query_locale",          "value": "string", "binding": {...},
  "condition": { "property": "operationId", "equals": "search" } }
{ "id": "autocomplete_query_locale",    "value": "string", "binding": {...},
  "condition": { "property": "operationId", "equals": "autocomplete" } }
{ "id": "feed_query_locale",            "value": "string", "binding": {...},
  "condition": { "property": "operationId", "equals": "feed" } }
{ "id": "recommendations_query_locale", "value": "string", "binding": {...},
  "condition": { "property": "operationId", "equals": "recommendations" } }

After merge-by-identity (which also picks the neutral id):

{ "id": "query_locale", "value": "string", "binding": {...},
  "condition": { "property": "operationId",
                 "oneOf": ["search", "autocomplete", "feed", "recommendations"] } }

If the oneOf had grown to cover every value in the operationId choices, totalize would drop the condition altogether.

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions