gnodet-bot commented on code in PR #27998:
URL: https://github.com/apache/camel/pull/27998#discussion_r4302014974
##########
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:
💡 **Nit:** `norm.stream().filter(w::equals).count()` scans all of `norm` for
each element of `want` — O(want × norm). A frequency map built once from `norm`
would make the uniqueness check O(1) per element. Not a correctness issue (both
are small in practice), but worth noting.
##########
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:
💡 **Nit:** `Pattern.compile("\"([^\"]+)\"")` is compiled inline rather than
as a `private static final` field. The class already defines patterns as static
fields — hoisting this one would be more consistent and avoids repeated
compilation on each invocation.
--
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]