davsclaus commented on code in PR #27998:
URL: https://github.com/apache/camel/pull/27998#discussion_r4302093759


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java:
##########
@@ -941,6 +996,259 @@ private static int[] uniqueTrimmedWindow(String content, 
String find) {
         return found;
     }
 
+    /** A line that is only a key, as "steps:" or "- log:": not distinctive. */
+    private static final Pattern BARE_KEY = 
Pattern.compile("^-?\\s*[\\w.-]+:\\s*$");
+
+    private static String fuzzyLine(String line) {
+        return line.strip().replaceAll("\\s+", " ").replace("\"", 
"").replace("'", "");
+    }
+
+    /**
+     * The window of the content that best matches the lines of find (their 
longest common subsequence), as {start
+     * offset, end offset, first line, last line}; null unless it holds a line 
of find that has a value and is unique in
+     * the content, has at least two lines of find, matches at least 60%, and 
no window apart from it matches nearly as
+     * well (CAMEL-25532).
+     */
+    static int[] fuzzyWindow(String content, String find) {
+        String[] lines = content.split("\n", -1);
+        List<String> norm = new ArrayList<>(lines.length);
+        for (String l : lines) {
+            norm.add(fuzzyLine(l));
+        }
+        List<String> want = 
find.lines().map(AuthoringTools::fuzzyLine).filter(l -> !l.isEmpty()).toList();
+        if (want.isEmpty()) {
+            return null;
+        }
+        Set<String> distinct = new LinkedHashSet<>();
+        for (String w : want) {
+            if (w.length() > 6 && !BARE_KEY.matcher(w).matches() && 
norm.stream().filter(w::equals).count() == 1) {

Review Comment:
   Fixed in e41d60e5e591: the occurrences of each line come from the same pass 
that normalizes the content, so the uniqueness check is a map lookup.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LenientRepairs.java:
##########
@@ -0,0 +1,573 @@
+/*
+ * 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.ai;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * CAMEL-25532: the YAML shapes a model writes for a Camel route that have one 
reading, repaired before a write is
+ * refused for them: an option at the column of its EIP, a plain value where 
an expression goes, a list where a map goes
+ * (and the other way round), a list item or a key one column off, an unquoted 
placeholder, an enum value in another
+ * case, and the names models use for an option that has another name (from 
the benchmark traces). Each repair is driven
+ * by the validator's message for that shape and changes only the lines it 
names; a name is never guessed by similarity.
+ * The caller validates the result again, and keeps it only when no new 
problem is left.
+ */
+final class LenientRepairs {
+
+    /** The repaired content and what was done, one line each ("line 13: 
steps: indented under split:"). */
+    record Result(String content, List<String> applied) {
+    }
+
+    /** A repair of one place of the content; a move changes the lines above 
it, so it goes alone. */
+    private record Repair(int line, boolean move, String description, Action 
action) {
+    }
+
+    @FunctionalInterface
+    private interface Action {
+        boolean apply(List<String> lines);
+    }
+
+    private LenientRepairs() {
+    }
+
+    private static final Pattern OPTION_AT_EIP = Pattern.compile(
+            "(\\w+): lines up with (\\w+):, so it is read as a second key of 
the - item; it is an option of \\2:");
+    private static final Pattern PLAIN_VALUE = Pattern.compile(
+            "^Line (\\d+): (\\w+): a plain value \\((.*)\\) found, an 
expression expected");
+    private static final Pattern PLAIN_EXPRESSION = Pattern.compile(
+            "^Line (\\d+): expression: a plain value \\((.*)\\) found, a 
language expected: .*write expression:"
+                                                                    + " 
\\{(\\w+): \\{expression: \"(.*)\"\\}\\}");
+    private static final Pattern ENUM = Pattern.compile(
+            "^Line (\\d+): (\\w+): does not have a value in the enumeration 
\\[([^\\]]*)\\]");
+    private static final Pattern LIST_FOR_MAP = Pattern.compile(
+            "^Line (\\d+): (otherwise|onFallback|doFinally): array found, 
object expected");
+    private static final Pattern MAP_FOR_LIST = Pattern.compile(
+            "^Line (\\d+): (get|post|put|delete|patch|head): object found, 
array expected");
+    private static final Pattern UNKNOWN_PROPERTY = Pattern.compile(
+            "^Line (\\d+): (\\w+): property '(\\w+)' is not defined in the 
schema");
+    private static final Pattern NO_ERROR_HANDLER = Pattern.compile(
+            "^Line (\\d+): noErrorHandler: boolean found, object expected");
+    private static final Pattern BEFORE_ROUTES = Pattern.compile(
+            "^Line (\\d+): (\\w+) must come before the routes: move this entry 
above the first - route: \\(line (\\d+)\\)");
+    private static final Pattern PLACEHOLDER = Pattern.compile(
+            "^line (\\d+): .* without quotes is read as a YAML map .*write the 
line as (.+)$");
+    private static final Pattern BRACES = Pattern.compile(
+            "^Line (\\d+): .*\\((\\{[^}]*\\}) without quotes is a YAML map");
+    private static final Pattern ITEM_COLUMN = Pattern.compile(
+            "^line (\\d+): this list item starts in column (\\d+), but the 
list that starts at line \\d+ has its items in"
+                                                               + " column 
(\\d+)");
+    private static final Pattern KEY_COLUMN = Pattern.compile(
+            "^line (\\d+): (\\S+) starts in column (\\d+), but the keys of the 
mapping that starts at line \\d+ are in"
+                                                              + " column 
(\\d+)");
+
+    /** How many columns a list item or key may be off to be moved: a slip, 
not another structure. */
+    private static final int MAX_SHIFT = 2;
+
+    /** body, header.foo, exchangeProperty.x, body.size(): a name the Simple 
language reads, written without ${ }. */
+    private static final Pattern BARE_NAME = 
Pattern.compile("^[A-Za-z_]\\w*(\\.[\\w\\[\\]()]+)*$");
+
+    private static boolean isBareName(String value) {
+        String v = value.strip();
+        return BARE_NAME.matcher(v).matches() && !"true".equalsIgnoreCase(v) 
&& !"false".equalsIgnoreCase(v)
+                && !"null".equals(v);
+    }
+
+    /** The names models write for an option, by EIP, and the option's name 
(seen in the benchmark traces). */
+    private static final Map<String, String> ALIASES = Map.ofEntries(
+            Map.entry("setHeader.headerName", "name"), 
Map.entry("setHeader.header", "name"),
+            Map.entry("setProperty.propertyName", "name"), 
Map.entry("setProperty.property", "name"),
+            Map.entry("setVariable.variableName", "name"), 
Map.entry("setVariable.variable", "name"),
+            Map.entry("removeHeader.headerName", "name"), 
Map.entry("removeProperty.propertyName", "name"),
+            Map.entry("removeVariable.variableName", "name"),
+            Map.entry("log.level", "loggingLevel"), Map.entry("log.logLevel", 
"loggingLevel"),
+            Map.entry("log.loggerName", "logName"),
+            Map.entry("onException.exceptionType", "exception"), 
Map.entry("onException.exceptions", "exception"));
+
+    /** The repairs for the messages that have one, applied to the content; 
null when none applies. */
+    static Result repair(String content, List<String> errors) {
+        List<String> lines = new ArrayList<>(Arrays.asList(content.split("\n", 
-1)));
+        List<Repair> repairs = new ArrayList<>();
+        for (String error : errors) {
+            repairs.addAll(repairsFor(error, lines));
+        }
+        if (repairs.isEmpty()) {
+            return null;
+        }
+        // a move changes the lines above it: alone, when nothing else is left 
to repair
+        List<Repair> local = repairs.stream().filter(r -> !r.move()).toList();
+        List<Repair> todo = local.isEmpty() ? List.of(repairs.get(0)) : new 
ArrayList<>(local);
+        // from the bottom, so the line numbers of the ones above stay right; 
one repair per line
+        todo = 
todo.stream().sorted(Comparator.comparingInt(Repair::line).reversed()).toList();
+        Set<Integer> done = new HashSet<>();
+        List<String> applied = new ArrayList<>();
+        for (Repair r : todo) {
+            if (done.add(r.line()) && r.action().apply(lines)) {
+                applied.add(0, r.description());
+            }
+        }
+        return applied.isEmpty() ? null : new Result(String.join("\n", lines), 
applied);
+    }
+
+    private static List<Repair> repairsFor(String error, List<String> lines) {
+        List<Repair> answer = new ArrayList<>();
+        Matcher m;
+        if (error.contains("lines up with") && error.startsWith("Line ")) {
+            int line = leadingLine(error);
+            Matcher o = OPTION_AT_EIP.matcher(error);
+            while (o.find()) {
+                String key = o.group(1);
+                String eip = o.group(2);
+                answer.add(new Repair(
+                        line, false, "line " + line + ": " + key + ": indented 
two spaces, as an option of " + eip + ":",
+                        ls -> indentOption(ls, line - 1, key)));
+            }
+            return answer;
+        }
+        if ((m = PLAIN_EXPRESSION.matcher(error)).find()) {
+            int line = Integer.parseInt(m.group(1));
+            String language = m.group(3);
+            String text = m.group(4).replace("\\\"", "\"");
+            if (!"simple".equals(language) && m.group(2).equals(text) && 
isBareName(text)) {
+                // expression: body - the text "body" or ${body}: not one 
reading (r9L-2 split the text "body")
+                return answer;
+            }
+            answer.add(new Repair(
+                    line, false, "line " + line + ": expression: written as " 
+ language + ":",
+                    ls -> wrapExpression(ls, line - 1, "expression", language, 
text)));
+        } else if ((m = PLAIN_VALUE.matcher(error)).find()) {
+            int line = Integer.parseInt(m.group(1));
+            String key = m.group(2);
+            String value = m.group(3);
+            if (isBareName(value)) {
+                return answer;
+            }
+            String language = value.contains("${") ? "simple" : "constant";
+            answer.add(new Repair(
+                    line, false, "line " + line + ": " + key + ": written as " 
+ language + ":",
+                    ls -> wrapExpression(ls, line - 1, key, language, null)));
+        } else if ((m = ENUM.matcher(error)).find()) {
+            int line = Integer.parseInt(m.group(1));
+            String key = m.group(2);
+            List<String> choices = new ArrayList<>();
+            Matcher q = Pattern.compile("\"([^\"]+)\"").matcher(m.group(3));

Review Comment:
   Fixed in e41d60e5e591: it is a static `QUOTED` pattern now, next to the 
other patterns of the class.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to