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 d343f961a389 CAMEL-24714: camel-core-model - complete the @AsPredicate
marking and have the Simple check read asPredicate from the catalog (#26399)
d343f961a389 is described below
commit d343f961a3896d4a122dacf3edbe305431d30376
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 14 21:16:25 2026 +0200
CAMEL-24714: camel-core-model - complete the @AsPredicate marking and have
the Simple check read asPredicate from the catalog (#26399)
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
.../camel/catalog/models/predicateValidator.json | 2 +-
.../java/org/apache/camel/spi/AsPredicate.java | 6 +-
.../camel/model/validator/predicateValidator.json | 2 +-
.../validator/PredicateValidatorDefinition.java | 2 +
.../apache/camel/model/AsPredicateModelTest.java | 192 +++++++++++++++++++++
.../dsl/jbang/core/commands/ai/SimpleChecks.java | 55 ++++--
.../dsl/jbang/core/commands/ai/YamlLines.java | 48 +++++-
.../commands/ai/SourceValidatorSimpleTest.java | 77 +++++++++
.../java/org/apache/camel/spi/AsPredicate.java | 6 +-
9 files changed, 373 insertions(+), 17 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/predicateValidator.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/predicateValidator.json
index df4898153f81..96812431b3ee 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/predicateValidator.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/predicateValidator.json
@@ -12,7 +12,7 @@
"output": false
},
"properties": {
- "expression": { "index": 0, "kind": "expression", "displayName":
"Expression", "group": "common", "required": true, "type": "object",
"javaType": "org.apache.camel.model.language.ExpressionDefinition", "oneOf": [
"constant", "datasonnet", "exchangeProperty", "groovy", "header", "hl7terser",
"jactl", "java", "joor", "jq", "js", "jsonpath", "language", "method", "mvel",
"ognl", "python", "python3", "quickjs", "ref", "simple", "spel", "tokenize",
"variable", "wasm", "xpath", "xquery", " [...]
+ "expression": { "index": 0, "kind": "expression", "displayName":
"Expression", "group": "common", "required": true, "type": "object",
"javaType": "org.apache.camel.model.language.ExpressionDefinition", "oneOf": [
"constant", "datasonnet", "exchangeProperty", "groovy", "header", "hl7terser",
"jactl", "java", "joor", "jq", "js", "jsonpath", "language", "method", "mvel",
"ognl", "python", "python3", "quickjs", "ref", "simple", "spel", "tokenize",
"variable", "wasm", "xpath", "xquery", " [...]
"type": { "index": 1, "kind": "attribute", "displayName": "Type", "group":
"common", "required": false, "type": "string", "javaType": "java.lang.String",
"deprecated": false, "autowired": false, "secret": false, "description": "The
data type name to validate. If you specify 'xml:XYZ', the validator is picked
up when message type is 'xml:XYZ'. If you specify just 'xml', the validator
matches all xml message types." }
}
}
diff --git
a/core/camel-api/src/generated/java/org/apache/camel/spi/AsPredicate.java
b/core/camel-api/src/generated/java/org/apache/camel/spi/AsPredicate.java
index 1653ce2ddb85..6075cdc39663 100644
--- a/core/camel-api/src/generated/java/org/apache/camel/spi/AsPredicate.java
+++ b/core/camel-api/src/generated/java/org/apache/camel/spi/AsPredicate.java
@@ -28,7 +28,11 @@ import java.lang.annotation.Target;
* For example the EIPs which accepts expression/languages may used them as
either expression or predicate. This
* annotation is used to mark situation where they should be used as
predicate. As by default they are used as
* expression. And example would be the Filter EIP which uses predicate. And
the transform EIP uses an expression. <br/>
- * Being able to distinguish between these two situations can be of importance
to tooling.
+ * Being able to distinguish between these two situations can be of importance
to tooling: the catalog carries the
+ * annotation as <tt>asPredicate</tt> on the option in the model JSON. <br/>
+ * The Loop EIP is the one EIP the annotation cannot describe, as its
expression is a predicate only when
+ * <tt>doWhile</tt> is enabled (otherwise it is the number of iterations), so
its expression is not annotated and
+ * tooling checks the <tt>doWhile</tt> option instead.
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
diff --git
a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/validator/predicateValidator.json
b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/validator/predicateValidator.json
index df4898153f81..96812431b3ee 100644
---
a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/validator/predicateValidator.json
+++
b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/validator/predicateValidator.json
@@ -12,7 +12,7 @@
"output": false
},
"properties": {
- "expression": { "index": 0, "kind": "expression", "displayName":
"Expression", "group": "common", "required": true, "type": "object",
"javaType": "org.apache.camel.model.language.ExpressionDefinition", "oneOf": [
"constant", "datasonnet", "exchangeProperty", "groovy", "header", "hl7terser",
"jactl", "java", "joor", "jq", "js", "jsonpath", "language", "method", "mvel",
"ognl", "python", "python3", "quickjs", "ref", "simple", "spel", "tokenize",
"variable", "wasm", "xpath", "xquery", " [...]
+ "expression": { "index": 0, "kind": "expression", "displayName":
"Expression", "group": "common", "required": true, "type": "object",
"javaType": "org.apache.camel.model.language.ExpressionDefinition", "oneOf": [
"constant", "datasonnet", "exchangeProperty", "groovy", "header", "hl7terser",
"jactl", "java", "joor", "jq", "js", "jsonpath", "language", "method", "mvel",
"ognl", "python", "python3", "quickjs", "ref", "simple", "spel", "tokenize",
"variable", "wasm", "xpath", "xquery", " [...]
"type": { "index": 1, "kind": "attribute", "displayName": "Type", "group":
"common", "required": false, "type": "string", "javaType": "java.lang.String",
"deprecated": false, "autowired": false, "secret": false, "description": "The
data type name to validate. If you specify 'xml:XYZ', the validator is picked
up when message type is 'xml:XYZ'. If you specify just 'xml', the validator
matches all xml message types." }
}
}
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/validator/PredicateValidatorDefinition.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/validator/PredicateValidatorDefinition.java
index 7da62ae97ca3..1e0550b6d320 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/validator/PredicateValidatorDefinition.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/validator/PredicateValidatorDefinition.java
@@ -24,6 +24,7 @@ import jakarta.xml.bind.annotation.XmlRootElement;
import org.apache.camel.Expression;
import org.apache.camel.model.ExpressionNodeHelper;
import org.apache.camel.model.language.ExpressionDefinition;
+import org.apache.camel.spi.AsPredicate;
import org.apache.camel.spi.Metadata;
/**
@@ -35,6 +36,7 @@ import org.apache.camel.spi.Metadata;
public class PredicateValidatorDefinition extends ValidatorDefinition {
@XmlElementRef
+ @AsPredicate
@Metadata(required = true, description = "The predicate expression to use
for validation.")
private ExpressionDefinition expression;
diff --git
a/core/camel-core/src/test/java/org/apache/camel/model/AsPredicateModelTest.java
b/core/camel-core/src/test/java/org/apache/camel/model/AsPredicateModelTest.java
new file mode 100644
index 000000000000..d363ebde8a68
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/model/AsPredicateModelTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.model;
+
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+import org.apache.camel.builder.AggregationStrategies;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.model.language.LanguageExpression;
+import org.apache.camel.model.validator.PredicateValidatorDefinition;
+import org.apache.camel.spi.AsPredicate;
+import org.apache.camel.spi.Language;
+import org.apache.camel.support.builder.ExpressionBuilder;
+import org.apache.camel.support.builder.PredicateBuilder;
+import org.apache.camel.support.scan.DefaultPackageScanClassResolver;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The expression options the model marks with {@link AsPredicate}
(asPredicate in the model JSON the catalog ships) are
+ * the ones the reifiers evaluate with createPredicate, so the annotation and
the runtime cannot drift apart.
+ */
+public class AsPredicateModelTest {
+
+ /**
+ * The one EIP the annotation cannot describe: the loop expression is a
predicate only when doWhile is enabled.
+ */
+ private static final String LOOP = "loop.expression";
+
+ /** Records for each site (the expression text) whether the reifier asked
for a predicate or an expression. */
+ private static class SpyLanguage implements Language {
+ private final Set<String> predicates = new TreeSet<>();
+ private final Set<String> expressions = new TreeSet<>();
+
+ @Override
+ public Predicate createPredicate(String expression) {
+ predicates.add(expression);
+ return PredicateBuilder.constant(true);
+ }
+
+ @Override
+ public Expression createExpression(String expression) {
+ expressions.add(expression);
+ return ExpressionBuilder.constantExpression("1");
+ }
+ }
+
+ /** A spy expression at the given site (model.option) that binds to the
predicate overloads of the DSL. */
+ private static Predicate site(String site) {
+ return new LanguageExpression("spy", site);
+ }
+
+ /** A spy expression at the given site for the options set as expression
sub element (handled, retryWhile). */
+ private static ExpressionSubElementDefinition subElement(String site) {
+ return new ExpressionSubElementDefinition(new
LanguageExpression("spy", site));
+ }
+
+ /** A spy expression at the given site that binds to the expression
overloads of the DSL. */
+ private static Expression expressionSite(String site) {
+ return new LanguageExpression("spy", site);
+ }
+
+ @Test
+ public void testAsPredicateMatchesReifier() throws Exception {
+ Map<String, String> marked = findAsPredicateOptions();
+ // the expression options marked as predicate; the element options
(choice.when, doCatch.onWhen) hold a when
+ // or onWhen node whose own expression is marked
+ Set<String> expected = new TreeSet<>();
+ Set<String> holders = new TreeSet<>();
+ marked.forEach((site, kind) -> ("expression".equals(kind) ? expected :
holders).add(site));
+ assertTrue(expected.size() >= 9, "Expected at least 9 predicate
expression options, was: " + expected);
+
+ SpyLanguage spy = new SpyLanguage();
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ context.getRegistry().bind("spy", spy);
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ // the fluent handled(Predicate), retryWhile(Predicate)
and completionPredicate(Predicate) keep
+ // the predicate as is (no language to spy on) so those
are set as expression sub elements
+ OnExceptionDefinition handled =
onException(Exception.class).onWhen(site("onException.onWhen"));
+ handled.setHandled(subElement("onException.handled"));
+
handled.setRetryWhile(subElement("onException.retryWhile"));
+
onException(IllegalStateException.class).setContinued(subElement("onException.continued"));
+
onCompletion().onWhen(site("onWhen.expression")).to("mock:done");
+
intercept().when(site("intercept.onWhen")).log("intercept");
+
interceptFrom().when(site("interceptFrom.onWhen")).log("interceptFrom");
+
interceptSendToEndpoint("mock:*").when(site("interceptSendToEndpoint.onWhen"))
+ .log("interceptSendToEndpoint");
+
+ AggregateDefinition aggregate
+ = from("direct:start")
+
.filter(site("filter.expression")).log("filter").end()
+
.choice().when(site("when.expression")).log("when").end()
+ .validate(site("validate.expression"))
+ .loopDoWhile(site(LOOP)).log("loop").end()
+
.doTry().log("try").doCatch(Exception.class).onWhen(site("doCatch.onWhen")).log("catch")
+ .endDoTry().end()
+ .aggregate(constant("key"),
AggregationStrategies.useLatest());
+
aggregate.setCompletionPredicate(subElement("aggregate.completionPredicate"));
+ aggregate.log("aggregate").end()
+ // expression sites, for contrast
+
.split(expressionSite("split.expression")).log("split").end()
+ .transform(expressionSite("transform.expression"))
+ .setBody(expressionSite("setBody.expression"))
+ .to("mock:result");
+ }
+ });
+ PredicateValidatorDefinition validator = new
PredicateValidatorDefinition();
+ validator.setType("spy");
+ validator.setExpression(new LanguageExpression("spy",
"predicateValidator.expression"));
+ context.registerValidator(validator);
+ context.start();
+ }
+
+ // every marked option was evaluated as a predicate, and only as a
predicate
+ for (String site : expected) {
+ assertTrue(spy.predicates.contains(site), site + " is marked
asPredicate but the reifier did not create a"
+ + " predicate for it (is
the test missing the site?)");
+ assertFalse(spy.expressions.contains(site), site + " is marked
asPredicate but is evaluated as expression");
+ }
+ // every site evaluated as a predicate is marked, or holds a node
whose expression is
+ for (String site : spy.predicates) {
+ if (LOOP.equals(site)) {
+ continue;
+ }
+ assertTrue(expected.contains(site) || holders.contains(site),
+ site + " is evaluated as predicate but not marked with
@AsPredicate");
+ }
+ // the contrast sites are expressions and not marked
+ Set<String> contrast = Set.of("split.expression",
"transform.expression", "setBody.expression");
+ assertTrue(spy.expressions.containsAll(contrast), "Expression sites
not evaluated: " + spy.expressions);
+ assertTrue(Collections.disjoint(spy.predicates, contrast), "Expression
sites evaluated as predicate");
+ assertFalse(marked.containsKey(LOOP), "loop is a predicate only with
doWhile and cannot be marked");
+ assertEquals("expression",
marked.get("predicateValidator.expression"));
+ }
+
+ /** The options with asPredicate in the model JSON, as model.option to the
kind of the option. */
+ private static Map<String, String> findAsPredicateOptions() throws
Exception {
+ Map<String, String> answer = new TreeMap<>();
+ DefaultPackageScanClassResolver resolver = new
DefaultPackageScanClassResolver();
+ resolver.start();
+ for (Class<?> clazz : resolver.findAnnotated(XmlRootElement.class,
Constants.JAXB_CONTEXT_PACKAGES.split(":"))) {
+ String name = clazz.getAnnotation(XmlRootElement.class).name();
+ String path = "META-INF/" + clazz.getPackageName().replace('.',
'/') + "/" + name + ".json";
+ InputStream is =
AsPredicateModelTest.class.getClassLoader().getResourceAsStream(path);
+ if (is == null) {
+ continue;
+ }
+ JsonObject json = (JsonObject)
Jsoner.deserialize(IOHelper.loadText(is));
+ JsonObject properties = json.getMap("properties");
+ for (String option : properties.keySet()) {
+ JsonObject p = properties.getMap(option);
+ if (p.getBooleanOrDefault("asPredicate", false)) {
+ answer.put(name + "." + option, p.getString("kind"));
+ }
+ }
+ }
+ return answer;
+ }
+
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
index c6ed0761cd88..0389cde96a97 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
@@ -18,16 +18,19 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.LanguageValidationResult;
import org.apache.camel.spi.SimpleLanguageFunctionFactory;
+import org.apache.camel.tooling.model.EipModel;
+import org.apache.camel.util.StringHelper;
import static
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.countLeadingSpaces;
import static
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.extractEipFromLine;
import static
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.extractYamlValue;
import static
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.findParentEip;
+import static
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.findParentEipLine;
+import static
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.findSiblingValue;
/**
* The Simple language check of {@link SourceValidator}: every simple: value
and log message parsed by the catalog, as a
@@ -38,12 +41,6 @@ final class SimpleChecks {
private SimpleChecks() {
}
- static final Set<String> PREDICATE_EIPS = Set.of(
- "filter", "when", "validate", "onWhen", "on-when",
- "handled", "continued", "retryWhile", "retry-while",
- "completionPredicate", "completion-predicate",
- "completion", "loopDoWhile", "loop-do-while");
-
/** The simple expressions of a YAML route checked against the catalog, as
predicate where the EIP expects one. */
public static List<String> validateYamlSimple(String content, CamelCatalog
catalog) {
List<String> errors = new ArrayList<>();
@@ -103,11 +100,7 @@ final class SimpleChecks {
}
// Determine predicate vs expression context
- boolean predicate = false;
- if (!isLogMessage) {
- String parentEip = findParentEip(lines, i, lineIndent);
- predicate = parentEip != null &&
PREDICATE_EIPS.contains(parentEip);
- }
+ boolean predicate = !isLogMessage && isPredicate(catalog, lines,
i, lineIndent);
try {
LanguageValidationResult result = predicate
@@ -137,6 +130,44 @@ final class SimpleChecks {
&& error.contains("service could be found in the
classpath");
}
+ /**
+ * Whether the EIP the line is nested in evaluates the simple text as a
predicate: the expression the model marks
+ * with @AsPredicate, which the catalog carries as asPredicate on the
option. The parent line is either the EIP
+ * itself (filter, when, validate: its expression option) or an expression
option of the EIP above it (the handled
+ * of onException, the completionPredicate of aggregate). loop is the one
EIP a flag on the option cannot describe:
+ * its expression is a predicate only when doWhile is true.
+ */
+ static boolean isPredicate(CamelCatalog catalog, String[] lines, int
lineIdx, int lineIndent) {
+ int parentIdx = findParentEipLine(lines, lineIdx, lineIndent);
+ if (parentIdx < 0) {
+ return false;
+ }
+ String parent =
StringHelper.dashToCamelCase(extractEipFromLine(lines[parentIdx].trim()));
+ if (parent == null) {
+ return false;
+ }
+ if ("loop".equals(parent)) {
+ return "true".equals(findSiblingValue(lines, lineIdx, lineIndent,
"doWhile"));
+ }
+ EipModel eip = catalog.eipModel(parent);
+ if (eip != null) {
+ return isPredicateOption(eip, "expression");
+ }
+ // not an EIP but an expression option of the EIP above it
+ int ownerIdx = findParentEipLine(lines, parentIdx,
countLeadingSpaces(lines[parentIdx]));
+ if (ownerIdx < 0) {
+ return false;
+ }
+ String ownerName =
StringHelper.dashToCamelCase(extractEipFromLine(lines[ownerIdx].trim()));
+ EipModel owner = ownerName != null ? catalog.eipModel(ownerName) :
null;
+ return owner != null && isPredicateOption(owner, parent);
+ }
+
+ private static boolean isPredicateOption(EipModel eip, String option) {
+ return eip.getOptions().stream()
+ .anyMatch(o -> option.equals(o.getName()) &&
"expression".equals(o.getKind()) && o.isAsPredicate());
+ }
+
/**
* ${size} or ${count} written inside an aggregate: the parser's
did-you-mean (${length}) is about the function,
* what the author wants is the number of aggregated messages, an exchange
property.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java
index 9c95ff6c80c2..0220b2ee7b86 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java
@@ -18,6 +18,8 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
import java.util.regex.Pattern;
+import org.apache.camel.util.StringHelper;
+
/**
* Line-level helpers over a YAML source shared by the checks of {@link
SourceValidator}: the enclosing EIP of a line,
* the value of a key, quotes, indentation.
@@ -45,6 +47,12 @@ final class YamlLines {
}
static String findParentEip(String[] lines, int lineIdx, int lineIndent) {
+ int parent = findParentEipLine(lines, lineIdx, lineIndent);
+ return parent >= 0 ? extractEipFromLine(lines[parent].trim()) : null;
+ }
+
+ /** The index of the line with the EIP the line is nested in, or -1 (as
{@link #findParentEip} but the line). */
+ static int findParentEipLine(String[] lines, int lineIdx, int lineIndent) {
int indent = lineIndent;
for (int j = lineIdx - 1; j >= 0; j--) {
String prev = lines[j];
@@ -59,12 +67,50 @@ final class YamlLines {
indent = prevIndent;
continue;
}
- return eip;
+ return j;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * The value of the given option among the siblings of the line (the
entries at the same indent of the same block),
+ * or null when there is none. The option is matched in camelCase and
kebab-case.
+ */
+ static String findSiblingValue(String[] lines, int lineIdx, int
lineIndent, String option) {
+ for (int j = lineIdx - 1; j >= 0; j--) {
+ String value = siblingValue(lines[j], lineIndent, option);
+ if (value != null) {
+ return value;
+ }
+ if (!lines[j].isBlank() && countLeadingSpaces(lines[j]) <
lineIndent) {
+ break;
+ }
+ }
+ for (int j = lineIdx + 1; j < lines.length; j++) {
+ String value = siblingValue(lines[j], lineIndent, option);
+ if (value != null) {
+ return value;
+ }
+ if (!lines[j].isBlank() && countLeadingSpaces(lines[j]) <
lineIndent) {
+ break;
}
}
return null;
}
+ private static String siblingValue(String line, int indent, String option)
{
+ if (line.isBlank() || countLeadingSpaces(line) != indent) {
+ return null;
+ }
+ String trimmed = line.trim();
+ String key = extractEipFromLine(trimmed);
+ if (key == null || !option.equals(StringHelper.dashToCamelCase(key))) {
+ return null;
+ }
+ return unquote(trimmed.substring(trimmed.indexOf(':') + 1).trim());
+ }
+
static String extractEipFromLine(String trimmed) {
if (trimmed.startsWith("- ")) {
trimmed = trimmed.substring(2).trim();
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
index 8fb32cb123af..4661b775abfc 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
@@ -254,4 +254,81 @@ class SourceValidatorSimpleTest {
""", catalog);
assertThat(msgs).isEmpty();
}
+
+ @Test
+ void thePredicateEipsComeFromTheCatalog() {
+ // the same text is a predicate (error: no ${}) where the catalog
marks the option asPredicate, and a
+ // literal expression elsewhere
+ List<String> msgs = SourceValidator.validateYamlSimple("""
+ - from:
+ uri: timer:tick
+ steps:
+ - filter:
+ simple: "body contains 'critical'"
+ - setBody:
+ simple: "body contains 'critical'"
+ """, catalog);
+ assertThat(msgs).hasSize(1);
+ assertThat(msgs.get(0)).startsWith("Line 5:").contains("Unexpected
token body");
+ }
+
+ @Test
+ void anExpressionOptionOfTheEipAboveIsAPredicateWhenTheCatalogSaysSo() {
+ // handled, continued, retryWhile of onException and
completionPredicate of aggregate are not EIPs but
+ // expression options the catalog marks asPredicate; onWhen is its own
model
+ List<String> msgs = SourceValidator.validateYamlSimple("""
+ - onException:
+ exception: java.lang.Exception
+ handled:
+ simple: "body contains 'a'"
+ retry-while:
+ simple: "body contains 'b'"
+ onWhen:
+ simple: "body contains 'c'"
+ steps:
+ - log: "x"
+ - from:
+ uri: timer:tick
+ steps:
+ - aggregate:
+ aggregationStrategy: myStrategy
+ correlationExpression:
+ simple: "body contains 'd'"
+ completionPredicate:
+ simple: "body contains 'e'"
+ steps:
+ - log: "y"
+ """, catalog);
+ assertThat(msgs).hasSize(4);
+ assertThat(msgs.get(0)).startsWith("Line 4:").contains("Unexpected
token body");
+ assertThat(msgs.get(1)).startsWith("Line 6:").contains("Unexpected
token body");
+ assertThat(msgs.get(2)).startsWith("Line 8:").contains("Unexpected
token body");
+ assertThat(msgs.get(3)).startsWith("Line 19:").contains("Unexpected
token body");
+ }
+
+ @Test
+ void theLoopExpressionIsAPredicateOnlyWithDoWhile() {
+ List<String> msgs = SourceValidator.validateYamlSimple("""
+ - from:
+ uri: timer:tick
+ steps:
+ - loop:
+ simple: "body contains 'a'"
+ steps:
+ - log: "x"
+ - loop:
+ doWhile: true
+ simple: "body contains 'b'"
+ steps:
+ - log: "y"
+ - loop:
+ simple: "body contains 'c'"
+ do-while: "true"
+ steps:
+ - log: "z"
+ """, catalog);
+ assertThat(msgs).hasSize(2);
+ assertThat(msgs.get(0)).startsWith("Line 10:").contains("Unexpected
token body");
+ assertThat(msgs.get(1)).startsWith("Line 14:").contains("Unexpected
token body");
+ }
}
diff --git
a/tooling/spi-annotations/src/main/java/org/apache/camel/spi/AsPredicate.java
b/tooling/spi-annotations/src/main/java/org/apache/camel/spi/AsPredicate.java
index 1653ce2ddb85..6075cdc39663 100644
---
a/tooling/spi-annotations/src/main/java/org/apache/camel/spi/AsPredicate.java
+++
b/tooling/spi-annotations/src/main/java/org/apache/camel/spi/AsPredicate.java
@@ -28,7 +28,11 @@ import java.lang.annotation.Target;
* For example the EIPs which accepts expression/languages may used them as
either expression or predicate. This
* annotation is used to mark situation where they should be used as
predicate. As by default they are used as
* expression. And example would be the Filter EIP which uses predicate. And
the transform EIP uses an expression. <br/>
- * Being able to distinguish between these two situations can be of importance
to tooling.
+ * Being able to distinguish between these two situations can be of importance
to tooling: the catalog carries the
+ * annotation as <tt>asPredicate</tt> on the option in the model JSON. <br/>
+ * The Loop EIP is the one EIP the annotation cannot describe, as its
expression is a predicate only when
+ * <tt>doWhile</tt> is enabled (otherwise it is the number of iterations), so
its expression is not annotated and
+ * tooling checks the <tt>doWhile</tt> option instead.
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented