/*
* 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")
);
}
}