This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 4d2cd13fa92b CAMEL-25900: camel-jbang - AI tools: the jsonpath filter
hints fit a path that goes on after the filter, and a when or filter (#28253)
4d2cd13fa92b is described below
commit 4d2cd13fa92bdd74b9552b6cac34991bf8ec1d60
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 12:58:21 2026 +0100
CAMEL-25900: camel-jbang - AI tools: the jsonpath filter hints fit a path
that goes on after the filter, and a when or filter (#28253)
Follow-up of #28210, addressing its review. The resultType java.util.Map
advice for a jsonpath filter is given only when an index ends the path; for a
path that goes on after the index the answer is the path without the index and
a resultType of the value's type. In a when or filter, an index or &&/||
outside the filter is answered with the filter alone as the condition. The
no-keys hint for ${body[qty]} on a List leads to the jsonpath filter first, as
the list hint does. BeanExpressi [...]
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../apache/camel/language/bean/BeanExpression.java | 20 ++++--
.../language/simple/SimpleSyntaxHintsTest.java | 4 +-
.../apache/camel/language/simple/SimpleTest.java | 12 +++-
.../dsl/jbang/core/commands/ai/CatalogDocs.java | 4 +-
.../core/commands/ai/ExpressionEvaluator.java | 25 +++++--
.../dsl/jbang/core/commands/ai/JsonPathChecks.java | 50 +++++++++----
.../jbang/core/commands/ai/CatalogDocsTest.java | 2 +-
.../ai/ExpressionEvaluatorLanguageTest.java | 24 ++++++-
.../jbang/core/commands/ai/JsonPathChecksTest.java | 82 +++++++++++++++++++++-
9 files changed, 190 insertions(+), 33 deletions(-)
diff --git
a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
index c81504b037c9..1e51db17bef9 100644
---
a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
+++
b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
@@ -593,11 +593,18 @@ public class BeanExpression implements Expression,
Predicate {
String type = bean instanceof List ? "a List" : bean instanceof
Object[] ? "an array" : "a Collection";
boolean field = methodName != null &&
FIELD_NAME.matcher(methodName).matches();
String name = field ? methodName : "field";
- // the lookup by a field first: given [0] first, models wrote
${body[0].orderId} and took the first element
- return " (" + type + " has no " + (field ? "method or key " +
methodName : "such method")
- + ": to pick the element whose " + name + " has a given value,
use jsonpath: \"$[?(@." + name
- + " == '${header." + name + "}')]\" with resultType
java.util.Map (the element, or null when none"
- + " matches); [0]." + name + " reads the first element; or
split it)";
+ return " (" + type + " has no " + (field ? "method or key " +
methodName : "such method") + ": "
+ + pickElementHint(name, "[0]." + name) + ")";
+ }
+
+ /**
+ * How to pick an element of a list by a field, and then the first
element: given [0] first, models wrote
+ * ${body[0].orderId} and took the first element.
+ */
+ private static String pickElementHint(String name, String first) {
+ return "to pick the element whose " + name + " has a given value, use
jsonpath: \"$[?(@." + name + " == '${header."
+ + name + "}')]\" with resultType java.util.Map (the element, or
null when none or several match); " + first
+ + " reads the first element; or split it";
}
private static final Pattern FIELD_NAME =
Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
@@ -812,7 +819,8 @@ public class BeanExpression implements Expression,
Predicate {
}
if (value instanceof Collection || value instanceof Object[]) {
String type = value instanceof List ? "a List" : value instanceof
Object[] ? "an array" : "a Collection";
- return " (" + type + " has no keys: pick an element first, such as
[0][" + key + "], or split it)";
+ String name = FIELD_NAME.matcher(key).matches() ? key : "field";
+ return " (" + type + " has no keys: " + pickElementHint(name,
"[0][" + key + "]") + ")";
}
return "";
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
index c0f5ffa9d3a6..e8c6096db880 100644
---
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
@@ -295,8 +295,8 @@ public class SimpleSyntaxHintsTest extends
ExchangeTestSupport {
// the lookup by a field first: given [0] first, models took the first
element
assertThat(e.getMessage()).contains("(a List has no method or key
orderId: to pick the element whose orderId"
+ " has a given value, use
jsonpath: \"$[?(@.orderId == '${header.orderId}')]\""
- + " with resultType java.util.Map
(the element, or null when none matches);"
- + " [0].orderId reads the first
element; or split it)");
+ + " with resultType java.util.Map
(the element, or null when none or several"
+ + " match); [0].orderId reads the
first element; or split it)");
}
@Test
diff --git
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
index c77072ad8ea9..1ec074ef84cc 100644
---
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
@@ -552,7 +552,11 @@ public class SimpleTest extends LanguageTestSupport {
() -> assertExpression("${body[qty]}", null));
IndexOutOfBoundsException cause =
assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause());
- assertTrue(cause.getMessage().contains("a List has no keys: pick an
element first, such as [0][qty]"),
+ // the lookup by a field first, as for ${body.qty}
+ assertTrue(cause.getMessage().contains("a List has no keys: to pick
the element whose qty has a given value, use"
+ + " jsonpath: \"$[?(@.qty ==
'${header.qty}')]\" with resultType"
+ + " java.util.Map (the element,
or null when none or several match);"
+ + " [0][qty] reads the first
element; or split it)"),
cause.getMessage());
}
@@ -563,7 +567,11 @@ public class SimpleTest extends LanguageTestSupport {
() -> assertExpression("${body[qty]}", null));
IndexOutOfBoundsException cause =
assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause());
- assertTrue(cause.getMessage().contains("an array has no keys: pick an
element first, such as [0][qty]"),
+ // the lookup by a field first, as for ${body.qty}
+ assertTrue(cause.getMessage().contains("an array has no keys: to pick
the element whose qty has a given value, use"
+ + " jsonpath: \"$[?(@.qty ==
'${header.qty}')]\" with resultType"
+ + " java.util.Map (the element,
or null when none or several match);"
+ + " [0][qty] reads the first
element; or split it)"),
cause.getMessage());
// [0] picks the element the hint points to
assertExpression("${body[0][qty]}", 42);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
index 5653cbe3c70d..52629a3fb29e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
@@ -1777,8 +1777,8 @@ public final class CatalogDocs {
static final String JSONPATH_ONE_ELEMENT = "to pick the one element of a
list by a field, write a filter with"
+ " resultType: java.util.Map,
such as expression: \"$[?(@.orderId =="
+ " '${header.id}')]\" and
resultType: java.util.Map: the element when one"
- + " matches, null when none
does (an index after the filter gives an empty"
- + " list); camel_catalog_sample
jsonpath shows it in a route";
+ + " matches, null when none or
several do (an index after the filter gives"
+ + " an empty list);
camel_catalog_sample jsonpath shows it in a route";
/** How many options of a component are listed in full when no filter is
given (CAMEL-25541). */
static final int DEFAULT_OPTIONS_LIMIT = 20;
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
index 39739bf833b2..3f3d44227959 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
@@ -46,6 +46,9 @@ public final class ExpressionEvaluator {
"\\s(==|=~|!=|!=~|>|>=|<|<=|~~|!~~|contains|!contains|regex|!regex|in|!in|is|!is|range|!range"
+
"|startsWith|!startsWith|endsWith|!endsWith|equals|!equals|&&|\\|\\|)\\s");
+ // a path that ends with a filter: $[?(@.sku == 'CAMEL-MUG')]
+ private static final Pattern FILTER_ENDS_PATH =
Pattern.compile("\\)]\\s*$");
+
private ExpressionEvaluator() {
}
@@ -83,23 +86,37 @@ public final class ExpressionEvaluator {
result.put("evaluatedIn", "a local scratch context (no integration
selected)");
evaluateLocally(ctx, lang, expression, body, predicate, result);
}
+ addFilterNote(result, lang, expression);
+ return result;
+ }
+
+ /** Adds the note about a jsonpath filter to the result of an evaluation,
after the note it has already. */
+ static void addFilterNote(JsonObject result, String lang, String
expression) {
String filter = filterNote(lang, expression,
result.getString("status"));
if (filter != null) {
result.put("note", result.containsKey("note") ?
result.getString("note") + "; " + filter : filter);
}
- return result;
}
/**
* A jsonpath filter gives a list, also for one match: models picked the
element with an index after the filter,
- * which gives an empty list, or a made-up syntax. In a route, resultType
java.util.Map gives the one element.
+ * which gives an empty list, or a made-up syntax. In a route, resultType
java.util.Map gives the one element when
+ * the path ends with the filter, and the type of the value gives the one
value of a path that goes on after it.
*/
static String filterNote(String lang, String expression, String status) {
if (!"jsonpath".equals(lang) || expression == null ||
!expression.contains("[?(") || !"ok".equals(status)) {
return null;
}
- return "a filter gives a list, also for one match; in a route,
resultType: java.util.Map gives the one element"
- + " it matches (null when none does), and an index after the
filter gives an empty list";
+ String problem = JsonPathChecks.filterProblem(expression, false);
+ if (problem != null) {
+ return problem;
+ }
+ if (FILTER_ENDS_PATH.matcher(expression).find()) {
+ return "a filter gives a list, also for one match; in a route,
resultType: java.util.Map gives the one"
+ + " element it matches (null when none or several do)";
+ }
+ return "a filter gives a list, also for one match; in a route, a
resultType that is not a list, such as"
+ + " java.lang.String or java.lang.Integer, gives the one value
when one element matches";
}
private static void evaluateInProcess(
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
index 5db46d434043..64fbfdc891d3 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
import java.util.ArrayList;
import java.util.List;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.camel.catalog.CamelCatalog;
@@ -34,9 +35,12 @@ public final class JsonPathChecks {
private static final Pattern COMPARISON = Pattern.compile("
(==|!=|<=|>=|<|>|=~|in|nin|size|empty) ");
// an index after a filter: $[?(@.orderId == 'ORD-1')][0]
- private static final Pattern INDEX_AFTER_FILTER =
Pattern.compile("\\[\\?\\(.*\\)]\\s*\\[\\s*-?\\d+\\s*]");
+ private static final Pattern INDEX_AFTER_FILTER =
Pattern.compile("(\\[\\?\\(.*\\)])\\s*\\[\\s*-?\\d+\\s*]");
private static final String ONE_ELEMENT = "set resultType: java.util.Map
(the element when one matches, null when"
- + " none does)";
+ + " none or several do)";
+ // in a when or filter a filter is the condition: an empty list is false
(ObjectHelper.evaluateValuePredicate)
+ private static final String FILTER_IS_THE_CONDITION = "in a when or
filter, the filter alone is the condition (true"
+ + " when an element
matches)";
private JsonPathChecks() {
}
@@ -74,7 +78,7 @@ public final class JsonPathChecks {
if (text == null || text.isEmpty()) {
continue;
}
- String filter = filterProblem(text);
+ String filter = filterProblem(text,
SimpleChecks.isPredicate(catalog, lines, i, countLeadingSpaces(line)));
if (filter != null) {
errors.add("Line " + lineNum + ": " + filter);
continue;
@@ -100,23 +104,43 @@ public final class JsonPathChecks {
}
/**
- * What is wrong around the filter of a path that should give the one
element it matches. Jayway applies an index
- * after a filter to each element the filter matched, so $[?(@.orderId ==
'ORD-1')][0] gives an empty list; and
- * && or || outside the filter is no JsonPath (500 in a REST
route). The one element comes with resultType
- * java.util.Map: a single match is the element, and no match null.
+ * What is wrong around the filter of a path. Jayway applies an index
after a filter to each element the filter
+ * matched, so $[?(@.orderId == 'ORD-1')][0] gives an empty list; and
&& or || outside the filter is no
+ * JsonPath (500 in a REST route). As a value, the one element comes with
resultType java.util.Map, and a field of
+ * it with the type of the field; in a when or filter, the filter alone is
the condition.
+ *
+ * @param text the jsonpath expression
+ * @param predicate whether it is the condition of a when or filter
*/
- static String filterProblem(String text) {
+ static String filterProblem(String text, boolean predicate) {
if (!text.contains("[?(")) {
return null;
}
- if (INDEX_AFTER_FILTER.matcher(text).find()) {
- return "an index after a filter gives an empty list in JsonPath
([?(...)][0] applies the index to each element"
- + " the filter matched): to get the one element, leave the
index out and " + ONE_ELEMENT;
+ Matcher index = INDEX_AFTER_FILTER.matcher(text);
+ if (index.find()) {
+ String problem = "an index after a filter gives an empty list in
JsonPath ([?(...)][0] applies the index to"
+ + " each element the filter matched): ";
+ if (predicate) {
+ return problem + "leave the index out: " +
FILTER_IS_THE_CONDITION;
+ }
+ String rest = text.substring(index.end()).strip();
+ if (rest.isEmpty()) {
+ return problem + "to get the one element, leave the index out
and " + ONE_ELEMENT;
+ }
+ // $[?(...)][0].qty: the path without the index gives the qty of
each match, a single match unwrapped
+ return problem + "to get a value of the one element, leave the
index out, as in "
+ + text.substring(0, index.start()) + index.group(1) + rest
+ + ", and set resultType to the type of that value, such as
java.lang.Integer for a number (the"
+ + " value when one element matches)";
}
String outside = outsideFilters(text);
if (outside.contains("&&") || outside.contains("||")) {
- return "&& and || work inside a filter only, such as $[?(@.status
== 'paid' && @.qty > 0)]; to test whether"
- + " a filter matched, " + ONE_ELEMENT + ", and check
${body} == null in a choice";
+ String problem = "&& and || work inside a filter only, such as
$[?(@.status == 'paid' && @.qty > 0)]";
+ if (predicate) {
+ return problem + ": " + FILTER_IS_THE_CONDITION + ", so move
the other conditions inside it";
+ }
+ return problem + "; to test whether an element matches, use the
filter as the condition: "
+ + FILTER_IS_THE_CONDITION;
}
return null;
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
index d78ee8201841..93b128e388a4 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
@@ -352,7 +352,7 @@ class CatalogDocsTest {
@Test
void aWrongHeaderNameIsAnsweredWithTheRightOne() {
- org.apache.camel.catalog.CamelCatalog catalog = new
org.apache.camel.catalog.DefaultCamelCatalog();
+ CamelCatalog catalog = new DefaultCamelCatalog();
JsonObject result = CatalogDocs.find(catalog, "CamelHttpStatusCode",
null, 10);
assertTrue(result.getString("header").startsWith("Camel has no header
CamelHttpStatusCode"), result.toJson());
assertTrue(result.getString("header").contains("CamelHttpResponseCode
(Exchange.HTTP_RESPONSE_CODE)"));
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
index 0fd7655f1100..6388f2f87bad 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
@@ -35,12 +35,34 @@ class ExpressionEvaluatorLanguageTest {
@Test
void aJsonPathFilterSaysHowToGetTheOneElement() {
assertThat(ExpressionEvaluator.filterNote("jsonpath", "$[?(@.sku ==
'CAMEL-MUG')]", "ok"))
- .contains("resultType: java.util.Map gives the one
element").contains("an index after the filter");
+ .contains("resultType: java.util.Map gives the one element it
matches (null when none or several do)");
+ // a path that goes on after the filter: a Map would convert 42 to null
+ assertThat(ExpressionEvaluator.filterNote("jsonpath", "$[?(@.sku ==
'CAMEL-MUG')].qty", "ok"))
+ .contains("such as java.lang.String or java.lang.Integer,
gives the one value").doesNotContain("Map");
+ assertThat(ExpressionEvaluator.filterNote("jsonpath", "$[?(@.sku ==
'CAMEL-MUG')][0].qty", "ok"))
+ .startsWith("an index after a filter gives an empty list")
+ .contains("leave the index out, as in $[?(@.sku ==
'CAMEL-MUG')].qty");
assertThat(ExpressionEvaluator.filterNote("jsonpath", "$.sku",
"ok")).isNull();
assertThat(ExpressionEvaluator.filterNote("simple", "${body}",
"ok")).isNull();
assertThat(ExpressionEvaluator.filterNote("jsonpath", "$[?(@.sku ==
'X')", "error")).isNull();
}
+ /** The note goes after the one the evaluation has already, as evaluate
adds it for any language it runs. */
+ @Test
+ void theFilterNoteIsAddedToTheResult() {
+ JsonObject result = new JsonObject();
+ result.put("status", "ok");
+ result.put("note", "evaluated with an empty body; pass body to
evaluate against a value");
+ ExpressionEvaluator.addFilterNote(result, "jsonpath", "$[?(@.sku ==
'CAMEL-MUG')]");
+ assertThat(result.getString("note")).startsWith("evaluated with an
empty body; pass body to evaluate against a"
+ + " value; a filter
gives a list, also for one match");
+
+ JsonObject failed = new JsonObject();
+ failed.put("status", "error");
+ ExpressionEvaluator.addFilterNote(failed, "jsonpath", "$[?(@.sku ==
'CAMEL-MUG')]");
+ assertThat(failed.get("note")).isNull();
+ }
+
@Test
void aLanguageThatCannotBeLoadedSaysWhatToDo() {
JsonObject result = ExpressionEvaluator.evaluate(new ToolContext(),
"no-such-language", "x", null);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
index ffe399a499e1..a807583054dc 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
@@ -71,7 +71,8 @@ class JsonPathChecksTest {
List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml,
catalog);
assertThat(errors).singleElement().satisfies(e -> assertThat(e)
.startsWith("Line 8: an index after a filter gives an empty
list in JsonPath")
- .contains("leave the index out and set resultType:
java.util.Map"));
+ .contains("leave the index out and set resultType:
java.util.Map (the element when one matches, null"
+ + " when none or several do)"));
}
@Test
@@ -88,9 +89,86 @@ class JsonPathChecksTest {
expression: "$[?(@.orderId ==
'${header.id}')] && size() > 0"
""";
List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml,
catalog);
+ // in a when, the filter alone is the condition: a Map would be null
also when several elements match
assertThat(errors).singleElement().satisfies(e -> assertThat(e)
.startsWith("Line 9: && and || work inside a filter only")
- .contains("set resultType: java.util.Map").contains("check
${body} == null"));
+ .contains("in a when or filter, the filter alone is the
condition (true when an element matches), so"
+ + " move the other conditions inside it")
+ .doesNotContain("java.util.Map"));
+ }
+
+ @Test
+ void orOutsideAFilterAsAValue() {
+ String yaml = """
+ - route:
+ from:
+ uri: direct:get-order
+ steps:
+ - setHeader:
+ name: found
+ expression:
+ jsonpath:
+ expression: "$[?(@.orderId == '${header.id}')]
|| $[?(@.sku == 'X')]"
+ """;
+ List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml,
catalog);
+ assertThat(errors).singleElement().satisfies(e -> assertThat(e)
+ .startsWith("Line 9: && and || work inside a filter only")
+ .contains("to test whether an element matches, use the filter
as the condition"));
+ }
+
+ @Test
+ void anIndexAfterAFilterThenAFieldSaysToLeaveTheIndexOut() {
+ // the Map advice gives [42] converted to a Map here: the field wants
the type of the field
+ String yaml = """
+ - route:
+ from:
+ uri: direct:stock
+ steps:
+ - setBody:
+ expression:
+ jsonpath:
+ expression: "$[?(@.sku == 'X')][0].qty"
+ """;
+ List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml,
catalog);
+ assertThat(errors).singleElement().satisfies(e -> assertThat(e)
+ .startsWith("Line 8: an index after a filter gives an empty
list in JsonPath")
+ .contains("leave the index out, as in $[?(@.sku == 'X')].qty,
and set resultType to the type of that"
+ + " value, such as java.lang.Integer")
+ .doesNotContain("java.util.Map"));
+ }
+
+ @Test
+ void anIndexAfterAFilterInAWhenSaysTheFilterIsTheCondition() {
+ String yaml = """
+ - route:
+ from:
+ uri: direct:get-order
+ steps:
+ - filter:
+ expression:
+ jsonpath:
+ expression: "$[?(@.orderId ==
'${header.id}')][0]"
+ """;
+ List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml,
catalog);
+ assertThat(errors).singleElement().satisfies(e -> assertThat(e)
+ .startsWith("Line 8: an index after a filter gives an empty
list in JsonPath")
+ .contains("leave the index out: in a when or filter, the
filter alone is the condition")
+ .doesNotContain("java.util.Map"));
+ }
+
+ @Test
+ void anIndexInAFieldAfterAFilterIsFine() {
+ String yaml = """
+ - route:
+ from:
+ uri: direct:get-order
+ steps:
+ - setBody:
+ expression:
+ jsonpath:
+ expression: "$.orders[?(@.id == 1)].items[0]"
+ """;
+ assertThat(JsonPathChecks.validateYamlJsonPath(yaml,
catalog)).isEmpty();
}
@Test