package com.xfestudio.xfeservermanager.core.trigger;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Compiler for the intentionally small, auditable XFE Script trigger DSL. */
public final class TriggerScriptCompiler {
private static final String CONDITION_BODY = "([A-Za-z0-9_.-]+)\\s+("
+ TriggerCatalog.OPERATORS.stream().map(Pattern::quote)
.collect(java.util.stream.Collectors.joining("|"))
+ ")(?:\\s+(.+))?";
private static final Pattern CONDITION = conditionPattern("when");
private static final Pattern ACTION_CONDITION = conditionPattern("if");
private static final Pattern PARAMETER = Pattern.compile(
"([A-Za-z][A-Za-z0-9_-]*)=(?:\\\"((?:\\\\.|[^\\\"])*)\\\"|'((?:\\\\.|[^'])*)'|([^\\s]+))");
private TriggerScriptCompiler() { }
public static TriggerDefinition.Program compile(String script) {
if (script == null || script.isBlank()) throw new IllegalArgumentException("script is empty");
if (script.length() > TriggerDefinition.MAX_SCRIPT_CHARACTERS) {
throw new IllegalArgumentException("script cannot exceed 65536 characters");
}
TriggerDefinition.TriggerEvent event = null;
TriggerDefinition.MatchMode mode = TriggerDefinition.MatchMode.ALL;
List<TriggerDefinition.Condition> conditions = new ArrayList<>();
List<TriggerDefinition.Action> actions = new ArrayList<>();
List<TriggerDefinition.VariableDefinition> variables = new ArrayList<>();
List<ArgumentDeclaration> argumentDeclarations = new ArrayList<>();
Deque<ActionBlock> actionBlocks = new ArrayDeque<>();
Map<String, String> eventConfiguration = new LinkedHashMap<>();
int lineNumber = 0;
for (String raw : script.lines().toList()) {
lineNumber++;
String line = raw.strip();
if (line.isEmpty() || line.startsWith("#") || line.startsWith("//")) continue;
try {
if (line.equalsIgnoreCase("end")) {
if (actionBlocks.isEmpty()) {
throw new IllegalArgumentException("end has no matching if");
}
ActionBlock block = actionBlocks.pop();
if (block.actions().isEmpty()) {
throw new IllegalArgumentException(
"if block from line " + block.lineNumber() + " needs at least one action");
}
addAction(actions, actionBlocks, new TriggerDefinition.Action(
TriggerActionTree.CONDITION_TYPE,
conditionParameters(block.condition()), block.actions()));
} else if (line.regionMatches(true, 0, "if ", 0, 3)) {
Matcher matcher = ACTION_CONDITION.matcher(line);
if (!matcher.matches()) throw new IllegalArgumentException("invalid if statement");
actionBlocks.push(new ActionBlock(condition(matcher), new ArrayList<>(), lineNumber));
} else if (line.regionMatches(true, 0, "on ", 0, 3)) {
requireTopLevel(actionBlocks, "on");
if (event != null) throw new IllegalArgumentException("only one on statement is allowed");
event = new TriggerDefinition.TriggerEvent(line.substring(3).strip(), Map.of());
} else if (line.regionMatches(true, 0, "set ", 0, 4)) {
requireTopLevel(actionBlocks, "set");
String[] pair = line.substring(4).strip().split("\\s*=\\s*", 2);
if (pair.length != 2) throw new IllegalArgumentException("set expects name=value");
eventConfiguration.put(pair[0].strip(), unquote(pair[1].strip()));
} else if (line.regionMatches(true, 0, "var ", 0, 4)) {
requireTopLevel(actionBlocks, "var");
String declaration = line.substring(4).strip();
int first = declaration.indexOf(' ');
int second = first < 0 ? -1 : declaration.indexOf(' ', first + 1);
if (first < 1) throw new IllegalArgumentException("var expects name type initialValue");
String name = declaration.substring(0, first).strip();
String type = second < 0 ? declaration.substring(first + 1).strip()
: declaration.substring(first + 1, second).strip();
String tail = second < 0 ? "" : declaration.substring(second + 1).strip();
if (tail.matches("(?is)^(?:initial|visibility|storage|lifetime|ttlSeconds)\\s*=.*")) {
Map<String, String> fields = parameters(tail);
for (String field : fields.keySet()) {
if (!Set.of("initial", "visibility", "storage", "lifetime", "ttlSeconds").contains(field)) {
throw new IllegalArgumentException("unsupported var field " + field);
}
}
Long ttlSeconds = fields.containsKey("ttlSeconds")
? Long.parseLong(fields.get("ttlSeconds")) : null;
variables.add(new TriggerDefinition.VariableDefinition(name, type,
fields.getOrDefault("initial", TriggerValueTypes.defaultVariableValue(type)),
fields.getOrDefault("visibility", "trigger"),
fields.getOrDefault("storage", "trigger"),
fields.getOrDefault("lifetime", "session"), ttlSeconds, 0L));
} else {
// v1 syntax: var <name> <type> <initialValue>
variables.add(new TriggerDefinition.VariableDefinition(name, type,
tail.isEmpty() ? "" : unquote(tail)));
}
} else if (line.regionMatches(true, 0, "arg ", 0, 4)) {
requireTopLevel(actionBlocks, "arg");
String declaration = line.substring(4).strip();
int first = declaration.indexOf(' ');
int second = first < 0 ? -1 : declaration.indexOf(' ', first + 1);
if (first < 1) throw new IllegalArgumentException("arg expects name type and optional fields");
String name = declaration.substring(0, first).strip();
String type = second < 0 ? declaration.substring(first + 1).strip()
: declaration.substring(first + 1, second).strip();
Map<String, String> fields = parameters(second < 0 ? "" : declaration.substring(second + 1));
argumentDeclarations.add(new ArgumentDeclaration(name, type, fields));
} else if (line.regionMatches(true, 0, "match ", 0, 6)) {
requireTopLevel(actionBlocks, "match");
mode = TriggerDefinition.MatchMode.valueOf(line.substring(6).strip().toUpperCase(java.util.Locale.ROOT));
} else if (line.regionMatches(true, 0, "when ", 0, 5)) {
requireTopLevel(actionBlocks, "when");
Matcher matcher = CONDITION.matcher(line);
if (!matcher.matches()) throw new IllegalArgumentException("invalid when statement");
conditions.add(condition(matcher));
} else if (line.regionMatches(true, 0, "do ", 0, 3)) {
String body = line.substring(3).strip();
int space = body.indexOf(' ');
String type = space < 0 ? body : body.substring(0, space);
String rawParameters = space < 0 ? "" : body.substring(space + 1);
addAction(actions, actionBlocks,
new TriggerDefinition.Action(type, parameters(rawParameters)));
} else {
throw new IllegalArgumentException("expected on, set, var, arg, match, when, if, do, or end");
}
} catch (RuntimeException exception) {
throw new IllegalArgumentException("script line " + lineNumber + ": " + exception.getMessage(), exception);
}
}
if (!actionBlocks.isEmpty()) {
ActionBlock block = actionBlocks.peek();
throw new IllegalArgumentException(
"script line " + block.lineNumber() + ": if block is not closed; expected end");
}
if (event == null) throw new IllegalArgumentException("script needs an on statement");
List<TriggerDefinition.CommandArgument> argumentTree = arguments(argumentDeclarations);
TriggerDefinition.Program program = new TriggerDefinition.Program(
new TriggerDefinition.TriggerEvent(event.type(), eventConfiguration, argumentTree, variables),
mode, conditions, actions);
TriggerActionTree.validateProgram(program);
return program;
}
private static Pattern conditionPattern(String keyword) {
return Pattern.compile("^" + keyword + "\\s+" + CONDITION_BODY + "$", Pattern.CASE_INSENSITIVE);
}
private static TriggerDefinition.Condition condition(Matcher matcher) {
return new TriggerDefinition.Condition(matcher.group(1), matcher.group(2),
matcher.group(3) == null ? "" : unquote(matcher.group(3).strip()));
}
private static Map<String, String> conditionParameters(TriggerDefinition.Condition condition) {
Map<String, String> result = new LinkedHashMap<>();
result.put("field", condition.field());
result.put("operator", condition.operator());
result.put("value", condition.value());
return result;
}
private static void addAction(
List<TriggerDefinition.Action> root,
Deque<ActionBlock> actionBlocks,
TriggerDefinition.Action action) {
if (actionBlocks.isEmpty()) root.add(action);
else actionBlocks.peek().actions().add(action);
}
private static void requireTopLevel(Deque<ActionBlock> actionBlocks, String statement) {
if (!actionBlocks.isEmpty()) {
throw new IllegalArgumentException(statement + " is not allowed inside an if block");
}
}
private static Map<String, String> parameters(String value) {
Map<String, String> result = new LinkedHashMap<>();
Matcher matcher = PARAMETER.matcher(value);
int end = 0;
while (matcher.find()) {
if (!value.substring(end, matcher.start()).isBlank()) {
throw new IllegalArgumentException("invalid action parameters");
}
String parameter = matcher.group(2) != null ? matcher.group(2)
: matcher.group(3) != null ? matcher.group(3) : matcher.group(4);
result.put(matcher.group(1), unescape(parameter));
end = matcher.end();
}
if (!value.substring(end).isBlank()) throw new IllegalArgumentException("invalid action parameters");
return result;
}
private static String unquote(String value) {
if (value.length() >= 2 && ((value.startsWith("\"") && value.endsWith("\""))
|| (value.startsWith("'") && value.endsWith("'")))) {
return unescape(value.substring(1, value.length() - 1));
}
return value;
}
private static String unescape(String value) {
StringBuilder result = new StringBuilder(value.length());
for (int index = 0; index < value.length(); index++) {
char current = value.charAt(index);
if (current != '\\' || index + 1 >= value.length()) {
result.append(current);
continue;
}
char escaped = value.charAt(++index);
switch (escaped) {
case 'n' -> result.append('\n');
case 'r' -> result.append('\r');
case '\"' -> result.append('\"');
case '\'' -> result.append('\'');
case '\\' -> result.append('\\');
default -> result.append('\\').append(escaped);
}
}
return result.toString();
}
private static List<TriggerDefinition.CommandArgument> arguments(List<ArgumentDeclaration> declarations) {
Map<String, ArgumentDeclaration> byName = new LinkedHashMap<>();
for (ArgumentDeclaration declaration : declarations) {
if (byName.putIfAbsent(declaration.name(), declaration) != null) {
throw new IllegalArgumentException("duplicate arg declaration " + declaration.name());
}
}
List<TriggerDefinition.CommandArgument> roots = new ArrayList<>();
for (ArgumentDeclaration declaration : declarations) {
if (declaration.fields().getOrDefault("parent", "").isBlank()) {
roots.add(argument(declaration, declarations, new java.util.LinkedHashSet<>()));
}
}
if (roots.isEmpty() && !declarations.isEmpty()) {
throw new IllegalArgumentException("arg declarations contain a parent cycle");
}
java.util.Set<String> reached = new java.util.LinkedHashSet<>();
collectArgumentNames(roots, reached);
if (reached.size() != declarations.size()) {
throw new IllegalArgumentException("arg parent is missing or cyclic");
}
return List.copyOf(roots);
}
private static TriggerDefinition.CommandArgument argument(
ArgumentDeclaration declaration, List<ArgumentDeclaration> declarations,
java.util.Set<String> ancestors) {
if (!ancestors.add(declaration.name())) throw new IllegalArgumentException("arg parent cycle");
List<TriggerDefinition.CommandArgument> children = declarations.stream()
.filter(value -> value.fields().getOrDefault("parent", "").equals(declaration.name()))
.map(value -> argument(value, declarations, new java.util.LinkedHashSet<>(ancestors))).toList();
Map<String, String> fields = declaration.fields();
java.util.Set<String> accepted = java.util.Set.of(
"parent", "literal", "optional", "error", "min", "max", "suggestions");
if (fields.keySet().stream().anyMatch(key -> !accepted.contains(key))) {
throw new IllegalArgumentException("arg contains an unsupported field");
}
List<String> suggestions = java.util.Arrays.stream(fields.getOrDefault("suggestions", "").split(","))
.map(String::strip).filter(value -> !value.isEmpty()).toList();
String optional = fields.getOrDefault("optional", "false").strip().toLowerCase(java.util.Locale.ROOT);
if (!java.util.Set.of("true", "false").contains(optional)) {
throw new IllegalArgumentException("arg optional must be true or false");
}
return new TriggerDefinition.CommandArgument(declaration.name(), declaration.type(),
fields.getOrDefault("literal", ""),
Boolean.parseBoolean(optional),
fields.getOrDefault("error", ""), fields.getOrDefault("min", ""),
fields.getOrDefault("max", ""), suggestions, children);
}
private static void collectArgumentNames(
List<TriggerDefinition.CommandArgument> arguments, java.util.Set<String> result) {
for (TriggerDefinition.CommandArgument argument : arguments) {
result.add(argument.name());
collectArgumentNames(argument.children(), result);
}
}
private record ActionBlock(
TriggerDefinition.Condition condition,
List<TriggerDefinition.Action> actions,
int lineNumber) { }
private record ArgumentDeclaration(String name, String type, Map<String, String> fields) { }
}
package com.xfestudio.xfeservermanager.core.trigger;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Compiler for the intentionally small, auditable XFE Script trigger DSL. */
public final class TriggerScriptCompiler {
private static final String CONDITION_BODY = "([A-Za-z0-9_.-]+)\\s+("
+ TriggerCatalog.OPERATORS.stream().map(Pattern::quote)
.collect(java.util.stream.Collectors.joining("|"))
+ ")(?:\\s+(.+))?";
private static final Pattern CONDITION = conditionPattern("when");
private static final Pattern ACTION_CONDITION = conditionPattern("if");
private static final Pattern PARAMETER = Pattern.compile(
"([A-Za-z][A-Za-z0-9_-]*)=(?:\\\"((?:\\\\.|[^\\\"])*)\\\"|'((?:\\\\.|[^'])*)'|([^\\s]+))");
private TriggerScriptCompiler() { }
public static TriggerDefinition.Program compile(String script) {
if (script == null || script.isBlank()) throw new IllegalArgumentException("script is empty");
if (script.length() > TriggerDefinition.MAX_SCRIPT_CHARACTERS) {
throw new IllegalArgumentException("script cannot exceed 65536 characters");
}
TriggerDefinition.TriggerEvent event = null;
TriggerDefinition.MatchMode mode = TriggerDefinition.MatchMode.ALL;
List<TriggerDefinition.Condition> conditions = new ArrayList<>();
List<TriggerDefinition.Action> actions = new ArrayList<>();
List<TriggerDefinition.VariableDefinition> variables = new ArrayList<>();
List<ArgumentDeclaration> argumentDeclarations = new ArrayList<>();
Deque<ActionBlock> actionBlocks = new ArrayDeque<>();
Map<String, String> eventConfiguration = new LinkedHashMap<>();
int lineNumber = 0;
for (String raw : script.lines().toList()) {
lineNumber++;
String line = raw.strip();
if (line.isEmpty() || line.startsWith("#") || line.startsWith("//")) continue;
try {
if (line.equalsIgnoreCase("end")) {
if (actionBlocks.isEmpty()) {
throw new IllegalArgumentException("end has no matching if");
}
ActionBlock block = actionBlocks.pop();
if (block.actions().isEmpty()) {
throw new IllegalArgumentException(
"if block from line " + block.lineNumber() + " needs at least one action");
}
addAction(actions, actionBlocks, new TriggerDefinition.Action(
TriggerActionTree.CONDITION_TYPE,
conditionParameters(block.condition()), block.actions()));
} else if (line.regionMatches(true, 0, "if ", 0, 3)) {
Matcher matcher = ACTION_CONDITION.matcher(line);
if (!matcher.matches()) throw new IllegalArgumentException("invalid if statement");
actionBlocks.push(new ActionBlock(condition(matcher), new ArrayList<>(), lineNumber));
} else if (line.regionMatches(true, 0, "on ", 0, 3)) {
requireTopLevel(actionBlocks, "on");
if (event != null) throw new IllegalArgumentException("only one on statement is allowed");
event = new TriggerDefinition.TriggerEvent(line.substring(3).strip(), Map.of());
} else if (line.regionMatches(true, 0, "set ", 0, 4)) {
requireTopLevel(actionBlocks, "set");
String[] pair = line.substring(4).strip().split("\\s*=\\s*", 2);
if (pair.length != 2) throw new IllegalArgumentException("set expects name=value");
eventConfiguration.put(pair[0].strip(), unquote(pair[1].strip()));
} else if (line.regionMatches(true, 0, "var ", 0, 4)) {
requireTopLevel(actionBlocks, "var");
String declaration = line.substring(4).strip();
int first = declaration.indexOf(' ');
int second = first < 0 ? -1 : declaration.indexOf(' ', first + 1);
if (first < 1) throw new IllegalArgumentException("var expects name type initialValue");
String name = declaration.substring(0, first).strip();
String type = second < 0 ? declaration.substring(first + 1).strip()
: declaration.substring(first + 1, second).strip();
String tail = second < 0 ? "" : declaration.substring(second + 1).strip();
if (tail.matches("(?is)^(?:initial|visibility|storage|lifetime|ttlSeconds)\\s*=.*")) {
Map<String, String> fields = parameters(tail);
for (String field : fields.keySet()) {
if (!Set.of("initial", "visibility", "storage", "lifetime", "ttlSeconds").contains(field)) {
throw new IllegalArgumentException("unsupported var field " + field);
}
}
Long ttlSeconds = fields.containsKey("ttlSeconds")
? Long.parseLong(fields.get("ttlSeconds")) : null;
variables.add(new TriggerDefinition.VariableDefinition(name, type,
fields.getOrDefault("initial", TriggerValueTypes.defaultVariableValue(type)),
fields.getOrDefault("visibility", "trigger"),
fields.getOrDefault("storage", "trigger"),
fields.getOrDefault("lifetime", "session"), ttlSeconds, 0L));
} else {
// v1 syntax: var <name> <type> <initialValue>
variables.add(new TriggerDefinition.VariableDefinition(name, type,
tail.isEmpty() ? "" : unquote(tail)));
}
} else if (line.regionMatches(true, 0, "arg ", 0, 4)) {
requireTopLevel(actionBlocks, "arg");
String declaration = line.substring(4).strip();
int first = declaration.indexOf(' ');
int second = first < 0 ? -1 : declaration.indexOf(' ', first + 1);
if (first < 1) throw new IllegalArgumentException("arg expects name type and optional fields");
String name = declaration.substring(0, first).strip();
String type = second < 0 ? declaration.substring(first + 1).strip()
: declaration.substring(first + 1, second).strip();
Map<String, String> fields = parameters(second < 0 ? "" : declaration.substring(second + 1));
argumentDeclarations.add(new ArgumentDeclaration(name, type, fields));
} else if (line.regionMatches(true, 0, "match ", 0, 6)) {
requireTopLevel(actionBlocks, "match");
mode = TriggerDefinition.MatchMode.valueOf(line.substring(6).strip().toUpperCase(java.util.Locale.ROOT));
} else if (line.regionMatches(true, 0, "when ", 0, 5)) {
requireTopLevel(actionBlocks, "when");
Matcher matcher = CONDITION.matcher(line);
if (!matcher.matches()) throw new IllegalArgumentException("invalid when statement");
conditions.add(condition(matcher));
} else if (line.regionMatches(true, 0, "do ", 0, 3)) {
String body = line.substring(3).strip();
int space = body.indexOf(' ');
String type = space < 0 ? body : body.substring(0, space);
String rawParameters = space < 0 ? "" : body.substring(space + 1);
addAction(actions, actionBlocks,
new TriggerDefinition.Action(type, parameters(rawParameters)));
} else {
throw new IllegalArgumentException("expected on, set, var, arg, match, when, if, do, or end");
}
} catch (RuntimeException exception) {
throw new IllegalArgumentException("script line " + lineNumber + ": " + exception.getMessage(), exception);
}
}
if (!actionBlocks.isEmpty()) {
ActionBlock block = actionBlocks.peek();
throw new IllegalArgumentException(
"script line " + block.lineNumber() + ": if block is not closed; expected end");
}
if (event == null) throw new IllegalArgumentException("script needs an on statement");
List<TriggerDefinition.CommandArgument> argumentTree = arguments(argumentDeclarations);
TriggerDefinition.Program program = new TriggerDefinition.Program(
new TriggerDefinition.TriggerEvent(event.type(), eventConfiguration, argumentTree, variables),
mode, conditions, actions);
TriggerActionTree.validateProgram(program);
return program;
}
private static Pattern conditionPattern(String keyword) {
return Pattern.compile("^" + keyword + "\\s+" + CONDITION_BODY + "$", Pattern.CASE_INSENSITIVE);
}
private static TriggerDefinition.Condition condition(Matcher matcher) {
return new TriggerDefinition.Condition(matcher.group(1), matcher.group(2),
matcher.group(3) == null ? "" : unquote(matcher.group(3).strip()));
}
private static Map<String, String> conditionParameters(TriggerDefinition.Condition condition) {
Map<String, String> result = new LinkedHashMap<>();
result.put("field", condition.field());
result.put("operator", condition.operator());
result.put("value", condition.value());
return result;
}
private static void addAction(
List<TriggerDefinition.Action> root,
Deque<ActionBlock> actionBlocks,
TriggerDefinition.Action action) {
if (actionBlocks.isEmpty()) root.add(action);
else actionBlocks.peek().actions().add(action);
}
private static void requireTopLevel(Deque<ActionBlock> actionBlocks, String statement) {
if (!actionBlocks.isEmpty()) {
throw new IllegalArgumentException(statement + " is not allowed inside an if block");
}
}
private static Map<String, String> parameters(String value) {
Map<String, String> result = new LinkedHashMap<>();
Matcher matcher = PARAMETER.matcher(value);
int end = 0;
while (matcher.find()) {
if (!value.substring(end, matcher.start()).isBlank()) {
throw new IllegalArgumentException("invalid action parameters");
}
String parameter = matcher.group(2) != null ? matcher.group(2)
: matcher.group(3) != null ? matcher.group(3) : matcher.group(4);
result.put(matcher.group(1), unescape(parameter));
end = matcher.end();
}
if (!value.substring(end).isBlank()) throw new IllegalArgumentException("invalid action parameters");
return result;
}
private static String unquote(String value) {
if (value.length() >= 2 && ((value.startsWith("\"") && value.endsWith("\""))
|| (value.startsWith("'") && value.endsWith("'")))) {
return unescape(value.substring(1, value.length() - 1));
}
return value;
}
private static String unescape(String value) {
StringBuilder result = new StringBuilder(value.length());
for (int index = 0; index < value.length(); index++) {
char current = value.charAt(index);
if (current != '\\' || index + 1 >= value.length()) {
result.append(current);
continue;
}
char escaped = value.charAt(++index);
switch (escaped) {
case 'n' -> result.append('\n');
case 'r' -> result.append('\r');
case '\"' -> result.append('\"');
case '\'' -> result.append('\'');
case '\\' -> result.append('\\');
default -> result.append('\\').append(escaped);
}
}
return result.toString();
}
private static List<TriggerDefinition.CommandArgument> arguments(List<ArgumentDeclaration> declarations) {
Map<String, ArgumentDeclaration> byName = new LinkedHashMap<>();
for (ArgumentDeclaration declaration : declarations) {
if (byName.putIfAbsent(declaration.name(), declaration) != null) {
throw new IllegalArgumentException("duplicate arg declaration " + declaration.name());
}
}
List<TriggerDefinition.CommandArgument> roots = new ArrayList<>();
for (ArgumentDeclaration declaration : declarations) {
if (declaration.fields().getOrDefault("parent", "").isBlank()) {
roots.add(argument(declaration, declarations, new java.util.LinkedHashSet<>()));
}
}
if (roots.isEmpty() && !declarations.isEmpty()) {
throw new IllegalArgumentException("arg declarations contain a parent cycle");
}
java.util.Set<String> reached = new java.util.LinkedHashSet<>();
collectArgumentNames(roots, reached);
if (reached.size() != declarations.size()) {
throw new IllegalArgumentException("arg parent is missing or cyclic");
}
return List.copyOf(roots);
}
private static TriggerDefinition.CommandArgument argument(
ArgumentDeclaration declaration, List<ArgumentDeclaration> declarations,
java.util.Set<String> ancestors) {
if (!ancestors.add(declaration.name())) throw new IllegalArgumentException("arg parent cycle");
List<TriggerDefinition.CommandArgument> children = declarations.stream()
.filter(value -> value.fields().getOrDefault("parent", "").equals(declaration.name()))
.map(value -> argument(value, declarations, new java.util.LinkedHashSet<>(ancestors))).toList();
Map<String, String> fields = declaration.fields();
java.util.Set<String> accepted = java.util.Set.of(
"parent", "literal", "optional", "error", "min", "max", "suggestions");
if (fields.keySet().stream().anyMatch(key -> !accepted.contains(key))) {
throw new IllegalArgumentException("arg contains an unsupported field");
}
List<String> suggestions = java.util.Arrays.stream(fields.getOrDefault("suggestions", "").split(","))
.map(String::strip).filter(value -> !value.isEmpty()).toList();
String optional = fields.getOrDefault("optional", "false").strip().toLowerCase(java.util.Locale.ROOT);
if (!java.util.Set.of("true", "false").contains(optional)) {
throw new IllegalArgumentException("arg optional must be true or false");
}
return new TriggerDefinition.CommandArgument(declaration.name(), declaration.type(),
fields.getOrDefault("literal", ""),
Boolean.parseBoolean(optional),
fields.getOrDefault("error", ""), fields.getOrDefault("min", ""),
fields.getOrDefault("max", ""), suggestions, children);
}
private static void collectArgumentNames(
List<TriggerDefinition.CommandArgument> arguments, java.util.Set<String> result) {
for (TriggerDefinition.CommandArgument argument : arguments) {
result.add(argument.name());
collectArgumentNames(argument.children(), result);
}
}
private record ActionBlock(
TriggerDefinition.Condition condition,
List<TriggerDefinition.Action> actions,
int lineNumber) { }
private record ArgumentDeclaration(String name, String type, Map<String, String> fields) { }
}