This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25411 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 621bf9ed33849cfe0be27c8b82b17bfd84bf7056 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Oct 7 11:26:55 2026 +0200 CAMEL-25411: camel-jbang - Source editor: run the Kamelet checks the AI tools run 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}}. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../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)); + } +}
