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