/*
* 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.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.DslDefinitionException;
import ch.dbalabs.intuitivedsl.exception.DslSyntaxException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Sentinel tests that freeze the product contract around the repeat operator.
*
* Product decision:
* - "..." is the only repetition operator.
* - "." remains a valid standalone delimiter and must not be treated as repetition.
*/
class RepeatOperatorContractSentinelTest {
private final GrammarCompiler compiler = new GrammarCompiler();
private IntuitiveDslEngine engine;
@BeforeEach
void setUp() {
engine = new IntuitiveDslEngine();
}
@Test
void shouldCompileCanonicalEllipsisRepeatOperator() {
GroupNode root = compiler.compile("GRANT ACCESS TO target_user ... ;");
AlternativeNode main = (AlternativeNode) root.children().get(0);
assertThat(main.children()).hasSize(3);
assertThat(main.children().get(0)).isInstanceOf(KeywordNode.class)
.extracting("word")
.isEqualTo("GRANT ACCESS TO");
assertThat(main.children().get(1)).isInstanceOf(ParameterNode.class);
assertThat(main.children().get(1).isRepeatable()).isTrue();
assertThat(main.children()).noneMatch(node -> node instanceof DelimiterNode d && ".".equals(d.value()));
}
@Test
void shouldTreatSingleDotAsLiteralDelimiterInsteadOfRepeatOperator() {
GroupNode root = compiler.compile("SET VERSION major . minor ;");
AlternativeNode main = (AlternativeNode) root.children().get(0);
assertThat(main.children()).hasSize(5);
assertThat(main.children().get(0)).isInstanceOf(KeywordNode.class)
.extracting("word")
.isEqualTo("SET VERSION");
assertThat(main.children().get(1)).isInstanceOf(ParameterNode.class)
.extracting("name")
.isEqualTo("major");
assertThat(main.children().get(1).isRepeatable()).isFalse();
assertThat(main.children().get(2)).isInstanceOf(DelimiterNode.class)
.extracting("value")
.isEqualTo(".");
assertThat(main.children().get(3)).isInstanceOf(ParameterNode.class)
.extracting("name")
.isEqualTo("minor");
}
@Test
void shouldNeverPromoteLiteralDotsToRepeatableNodes() {
GroupNode root = compiler.compile("PAIR left . middle . right ;");
AlternativeNode main = (AlternativeNode) root.children().get(0);
assertThat(main.children()).hasSize(7);
assertThat(main.children().get(0)).isInstanceOf(KeywordNode.class)
.extracting("word")
.isEqualTo("PAIR");
assertThat(main.children().get(1)).isInstanceOf(ParameterNode.class)
.extracting("name")
.isEqualTo("left");
assertThat(main.children().get(2)).isInstanceOf(DelimiterNode.class)
.extracting("value")
.isEqualTo(".");
assertThat(main.children().get(3)).isInstanceOf(ParameterNode.class)
.extracting("name")
.isEqualTo("middle");
assertThat(main.children().get(4)).isInstanceOf(DelimiterNode.class)
.extracting("value")
.isEqualTo(".");
assertThat(main.children().get(5)).isInstanceOf(ParameterNode.class)
.extracting("name")
.isEqualTo("right");
assertThat(main.children())
.filteredOn(SyntaxNode::isRepeatable)
.as("Literal dot delimiters must never turn adjacent nodes into repeatable nodes.")
.isEmpty();
}
@Test
void shouldRejectLeadingEllipsisWithoutTargetNode() {
assertThatThrownBy(() -> compiler.compile("... user ;"))
.isInstanceOf(DslDefinitionException.class)
.hasMessageContaining("Operator '...' needs a preceding node");
}
@Test
void shouldRejectLiteralDotInUserInputForRepeatableSyntax() {
engine.register(GrantAccessCommand.class);
assertThatThrownBy(() -> engine.execute("GRANT ACCESS TO alice . ;", ExecutionContext.createDefault()))
.isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getUnexpectedToken()).isNotNull();
assertThat(ex.getUnexpectedToken().value()).isEqualTo(".");
});
}
@Test
void shouldAcceptLiteralDotWhenGrammarExplicitlyRequiresIt() {
engine.register(VersionCommand.class);
VersionCommand command = engine.execute("SET VERSION 1 . 2 ;", ExecutionContext.createDefault());
assertThat(command.major).isEqualTo("1");
assertThat(command.minor).isEqualTo("2");
}
@DslCommand(name = "Grant Access", syntax = "GRANT ACCESS TO target_user ... ;")
public static class GrantAccessCommand implements Runnable {
@Bind("target_user")
private String targetUser;
@Override
public void run() {
}
}
@DslCommand(name = "Version", syntax = "SET VERSION major . minor ;")
public static class VersionCommand implements Runnable {
@Bind("major")
private String major;
@Bind("minor")
private String minor;
@Override
public void run() {
}
}
}