Skip to content

Commit cc42817

Browse files
ptrthomasclaude
andcommitted
fix(core): don't inline-interpolate into a fuzzy-match marker
A match RHS like `'#[] ##(itemSchema)'` reached processInlineEmbedded because it contains `#(`. The inline pass evaluated the embedded schema to a Map and stringified it back into the marker, corrupting it — the match engine then threw `expected: [R_CURLY]`. v1 never inline-interpolated (only whole-value `#(...)` / `##(...)` strings were processed), so a `#`-leading marker survived verbatim to the match engine, where the `#[]` each-schema path resolved the bare / embedded reference. Mirror that: processEmbeddedString now leaves any `#`-leading string intact and defers `#(...)` / `##(...)` resolution to the match engine. The documented shortcut is the bare reference `'#[] schema'`; `'#[] #(schema)'` is a redundant synonym. The fix simply stops the undocumented embedded variants from parse-erroring. Adds tests pinning the canonical form, the synonym equivalence, and the whole-array vs per-element nullability distinctions. fixes #2893 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0b594f0 commit cc42817

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

karate-core/src/main/java/io/karatelabs/core/StepExecutor.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3072,6 +3072,16 @@ private Object processEmbeddedString(String str) {
30723072
return str;
30733073
}
30743074
}
3075+
// A string that *starts* with '#' is a fuzzy-match marker, not free text —
3076+
// e.g. '#string', '#[] schema', '#[] ##(itemSchema)'. Such a marker may embed
3077+
// a #(...) / ##(...) expression that the match engine resolves later (the #[]
3078+
// each-schema path). Inline-interpolating here would stringify the resolved
3079+
// schema into the marker and corrupt it. v1 never inline-interpolated (it only
3080+
// processed whole-value #(...)/##(...) strings), so markers survived verbatim;
3081+
// mirror that and leave the marker intact for the match engine.
3082+
if (str.startsWith("#")) {
3083+
return str;
3084+
}
30753085
// Check for embedded expressions within a larger string
30763086
// e.g., "Hello #(name)!" or "Value: ##(optional)"
30773087
if (str.contains("#(")) {

karate-core/src/test/java/io/karatelabs/core/StepMatchTest.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,4 +732,85 @@ void testMatchSchemaTemplateAcrossCallBoundary() {
732732
assertPassed(sr);
733733
}
734734

735+
// ========== Array-marker + schema-reference shortcut ('#[] schema') ==========
736+
// The documented v1 shortcut is the BARE reference: '#[] schema' — after the
737+
// array marker comes a raw expression (variable, property path, function call)
738+
// that the match engine evaluates to the per-element schema. The '#(schema)'
739+
// embedded-expression wrapper is a redundant synonym for the bare form.
740+
//
741+
// v1 never inline-interpolated embedded expressions (only whole-value #(...) /
742+
// ##(...) strings were processed), so a marker like '#[] ##(schema)' survived
743+
// verbatim to the match engine. v2 added inline interpolation, which used to
744+
// stringify the resolved schema into the marker and corrupt it (parse error).
745+
// processEmbeddedString now leaves any '#'-leading marker intact, restoring the
746+
// behavior and keeping the marker well-formed for the match engine.
747+
748+
@Test
749+
void testArrayMarkerBareSchemaReference() {
750+
// Canonical / recommended form.
751+
ScenarioRuntime sr = run("""
752+
* def itemSchema = { id: '#number', name: '#string' }
753+
* def response = { items: [ { id: 1, name: 'a' }, { id: 2, name: 'b' } ] }
754+
* match response == { items: '#[] itemSchema' }
755+
""");
756+
assertPassed(sr);
757+
}
758+
759+
@Test
760+
void testArrayMarkerEmbeddedSchemaReferenceIsSynonymOfBare() {
761+
// '#[] #(schema)' behaves identically to '#[] schema' — the #(...) adds
762+
// nothing here. Pinned so the embedded form no longer throws a parse error.
763+
ScenarioRuntime sr = run("""
764+
* def itemSchema = { id: '#number', name: '#string' }
765+
* def response = { items: [ { id: 1, name: 'a' }, { id: 2, name: 'b' } ] }
766+
* match response == { items: '#[] #(itemSchema)' }
767+
""");
768+
assertPassed(sr);
769+
}
770+
771+
@Test
772+
void testOptionalArrayMarkerAllowsNullArray() {
773+
// '##[]' makes the whole array optional — a null (or absent) value passes.
774+
ScenarioRuntime sr = run("""
775+
* def itemSchema = { id: '#number', name: '#string' }
776+
* def response = { items: null }
777+
* match response == { items: '##[] itemSchema' }
778+
""");
779+
assertPassed(sr);
780+
}
781+
782+
@Test
783+
void testArrayMarkerRequiresPresentArray() {
784+
// '#[]' (single hash) requires the array to be present — null fails.
785+
ScenarioRuntime sr = run("""
786+
* def itemSchema = { id: '#number', name: '#string' }
787+
* def response = { items: null }
788+
* match response == { items: '#[] itemSchema' }
789+
""");
790+
assertFailed(sr);
791+
}
792+
793+
@Test
794+
void testPerElementOptionalSchemaAllowsNullElement() {
795+
// The only thing the embedded form can express that the bare form can't:
796+
// a per-element '##(schema)' lets an individual array element be null.
797+
ScenarioRuntime sr = run("""
798+
* def itemSchema = { id: '#number', name: '#string' }
799+
* def response = { items: [ { id: 1, name: 'a' }, null ] }
800+
* match response == { items: '#[] ##(itemSchema)' }
801+
""");
802+
assertPassed(sr);
803+
}
804+
805+
@Test
806+
void testPerElementRequiredSchemaRejectsNullElement() {
807+
// Complement of the above: bare / '#(schema)' per-element does NOT allow nulls.
808+
ScenarioRuntime sr = run("""
809+
* def itemSchema = { id: '#number', name: '#string' }
810+
* def response = { items: [ { id: 1, name: 'a' }, null ] }
811+
* match response == { items: '#[] itemSchema' }
812+
""");
813+
assertFailed(sr);
814+
}
815+
735816
}

0 commit comments

Comments
 (0)