davsclaus commented on code in PR #26636:
URL: https://github.com/apache/camel/pull/26636#discussion_r4057776734


##########
docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc:
##########
@@ -782,6 +782,12 @@ cost of a slower startup. If you passed `--runtime=main` 
explicitly and want the
 run, use `--runtime=jbang` (or leave the option out). The options 
`--background`, `--code`, `--open-api`,
 `--empty` and `--mcp-stdio` are not supported with `--runtime=main`.
 
+When a YAML route file does not load, `camel run` now prints the report of 
`camel validate yaml` for the route

Review Comment:
   This paragraph lands between the `--runtime=main` paragraph and its 
continuation ("Running an existing Maven project (`camel run pom.xml`) is 
unchanged…"), so the `--runtime` explanation is now split in two. Suggest 
moving it after that continuation paragraph, or to the end of the `=== 
camel-jbang` section.
   
   Also, CLAUDE.md keeps the upgrade guide migration-only. The part that 
matters for upgraders is the last sentence: the file watcher (camel-support) 
now emits `CamelContextReloadFailure` per failed file reload, so an existing 
listener will fire for a single-file failure that is not a full context reload. 
The description of the `camel run` report is a feature and could be trimmed to 
a short mention.



##########
dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReportTest.java:
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
+import org.apache.camel.impl.engine.SimpleCamelContext;
+import org.apache.camel.impl.event.CamelContextReloadFailureEvent;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** CAMEL-24851: a YAML file that did not load gets the validator's report, 
which says what to write. */
+class YamlLoadFailureReportTest {
+
+    @TempDir
+    Path dir;
+
+    private static final String BAD_ROUTE = """
+            - route:
+                from:
+                  uri: timer:tick
+                  steps:
+                    - pollEnrich:
+                        uri: file:./order.json
+                    - to:
+                        uri: log:done
+            """;
+
+    @Test
+    void aDeserializationErrorIsAYamlLoadFailure() {
+        Exception loader
+                = new YamlDeserializationException("Error constructing YAML 
node id: pollEnrich: unsupported field: uri");
+        assertThat(YamlLoadFailureReport.isYamlLoadFailure(new 
RuntimeCamelException("Error starting Camel", loader))).isTrue();
+        assertThat(
+                YamlLoadFailureReport.isYamlLoadFailure(new 
RuntimeCamelException("Error pre-parsing resource: file:x.yaml")))
+                .isTrue();
+        assertThat(YamlLoadFailureReport.isYamlLoadFailure(new 
IllegalArgumentException("Invalid directory: archived/${x}")))
+                .isFalse();
+    }
+
+    @Test
+    void theReportSaysWhatToWrite() throws Exception {
+        Path route = dir.resolve("orders.camel.yaml");
+        Files.writeString(route, BAD_ROUTE);
+        List<String> lines = YamlLoadFailureReport.report(List.of(route));
+        assertThat(lines).hasSize(4);
+        assertThat(lines.get(0)).isEqualTo("The route file did not load. camel 
validate yaml says what to write:");
+        assertThat(lines.get(1)).isEqualTo("  orders.camel.yaml:");
+        // the schema message (with the CAMEL-24850 hint once it is in: "write 
pollEnrich: {expression: ...}")
+        assertThat(lines.get(2)).startsWith("    
").contains("pollEnrich").contains("'uri'");
+        assertThat(lines.get(3)).startsWith("  (camel validate yaml <file> for 
the full report");
+    }
+
+    @Test
+    void aFileTheValidatorAcceptsGivesNoReport() throws Exception {
+        Path route = dir.resolve("ok.camel.yaml");
+        Files.writeString(route, BAD_ROUTE.replace("uri: file:./order.json",
+                "expression:\n              constant:\n                
expression: \"file:./order.json\""));
+        assertThat(YamlLoadFailureReport.report(List.of(route))).isEmpty();
+    }
+
+    @Test
+    void theRunFilesAreFilteredToLocalYaml() throws Exception {
+        Path route = dir.resolve("a.yaml");
+        Files.writeString(route, BAD_ROUTE);
+        List<Path> files
+                = YamlLoadFailureReport.yamlFiles(List.of("file:" + route, 
route.toString(), dir.resolve("Foo.java").toString(),
+                        "github:apache:camel:x.yaml", 
dir.resolve("missing.yaml").toString()));
+        assertThat(files).containsExactly(route, route);

Review Comment:
   This asserts that the same file listed twice (`file:` form and plain) is 
returned twice, which would print the report block twice. `Run` de-duplicates 
`files` (line 842) so it does not happen in practice, but a `distinct()` in 
`yamlFiles` would make the helper self-contained and let this assert 
`containsExactly(route)`, which reads as the intent.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReport.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
+import org.apache.camel.impl.event.CamelContextReloadFailureEvent;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.support.SimpleEventNotifierSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * When a YAML route file fails to load, the loader's message says which node 
is wrong but not what to write; the schema
+ * validator has the message that does, with its hints. This runs the 
validator on the route files of a failed start (or
+ * of a failed reload in dev mode) and prints its report, so the hints reach 
everyone who runs, whether they validated
+ * first or not (CAMEL-24851).
+ */
+public final class YamlLoadFailureReport {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(YamlLoadFailureReport.class);
+
+    private static volatile CamelCatalog catalog;
+
+    private YamlLoadFailureReport() {
+    }
+
+    /** Whether the failure comes from loading a YAML route file: a 
deserialization error or a pre-parse error. */
+    public static boolean isYamlLoadFailure(Throwable failure) {
+        for (Throwable t = failure; t != null; t = t.getCause() == t ? null : 
t.getCause()) {
+            if (t instanceof YamlDeserializationException) {
+                return true;
+            }
+            String msg = t.getMessage();
+            if (msg != null && (msg.startsWith("Error pre-parsing resource") 
|| msg.contains("Error constructing YAML node")
+                    || msg.contains("Error parsing YAML"))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Logs the validator's report for the YAML files of a run whose start 
failed on loading one of them; nothing for
+     * any other failure. Logged (not printed) so that it is in the console 
and in the run's log file alike, where the
+     * camel-jbang-mcp tools read it, and before the loader's own error.
+     */
+    public static void logIfYamlLoadFailure(Throwable failure, List<String> 
files) {
+        if (!isYamlLoadFailure(failure)) {
+            return;
+        }
+        List<String> lines = report(yamlFiles(files));
+        if (!lines.isEmpty()) {
+            LOG.error(String.join(System.lineSeparator(), lines));
+        }
+    }
+
+    /**
+     * The validator's report for the YAML files, as lines to print: a header, 
then per file with problems its name and
+     * the messages, then a footer; empty when the validator finds nothing 
(then the loader's message is all there is).
+     */
+    public static List<String> report(List<Path> yamlFiles) {
+        List<String> lines = new ArrayList<>();
+        for (Path file : yamlFiles) {
+            if (!Files.isRegularFile(file)) {
+                continue;
+            }
+            List<String> errors;
+            try {
+                String content = Files.readString(file);
+                Path dir = file.toAbsolutePath().getParent();
+                errors = 
SourceValidator.validate(file.getFileName().toString(), content, catalog(), 
null, dir);
+            } catch (Exception e) {

Review Comment:
   Correct that a validator problem must never hide the loader's error, but a 
`LOG.debug("Cannot validate {}", file, e)` here would make a broken validator 
diagnosable (same for the `catch (Exception ignore)` in `KameletMain.doFail`).



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReport.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
+import org.apache.camel.impl.event.CamelContextReloadFailureEvent;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.support.SimpleEventNotifierSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * When a YAML route file fails to load, the loader's message says which node 
is wrong but not what to write; the schema
+ * validator has the message that does, with its hints. This runs the 
validator on the route files of a failed start (or
+ * of a failed reload in dev mode) and prints its report, so the hints reach 
everyone who runs, whether they validated
+ * first or not (CAMEL-24851).
+ */
+public final class YamlLoadFailureReport {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(YamlLoadFailureReport.class);
+
+    private static volatile CamelCatalog catalog;
+
+    private YamlLoadFailureReport() {
+    }
+
+    /** Whether the failure comes from loading a YAML route file: a 
deserialization error or a pre-parse error. */
+    public static boolean isYamlLoadFailure(Throwable failure) {
+        for (Throwable t = failure; t != null; t = t.getCause() == t ? null : 
t.getCause()) {
+            if (t instanceof YamlDeserializationException) {
+                return true;
+            }
+            String msg = t.getMessage();
+            if (msg != null && (msg.startsWith("Error pre-parsing resource") 
|| msg.contains("Error constructing YAML node")
+                    || msg.contains("Error parsing YAML"))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Logs the validator's report for the YAML files of a run whose start 
failed on loading one of them; nothing for
+     * any other failure. Logged (not printed) so that it is in the console 
and in the run's log file alike, where the
+     * camel-jbang-mcp tools read it, and before the loader's own error.
+     */
+    public static void logIfYamlLoadFailure(Throwable failure, List<String> 
files) {
+        if (!isYamlLoadFailure(failure)) {
+            return;
+        }
+        List<String> lines = report(yamlFiles(files));
+        if (!lines.isEmpty()) {
+            LOG.error(String.join(System.lineSeparator(), lines));
+        }
+    }
+
+    /**
+     * The validator's report for the YAML files, as lines to print: a header, 
then per file with problems its name and
+     * the messages, then a footer; empty when the validator finds nothing 
(then the loader's message is all there is).
+     */
+    public static List<String> report(List<Path> yamlFiles) {
+        List<String> lines = new ArrayList<>();
+        for (Path file : yamlFiles) {
+            if (!Files.isRegularFile(file)) {
+                continue;
+            }
+            List<String> errors;
+            try {
+                String content = Files.readString(file);
+                Path dir = file.toAbsolutePath().getParent();
+                errors = 
SourceValidator.validate(file.getFileName().toString(), content, catalog(), 
null, dir);
+            } catch (Exception e) {
+                continue;
+            }
+            if (!errors.isEmpty()) {
+                if (lines.isEmpty()) {
+                    lines.add("The route file did not load. camel validate 
yaml says what to write:");
+                }
+                lines.add("  " + file.getFileName() + ":");
+                for (String error : errors) {
+                    lines.add("    " + error);
+                }
+            }
+        }
+        if (!lines.isEmpty()) {
+            lines.add("  (camel validate yaml <file> for the full report; the 
loader's error follows)");
+        }
+        return lines;
+    }
+
+    /** The YAML route files among the files of a run: local files ending in 
.yaml or .yml. */
+    public static List<Path> yamlFiles(List<String> files) {
+        List<Path> answer = new ArrayList<>();
+        if (files == null) {
+            return answer;
+        }
+        for (String f : files) {
+            if (f == null) {
+                continue;
+            }
+            if (f.startsWith("file:")) {
+                f = f.substring(5);
+            }
+            if (f.contains(":")) {

Review Comment:
   `f.contains(":")` also skips Windows absolute paths (`C:\routes\a.yaml`), so 
those users silently get no report. `Run.java` (around line 1136) makes the 
same decision with `ResourceHelper.hasScheme(file) || 
file.startsWith("github:")`, which is drive-letter safe; reusing it keeps the 
two in sync (needs the `org.apache.camel.support.ResourceHelper` import).
   
   ```suggestion
               if (ResourceHelper.hasScheme(f) || f.startsWith("github:")) {
   ```



##########
dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java:
##########
@@ -173,8 +176,23 @@ public static void main(String... args) throws Exception {
         System.exit(code);
     }
 
+    /**
+     * Called with the exception when Camel fails to start, before the error 
is logged: the CLI uses it to report what
+     * the schema validator says about a YAML route file that did not load 
(CAMEL-24851).
+     */
+    public void setStartFailureListener(Consumer<Exception> 
startFailureListener) {
+        this.startFailureListener = startFailureListener;
+    }
+
     @Override
     protected void doFail(Exception e) {
+        if (startFailureListener != null) {
+            try {
+                startFailureListener.accept(e);
+            } catch (Exception ignore) {

Review Comment:
   Same as in `YamlLoadFailureReport.report`: swallowing is right, but a 
`LOG.debug` with the exception would help when the listener itself misbehaves.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to