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 446b6e2e4886 CAMEL-24917: camel-yaml-dsl-validator - a to: uri that
holds an expression says to use toD: (#26758)
446b6e2e4886 is described below
commit 446b6e2e4886de04c30628256d08c3480f497ed6
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 23 13:38:51 2026 +0200
CAMEL-24917: camel-yaml-dsl-validator - a to: uri that holds an expression
says to use toD: (#26758)
to: http://host/stock/${header.sku} sends the placeholder as text,
url-encoded, because the endpoint of a to: is resolved once when the route
starts. The validator now says to write toD:, so camel validate and the
camel-jbang tools catch it before the route runs. Only the path is checked,
never the options after the ?, and the components that evaluate their path are
left alone. A documentation block can be marked with // yaml-validator: skip to
stay out of the validation and the sampl [...]
---
.../org/apache/camel/catalog/docs/to-eip.adoc | 1 +
.../src/main/docs/modules/eips/pages/to-eip.adoc | 1 +
.../dsl/jbang/core/commands/ai/eip-samples.json | 4 -
.../core/commands/ai/CatalogDocExamplesTest.java | 19 +-
.../dsl/yaml/validator/GenerateDocSamplesMojo.java | 15 +-
.../apache/camel/dsl/yaml/validator/DocBlocks.java | 73 ++++++++
.../camel/dsl/yaml/validator/YamlValidator.java | 82 +++++++++
.../dsl/yaml/validator/EipDocExamplesTest.java | 18 +-
.../validator/YamlValidatorDynamicUriTest.java | 195 +++++++++++++++++++++
9 files changed, 362 insertions(+), 46 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/to-eip.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/to-eip.adoc
index 13f04283fb71..3869fed116a7 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/to-eip.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/to-eip.adoc
@@ -126,6 +126,7 @@ XML::
YAML::
+
+// yaml-validator: skip
[source,yaml]
----
- route:
diff --git
a/core/camel-core-engine/src/main/docs/modules/eips/pages/to-eip.adoc
b/core/camel-core-engine/src/main/docs/modules/eips/pages/to-eip.adoc
index 13f04283fb71..3869fed116a7 100644
--- a/core/camel-core-engine/src/main/docs/modules/eips/pages/to-eip.adoc
+++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/to-eip.adoc
@@ -126,6 +126,7 @@ XML::
YAML::
+
+// yaml-validator: skip
[source,yaml]
----
- route:
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
index d8303bc79bc2..529cbbc4044d 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
@@ -1458,10 +1458,6 @@
"source": "to-eip.adoc",
"yaml": "- route:\n from:\n uri: file:messages\/foo\n
steps:\n - to:\n uri: jms:queue:foo\n"
},
- {
- "source": "to-eip.adoc",
- "yaml": "- route:\n from:\n uri: file:messages\/foo\n
steps:\n - to:\n uri:
\"freemarker:\/\/templateHome\/${body.templateName}.ftl\"\n - to:\n
uri: jms:queue:foo\n"
- },
{
"source": "to-eip.adoc",
"yaml": "- route:\n from:\n uri: file:messages\/foo\n
steps:\n - toD:\n uri:
\"freemarker:\/\/templateHome\/${body.templateName}.ftl\"\n - to:\n
uri: jms:queue:foo\n"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExamplesTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExamplesTest.java
index d6ecff4cb3d6..24e48c9a2e93 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExamplesTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExamplesTest.java
@@ -19,11 +19,10 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.yaml.validator.DocBlocks;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -36,8 +35,6 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class CatalogDocExamplesTest {
- private static final Pattern YAML_BLOCK =
Pattern.compile("\\[source,yaml\\]\\n-{4}\\n(.*?)\\n-{4}", Pattern.DOTALL);
-
/**
* Pages whose examples are right for the runtime but fail the catalog
because the component metadata cannot
* describe what the component accepts (the rest of CAMEL-24748 is fixed).
@@ -93,22 +90,16 @@ class CatalogDocExamplesTest {
if (doc == null) {
continue;
}
- int n = 0;
- Matcher m = YAML_BLOCK.matcher(doc);
- while (m.find()) {
- String yaml = m.group(1).stripTrailing() + "\n";
- if (!yaml.stripLeading().startsWith("- ")) {
- // a fragment (an option list, a snippet), not a route file
- continue;
- }
- n++;
+ List<String> blocks = DocBlocks.examples(doc);
+ for (int n = 0; n < blocks.size(); n++) {
+ String yaml = blocks.get(n);
String skipped = EXAMPLES_SKIPPED.get(page);
if (skipped != null && yaml.contains(skipped)) {
continue;
}
examples++;
for (String msg : SourceValidator.validateCamelYaml(yaml,
catalog)) {
- failures.add(page + " example " + n + ": " + msg);
+ failures.add(page + " example " + (n + 1) + ": " + msg);
}
}
}
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojo.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojo.java
index fef7f9458803..c31b0054ca63 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojo.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/java/org/apache/camel/dsl/yaml/validator/GenerateDocSamplesMojo.java
@@ -25,7 +25,6 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.networknt.schema.Error;
@@ -61,9 +60,6 @@ import org.apache.maven.project.MavenProject;
@Mojo(name = "generate-doc-samples", threadSafe = true)
public class GenerateDocSamplesMojo extends AbstractMojo {
- static final Pattern YAML_BLOCK =
Pattern.compile("\\[source,yaml\\]\\s*\\n----\\n(.*?)\\n----", Pattern.DOTALL);
- private static final Pattern CALLOUT = Pattern.compile("[ \\t]*#[
\\t]*<\\d+>[ \\t]*$", Pattern.MULTILINE);
-
@Parameter(property = "project", required = true, readonly = true)
protected MavenProject project;
@@ -267,16 +263,7 @@ public class GenerateDocSamplesMojo extends AbstractMojo {
/** The route examples of the page: the YAML blocks that start with a
top-level list entry, without callouts. */
static List<String> examples(File page) throws IOException {
- List<String> answer = new ArrayList<>();
- String doc = Files.readString(page.toPath(), StandardCharsets.UTF_8);
- Matcher m = YAML_BLOCK.matcher(doc);
- while (m.find()) {
- String yaml =
CALLOUT.matcher(m.group(1)).replaceAll("").stripTrailing() + "\n";
- if (yaml.stripLeading().startsWith("- ")) {
- answer.add(yaml);
- }
- }
- return answer;
+ return DocBlocks.examples(Files.readString(page.toPath(),
StandardCharsets.UTF_8));
}
private static boolean validate(YamlValidator validator, File page, String
yaml, List<String> failures)
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/DocBlocks.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/DocBlocks.java
new file mode 100644
index 000000000000..99f19e3523b8
--- /dev/null
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/DocBlocks.java
@@ -0,0 +1,73 @@
+/*
+ * 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.dsl.yaml.validator;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The YAML route examples of an AsciiDoc page, for everything that reads the
documentation: the doc-samples plugin and
+ * the tests that validate every example in the repository.
+ * <p/>
+ * A page that shows what to avoid - the to-eip page shows a {@code to:} with
an expression in its uri to say why
+ * {@code toD} exists - marks the block with {@value #SKIP_MARKER} on a line
before it, and it is then neither validated
+ * nor offered as a sample (CAMEL-24917).
+ */
+public final class DocBlocks {
+
+ /** The AsciiDoc comment that keeps a block out of the examples. */
+ public static final String SKIP_MARKER = "// yaml-validator: skip";
+
+ private static final Pattern YAML_BLOCK =
Pattern.compile("\\[source,yaml\\]\\s*\\n----\\n(.*?)\\n----",
+ Pattern.DOTALL);
+
+ /** An AsciiDoc callout marker at the end of a line, which is
documentation and not part of the route. */
+ private static final Pattern CALLOUT = Pattern.compile("[ \\t]*#[
\\t]*<\\d+>[ \\t]*$", Pattern.MULTILINE);
+
+ /** How far back the marker is looked for, enough for the tabs and titles
between it and the block. */
+ private static final int MARKER_LOOKBEHIND = 200;
+
+ private DocBlocks() {
+ }
+
+ /**
+ * The route examples of the page: the YAML blocks that start with a
top-level list entry, without their callout
+ * markers, and without the blocks that carry {@value #SKIP_MARKER}.
+ */
+ public static List<String> examples(String doc) {
+ List<String> answer = new ArrayList<>();
+ if (doc == null) {
+ return answer;
+ }
+ Matcher m = YAML_BLOCK.matcher(doc);
+ while (m.find()) {
+ String yaml =
CALLOUT.matcher(m.group(1)).replaceAll("").stripTrailing() + "\n";
+ if (yaml.stripLeading().startsWith("- ") && !markedToSkip(doc,
m.start())) {
+ answer.add(yaml);
+ }
+ }
+ return answer;
+ }
+
+ /** Whether the block at this offset carries {@link #SKIP_MARKER} in the
lines before it. */
+ public static boolean markedToSkip(String doc, int blockStart) {
+ int from = Math.max(0, blockStart - MARKER_LOOKBEHIND);
+ return doc.substring(from, blockStart).contains(SKIP_MARKER);
+ }
+}
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
index 47d4c69cd5c9..3f385dfd518e 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
@@ -573,6 +573,7 @@ public class YamlValidator {
}
if (errors.isEmpty()) {
checkSimpleSyntaxInScripts(target, new
NodePath(PathType.JSON_POINTER), errors);
+ checkDynamicUri(target, new NodePath(PathType.JSON_POINTER),
errors);
}
if (canonical) {
checkOneOfCardinality(target, new NodePath(PathType.JSON_POINTER),
errors);
@@ -678,6 +679,87 @@ public class YamlValidator {
}
}
+ /**
+ * to: http://host/stock/${header.sku}: the endpoint of a to: is resolved
once when the route starts, so an
+ * expression in its path is never evaluated - it is sent as the text it
is, url-encoded. That is what toD: is for
+ * (CAMEL-24917).
+ * <p/>
+ * Only the path is checked, never the options after the {@code ?}: an
option such as the file component's
+ * {@code fileName=${date:now:yyyyMMdd}} is evaluated by the producer and
is correct on a plain to:.
+ */
+ void checkDynamicUri(JsonNode node, NodePath path, List<Error> errors) {
+ if (node == null) {
+ return;
+ }
+ if (node.isArray()) {
+ for (int i = 0; i < node.size(); i++) {
+ checkDynamicUri(node.get(i), path.append(i), errors);
+ }
+ return;
+ }
+ if (!node.isObject()) {
+ return;
+ }
+ var fields = node.fieldNames();
+ while (fields.hasNext()) {
+ String name = fields.next();
+ JsonNode value = node.get(name);
+ if ("to".equals(name)) {
+ String uri = null;
+ NodePath at = path.append(name);
+ if (value.isTextual()) {
+ uri = value.asText();
+ } else if (value.isObject() && value.has("uri") &&
value.get("uri").isTextual()) {
+ uri = value.get("uri").asText();
+ at = at.append("uri");
+ }
+ String expression = expressionInPath(uri);
+ if (expression != null) {
+ errors.add(Error.builder()
+ .keyword("type")
+ .instanceLocation(at)
+ .messageKey("type")
+ .format(new MessageFormat("{0}"))
+ .arguments("to: the uri holds an expression (" +
expression + ") but the endpoint of a to:"
+ + " is fixed when the route starts, so
it is sent as text: write toD: to build"
+ + " the uri for each message")
+ .build());
+ }
+ }
+ checkDynamicUri(value, path.append(name), errors);
+ }
+ }
+
+ /**
+ * The first simple expression in the path of the uri (what comes before
the options), or null when there is none.
+ */
+ private static String expressionInPath(String uri) {
+ if (uri == null) {
+ return null;
+ }
+ int scheme = uri.indexOf(':');
+ if (scheme > 0 && EVALUATED_PATH.contains(uri.substring(0, scheme))) {
+ return null;
+ }
+ String head = uri.indexOf('?') > 0 ? uri.substring(0,
uri.indexOf('?')) : uri;
+ int start = head.indexOf("${");
+ if (start < 0) {
+ return null;
+ }
+ if (start >= 2 && head.startsWith(":#", start - 2)) {
+ return null; // :#${...} is a parameter the component binds per
message, not part of the address
+ }
+ int end = head.indexOf('}', start);
+ return end > 0 ? head.substring(start, end + 1) :
head.substring(start);
+ }
+
+ /**
+ * Components that evaluate their path for each message, where an
expression in it is what the component is for: the
+ * language component's script, and the metric name of the two metrics
components. Each was read in the component's
+ * own producer; CAMEL-24918 replaces this list with metadata in the
catalog, so that a component says it itself.
+ */
+ private static final Set<String> EVALUATED_PATH = Set.of("language",
"micrometer", "opentelemetry-metrics");
+
/** Adds an error for every expression node in the tree that has neither
expression: nor a language key. */
void checkRequiredExpressions(JsonNode node, NodePath path, List<Error>
errors) {
if (node == null) {
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/EipDocExamplesTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/EipDocExamplesTest.java
index 248ea4363421..4e8160596d8e 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/EipDocExamplesTest.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/EipDocExamplesTest.java
@@ -20,8 +20,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import com.networknt.schema.Error;
import org.apache.camel.catalog.CamelCatalog;
@@ -47,8 +45,6 @@ class EipDocExamplesTest {
"point-to-point-channel", "publish-subscribe-channel",
"return-address", "scatter-gather",
"selective-consumer", "service-activator", "transactional-client");
- private static final Pattern YAML_BLOCK =
Pattern.compile("\\[source,yaml\\]\\n-{4}\\n(.*?)\\n-{4}", Pattern.DOTALL);
-
/**
* Examples that show YAML the schema cannot know, by page and a text
found in the example: the yaml-dsl page shows
* a step contributed by a custom YAML deserializer.
@@ -128,15 +124,9 @@ class EipDocExamplesTest {
if (doc == null) {
continue;
}
- int n = 0;
- Matcher m = YAML_BLOCK.matcher(doc);
- while (m.find()) {
- String yaml = m.group(1).stripTrailing() + "\n";
- if (!yaml.stripLeading().startsWith("- ")) {
- // a fragment (an option list, a snippet), not a route file
- continue;
- }
- n++;
+ List<String> blocks = DocBlocks.examples(doc);
+ for (int n = 0; n < blocks.size(); n++) {
+ String yaml = blocks.get(n);
String skipped = EXAMPLES_SKIPPED.get(page);
if (skipped != null && yaml.contains(skipped)) {
continue;
@@ -144,7 +134,7 @@ class EipDocExamplesTest {
examples++;
List<Error> errors = validator.validate(yaml);
if (!errors.isEmpty()) {
- failures.add(page + " example " + n + ": " +
errors.get(0).getMessage());
+ failures.add(page + " example " + (n + 1) + ": " +
errors.get(0).getMessage());
}
}
}
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorDynamicUriTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorDynamicUriTest.java
new file mode 100644
index 000000000000..b29ed1a74bf9
--- /dev/null
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorDynamicUriTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.dsl.yaml.validator;
+
+import java.util.List;
+
+import com.networknt.schema.Error;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24917: to: with an expression in the uri path sends the text as it
stands, url-encoded, and every call fails at
+ * runtime. The validator says to write toD: instead.
+ */
+public class YamlValidatorDynamicUriTest {
+
+ private static YamlValidator classic;
+ private static YamlValidator canonical;
+
+ @BeforeAll
+ public static void setup() throws Exception {
+ classic = new YamlValidator();
+ classic.init();
+ canonical = new YamlValidator(true);
+ canonical.init();
+ }
+
+ @Test
+ public void testToWithAnExpressionInThePath() {
+ String yaml = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to:
+ uri: "http://localhost:8080/stock/${header.sku}"
+ """;
+ assertHint(yaml, "${header.sku}", "write toD:");
+ }
+
+ @Test
+ public void testToInItsShortForm() {
+ // canonical mode has its own word about the short form, so the hint
is the classic mode's
+ String yaml = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to: "http://localhost:8080/stock/${header.sku}"
+ """;
+ List<Error> errors = validate(classic, yaml);
+ assertThat(errors).anyMatch(e ->
e.getMessage().contains("${header.sku}"))
+ .anyMatch(e -> e.getMessage().contains("write toD:"));
+ }
+
+ @Test
+ public void testToDIsWhatToWrite() {
+ String yaml = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - toD:
+ uri: "http://localhost:8080/stock/${header.sku}"
+ """;
+ assertNoHint(yaml);
+ }
+
+ @Test
+ public void testAnOptionThatTheProducerEvaluatesIsFine() {
+ // the file component evaluates fileName per message, so this is
correct on a plain to:
+ String yaml = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to:
+ uri: "file:out?fileName=${date:now:yyyyMMdd}.txt"
+ """;
+ assertNoHint(yaml);
+ }
+
+ @Test
+ public void testASqlParameterIsFine() {
+ // the sql component binds :#${...} per message; this is what the user
manual shows
+ String yaml = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to:
+ uri: "sql:SELECT * FROM inventory WHERE id =
:#${body.itemId}"
+ """;
+ assertNoHint(yaml);
+ }
+
+ @Test
+ public void testALanguageScriptIsFine() {
+ // the language component's path is the script it evaluates
+ String yaml = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to:
+ uri: "language:simple:Hello ${body}"
+ """;
+ assertNoHint(yaml);
+ }
+
+ @Test
+ public void testAComponentWhosePathIsEvaluatedIsFine() {
+ // micrometer evaluates the metric name for each message
+ String yaml = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to:
+ uri: "micrometer:counter:orders.${header.region}"
+ """;
+ assertNoHint(yaml);
+ }
+
+ @Test
+ public void testAComponentWhosePathIsAnAddressIsReported() {
+ // xslt takes a resource name: a stylesheet per message needs toD
+ String yaml = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to:
+ uri: "xslt:styles/${header.style}.xsl"
+ """;
+ assertHint(yaml, "${header.style}", "write toD:");
+ }
+
+ @Test
+ public void testAPropertyPlaceholderIsFine() {
+ String yaml = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to:
+ uri: "http://{{stock.host}}/stock"
+ """;
+ assertNoHint(yaml);
+ }
+
+ private void assertHint(String yaml, String... expectedInMessage) {
+ for (YamlValidator validator : List.of(classic, canonical)) {
+ String mode = validator.isCanonical() ? "canonical" : "classic";
+ List<Error> errors = validate(validator, yaml);
+ assertThat(errors).as("%s mode must report the expression:\n%s",
mode, yaml).isNotEmpty();
+ for (String expected : expectedInMessage) {
+ assertThat(errors).as("%s mode must say '%s'", mode, expected)
+ .anyMatch(e -> e.getMessage().contains(expected));
+ }
+ }
+ }
+
+ private void assertNoHint(String yaml) {
+ for (YamlValidator validator : List.of(classic, canonical)) {
+ assertThat(validate(validator, yaml))
+ .as("%s mode must accept:\n%s", validator.isCanonical() ?
"canonical" : "classic", yaml)
+ .noneMatch(e -> e.getMessage().contains("write toD:"));
+ }
+ }
+
+ private List<Error> validate(YamlValidator validator, String yaml) {
+ try {
+ return validator.validate(yaml);
+ } catch (Exception e) {
+ throw new AssertionError("Failed to validate:\n" + yaml, e);
+ }
+ }
+}