/*
* This file is part of the Intuitive DSL project.
* Copyright (c) 2026 DBA Labs - Switzerland. All rights reserved.
*
* This program is dual-licensed under a commercial license and the AGPLv3.
* For commercial licensing, contact us at [email protected] or visit https://www.dbalabs.ch.
*
* AGPLv3 licensing:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 (19 November 2007).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.html>.
*/
package ch.dbalabs.intuitivedsl.parser;
import ch.dbalabs.intuitivedsl.annotation.Bind;
import ch.dbalabs.intuitivedsl.annotation.DslCommand;
import ch.dbalabs.intuitivedsl.core.ExecutionContext;
import ch.dbalabs.intuitivedsl.core.IntuitiveDslEngine;
import ch.dbalabs.intuitivedsl.exception.DslSyntaxException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Additional syntax diagnostics tests.
* The goal here is not just to fail, but to fail with the correct position,
* the correct expected symbols, and a useful rich message.
*/
class SyntaxDiagnosticsTest {
private IntuitiveDslEngine engine;
private Lexer lexer;
@BeforeEach
void setUp() {
engine = new IntuitiveDslEngine();
lexer = new Lexer();
}
@Test
void shouldMergeExpectedPossibilitiesAcrossCompetingCommandsAtSamePosition() {
engine.register(SendMailToCommand.class);
engine.register(SendMailFromCommand.class);
assertThatThrownBy(() -> engine.execute("SEND MAIL VIA bob ;", ExecutionContext.createDefault()))
.isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getUnexpectedToken()).isNotNull();
assertThat(ex.getUnexpectedToken().value()).isEqualTo("VIA");
assertThat(ex.getExpectedPossibilities()).contains("TO", "FROM");
assertThat(ex.getRichMessage()).contains("Unexpected token 'VIA'");
});
}
@Test
void shouldExposeExpectedMacroPlaceholderWhenMacroChoiceIsUnknown() {
engine.registerMacro("task", registry -> List.of("backup", "cleanup"));
engine.register(ExecuteTaskCommand.class);
assertThatThrownBy(() -> engine.execute("EXECUTE destroy_all ;", ExecutionContext.createDefault()))
.isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getExpectedPossibilities()).contains("<task>");
assertThat(ex.getUnexpectedToken()).isNotNull();
assertThat(ex.getUnexpectedToken().value()).isEqualTo("destroy_all");
});
}
@Test
void shouldReportClosingQuoteExpectationForUnclosedString() {
assertThatThrownBy(() -> lexer.tokenize("SET TITLE 'unterminated ;"))
.isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getExpectedPossibilities()).containsExactly("closing quote (')");
});
}
@Test
void shouldReportLiteralDotAsExpectedWhenDateSeparatorIsWrong() {
engine.register(DatePartsCommand.class);
assertThatThrownBy(() -> engine.execute("SET DATE 07 / 03 / 2026 ;", ExecutionContext.createDefault()))
.isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getUnexpectedToken()).isNotNull();
assertThat(ex.getUnexpectedToken().value()).isEqualTo("/");
assertThat(ex.getExpectedPossibilities()).contains("'.'");
});
}
@Test
void shouldPointRichMessageAtUnexpectedExtraToken() {
engine.register(PingCommand.class);
assertThatThrownBy(() -> engine.execute("PING extra ;", ExecutionContext.createDefault()))
.isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getUnexpectedToken()).isNotNull();
assertThat(ex.getUnexpectedToken().value()).isEqualTo("extra");
assertThat(ex.getRichMessage()).contains("PING extra ;");
assertThat(ex.getRichMessage()).contains("Expected: { ';' }");
assertThat(ex.getRichMessage()).contains("^");
});
}
@Test
void shouldExposeTargetPlaceholderWhenInputEndsTooEarly() {
engine.register(PingTargetCommand.class);
assertThatThrownBy(() -> engine.execute("PING", ExecutionContext.createDefault()))
.isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getExpectedPossibilities()).contains("<target>");
assertThat(ex.getRichMessage()).contains("Expected: { <target> }");
if (ex.getUnexpectedToken() != null) {
assertThat(ex.getUnexpectedToken().type()).isEqualTo(TokenType.EOF);
}
});
}
@DslCommand(name = "SEND MAIL TO", syntax = "SEND MAIL TO recipient ;")
public static class SendMailToCommand implements Runnable {
@Bind("recipient") String recipient;
@Override public void run() {}
}
@DslCommand(name = "SEND MAIL FROM", syntax = "SEND MAIL FROM sender ;")
public static class SendMailFromCommand implements Runnable {
@Bind("sender") String sender;
@Override public void run() {}
}
@DslCommand(name = "EXECUTE TASK", syntax = "EXECUTE ${task} ;")
public static class ExecuteTaskCommand implements Runnable {
@Bind("task") String task;
@Override public void run() {}
}
@DslCommand(name = "DATE PARTS", syntax = "SET DATE day . month . year ;")
public static class DatePartsCommand implements Runnable {
@Bind("day") String day;
@Bind("month") String month;
@Bind("year") String year;
@Override public void run() {}
}
@DslCommand(name = "PING", syntax = "PING ;")
public static class PingCommand implements Runnable {
@Override public void run() {}
}
@DslCommand(name = "PING TARGET", syntax = "PING target ;")
public static class PingTargetCommand implements Runnable {
@Bind("target") String target;
@Override public void run() {}
}
}