/*
* 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.grammar;
import ch.dbalabs.intuitivedsl.exception.DslDefinitionException;
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;
/**
* Contract tests for grammar compilation.
*
* These tests intentionally lock down negative-definition behavior and the exact
* distinction between literal delimiters and grammar operators.
*/
class GrammarContractTest {
private final GrammarCompiler compiler = new GrammarCompiler();
@Test
void shouldAllowStandaloneDotDelimiterAsLiteralSeparator() {
GroupNode root = compiler.compile("SET DATE day . month . year ;");
assertThat(root.children()).hasSize(1);
assertThat(root.children().get(0)).isInstanceOf(AlternativeNode.class);
AlternativeNode alt = (AlternativeNode) root.children().get(0);
assertThat(alt.children()).hasSize(7);
assertThat(alt.children().get(0)).isInstanceOf(KeywordNode.class);
assertThat(((KeywordNode) alt.children().get(0)).word()).isEqualTo("SET DATE");
assertThat(alt.children().get(1)).isInstanceOf(ParameterNode.class);
assertThat(((ParameterNode) alt.children().get(1)).name()).isEqualTo("day");
assertThat(alt.children().get(2)).isInstanceOf(DelimiterNode.class);
assertThat(((DelimiterNode) alt.children().get(2)).value()).isEqualTo(".");
assertThat(alt.children().get(3)).isInstanceOf(ParameterNode.class);
assertThat(((ParameterNode) alt.children().get(3)).name()).isEqualTo("month");
assertThat(alt.children().get(4)).isInstanceOf(DelimiterNode.class);
assertThat(((DelimiterNode) alt.children().get(4)).value()).isEqualTo(".");
assertThat(alt.children().get(5)).isInstanceOf(ParameterNode.class);
assertThat(((ParameterNode) alt.children().get(5)).name()).isEqualTo("year");
assertThat(alt.children().get(6)).isInstanceOf(DelimiterNode.class);
assertThat(((DelimiterNode) alt.children().get(6)).value()).isEqualTo(";");
}
@Test
void shouldMarkOnlyCanonicalEllipsisAsRepeatOperator() {
GroupNode root = compiler.compile("GRANT ACCESS TO user_name ... ;");
AlternativeNode alt = (AlternativeNode) root.children().get(0);
assertThat(alt.children()).hasSize(3);
assertThat(((KeywordNode) alt.children().get(0)).word()).isEqualTo("GRANT ACCESS TO");
assertThat(alt.children().get(1)).isInstanceOf(ParameterNode.class);
assertThat(((ParameterNode) alt.children().get(1)).name()).isEqualTo("user_name");
assertThat(alt.children().get(1).isRepeatable()).isTrue();
assertThat(((DelimiterNode) alt.children().get(2)).value()).isEqualTo(";");
}
@Test
void shouldRejectWhitespaceOnlyModernMacroName() {
assertThatThrownBy(() -> compiler.compile("DO ${ } ;"))
.isInstanceOf(DslDefinitionException.class)
.hasMessageContaining("Empty macro name");
}
@Test
void shouldRejectWhitespaceOnlyLegacyMacroName() {
assertThatThrownBy(() -> compiler.compile("DO < > ;"))
.isInstanceOf(DslDefinitionException.class)
.hasMessageContaining("Empty legacy macro name");
}
@Test
void shouldRejectNestedEmptyAlternativeBranch() {
assertThatThrownBy(() -> compiler.compile("DO { A | { B | } } ;"))
.isInstanceOf(DslDefinitionException.class)
.hasMessageContaining("Empty alternative branch");
}
@Test
void shouldRejectEllipsisWithoutPrecedingNodeInsideOptionalGroup() {
assertThatThrownBy(() -> compiler.compile("DO [ ... value ] ;"))
.isInstanceOf(DslDefinitionException.class)
.hasMessageContaining("preceding node");
}
@Test
void shouldRejectMismatchedNestedClosers() {
assertThatThrownBy(() -> compiler.compile("DO [ { A | B ] ;"))
.isInstanceOf(DslDefinitionException.class)
// This branch is rejected by the top-level depth consistency check,
// not by the local findClosing(...) helper. The contract we want to lock
// down is therefore the actual mismatch message produced by the compiler.
.hasMessageContaining("Mismatched brackets/braces")
.hasMessageContaining("Expected '}' or ']'");
}
@Test
void shouldRejectTrailingAlternativeInsideNestedOptionalGroup() {
assertThatThrownBy(() -> compiler.compile("DO [ A | ] ;"))
.isInstanceOf(DslDefinitionException.class)
.hasMessageContaining("Empty alternative branch");
}
@Test
void shouldPreserveLiteralCommaAndDotDelimitersTogether() {
GroupNode root = compiler.compile("SET VERSION major . minor , patch ;");
AlternativeNode alt = (AlternativeNode) root.children().get(0);
List<SyntaxNode> children = alt.children();
assertThat(children).extracting(node -> node.getClass().getSimpleName())
.containsExactly(
"KeywordNode",
"ParameterNode",
"DelimiterNode",
"ParameterNode",
"DelimiterNode",
"ParameterNode",
"DelimiterNode"
);
assertThat(((DelimiterNode) children.get(2)).value()).isEqualTo(".");
assertThat(((DelimiterNode) children.get(4)).value()).isEqualTo(",");
assertThat(((DelimiterNode) children.get(6)).value()).isEqualTo(";");
}
}