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 16ff7c6aadda CAMEL-24372: Validate Simple expressions on save in TUI
editor
16ff7c6aadda is described below
commit 16ff7c6aaddaf811c0500471234723bc2fb00a9b
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Aug 10 21:42:05 2026 +0200
CAMEL-24372: Validate Simple expressions on save in TUI editor
Add Simple language validation to the TUI source editor's validate-on-save
feature. Detects simple: expressions in YAML DSL, determines predicate vs
expression context from the parent EIP, and validates using the CamelCatalog
API. Also validates log message: fields which use Simple implicitly.
CAMEL-24372: Fix Simple validation for list-item and stale source panel
Handle YAML list-item prefix (- simple:) in Simple expression
validation. Fix Source tab showing stale content when switching
between different integrations by checking PID on tab selection.
CAMEL-24372: Validate before saving, not after
Move validation before the file write in both saveEdit and
saveContinueEdit so the file is not written to disk when
validation fails.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/SourceTab.java | 121 ++++++++++++-
.../dsl/jbang/core/commands/tui/SourceViewer.java | 24 ++-
.../commands/tui/YamlSimpleValidationTest.java | 197 +++++++++++++++++++++
3 files changed, 336 insertions(+), 6 deletions(-)
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 4c56aea0bd9d..71aebf8fc91d 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
@@ -62,6 +62,7 @@ import dev.tamboui.widgets.scrollbar.ScrollbarState;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.ConfigurationPropertiesValidationResult;
import org.apache.camel.catalog.EndpointValidationResult;
+import org.apache.camel.catalog.LanguageValidationResult;
import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
import org.apache.camel.tooling.model.BaseOptionModel;
import org.apache.camel.tooling.model.ComponentModel;
@@ -161,8 +162,13 @@ class SourceTab extends AbstractTab {
@Override
public void onTabSelected() {
- lastSeenPid = ctx.selectedPid;
- refreshFiles();
+ if (ctx.selectedPid != null && !ctx.selectedPid.equals(lastSeenPid)) {
+ lastSeenPid = ctx.selectedPid;
+ onIntegrationChanged();
+ } else {
+ lastSeenPid = ctx.selectedPid;
+ refreshFiles();
+ }
}
@Override
@@ -672,6 +678,7 @@ class SourceTab extends AbstractTab {
sourceViewer.setAutocompleteProvider(this::provideYamlKeyCompletions);
sourceViewer.setAutocompleteValueProvider(this::provideYamlValueCompletions);
sourceViewer.setEndpointValidator(this::validateYamlEndpoints);
+
sourceViewer.setSimpleValidator(this::validateYamlSimple);
sourceViewer.setListItemNodeChecker(this::isListChildrenNode);
} else {
sourceViewer.setAutocompleteProvider(null);
@@ -1707,6 +1714,115 @@ class SourceTab extends AbstractTab {
return doValidateYamlEndpoints(content, catalog);
}
+ private List<String> validateYamlSimple(String content) {
+ CamelCatalog catalog = getCatalog();
+ if (catalog == null) {
+ return List.of();
+ }
+ return doValidateYamlSimple(content, catalog);
+ }
+
+ private static final Set<String> PREDICATE_EIPS = Set.of(
+ "filter", "when", "validate", "onWhen", "on-when",
+ "handled", "continued", "retryWhile", "retry-while",
+ "completionPredicate", "completion-predicate",
+ "completion", "loopDoWhile", "loop-do-while");
+
+ static List<String> doValidateYamlSimple(String content, CamelCatalog
catalog) {
+ List<String> errors = new ArrayList<>();
+ String[] lines = content.split("\n", -1);
+
+ for (int i = 0; i < lines.length; i++) {
+ String line = lines[i];
+ if (line.isBlank()) {
+ continue;
+ }
+ String trimmed = line.trim();
+ if (trimmed.startsWith("#")) {
+ continue;
+ }
+
+ String simpleText = null;
+ int lineNum = i + 1;
+ int lineIndent = countLeadingSpaces(line);
+ boolean isLogMessage = false;
+
+ // Strip YAML list prefix for matching
+ String key = trimmed.startsWith("- ") ? trimmed.substring(2) :
trimmed;
+
+ // Match "simple: <value>" (inline shorthand)
+ if (key.startsWith("simple:") && !key.equals("simple:")) {
+ simpleText = extractYamlValue(key, "simple");
+ }
+ // Match "simple:" followed by "expression: <value>" on next line
+ else if (key.equals("simple:")) {
+ for (int j = i + 1; j < lines.length; j++) {
+ String next = lines[j].trim();
+ if (next.isBlank()) {
+ continue;
+ }
+ if (next.startsWith("expression:")) {
+ simpleText = extractYamlValue(next, "expression");
+ lineNum = j + 1;
+ }
+ break;
+ }
+ }
+ // Match "message: <value>" under log: EIP
+ else if (key.startsWith("message:") && !key.equals("message:")) {
+ String parentEip = findParentEip(lines, i, lineIndent);
+ if ("log".equals(parentEip)) {
+ simpleText = extractYamlValue(key, "message");
+ isLogMessage = true;
+ }
+ }
+
+ if (simpleText == null || simpleText.isEmpty()) {
+ continue;
+ }
+ // Skip placeholder-only expressions
+ if (simpleText.startsWith("{{") && simpleText.endsWith("}}")) {
+ continue;
+ }
+
+ // Determine predicate vs expression context
+ boolean predicate = false;
+ if (!isLogMessage) {
+ String parentEip = findParentEip(lines, i, lineIndent);
+ predicate = parentEip != null &&
PREDICATE_EIPS.contains(parentEip);
+ }
+
+ try {
+ LanguageValidationResult result = predicate
+ ? catalog.validateLanguagePredicate(null, "simple",
simpleText)
+ : catalog.validateLanguageExpression(null, "simple",
simpleText);
+ if (!result.isSuccess()) {
+ String error = result.getShortError() != null ?
result.getShortError() : result.getError();
+ if (error != null) {
+ errors.add("Line " + lineNum + ": Simple syntax error:
" + error);
+ }
+ }
+ } catch (Exception e) {
+ // best effort
+ }
+ }
+ return errors;
+ }
+
+ private static String findParentEip(String[] lines, int lineIdx, int
lineIndent) {
+ for (int j = lineIdx - 1; j >= 0; j--) {
+ String prev = lines[j];
+ if (prev.isBlank()) {
+ continue;
+ }
+ int prevIndent = countLeadingSpaces(prev);
+ if (prevIndent < lineIndent) {
+ return extractEipFromLine(prev.trim());
+ }
+ }
+ return null;
+ }
+
static List<String> doValidateYamlEndpoints(String content, CamelCatalog
catalog) {
List<String> errors = new ArrayList<>();
String[] lines = content.split("\n", -1);
@@ -2437,6 +2553,7 @@ class SourceTab extends AbstractTab {
sourceViewer.setAutocompleteProvider(this::provideYamlKeyCompletions);
sourceViewer.setAutocompleteValueProvider(this::provideYamlValueCompletions);
sourceViewer.setEndpointValidator(this::validateYamlEndpoints);
+
sourceViewer.setSimpleValidator(this::validateYamlSimple);
sourceViewer.setListItemNodeChecker(this::isListChildrenNode);
} else {
sourceViewer.setAutocompleteProvider(null);
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 8fb5db00a03d..e020f7f932f5 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
@@ -162,6 +162,7 @@ class SourceViewer {
private org.apache.camel.dsl.yaml.validator.YamlValidator yamlValidator;
private PropertiesValidator propertiesValidator;
private EndpointValidator endpointValidator;
+ private EndpointValidator simpleValidator;
private List<String> validationErrors;
private int validationErrorScroll;
private final SourceEditHistory editHistory = new SourceEditHistory();
@@ -215,6 +216,10 @@ class SourceViewer {
this.endpointValidator = endpointValidator;
}
+ void setSimpleValidator(EndpointValidator simpleValidator) {
+ this.simpleValidator = simpleValidator;
+ }
+
void hide() {
exitEditMode();
visible = false;
@@ -225,6 +230,7 @@ class SourceViewer {
editableFile = null;
propertiesValidator = null;
endpointValidator = null;
+ simpleValidator = null;
}
void reset() {
@@ -263,6 +269,7 @@ class SourceViewer {
editableFile = null;
propertiesValidator = null;
endpointValidator = null;
+ simpleValidator = null;
}
boolean isMarkdownMode() {
@@ -1761,12 +1768,12 @@ class SourceViewer {
}
try {
String content = editState.text();
- Files.writeString(editableFile, content, StandardCharsets.UTF_8);
- dirty = false;
validateAndNotify(content);
if (validationErrors != null) {
return;
}
+ Files.writeString(editableFile, content, StandardCharsets.UTF_8);
+ dirty = false;
Path path = editableFile;
boolean restoreMarkdownMode = markdownModeBeforeEdit;
editMode = false;
@@ -1787,11 +1794,15 @@ class SourceViewer {
}
try {
String content = editState.text();
+ validateAndNotify(content);
+ if (validationErrors != null) {
+ return;
+ }
Files.writeString(editableFile, content, StandardCharsets.UTF_8);
dirty = false;
originalEditText = content;
lineStatuses = null;
- validateAndNotify(content);
+ notifySave("Saved: " + editableFile.getFileName(), false);
} catch (IOException e) {
notifySave("Save failed: " + e.getMessage(), true);
}
@@ -1823,6 +1834,12 @@ class SourceViewer {
msgs.addAll(endpointErrors);
}
}
+ if (simpleValidator != null) {
+ List<String> simpleErrors = simpleValidator.validate(content);
+ if (simpleErrors != null) {
+ msgs.addAll(simpleErrors);
+ }
+ }
if (!msgs.isEmpty()) {
validationErrors = msgs;
validationErrorScroll = 0;
@@ -1836,7 +1853,6 @@ class SourceViewer {
return;
}
}
- notifySave("Saved: " + editableFile.getFileName(), false);
}
private List<String> validateProperties(String content) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlSimpleValidationTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlSimpleValidationTest.java
new file mode 100644
index 000000000000..eca66243fb3b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlSimpleValidationTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.util.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class YamlSimpleValidationTest {
+
+ private static CamelCatalog catalog;
+
+ @BeforeAll
+ static void loadCatalog() {
+ catalog = new DefaultCamelCatalog();
+ }
+
+ @Test
+ void validExpressionInSetBody() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - setBody:
+ simple: "${body}"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).isEmpty();
+ }
+
+ @Test
+ void invalidExpressionUnclosedBrace() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - setBody:
+ simple: "${body"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).hasSize(1);
+ assertThat(errors.get(0)).contains("Simple syntax error");
+ }
+
+ @Test
+ void validPredicateInFilter() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - filter:
+ simple: "${header.foo} == 'bar'"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).isEmpty();
+ }
+
+ @Test
+ void validExpandedForm() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - setBody:
+ expression:
+ simple:
+ expression: "${header.name}"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).isEmpty();
+ }
+
+ @Test
+ void invalidExpandedForm() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - setBody:
+ expression:
+ simple:
+ expression: "${body"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).hasSize(1);
+ assertThat(errors.get(0)).contains("Simple syntax error");
+ }
+
+ @Test
+ void placeholderOnlySkipped() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - setBody:
+ simple: "{{myPlaceholder}}"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).isEmpty();
+ }
+
+ @Test
+ void logMessageValidated() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - log:
+ message: "${body"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).hasSize(1);
+ assertThat(errors.get(0)).contains("Simple syntax error");
+ }
+
+ @Test
+ void validLogMessage() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - log:
+ message: "Order: ${body}"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).isEmpty();
+ }
+
+ @Test
+ void listItemSimpleWithExpression() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - choice:
+ when:
+ - simple:
+ expression: "${body} >X= 30"
+ steps:
+ - log: "big"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).hasSize(1);
+ assertThat(errors.get(0)).contains("Simple syntax error");
+ }
+
+ @Test
+ void listItemInlineSimple() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - choice:
+ when:
+ - simple: "${body} >= 30"
+ steps:
+ - log: "big"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).isEmpty();
+ }
+
+ @Test
+ void multipleErrors() {
+ String yaml = """
+ - from:
+ uri: timer:tick
+ steps:
+ - setBody:
+ simple: "${body"
+ - setHeader:
+ name: foo
+ simple: "${header.bar"
+ """;
+ List<String> errors = SourceTab.doValidateYamlSimple(yaml, catalog);
+ assertThat(errors).hasSizeGreaterThanOrEqualTo(2);
+ }
+}