Intuitive DSL for Java

Version 2.0.0 · src/test/java/ch/dbalabs/intuitivedsl/parser/SyntaxDiagnosticsTest.java

Git clone
git clone https://www.dbalabs.ch/git/intuitive-dsl-java.git

SyntaxDiagnosticsTest.java

/*
 * 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() {}
    }
}