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 e1bdb2f124b4 CAMEL-25411: camel-jbang - Source editor: run the Kamelet 
checks the AI tools run (#27486)
e1bdb2f124b4 is described below

commit e1bdb2f124b4ee04747fb82527dc4fdf49747bdb
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Oct 7 11:43:31 2026 +0200

    CAMEL-25411: camel-jbang - Source editor: run the Kamelet checks the AI 
tools run (#27486)
    
    The source editor validated a YAML file with the schema, the endpoint checks
    and the simple checks, but not the Kamelet checks of camel_validate_source:
    the shape of a Kamelet file (properties under spec, a template that starts
    from the Kamelet itself) and the kamelet: endpoints of a route against the
    catalog and the project's own Kamelet files (an unknown property, a required
    one left out). They are now marked on load, while typing and on save, where
    they keep the file from being saved as the other problems of a YAML file do.
    
    The dependency notes of a Kamelet file (CAMEL-25403) are marked and said
    when saved, without blocking the save. In a Kamelet's template, a property
    written as ${header.tag} gets the hint that it is the placeholder {{tag}}.
    
    
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../jbang/core/commands/tui/SourceEditAssist.java  |  28 ++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     |   9 +-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  44 +++++
 .../commands/tui/SourceKameletValidationTest.java  | 183 +++++++++++++++++++++
 4 files changed, 263 insertions(+), 1 deletion(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
index 8a687a7227e6..3a9c306f10d3 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
@@ -28,6 +28,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -38,6 +39,7 @@ import java.util.regex.Pattern;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.KameletChecks;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteAssist;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteNodes;
 import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
@@ -1732,6 +1734,32 @@ final class SourceEditAssist {
         return SourceValidator.validateYamlEndpoints(content, 
validationCatalog());
     }
 
+    /**
+     * The Kamelet checks of a YAML file, as camel_validate_source runs them 
(CAMEL-25411): the shape of a Kamelet file,
+     * then the kamelet: endpoints against the catalog and the Kamelets of the 
file's directory. A Kamelet no catalog or
+     * project has yet is not reported, as on a write: it may be the project's 
own one, not written yet.
+     */
+    static List<String> validateKamelets(Path file, String content) {
+        List<String> msgs = new ArrayList<>();
+        if (isKameletFile(file)) {
+            msgs.addAll(KameletChecks.validateKameletFile(content));
+        }
+        if (msgs.isEmpty()) {
+            msgs.addAll(KameletChecks.validateYaml(content, 
file.toAbsolutePath().getParent(), false));
+        }
+        return msgs;
+    }
+
+    /** The notes of a Kamelet file (CAMEL-25403): a camel: dependency its 
template does not use, and camel:core. */
+    static List<String> kameletNotes(Path file, String content) {
+        return isKameletFile(file) ? KameletChecks.unusedDependencies(content) 
: List.of();
+    }
+
+    static boolean isKameletFile(Path file) {
+        String name = file.getFileName().toString().toLowerCase(Locale.ROOT);
+        return name.endsWith(".kamelet.yaml") || name.endsWith(".kamelet.yml");
+    }
+
     /**
      * The Camel checks of a Java or XML DSL route file (CAMEL-25208): the 
endpoint uris and simple expressions the
      * compiler cannot see, as "Line N: message". The endpoints no route 
consumes are left to camel validate: the editor
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 3e0e58454bdc..966f04171377 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -31,6 +31,7 @@ import java.util.Comparator;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Supplier;
@@ -59,6 +60,7 @@ import dev.tamboui.widgets.paragraph.Paragraph;
 import dev.tamboui.widgets.scrollbar.Scrollbar;
 import dev.tamboui.widgets.scrollbar.ScrollbarState;
 import org.apache.camel.dsl.jbang.core.commands.RouteDslConverter;
+import org.apache.camel.dsl.jbang.core.commands.ai.KameletChecks;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
@@ -908,7 +910,12 @@ class SourceTab extends AbstractTab {
                 
sourceViewer.setAutocompleteProvider(assist::provideYamlKeyCompletions);
                 
sourceViewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions);
                 
sourceViewer.setEndpointValidator(assist::validateYamlEndpoints);
-                sourceViewer.setSimpleValidator(assist::validateYamlSimple);
+                // in a Kamelet's template, a property written as 
${header.tag} is said to be the placeholder {{tag}}
+                sourceViewer.setSimpleValidator(content -> 
KameletChecks.withTemplateHints(
+                        name.toLowerCase(Locale.ROOT), content, 
assist.validateYamlSimple(content)));
+                // the shape of a Kamelet file and the kamelet: endpoints of a 
route (CAMEL-25411)
+                sourceViewer.setKameletValidator(content -> 
SourceEditAssist.validateKamelets(filePath, content));
+                sourceViewer.setKameletNotes(content -> 
SourceEditAssist.kameletNotes(filePath, content));
                 
sourceViewer.setListItemNodeChecker(assist::isListChildrenNode);
                 
sourceViewer.setEditQuickDocProvider(withProjectDocs(assist::provideEditQuickDoc));
             } else {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index ecbd318dfc39..ffe17de3f105 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -215,6 +215,9 @@ class SourceViewer {
     private PropertiesValidator propertiesValidator;
     private EndpointValidator endpointValidator;
     private EndpointValidator simpleValidator;
+    // the Kamelet checks of a YAML file (CAMEL-25411): its problems, and the 
notes that do not block a save
+    private EndpointValidator kameletValidator;
+    private EndpointValidator kameletNotes;
     private EndpointValidator routeValidator;
     private LiveRunData liveRunData;
     private LineFailures lineFailures;
@@ -313,6 +316,23 @@ class SourceViewer {
         this.simpleValidator = simpleValidator;
     }
 
+    /**
+     * The Kamelet checks of a YAML file (CAMEL-25411): the shape of a Kamelet 
file, and the kamelet: endpoints of a
+     * route against the catalog and the project's own Kamelets. Their 
problems are marked and keep the file from being
+     * saved, as the other problems of a YAML file.
+     */
+    void setKameletValidator(EndpointValidator kameletValidator) {
+        this.kameletValidator = kameletValidator;
+    }
+
+    /**
+     * What a Kamelet file does that works but is not right (a camel: 
dependency its template does not use): marked and
+     * said when saved, but not blocking the save.
+     */
+    void setKameletNotes(EndpointValidator kameletNotes) {
+        this.kameletNotes = kameletNotes;
+    }
+
     /**
      * The Camel checks of a Java or XML DSL route file (CAMEL-25208): its 
problems are marked on their lines while
      * editing. An XML file with problems is not saved, as a YAML file; a Java 
file is saved and the problems are said,
@@ -523,6 +543,8 @@ class SourceViewer {
         propertiesValidator = null;
         endpointValidator = null;
         simpleValidator = null;
+        kameletValidator = null;
+        kameletNotes = null;
         routeValidator = null;
         uriCompletion = null;
         simpleCompletion = null;
@@ -568,6 +590,8 @@ class SourceViewer {
         propertiesValidator = null;
         endpointValidator = null;
         simpleValidator = null;
+        kameletValidator = null;
+        kameletNotes = null;
         routeValidator = null;
         uriCompletion = null;
         simpleCompletion = null;
@@ -2050,12 +2074,21 @@ class SourceViewer {
                     msgs.addAll(simpleErrors);
                 }
             }
+            addAll(msgs, kameletValidator, content);
             if (!msgs.isEmpty()) {
                 validationErrors = msgs;
                 validationErrorScroll = 0;
                 inlineErrors = buildInlineErrors(msgs, content);
                 return;
             }
+            List<String> notes = new ArrayList<>();
+            addAll(notes, kameletNotes, content);
+            if (!notes.isEmpty()) {
+                // marked, and said when saved, but not blocking the save
+                routeProblems = notes;
+                inlineErrors = buildInlineErrors(notes, content);
+                return;
+            }
         } else if (validateOnSave && isPropertiesFile() && propertiesValidator 
!= null) {
             List<String> msgs = validateProperties(content);
             if (!msgs.isEmpty()) {
@@ -2268,6 +2301,8 @@ class SourceViewer {
                     msgs.addAll(simpleErrors);
                 }
             }
+            addAll(msgs, kameletValidator, content);
+            addAll(msgs, kameletNotes, content);
         } else if (isPropertiesFile() && propertiesValidator != null) {
             msgs.addAll(validateProperties(content));
         } else if (routeValidator != null) {
@@ -2279,6 +2314,15 @@ class SourceViewer {
         return msgs;
     }
 
+    private static void addAll(List<String> msgs, EndpointValidator validator, 
String content) {
+        if (validator != null) {
+            List<String> found = validator.validate(content);
+            if (found != null) {
+                msgs.addAll(found);
+            }
+        }
+    }
+
     /** The problems of the file just loaded, by line; none when the checks 
fail or do not apply. */
     private Map<Integer, String> problemsOnLoad(String content) {
         if (isMarkdownFile) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
new file mode 100644
index 000000000000..67b4620f950f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.tui;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The Kamelet checks in the Source tab (CAMEL-25411), as 
camel_validate_source runs them: the shape of a Kamelet file,
+ * the kamelet: endpoints of a route against the project's own Kamelets, and 
the dependency notes that do not block a
+ * save. The Kamelet is the one a local model wrote in the Kamelet side check 
of the benchmark.
+ */
+class SourceKameletValidationTest {
+
+    private static final String KAMELET = """
+            apiVersion: camel.apache.org/v1
+            kind: Kamelet
+            metadata:
+              name: tag-order-action
+              labels:
+                camel.apache.org/kamelet.type: action
+            spec:
+              definition:
+                title: Tag Order Action
+                required:
+                  - tag
+                properties:
+                  tag:
+                    title: Tag
+                    type: string
+              dependencies:
+                - "camel:kamelet"
+              template:
+                from:
+                  uri: kamelet:source
+                  steps:
+                    - setBody:
+                        expression:
+                          simple:
+                            expression: "${body} [{{tag}}]"
+            """;
+
+    private static final String ROUTE = """
+            - route:
+                from:
+                  uri: timer:tick
+                  steps:
+                    - setBody:
+                        constant: "Order ORD-5"
+                    - to:
+                        uri: kamelet:tag-order-action
+                        parameters:
+                          tagg: priority
+            """;
+
+    @TempDir
+    Path tempDir;
+
+    private final AtomicReference<String> lastNotification = new 
AtomicReference<>();
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+    }
+
+    @Test
+    void theShapeOfAKameletFileIsChecked() {
+        Path file = tempDir.resolve("tag-order-action.kamelet.yaml");
+        assertThat(SourceEditAssist.validateKamelets(file, KAMELET)).isEmpty();
+        assertThat(SourceEditAssist.validateKamelets(file,
+                KAMELET.replace("uri: kamelet:source", "uri: 
kamelet:tag-order-action")))
+                .singleElement().asString().contains("entered from 
kamelet:source");
+        String propertiesUnderSpec = KAMELET.replace("""
+                    required:
+                      - tag
+                    properties:
+                """, """
+                  properties:
+                """);
+        assertThat(SourceEditAssist.validateKamelets(file, 
propertiesUnderSpec))
+                .anySatisfy(m -> assertThat(m).contains("spec.properties is 
not a key of a Kamelet"));
+    }
+
+    @Test
+    void theKameletOfARouteIsCheckedAgainstTheProjectFile() throws Exception {
+        Files.writeString(tempDir.resolve("tag-order-action.kamelet.yaml"), 
KAMELET, StandardCharsets.UTF_8);
+        List<String> errors = 
SourceEditAssist.validateKamelets(tempDir.resolve("orders.camel.yaml"), ROUTE);
+        assertThat(errors).anySatisfy(m -> assertThat(m).contains("unknown 
property 'tagg'"));
+        assertThat(errors).anySatisfy(m -> assertThat(m).contains("the 
required property tag is missing"));
+        
assertThat(SourceEditAssist.validateKamelets(tempDir.resolve("orders.camel.yaml"),
+                ROUTE.replace("tagg:", "tag:"))).isEmpty();
+    }
+
+    @Test
+    void aKameletFileWithAProblemIsMarkedOnLoadAndNotSaved() throws Exception {
+        Path file = tempDir.resolve("tag-order-action.kamelet.yaml");
+        String self = KAMELET.replace("uri: kamelet:source", "uri: 
kamelet:tag-order-action");
+        Files.writeString(file, self, StandardCharsets.UTF_8);
+        SourceViewer viewer = viewer(file);
+        viewer.loadFile(file);
+        int fromLine = lineOf(self, "uri: kamelet:tag-order-action");
+        assertThat(viewer.viewErrors()).containsOnlyKeys(fromLine);
+
+        viewer.enterEditMode();
+        appendSpaceToLine(viewer, 1);
+        viewer.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.CTRL));
+        assertThat(Files.readString(file, 
StandardCharsets.UTF_8)).isEqualTo(self);
+        assertThat(viewer.inlineErrors()).containsOnlyKeys(fromLine);
+    }
+
+    @Test
+    void anUnusedDependencyIsMarkedButTheFileIsSaved() throws Exception {
+        Path file = tempDir.resolve("tag-order-action.kamelet.yaml");
+        String timer = KAMELET.replace("    - \"camel:kamelet\"\n", "    - 
\"camel:kamelet\"\n    - \"camel:timer\"\n");
+        Files.writeString(file, timer, StandardCharsets.UTF_8);
+        SourceViewer viewer = viewer(file);
+        viewer.loadFile(file);
+        int timerLine = lineOf(timer, "\"camel:timer\"");
+        assertThat(viewer.viewErrors()).containsOnlyKeys(timerLine);
+
+        viewer.enterEditMode();
+        appendSpaceToLine(viewer, 1);
+        viewer.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.CTRL));
+        assertThat(Files.readString(file, 
StandardCharsets.UTF_8)).contains("kind: Kamelet ");
+        assertThat(lastNotification.get()).startsWith("Saved: 
tag-order-action.kamelet.yaml with 1 Camel problem: ")
+                .contains("camel:timer is not used by the template");
+        assertThat(viewer.inlineErrors()).containsOnlyKeys(timerLine);
+    }
+
+    private SourceViewer viewer(Path file) {
+        SourceViewer viewer = new SourceViewer();
+        viewer.setNotificationCallback((msg, error) -> 
lastNotification.set(msg));
+        viewer.setKameletValidator(content -> 
SourceEditAssist.validateKamelets(file, content));
+        viewer.setKameletNotes(content -> SourceEditAssist.kameletNotes(file, 
content));
+        return viewer;
+    }
+
+    /** The line, from 0, that has the text. */
+    private static int lineOf(String content, String text) {
+        List<String> lines = List.of(content.split("\n"));
+        for (int i = 0; i < lines.size(); i++) {
+            if (lines.get(i).contains(text)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /** Moves to the line (from 0) and types a space at its end, a change that 
keeps the YAML as it was. */
+    private static void appendSpaceToLine(SourceViewer viewer, int line) {
+        for (int i = 0; i < line; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
+        }
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.END, KeyModifiers.NONE));
+        viewer.handleKeyEvent(KeyEvent.ofChar(' ', KeyModifiers.NONE));
+    }
+}

Reply via email to