Commit graph

2 commits

Author SHA1 Message Date
Sipke Schoorstra 6a3d65ff7e
feat(bpmn): the work binder and the elsa: binding format (#7946)
* feat(bpmn): bind BPMN work declarations to Elsa activities

Turns the reader's BpmnWorkBinding declarations into the activity nodes a
BpmnProcess scope runs. Six of the seven kinds bind automatically: TimerWait to
Delay, MessageWait/SignalWait to Event, MessagePublish to PublishEvent,
CallProcess to DispatchWorkflow, NestedProcess to a nested BpmnProcess. The
seventh, UnboundTask, is an authoring decision and is read from a new elsa:
vendor extension inside the document, so an exported .bpmn is self-contained.

Every binding for a scope is bound whatever its slot, so a ScopeListener needs
no special case. Each binding gets its own freshly built activity with a
scope-qualified id: ActivityVisitor skips an activity it has already collected,
so one instance shared between two scopes would leave the second scope with no
child in Elsa's identity graph.

The binder lives in Elsa.Bpmn.Interchange because BpmnWorkBinding is a
Bpmn.Interchange type; binding it in Elsa.Bpmn would pull the interchange
library into the execution module's closure, which is the split D12 draws.

Every ambiguity resolves loudly: an unbound task, a dead binding declaration, a
malformed ISO-8601 duration, a call activity with nothing to call, and an
activity type nothing registered all refuse at bind time rather than producing a
process that runs to completion doing none of what the document says.

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

* fix(bpmn): declare document variables on the bound scope, refuse duplicate input names

BpmnWorkBinder.BindScope never copied BpmnProcessDefinition.Variables onto the
produced BpmnProcess's Elsa Variables, so a document-declared collection variable
was Absent to IBpmnVariableReader and a collection-mode multi-instance over it
faulted the element instead of running once per item. BindScope now declares an
Elsa Variable for each document variable, seeding the declared default as the
JsonElement it already is.

BpmnActivityBindingFormat.Read silently let a second <elsa:input name="..."> with
a duplicate name overwrite the first rather than refusing it, unlike every other
malformed-document case this binder already refuses. It now throws
BpmnBindingException naming the binding and the duplicated input, and the XML doc
now states that rule plus the (verified) XML text-node escaping that already
applies to input JSON.

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

* fix(bpmn): carry every declared activity input through the binding format

BpmnActivityBindingFormat.Write only found properties whose CLR type derives
from Input, silently dropping attribute-declared inputs like Switch.Cases from
an export. Read accepted any <elsa:input name="..."> without checking the
activity declares it, so a mistyped or stale name imported silently with the
configuration missing since Elsa's deserializer ignores unknown members. Both
now go through IActivityDescriber.GetInputProperties, the same enumeration
ActivityDescriptor.Inputs is built from, so Write and Read agree on what an
activity's inputs are and Read refuses a name that enumeration does not
report.

Also makes BpmnWorkBinder.RefuseUnusedDeclarations filter its loop explicitly
with .Where(...) instead of an implicit if, per static analysis.

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

* refactor(bpmn): filter undeclared input names explicitly

Express the undeclared-input-name check as an explicit Where filter
instead of an implicit filter inside the loop body, and report every
undeclared name at once rather than only the first. Also fix the
refusal message, which previously named the activity type twice.

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

* docs(bpmn): describe the input payload shape accurately

The XML doc on BpmnActivityBindingFormat claimed every <elsa:input> is the
{"typeName":...,"expression":...} wrapper a stored workflow definition uses.
That only holds for Input<T>-typed properties: an [Input]-attributed
plain-typed property such as Switch.Cases is serialized as its own JSON
shape (an array), not the wrapper, which Write already does correctly and
the round-trip test already covers. Correct the doc to describe the payload
as the configured activity serializer's output for that input, dependent on
how the activity declares it, and add a second short example showing the
attribute-declared shape.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:38:03 +02:00
Sipke Schoorstra 22886809e0
test(bpmn): guard that Elsa.Bpmn never duplicates the library (#7939)
* test(bpmn): guard against Elsa.Bpmn* reimplementing Bpmn.* library types

Adds a reflection-based architecture test asserting no type under Elsa.Bpmn
or Elsa.Bpmn.Interchange shares a type name with Bpmn.Model/Bpmn.Semantics
(and Bpmn.Interchange for the interchange assembly), plus a positive check
that both assemblies still depend on their respective library packages -
closing the gap that let elsa-foundation grow a parallel BpmnElement/BpmnGraph
semantics core undetected.

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

* test(bpmn): move the library-duplication guard to the interchange test project

Elsa.Bpmn.UnitTests referencing Elsa.Bpmn.Interchange (plus three
redundant PackageReferences already flowing transitively) inverted the
layering the guard exists to protect. Elsa.Bpmn.Interchange.UnitTests
already sees both assemblies transitively with no new references, so
the guard moves there unchanged apart from namespace and doc. Also
drops the silent null-forgive on Assembly.GetName().Name in favor of
an explained fallback.

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

* test(bpmn): drop the dependency-direction assertions the compiler already enforces

Mutation testing showed AssertDependsOnPackage and the two positive facts using
it can never go red: any state that would trip them fails the test project's
build first (CS0234), because the guard's own typeof bindings already require
the Bpmn.* packages. Delete the decorative facts and the now-unused helper,
and document that the typeof bindings are load-bearing on purpose.

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

* test(bpmn): prove the duplication detector actually detects

Split AssertNoTypeNameCollisions into a thin assertion wrapper around a
new FindTypeNameCollisions helper, and add a fact that points the
detector at the test assembly (which carries a deliberately colliding
BpmnGraph fixture) so CI sees the guard fail as well as pass.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 04:09:53 +02:00