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 586c4161e3db CAMEL-24842, CAMEL-24846: camel validate yaml flags query 
options in a uri next to a parameters block, and an onException written after 
the routes (#26620)
586c4161e3db is described below

commit 586c4161e3db680f75108f963d2280cb9f23a04c
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Sep 20 10:49:00 2026 +0200

    CAMEL-24842, CAMEL-24846: camel validate yaml flags query options in a uri 
next to a parameters block, and an onException written after the routes (#26620)
    
    Both validated and failed at startup: the YAML DSL refuses options in the 
uri and under parameters at the same time, and the runtime requires 
onException, onCompletion and the intercepts before the first route. The 
validator now says each in YAML words, with the line and what to write. The rss 
YAML documentation example mixed both forms. Found in the round-2 local-model 
benchmark on the camel-jbang-examples ladder.
---
 .../apache/camel/catalog/docs/rss-component.adoc   |   5 +-
 .../camel-rss/src/main/docs/rss-component.adoc     |   5 +-
 .../dsl/jbang/core/commands/ai/EndpointChecks.java |  14 +++
 .../jbang/core/commands/ai/SourceValidator.java    |   1 +
 .../jbang/core/commands/ai/StructureChecks.java    |  76 ++++++++++++++
 .../commands/ai/SourceValidatorEndpointTest.java   |  46 ++++++++-
 .../core/commands/ai/StructureChecksTest.java      | 111 +++++++++++++++++++++
 7 files changed, 247 insertions(+), 11 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rss-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rss-component.adoc
index c4bc3e28bcec..c48800d8aa5f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rss-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/rss-component.adoc
@@ -93,10 +93,7 @@ YAML::
 ----
 - route:
     from:
-      uri: "rss:http://someserver.com/feeds/posts/default?alt=rss";
-      parameters:
-        splitEntries: false
-        delay: 1000
+      uri: 
"rss:http://someserver.com/feeds/posts/default?alt=rss&splitEntries=false&delay=1000";
       steps:
         - to:
             uri: bean:rss
diff --git a/components/camel-rss/src/main/docs/rss-component.adoc 
b/components/camel-rss/src/main/docs/rss-component.adoc
index c4bc3e28bcec..c48800d8aa5f 100644
--- a/components/camel-rss/src/main/docs/rss-component.adoc
+++ b/components/camel-rss/src/main/docs/rss-component.adoc
@@ -93,10 +93,7 @@ YAML::
 ----
 - route:
     from:
-      uri: "rss:http://someserver.com/feeds/posts/default?alt=rss";
-      parameters:
-        splitEntries: false
-        delay: 1000
+      uri: 
"rss:http://someserver.com/feeds/posts/default?alt=rss&splitEntries=false&delay=1000";
       steps:
         - to:
             uri: bean:rss
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
index 9c0aceb7905b..4f78eb90a80b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
@@ -150,6 +150,20 @@ final class EndpointChecks {
                 }
                 String nextTrimmed = next.trim();
                 if (nextIndent == lineIndent && 
nextTrimmed.startsWith("parameters:")) {
+                    if (hasParams) {
+                        // to: {uri: "file:inbox?fileExist=Override", 
parameters: {fileName: x}}: the YAML DSL refuses
+                        // options in both places at startup ("Uri should not 
contains query parameters"), the schema
+                        // does not see it (CAMEL-24842); say it here, with 
what to write
+                        String query = uri.substring(uri.indexOf('?') + 1);
+                        String first = query.contains("&") ? 
query.substring(0, query.indexOf('&')) : query;
+                        String asYaml = first.contains("=")
+                                ? first.substring(0, first.indexOf('=')) + ": 
" + first.substring(first.indexOf('=') + 1)
+                                : first;
+                        errors.add(linePrefix(i) + "the uri has query options 
(" + query + ") and the step also has"
+                                   + " parameters: put every option under 
parameters: (" + asYaml + ") or all of them"
+                                   + " in the uri, not both (the runtime 
refuses the mix with 'Uri should not contains"
+                                   + " query parameters')");
+                    }
                     int paramBlockIndent = nextIndent;
                     int blockScalarIndent = -1;
                     String mapKey = null;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
index d96a78910e84..d1be20fccf42 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
@@ -226,6 +226,7 @@ public final class SourceValidator {
         if (content == null || content.isBlank()) {
             return msgs;
         }
+        msgs.addAll(StructureChecks.validateTopLevelOrder(content));
         msgs.addAll(validateYamlEndpoints(content, catalog));
         msgs.addAll(validateYamlSimple(content, catalog));
         msgs.addAll(validateKnownHeaders(content, catalog));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/StructureChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/StructureChecks.java
new file mode 100644
index 000000000000..c460e54c81e8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/StructureChecks.java
@@ -0,0 +1,76 @@
+/*
+ * 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.jbang.core.commands.ai;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Checks on the order of the top-level entries of a YAML route file. The 
schema accepts the entries in any order; the
+ * runtime does not: an onException, onCompletion or intercept written after a 
route fails at startup with "onException
+ * must be defined before any routes in the RouteBuilder", a Java DSL sentence 
for a YAML author (CAMEL-24846). The
+ * check names the line, the entry, and where it goes.
+ */
+public final class StructureChecks {
+
+    /** A top-level list item: "- route:", "- onException:" (indent 0). */
+    private static final Pattern TOP_LEVEL_ENTRY = Pattern.compile("^- 
([A-Za-z][A-Za-z0-9]*)\\s*:");
+
+    /** The entries the runtime requires before the first route. */
+    private static final Set<String> BEFORE_ROUTES
+            = Set.of("onException", "onCompletion", "intercept", 
"interceptFrom", "interceptSendToEndpoint");
+
+    /** The entries that are a route. */
+    private static final Set<String> ROUTES = Set.of("route", "from");
+
+    private StructureChecks() {
+    }
+
+    /**
+     * The entries written after the first route that the runtime requires 
before it, one message per entry with the
+     * line of the entry and the line of the first route.
+     */
+    public static List<String> validateTopLevelOrder(String content) {
+        List<String> errors = new ArrayList<>();
+        if (content == null || content.isBlank()) {
+            return errors;
+        }
+        String[] lines = content.split("\n", -1);
+        int firstRoute = -1;
+        for (int i = 0; i < lines.length; i++) {
+            Matcher m = TOP_LEVEL_ENTRY.matcher(lines[i]);
+            if (!m.find()) {
+                continue;
+            }
+            String entry = m.group(1);
+            if (ROUTES.contains(entry)) {
+                if (firstRoute < 0) {
+                    firstRoute = i;
+                }
+            } else if (firstRoute >= 0 && BEFORE_ROUTES.contains(entry)) {
+                errors.add("Line " + (i + 1) + ": " + entry + " must come 
before the routes: move this entry above the"
+                           + " first - " + 
(lines[firstRoute].trim().startsWith("- from") ? "from" : "route")
+                           + ": (line " + (firstRoute + 1) + "); the runtime 
refuses it at startup ('" + entry
+                           + " must be defined before any routes')");
+            }
+        }
+        return errors;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
index 1005a0c05bc7..9c4d44acc4e3 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
@@ -49,6 +49,42 @@ class SourceValidatorEndpointTest {
         assertThat(errors).isEmpty();
     }
 
+    @Test
+    void queryOptionsInTheUriAndAParametersBlock() {
+        // CAMEL-24842: the runtime refuses options in both places; the schema 
does not see it
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - to:
+                            uri: file://inbox?fileExist=Override
+                            parameters:
+                              fileName: invoice-2001.json
+                """;
+        List<String> errors = SourceValidator.validateYamlEndpoints(yaml, 
catalog);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).startsWith("Line 6: ")
+                .contains("query options (fileExist=Override)")
+                .contains("parameters: (fileExist: Override)")
+                .contains("Uri should not contains query parameters");
+    }
+
+    @Test
+    void queryOptionsInTheUriAndAParametersBlockOnFrom() {
+        String yaml = """
+                - from:
+                    uri: "timer:t?period=1000"
+                    parameters:
+                      repeatCount: 1
+                    steps:
+                      - log: "hello"
+                """;
+        List<String> errors = SourceValidator.validateYamlEndpoints(yaml, 
catalog);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).startsWith("Line 2: ").contains("(period: 
1000)");
+    }
+
     @Test
     void expandedFormUnknownOption() {
         String yaml = """
@@ -119,6 +155,7 @@ class SourceValidatorEndpointTest {
 
     @Test
     void expandedUriWithQueryParamsAndParametersBlock() {
+        // the options are valid, but the runtime refuses them in both places 
(CAMEL-24842)
         String yaml = """
                 - from:
                     uri: timer:tick?period=1000
@@ -128,7 +165,8 @@ class SourceValidatorEndpointTest {
                     - log: "${body}"
                 """;
         List<String> errors = SourceValidator.validateYamlEndpoints(yaml, 
catalog);
-        assertThat(errors).isEmpty();
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).startsWith("Line 2: ").contains("query 
options (period=1000)");
     }
 
     @Test
@@ -142,8 +180,10 @@ class SourceValidatorEndpointTest {
                     - log: "${body}"
                 """;
         List<String> errors = SourceValidator.validateYamlEndpoints(yaml, 
catalog);
-        assertThat(errors).isNotEmpty();
-        assertThat(errors.get(0)).contains("timer:");
+        // the mix is reported, and the unknown option still is
+        assertThat(errors).hasSize(2);
+        assertThat(errors.get(0)).contains("query options (period=1000)");
+        
assertThat(errors.get(1)).contains("timer:").containsIgnoringCase("unknown");
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/StructureChecksTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/StructureChecksTest.java
new file mode 100644
index 000000000000..1ed9d528bf27
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/StructureChecksTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.jbang.core.commands.ai;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class StructureChecksTest {
+
+    @Test
+    void onExceptionAfterTheRoutes() {
+        // CAMEL-24846: validates today, fails at startup with a RouteBuilder 
sentence
+        String yaml = """
+                - route:
+                    from:
+                      uri: "timer:t?repeatCount=1"
+                      steps:
+                        - log: "hello"
+                - onException:
+                    exception:
+                      - java.lang.Exception
+                    handled:
+                      constant: "true"
+                    steps:
+                      - log: "handled"
+                """;
+        List<String> errors = StructureChecks.validateTopLevelOrder(yaml);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).isEqualTo("Line 6: onException must come 
before the routes: move this entry above"
+                                            + " the first - route: (line 1); 
the runtime refuses it at startup"
+                                            + " ('onException must be defined 
before any routes')");
+    }
+
+    @Test
+    void onExceptionBeforeTheRoutes() {
+        String yaml = """
+                - onException:
+                    exception:
+                      - java.lang.Exception
+                    steps:
+                      - log: "handled"
+                - route:
+                    from:
+                      uri: "timer:t?repeatCount=1"
+                      steps:
+                        - log: "hello"
+                """;
+        assertThat(StructureChecks.validateTopLevelOrder(yaml)).isEmpty();
+    }
+
+    @Test
+    void interceptAndOnCompletionAfterAFromRoute() {
+        String yaml = """
+                - beans:
+                    - name: myBean
+                      type: "#class:com.example.MyBean"
+                - from:
+                    uri: "timer:t?repeatCount=1"
+                    steps:
+                      - log: "hello"
+                - intercept:
+                    steps:
+                      - log: "intercepted"
+                - onCompletion:
+                    steps:
+                      - log: "done"
+                """;
+        List<String> errors = StructureChecks.validateTopLevelOrder(yaml);
+        assertThat(errors).hasSize(2);
+        assertThat(errors.get(0)).startsWith("Line 8: intercept must come 
before the routes").contains("- from: (line 4)");
+        assertThat(errors.get(1)).startsWith("Line 11: onCompletion must come 
before the routes");
+    }
+
+    @Test
+    void anOnExceptionInsideARouteIsNotATopLevelEntry() {
+        // the schema reports that one ('onException' is a top-level entry: 
write it as a list item ...)
+        String yaml = """
+                - route:
+                    from:
+                      uri: "timer:t?repeatCount=1"
+                      steps:
+                        - onException:
+                            exception:
+                              - java.lang.Exception
+                """;
+        assertThat(StructureChecks.validateTopLevelOrder(yaml)).isEmpty();
+    }
+
+    @Test
+    void noRoutesNoErrors() {
+        assertThat(StructureChecks.validateTopLevelOrder("- onException:\n    
steps:\n      - log: x\n")).isEmpty();
+        assertThat(StructureChecks.validateTopLevelOrder("")).isEmpty();
+    }
+}

Reply via email to