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);
+        }
+    }
+}

Reply via email to