Skip to content

feat(DataMapper): wrap collection choice mapping with for-each (#2815)#3213

Open
mmelko wants to merge 1 commit into
KaotoIO:mainfrom
mmelko:fix/2815-collection-choice
Open

feat(DataMapper): wrap collection choice mapping with for-each (#2815)#3213
mmelko wants to merge 1 commit into
KaotoIO:mainfrom
mmelko:fix/2815-collection-choice

Conversation

@mmelko
Copy link
Copy Markdown
Contributor

@mmelko mmelko commented May 14, 2026

  • Detect collection choice wrapper + collection target and wrap choose/when/otherwise inside a for-each
  • Add tests for collection choice scenarios

fixes: #2815

Screen.Recording.2026-05-14.at.10.31.36.mov

Summary by CodeRabbit

Release Notes

  • Tests

    • Enhanced test coverage for collection-choice-wrapper mapping scenarios, including validation of wrapping behavior and deduplication handling.
  • Refactor

    • Optimized choice-to-mapping operations for collection fields with improved internal structure.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef88398c-d214-4758-8568-ca1557a9ba2f

📥 Commits

Reviewing files that changed from the base of the PR and between c0b607d and dca9f49.

📒 Files selected for processing (2)
  • packages/ui/src/services/visualization/mapping-action.service.choice.test.ts
  • packages/ui/src/services/visualization/mapping-action.service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/ui/src/services/visualization/mapping-action.service.ts
  • packages/ui/src/services/visualization/mapping-action.service.choice.test.ts

📝 Walkthrough

Walkthrough

MappingActionService now detects when a collection choice wrapper field is dragged to a collection target field and generates a for-each loop containing choose/when/otherwise logic. Two new private helpers construct the nested structure with proper member-based conditions and XPath expressions. The existing choose construction was refactored to delegate logic to a centralized builder. Tests verify the for-each/choose nesting, expression population, and de-duplication behavior.

Changes

Collection-Choice-to-Collection-Target Mapping

Layer / File(s) Summary
Service routing detection and for-each/choose helpers
packages/ui/src/services/visualization/mapping-action.service.ts
engageMapping() now detects when source is a collection choice wrapper and target is a collection field, routing to createForEachWithChooseFromChoice. Two new helpers build the for-each-wrapped choose/when/otherwise structure: createForEachWithChooseFromChoice wraps the target in a ForEachItem and populates the for-each expression before inserting the choose structure; buildChooseFromChoiceMembers constructs a ChooseItem by iterating source members to create when branches (mapping each member to condition and field) and appending otherwise.
Refactored choose construction
packages/ui/src/services/visualization/mapping-action.service.ts
createChooseFromChoice now delegates ChooseItem construction to buildChooseFromChoiceMembers instead of building inline, then pushes the result into target children after filtering existing ValueSelector nodes.
Test coverage for collection-choice mappings
packages/ui/src/services/visualization/mapping-action.service.choice.test.ts
Import list refactored to multi-line format. Added test helper factories for constructing collection-choice-wrapper and collection target fields. New test suite validates that mapping a collection-choice wrapper to a collection target creates a ForEachItem wrapping a ChooseItem, that ForEachItem expression is populated and ChooseItem.when/ValueSelector entries reference correct member XPaths, that ForEachItem is not created for non-collection targets or non-collection wrappers, and that duplicate mappings of the same source do not create multiple ForEachItem nodes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • KaotoIO/kaoto#3161: Modifies choice-to-mapping logic in MappingActionService (createChooseFromChoice and choice mapping internals), directly related code-level changes.

Suggested reviewers

  • igarashitm
  • PVinaches

Poem

🐰 A choice wraps many paths, yet loops they must take,
For each iteration, when/otherwise awake,
The service now nests them with purpose and grace,
ForEachItem and ChooseItem in perfect embrace! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(DataMapper): wrap collection choice mapping with for-each' accurately describes the primary change: implementing for-each wrapping for collection choice mappings.
Linked Issues check ✅ Passed The PR implementation meets all coding requirements from issue #2815: detects collection choice wrapper + collection target combination, creates ForEachItem wrapping ChooseItem with when/otherwise, adds comprehensive tests for S2 scenario, and maintains S1 backward compatibility.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #2815: refactored choose-building internals to support nested for-each/choose structures, added helper methods for collection choice mapping, and included test coverage for the new behavior.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.26%. Comparing base (053fe3f) to head (dca9f49).

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #3213    +/-   ##
========================================
  Coverage   92.25%   92.26%            
========================================
  Files         636      636            
  Lines       24633    24649    +16     
  Branches     5843     5847     +4     
========================================
+ Hits        22726    22742    +16     
- Misses       1797     1905   +108     
+ Partials      110        2   -108     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mmelko mmelko force-pushed the fix/2815-collection-choice branch from 13880f8 to c0b607d Compare May 14, 2026 12:28
@mmelko
Copy link
Copy Markdown
Contributor Author

mmelko commented May 14, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/ui/src/services/visualization/mapping-action.service.ts (1)

244-249: 💤 Low value

Optional: rename parameters for clarity.

Both parent: MappingItem and targetItem: MappingItem carry the same type but very different roles — parent is only used as the constructor parent of the returned ChooseItem, while targetItem is only consulted to derive the field context. A future reader (or accidental swap at a call site) won't get help from the type system. Consider names like chooseParent and fieldContext (or fieldOwner) to make the asymmetry explicit.

♻️ Suggested rename
-  private static buildChooseFromChoiceMembers(
-    parent: MappingItem,
-    sourceField: IField,
-    targetItem: MappingItem,
-  ): ChooseItem {
-    const chooseItem = new ChooseItem(parent, targetItem instanceof FieldItem ? targetItem.field : undefined);
+  private static buildChooseFromChoiceMembers(
+    chooseParent: MappingItem,
+    sourceField: IField,
+    fieldContext: MappingItem,
+  ): ChooseItem {
+    const chooseItem = new ChooseItem(
+      chooseParent,
+      fieldContext instanceof FieldItem ? fieldContext.field : undefined,
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/services/visualization/mapping-action.service.ts` around
lines 244 - 249, The parameter names in buildChooseFromChoiceMembers are
confusing because both parent and targetItem share type MappingItem but serve
different roles; rename parent to chooseParent (used only as the ChooseItem
constructor parent) and targetItem to fieldContext or fieldOwner (used only to
derive the field via targetItem instanceof FieldItem ? targetItem.field :
undefined) so the asymmetry is explicit and accidental swaps are harder; update
the function signature and all internal references (including the new ChooseItem
instantiation) and any call sites that pass these arguments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/ui/src/services/visualization/mapping-action.service.choice.test.ts`:
- Around line 516-547: Replace the loose truthy/contains assertions with exact
XPath equality checks: assert ForEachItem.expression equals the collection
wrapper XPath produced by createMockCollectionChoiceField, and assert the
chooseItem.when[*].expression and the ValueSelector.expression for email/phone
equal the exact member XPaths used in the S1 tests (e.g. '/ns0:email' and
'/ns0:phone'); update the assertions in the test around
MappingActionService.engageMapping, ForEachItem, ChooseItem, and ValueSelector
to compare full expected XPath strings rather than using
toBeTruthy()/toContain().

---

Nitpick comments:
In `@packages/ui/src/services/visualization/mapping-action.service.ts`:
- Around line 244-249: The parameter names in buildChooseFromChoiceMembers are
confusing because both parent and targetItem share type MappingItem but serve
different roles; rename parent to chooseParent (used only as the ChooseItem
constructor parent) and targetItem to fieldContext or fieldOwner (used only to
derive the field via targetItem instanceof FieldItem ? targetItem.field :
undefined) so the asymmetry is explicit and accidental swaps are harder; update
the function signature and all internal references (including the new ChooseItem
instantiation) and any call sites that pass these arguments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0880649d-1ad3-4c34-becf-9cec4e67633f

📥 Commits

Reviewing files that changed from the base of the PR and between 30f1e35 and c0b607d.

📒 Files selected for processing (2)
  • packages/ui/src/services/visualization/mapping-action.service.choice.test.ts
  • packages/ui/src/services/visualization/mapping-action.service.ts

Comment on lines +516 to +547
it('ForEachItem expression should be the XPath of the collection choice wrapper', () => {
const collectionChoiceField = createMockCollectionChoiceField([{ name: 'email' }, { name: 'phone' }]);
const choiceNode = new ChoiceFieldNodeData(sourceDocNode, collectionChoiceField);
const collectionTargetField = createMockCollectionField();
const targetFieldNode = new TargetFieldNodeData(localTargetDocNode, collectionTargetField);

MappingActionService.engageMapping(tree, choiceNode, targetFieldNode);

const forEachItem = tree.children[0].children[0] as ForEachItem;
// ForEachItem expression should be set (XPath to the collection choice wrapper)
expect(forEachItem.expression).toBeTruthy();
expect(forEachItem.expression.length).toBeGreaterThan(0);
});

it('WhenItem expressions inside ForEachItem should reference the choice members', () => {
const collectionChoiceField = createMockCollectionChoiceField([{ name: 'email' }, { name: 'phone' }]);
const choiceNode = new ChoiceFieldNodeData(sourceDocNode, collectionChoiceField);
const collectionTargetField = createMockCollectionField();
const targetFieldNode = new TargetFieldNodeData(localTargetDocNode, collectionTargetField);

MappingActionService.engageMapping(tree, choiceNode, targetFieldNode);

const forEachItem = tree.children[0].children[0] as ForEachItem;
const chooseItem = forEachItem.children[0] as ChooseItem;
// WhenItem test expressions reference the choice members
expect(chooseItem.when[0].expression).toContain('email');
expect(chooseItem.when[1].expression).toContain('phone');
// WhenItem value selectors should have paths to the members
const emailSelector = chooseItem.when[0].children.find((c) => c instanceof ValueSelector) as ValueSelector;
const phoneSelector = chooseItem.when[1].children.find((c) => c instanceof ValueSelector) as ValueSelector;
expect(emailSelector.expression).toContain('email');
expect(phoneSelector.expression).toContain('phone');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten expression assertions so they verify the XPath, not just its existence.

The new S2 tests use toBeTruthy() for the for-each expression and toContain('email') / toContain('phone') for the when/value-selector expressions, whereas the S1 counterparts (lines 128-129, 142-143) assert exact XPath ('/ns0:email'). The loose checks here will pass regardless of whether the engine emits:

  • /ns0:email (absolute — incorrect when evaluated inside an xsl:for-each iteration)
  • email or ./email (relative — correct)
  • the wrapper's path vs a member's path

Given the helper reuses the same MappingService.mapToCondition / mapToField calls as S1, it most likely emits absolute paths, which would generate semantically wrong XSLT inside the for-each. Asserting exact strings would either confirm the implementation is correct or surface the bug.

💚 Suggested assertion tightening
-      const forEachItem = tree.children[0].children[0] as ForEachItem;
-      // ForEachItem expression should be set (XPath to the collection choice wrapper)
-      expect(forEachItem.expression).toBeTruthy();
-      expect(forEachItem.expression.length).toBeGreaterThan(0);
+      const forEachItem = tree.children[0].children[0] as ForEachItem;
+      // ForEachItem expression should be the XPath to the collection choice wrapper
+      expect(forEachItem.expression).toEqual('/ns0:__choice__'); // adjust to the actual expected XPath
-      // WhenItem test expressions reference the choice members
-      expect(chooseItem.when[0].expression).toContain('email');
-      expect(chooseItem.when[1].expression).toContain('phone');
-      // WhenItem value selectors should have paths to the members
-      const emailSelector = chooseItem.when[0].children.find((c) => c instanceof ValueSelector) as ValueSelector;
-      const phoneSelector = chooseItem.when[1].children.find((c) => c instanceof ValueSelector) as ValueSelector;
-      expect(emailSelector.expression).toContain('email');
-      expect(phoneSelector.expression).toContain('phone');
+      // Inside an xsl:for-each the test/select must be relative to the current() item
+      expect(chooseItem.when[0].expression).toEqual('email');   // or whatever the relative form should be
+      expect(chooseItem.when[1].expression).toEqual('phone');
+      const emailSelector = chooseItem.when[0].children.find((c) => c instanceof ValueSelector) as ValueSelector;
+      const phoneSelector = chooseItem.when[1].children.find((c) => c instanceof ValueSelector) as ValueSelector;
+      expect(emailSelector.expression).toEqual('email');
+      expect(phoneSelector.expression).toEqual('phone');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/services/visualization/mapping-action.service.choice.test.ts`
around lines 516 - 547, Replace the loose truthy/contains assertions with exact
XPath equality checks: assert ForEachItem.expression equals the collection
wrapper XPath produced by createMockCollectionChoiceField, and assert the
chooseItem.when[*].expression and the ValueSelector.expression for email/phone
equal the exact member XPaths used in the S1 tests (e.g. '/ns0:email' and
'/ns0:phone'); update the assertions in the test around
MappingActionService.engageMapping, ForEachItem, ChooseItem, and ValueSelector
to compare full expected XPath strings rather than using
toBeTruthy()/toContain().

…IO#2815)

- Detect collection choice wrapper + collection target and wrap
  choose/when/otherwise inside a for-each
- Add tests for collection choice scenarios
@mmelko mmelko force-pushed the fix/2815-collection-choice branch from c0b607d to dca9f49 Compare May 14, 2026 13:37
@sonarqubecloud
Copy link
Copy Markdown

@mmelko mmelko marked this pull request as ready for review May 14, 2026 13:42
@mmelko mmelko requested a review from a team May 14, 2026 13:42
Copy link
Copy Markdown
Member

@igarashitm igarashitm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to cause a validation error when the collection choice field contains any container field(s). We could even make choice field not draggable in that case (MappingValidationService.isDraggable() -> false)

And it makes me also think if we're validating source-choice->target-container on both cases of when source-choice is a collection or non-collection.

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.

xs:choice: Choice with maxOccurs > 1

2 participants