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

Reply via email to