Intuitive DSL for Java

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

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

AstNavigatorTest.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.grammar.GrammarCompiler;
import ch.dbalabs.intuitivedsl.grammar.GroupNode;
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.tuple;

/**
 * Comprehensive unit tests for the AstNavigator component.
 * Evaluates various business domains and creative grammar configurations,
 * including extreme edge cases for absolute reliability.
 *
 * @author DBA Labs
 */
class AstNavigatorTest {

    private GrammarCompiler compiler;
    private Lexer lexer;
    private AstNavigator navigator;

    @BeforeEach
    void setUp() {
        compiler = new GrammarCompiler();
        lexer = new Lexer();
        navigator = new AstNavigator();
    }

    @Test
    void shouldParseSmartHomeCommandWithOptionalAndRequiredGroups() {
        // Arrange
        GroupNode ast = compiler.compile("TURN { ON | OFF } [ALL] LIGHTS IN room_name ;");
        List<Token> tokens = lexer.tokenize("TURN OFF ALL LIGHTS IN master_bedroom ;");

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, macro -> List.of());

        // Assert
        assertThat(result.hasKeyword("TURN")).isTrue();
        assertThat(result.hasKeyword("OFF")).isTrue();
        assertThat(result.hasKeyword("ON")).isFalse();
        assertThat(result.hasKeyword("ALL")).isTrue();
        assertThat(result.hasKeyword("LIGHTS IN")).isTrue();

