package com.xfestudio.xfeservermanager.core.trigger;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/** Canonical schema-version 2 trigger program shared by visual and script front ends. */
public record TriggerProgramV2(
int schemaVersion,
List<EventBinding> events,
List<VariableDeclaration> declarations,
List<FunctionDeclaration> functions,
List<Statement> statements) {
public static final int SCHEMA_VERSION = 2;
public TriggerProgramV2 {
if (schemaVersion != SCHEMA_VERSION) {
throw new IllegalArgumentException("unsupported trigger schemaVersion: " + schemaVersion);
}
events = immutable(events, "event");
declarations = immutable(declarations, "declaration");
functions = immutable(functions, "function");
statements = immutable(statements, "statement");
if (events.isEmpty()) throw new IllegalArgumentException("a V2 program needs at least one event");
TriggerProgramValidator.validate(events, declarations, functions, statements);
}
public record EventBinding(UUID nodeId, String type, Map<String, String> configuration) {
public EventBinding {
nodeId = stable(nodeId);
type = id(type, "event type");
configuration = Map.copyOf(configuration == null ? Map.of() : configuration);
}
}
public record VariableDeclaration(
UUID nodeId,
String name,
String valueType,
TriggerExpression initialValue,
String visibility,
String storage,
Lifetime lifetime,
Long ttlSeconds) {
public VariableDeclaration {
nodeId = stable(nodeId);
name = id(name, "variable name");
if (!name.matches("[A-Za-z_][A-Za-z0-9_-]{0,47}")) {
throw new IllegalArgumentException("variable name must be a portable 1-48 character identifier");
}
valueType = TriggerValueTypes.normalizeVariableType(text(valueType, "variable type"));
if (initialValue == null && !valueType.startsWith("optional<")) {
throw new IllegalArgumentException("non-optional variable requires an initial value");
}
visibility = normalize(visibility, "trigger");
storage = normalize(storage, visibility.equals("global") ? "server" : "trigger");
lifetime = lifetime == null ? Lifetime.SESSION : lifetime;
if (lifetime == Lifetime.TTL) {
if (ttlSeconds == null || ttlSeconds < 1 || ttlSeconds > 31_536_000L) {
throw new IllegalArgumentException("ttl variable requires ttlSeconds between 1 and 31536000");
}
} else {
ttlSeconds = null;
}
}
}
public enum Lifetime { SESSION, TTL, PERSISTENT }
public record FunctionDeclaration(
UUID nodeId,
String name,
List<Parameter> parameters,
String returnType,
List<VariableDeclaration> locals,
List<Statement> statements) {
public FunctionDeclaration {
nodeId = stable(nodeId);
name = id(name, "function name");
parameters = immutable(parameters, "function parameter");
returnType = normalize(returnType, "void");
if (!returnType.equals("void")) returnType = TriggerValueTypes.normalizeVariableType(returnType);
locals = immutable(locals, "local variable");
statements = immutable(statements, "function statement");
}
}
public record Parameter(String name, String valueType, ParameterMode mode) {
public Parameter {
name = id(name, "parameter name");
valueType = TriggerValueTypes.normalizeVariableType(text(valueType, "parameter type"));
mode = mode == null ? ParameterMode.IN : mode;
}
}
public enum ParameterMode { IN, OUT, INOUT }
public record Statement(
UUID nodeId,
StatementKind kind,
String name,
Map<String, TriggerExpression> inputs,
TriggerExpression expression,
List<SwitchCase> cases,
List<Statement> statements,
List<Statement> elseStatements) {
public Statement {
nodeId = stable(nodeId);
Objects.requireNonNull(kind, "statement kind");
name = name == null ? "" : name.strip();
inputs = Map.copyOf(inputs == null ? Map.of() : inputs);
statements = immutable(statements, "child statement");
elseStatements = immutable(elseStatements, "else statement");
cases = immutable(cases, "switch case");
if ((kind == StatementKind.ACTION || kind == StatementKind.SET
|| kind == StatementKind.CALL || kind == StatementKind.FOREACH) && name.isBlank()) {
throw new IllegalArgumentException(kind.name().toLowerCase(Locale.ROOT) + " statement requires a name");
}
}
}
public enum StatementKind {
ACTION, SET, IF, SWITCH, REPEAT, WHILE, FOREACH,
BREAK, CONTINUE, CALL, RETURN, TRY
}
public record SwitchCase(UUID nodeId, TriggerExpression match, List<Statement> statements) {
public SwitchCase {
nodeId = stable(nodeId);
Objects.requireNonNull(match, "switch case match");
statements = immutable(statements, "switch case statement");
}
}
private static UUID stable(UUID value) {
return value == null ? UUID.randomUUID() : value;
}
private static String id(String value, String field) {
String result = text(value, field);
if (!result.matches("[A-Za-z_][A-Za-z0-9_.:-]{0,127}")) {
throw new IllegalArgumentException(field + " contains unsupported characters");
}
return result;
}
private static String text(String value, String field) {
if (value == null || value.isBlank()) throw new IllegalArgumentException(field + " is required");
return value.strip();
}
private static String normalize(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.strip().toLowerCase(Locale.ROOT);
}
private static <T> List<T> immutable(List<T> values, String element) {
List<T> result = List.copyOf(values == null ? List.of() : values);
if (result.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException(element + " must not be null");
}
return result;
}
}
package com.xfestudio.xfeservermanager.core.trigger;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/** Canonical schema-version 2 trigger program shared by visual and script front ends. */
public record TriggerProgramV2(
int schemaVersion,
List<EventBinding> events,
List<VariableDeclaration> declarations,
List<FunctionDeclaration> functions,
List<Statement> statements) {
public static final int SCHEMA_VERSION = 2;
public TriggerProgramV2 {
if (schemaVersion != SCHEMA_VERSION) {
throw new IllegalArgumentException("unsupported trigger schemaVersion: " + schemaVersion);
}
events = immutable(events, "event");
declarations = immutable(declarations, "declaration");
functions = immutable(functions, "function");
statements = immutable(statements, "statement");
if (events.isEmpty()) throw new IllegalArgumentException("a V2 program needs at least one event");
TriggerProgramValidator.validate(events, declarations, functions, statements);
}
public record EventBinding(UUID nodeId, String type, Map<String, String> configuration) {
public EventBinding {
nodeId = stable(nodeId);
type = id(type, "event type");
configuration = Map.copyOf(configuration == null ? Map.of() : configuration);
}
}
public record VariableDeclaration(
UUID nodeId,
String name,
String valueType,
TriggerExpression initialValue,
String visibility,
String storage,
Lifetime lifetime,
Long ttlSeconds) {
public VariableDeclaration {
nodeId = stable(nodeId);
name = id(name, "variable name");
if (!name.matches("[A-Za-z_][A-Za-z0-9_-]{0,47}")) {
throw new IllegalArgumentException("variable name must be a portable 1-48 character identifier");
}
valueType = TriggerValueTypes.normalizeVariableType(text(valueType, "variable type"));
if (initialValue == null && !valueType.startsWith("optional<")) {
throw new IllegalArgumentException("non-optional variable requires an initial value");
}
visibility = normalize(visibility, "trigger");
storage = normalize(storage, visibility.equals("global") ? "server" : "trigger");
lifetime = lifetime == null ? Lifetime.SESSION : lifetime;
if (lifetime == Lifetime.TTL) {
if (ttlSeconds == null || ttlSeconds < 1 || ttlSeconds > 31_536_000L) {
throw new IllegalArgumentException("ttl variable requires ttlSeconds between 1 and 31536000");
}
} else {
ttlSeconds = null;
}
}
}
public enum Lifetime { SESSION, TTL, PERSISTENT }
public record FunctionDeclaration(
UUID nodeId,
String name,
List<Parameter> parameters,
String returnType,
List<VariableDeclaration> locals,
List<Statement> statements) {
public FunctionDeclaration {
nodeId = stable(nodeId);
name = id(name, "function name");
parameters = immutable(parameters, "function parameter");
returnType = normalize(returnType, "void");
if (!returnType.equals("void")) returnType = TriggerValueTypes.normalizeVariableType(returnType);
locals = immutable(locals, "local variable");
statements = immutable(statements, "function statement");
}
}
public record Parameter(String name, String valueType, ParameterMode mode) {
public Parameter {
name = id(name, "parameter name");
valueType = TriggerValueTypes.normalizeVariableType(text(valueType, "parameter type"));
mode = mode == null ? ParameterMode.IN : mode;
}
}
public enum ParameterMode { IN, OUT, INOUT }
public record Statement(
UUID nodeId,
StatementKind kind,
String name,
Map<String, TriggerExpression> inputs,
TriggerExpression expression,
List<SwitchCase> cases,
List<Statement> statements,
List<Statement> elseStatements) {
public Statement {
nodeId = stable(nodeId);
Objects.requireNonNull(kind, "statement kind");
name = name == null ? "" : name.strip();
inputs = Map.copyOf(inputs == null ? Map.of() : inputs);
statements = immutable(statements, "child statement");
elseStatements = immutable(elseStatements, "else statement");
cases = immutable(cases, "switch case");
if ((kind == StatementKind.ACTION || kind == StatementKind.SET
|| kind == StatementKind.CALL || kind == StatementKind.FOREACH) && name.isBlank()) {
throw new IllegalArgumentException(kind.name().toLowerCase(Locale.ROOT) + " statement requires a name");
}
}
}
public enum StatementKind {
ACTION, SET, IF, SWITCH, REPEAT, WHILE, FOREACH,
BREAK, CONTINUE, CALL, RETURN, TRY
}
public record SwitchCase(UUID nodeId, TriggerExpression match, List<Statement> statements) {
public SwitchCase {
nodeId = stable(nodeId);
Objects.requireNonNull(match, "switch case match");
statements = immutable(statements, "switch case statement");
}
}
private static UUID stable(UUID value) {
return value == null ? UUID.randomUUID() : value;
}
private static String id(String value, String field) {
String result = text(value, field);
if (!result.matches("[A-Za-z_][A-Za-z0-9_.:-]{0,127}")) {
throw new IllegalArgumentException(field + " contains unsupported characters");
}
return result;
}
private static String text(String value, String field) {
if (value == null || value.isBlank()) throw new IllegalArgumentException(field + " is required");
return value.strip();
}
private static String normalize(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.strip().toLowerCase(Locale.ROOT);
}
private static <T> List<T> immutable(List<T> values, String element) {
List<T> result = List.copyOf(values == null ? List.of() : values);
if (result.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException(element + " must not be null");
}
return result;
}
}