feat(stepfunctions): implement States.JsonMerge intrinsic - #1661
Conversation
AslExecutor did not support the States.JsonMerge intrinsic, failing with 'Unsupported intrinsic function: States.JsonMerge'. Implement the shallow merge (second object's top-level fields win on a key conflict); deep-merge (third argument true) and non-object arguments are rejected, matching AWS Step Functions. Also resolve boolean/null literal intrinsic arguments so the third argument parses. Covered by AslExecutorJsonMergeTest.
|
| Filename | Overview |
|---|---|
| src/main/java/io/github/hectorvent/floci/services/stepfunctions/AslExecutor.java | Adds States.JsonMerge intrinsic and true/false/null literal support in resolveIntrinsicArg; the deep-flag extraction uses .asBoolean() without validating the node is actually a boolean, allowing null or other non-boolean references to silently become false. |
| src/test/java/io/github/hectorvent/floci/services/stepfunctions/AslExecutorJsonMergeTest.java | New test class covering the four main JsonMerge scenarios; class name does not follow the project's *ServiceTest.java unit-test convention from AGENTS.md, and a test for the null-as-third-arg edge case is absent. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["States.JsonMerge(a, b, deep)"] --> B[splitIntrinsicArgs]
B --> C{3 args?}
C -->|No| ERR1[FailStateException: requires exactly 3 arguments]
C -->|Yes| D[resolveIntrinsicArg arg1 - JsonNode a]
D --> E[resolveIntrinsicArg arg2 - JsonNode b]
E --> F["resolveIntrinsicArg arg3 → .asBoolean() → deep"]
F --> G{deep is true?}
G -->|Yes| ERR2[FailStateException: shallow merge only]
G -->|No| H{a and b are objects?}
H -->|No| ERR3[FailStateException: requires two JSON objects]
H -->|Yes| I[createObjectNode merged]
I --> J[copy all fields from a]
J --> K[copy all fields from b - overrides on conflict]
K --> L[yield merged ObjectNode]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["States.JsonMerge(a, b, deep)"] --> B[splitIntrinsicArgs]
B --> C{3 args?}
C -->|No| ERR1[FailStateException: requires exactly 3 arguments]
C -->|Yes| D[resolveIntrinsicArg arg1 - JsonNode a]
D --> E[resolveIntrinsicArg arg2 - JsonNode b]
E --> F["resolveIntrinsicArg arg3 → .asBoolean() → deep"]
F --> G{deep is true?}
G -->|Yes| ERR2[FailStateException: shallow merge only]
G -->|No| H{a and b are objects?}
H -->|No| ERR3[FailStateException: requires two JSON objects]
H -->|Yes| I[createObjectNode merged]
I --> J[copy all fields from a]
J --> K[copy all fields from b - overrides on conflict]
K --> L[yield merged ObjectNode]
Reviews (1): Last reviewed commit: "feat(stepfunctions): implement States.Js..." | Re-trigger Greptile
| } | ||
| JsonNode a = resolveIntrinsicArg(parts.get(0).trim(), root); | ||
| JsonNode b = resolveIntrinsicArg(parts.get(1).trim(), root); | ||
| boolean deep = resolveIntrinsicArg(parts.get(2).trim(), root).asBoolean(); |
There was a problem hiding this comment.
The third argument is read with
.asBoolean() directly, which returns false for any non-boolean node (null, string, number). If the argument is a reference path that resolves to null or a non-boolean value at runtime, the executor silently treats it as false and performs the merge instead of rejecting the call. Adding an explicit isBoolean() guard preserves AWS's rejection of non-boolean third arguments.
| boolean deep = resolveIntrinsicArg(parts.get(2).trim(), root).asBoolean(); | |
| JsonNode deepArg = resolveIntrinsicArg(parts.get(2).trim(), root); | |
| if (!deepArg.isBoolean()) { | |
| throw new FailStateException("States.Runtime", | |
| "States.JsonMerge third argument must be a boolean"); | |
| } | |
| boolean deep = deepArg.booleanValue(); |
| * previously failed with "Unsupported intrinsic function: States.JsonMerge". | ||
| */ | ||
| class AslExecutorJsonMergeTest { | ||
|
|
There was a problem hiding this comment.
Test naming deviates from AGENTS.md convention
AGENTS.md specifies that unit tests should be named *ServiceTest.java. The class AslExecutorJsonMergeTest doesn't follow that pattern. Renaming it to AslExecutorServiceTest (or appending to any existing AslExecutorServiceTest) would keep the test suite consistent with the project convention.
Context Used: AGENTS.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
AslExecutordid not implement theStates.JsonMergeintrinsic, so a state machine using it failedwith
Unsupported intrinsic function: States.JsonMerge. This implements the shallow merge: thesecond object's top-level fields override the first's on a key conflict. Deep-merge (third argument
true) and non-object arguments are rejected, matching AWS. The intrinsic-arg resolver also learnstrue/false/nullliterals so the third argument parses.Type of change
feat:)AWS Compatibility
AWS Step Functions'
States.JsonMerge(obj1, obj2, false)performs a shallow merge and onlysupports the shallow form — passing
true(deep merge) is a documented error. Behavior verifiedagainst the intrinsic functions reference:
second object wins on conflict, deep-merge rejected, non-object args rejected.
Checklist
./mvnw testpasses locally —AslExecutorJsonMergeTest(run ineclipse-temurin:25-jdk)AslExecutorJsonMergeTest(shallow merge, deep-merge rejection, non-object rejection, arg-count)