        assertThat(result.getParameters()).hasSize(1);
        assertThat(result.getParameters().get(0))
                .extracting("name", "value")
                .containsExactly("room_name", "master_bedroom");
    }

    @Test
    void shouldParseCloudOpsCommandWithMultipleAlternatives() {
        // Arrange
        GroupNode ast = compiler.compile("SCALE CLUSTER cluster_name TO nodes_count NODES { IMMEDIATELY | GRACEFULLY | SCHEDULED } ;");
        List<Token> tokens = lexer.tokenize("SCALE CLUSTER web_front TO 5 NODES GRACEFULLY ;");

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, macro -> List.of());

        // Assert
        assertThat(result.hasKeyword("GRACEFULLY")).isTrue();
        assertThat(result.hasKeyword("IMMEDIATELY")).isFalse();

        assertThat(result.getParameters())
                .extracting("name", "value")
                .containsExactlyInAnyOrder(
                        tuple("cluster_name", "web_front"),
                        tuple("nodes_count", "5")
                );
    }

    @Test
    void shouldParseEcommerceCommandWithMultiWordMacro() {
        // Arrange
        GroupNode ast = compiler.compile("APPLY DISCOUNT amount PERCENT ON CATEGORY ${product_categories} ;");
        List<Token> tokens = lexer.tokenize("APPLY DISCOUNT 15 PERCENT ON CATEGORY HOME APPLIANCES ;");

        java.util.function.Function<String, List<String>> dbMock = macroName -> {
            if ("product_categories".equals(macroName)) {
                return List.of("ELECTRONICS", "HOME APPLIANCES", "BOOKS");
            }
            return List.of();
        };

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, dbMock);

        // Assert
        assertThat(result.getParameters()).hasSize(2);
        assertThat(result.getParameters())
                .extracting("name", "value")
                .containsExactlyInAnyOrder(
                        tuple("amount", "15"),
                        tuple("product_categories", "HOME APPLIANCES")
                );
    }

    @Test
    void shouldParseSciFiCommandSkippingFirstOptionalGroup() {
        // Arrange
        GroupNode ast = compiler.compile("SET COURSE TO destination [VIA relay_station] [USING WARP FACTOR speed] ;");
        List<Token> tokens = lexer.tokenize("SET COURSE TO Alpha_Centauri USING WARP FACTOR 9 ;");

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, macro -> List.of());

        // Assert
        assertThat(result.hasKeyword("VIA")).isFalse();
        assertThat(result.hasKeyword("USING WARP FACTOR")).isTrue();

        assertThat(result.getParameters())
                .extracting("name", "value")
                .containsExactlyInAnyOrder(
                        tuple("destination", "Alpha_Centauri"),
                        tuple("speed", "9")
                );

        assertThat(result.getParameters().stream().anyMatch(p -> p.name().equals("relay_station"))).isFalse();
    }

    @Test
    void shouldParseSecurityCommandWithRepeatableParameter() {
        // Arrange
        GroupNode ast = compiler.compile("GRANT { READ | WRITE } ON RESOURCE resource_name TO user_name ... ;");
        List<Token> tokens = lexer.tokenize("GRANT READ ON RESOURCE secret_document TO joe bob charlie ;");

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, macro -> List.of());

        // Assert
        assertThat(result.hasKeyword("GRANT")).isTrue();
        assertThat(result.hasKeyword("READ")).isTrue();

        assertThat(result.getParameters()).hasSize(4);
        assertThat(result.getParameters())
                .extracting("name", "value")
                .contains(
                        tuple("resource_name", "secret_document"),
                        tuple("user_name", "joe"),
                        tuple("user_name", "bob"),
                        tuple("user_name", "charlie")
                );
    }

    @Test
    void shouldParseDatabaseCommandWithNestedGroups() {
        // Arrange
        GroupNode ast = compiler.compile("BACKUP DATABASE db_name [ TO { LOCAL disk_path | CLOUD bucket_name } ] ;");

        List<Token> tokens1 = lexer.tokenize("BACKUP DATABASE customers ;");
        List<Token> tokens2 = lexer.tokenize("BACKUP DATABASE customers TO CLOUD aws_s3_archive ;");

        // Act & Assert 1
        ParseResult result1 = navigator.parse("raw input", tokens1, ast, macro -> List.of());
        assertThat(result1.hasKeyword("TO")).isFalse();
        assertThat(result1.getParameters()).extracting("name").containsExactly("db_name");

        // Act & Assert 2
        ParseResult result2 = navigator.parse("raw input", tokens2, ast, macro -> List.of());
        assertThat(result2.hasKeyword("TO")).isTrue();
        assertThat(result2.hasKeyword("CLOUD")).isTrue();
        assertThat(result2.hasKeyword("LOCAL")).isFalse();

        assertThat(result2.getParameters())
                .extracting("name", "value")
                .containsExactlyInAnyOrder(
                        tuple("db_name", "customers"),
                        tuple("bucket_name", "aws_s3_archive")
                );
    }

    @Test
    void shouldParseNetworkingCommandWithGroupRepetition() {
        // Arrange
        GroupNode ast = compiler.compile("BLOCK TRAFFIC FROM { IP ip_address | SUBNET subnet_mask } ... ;");
        List<Token> tokens = lexer.tokenize("BLOCK TRAFFIC FROM IP '192.168.1.1' SUBNET '10.0.0.0/8' IP '10.0.0.1' ;");

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, macro -> List.of());

        // Assert
        assertThat(result.hasKeyword("IP")).isTrue();
        assertThat(result.hasKeyword("SUBNET")).isTrue();

        assertThat(result.getParameters()).hasSize(3);
        assertThat(result.getParameters())
                .extracting("name", "value")
                .containsExactlyInAnyOrder(
                        tuple("ip_address", "192.168.1.1"),
                        tuple("subnet_mask", "10.0.0.0/8"),
                        tuple("ip_address", "10.0.0.1")
                );
    }

    @Test
    void shouldParseMessagingCommandWithStringLiterals() {
        // Arrange
        GroupNode ast = compiler.compile("SEND MESSAGE content TO target_user ;");
        List<Token> tokens = lexer.tokenize("SEND MESSAGE 'Hello world, how are you today?' TO alice ;");

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, macro -> List.of());

        // Assert
        assertThat(result.getParameters())
                .extracting("name", "value")
                .containsExactlyInAnyOrder(
                        tuple("content", "Hello world, how are you today?"),
                        tuple("target_user", "alice")
                );
    }

    @Test
    void shouldParseCommandCaseInsensitively() {
        // Arrange
        // The grammar defines keywords in UPPERCASE.
        // We place FORCE in an optional group [ ] to prevent the GrammarCompiler
        // from merging "SYSTEM RESTART FORCE" into a single continuous KeywordNode.
        GroupNode ast = compiler.compile("SYSTEM RESTART [ FORCE ] ;");

        // The user input is entirely in lowercase
        List<Token> tokens = lexer.tokenize("system restart force ;");

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, macro -> List.of());

        // Assert
        // The parser normalizes matched keywords to uppercase during extraction
        assertThat(result.hasKeyword("SYSTEM RESTART")).isTrue();
        assertThat(result.hasKeyword("FORCE")).isTrue();
    }

    @Test
    void shouldParseEmptyStringLiteralAsParameter() {
        // Arrange
        // Ensures that providing '' doesn't crash the lexer or the binder
        GroupNode ast = compiler.compile("UPDATE PROFILE description ;");
        List<Token> tokens = lexer.tokenize("UPDATE PROFILE '' ;");

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, macro -> List.of());

        // Assert
        assertThat(result.getParameters()).hasSize(1);
        assertThat(result.getParameters().get(0))
                .extracting("name", "value")
                .containsExactly("description", ""); // The value is an empty string
    }

    @Test
    void shouldParseCommandWithMultipleDifferentMacros() {
        // Arrange
        // Tests the parser's ability to resolve multiple distinct macros in the same branch
        GroupNode ast = compiler.compile("MIGRATE ${source_db} TO ${target_db} ;");
        List<Token> tokens = lexer.tokenize("MIGRATE oracle_prod TO postgres_dev ;");

        java.util.function.Function<String, List<String>> dbMock = macroName -> {
            if ("source_db".equals(macroName)) return List.of("oracle_prod", "mysql_legacy");
            if ("target_db".equals(macroName)) return List.of("postgres_dev", "mongo_cluster");
            return List.of();
        };

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, dbMock);

        // Assert
        assertThat(result.getParameters()).hasSize(2);
        assertThat(result.getParameters())
                .extracting("name", "value")
                .containsExactlyInAnyOrder(
                        tuple("source_db", "oracle_prod"),
                        tuple("target_db", "postgres_dev")
                );
    }

    @Test
    void shouldParseRepeatedOptionalGroup() {
        // Arrange
        // Tests applying the '...' operator to an optional block [ ] instead of a required one { }
        GroupNode ast = compiler.compile("GENERATE REPORT [ WITH metric_name ] ... ;");

        // We repeat the optional block twice
        List<Token> tokens = lexer.tokenize("GENERATE REPORT WITH cpu_usage WITH ram_usage ;");

        // Act
        ParseResult result = navigator.parse("raw input", tokens, ast, macro -> List.of());

        // Assert
        assertThat(result.hasKeyword("WITH")).isTrue();
        assertThat(result.getParameters()).hasSize(2);
        assertThat(result.getParameters())
                .extracting("name", "value")
                .containsExactlyInAnyOrder(
                        tuple("metric_name", "cpu_usage"),
                        tuple("metric_name", "ram_usage")
                );
    }

}