/*
* 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.exception.DslSyntaxException;
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.assertThatThrownBy;
import static org.assertj.core.groups.Tuple.tuple;
class AlternativeAndGroupParsingTest {
private GrammarCompiler compiler;
private Lexer lexer;
private AstNavigator navigator;
@BeforeEach
void setUp() {
compiler = new GrammarCompiler();
lexer = new Lexer();
navigator = new AstNavigator();
}
@Test
void shouldParseNestedRequiredAndOptionalGroupsAcrossBranches() {
GroupNode ast = compiler.compile("DEPLOY { SERVICE service_name [ VERSION version ] | JOB job_name } ;");
ParseResult serviceResult = navigator.parse(
"DEPLOY SERVICE billing VERSION v2 ;",
lexer.tokenize("DEPLOY SERVICE billing VERSION v2 ;"),
ast,
m -> List.of()
);
ParseResult jobResult = navigator.parse(
"DEPLOY JOB cleanup ;",
lexer.tokenize("DEPLOY JOB cleanup ;"),
ast,
m -> List.of()
);
assertThat(serviceResult.getParameters())
.extracting(ParseResult.BoundParameter::name, ParseResult.BoundParameter::value)
.containsExactly(tuple("service_name", "billing"), tuple("version", "v2"));
assertThat(jobResult.getParameters())
.extracting(ParseResult.BoundParameter::name, ParseResult.BoundParameter::value)
.containsExactly(tuple("job_name", "cleanup"));
}
@Test
void shouldRejectPartialOptionalGroupOnceStarted() {
GroupNode ast = compiler.compile("CREATE USER username [ PASSWORD password ] ;");
assertThatThrownBy(() -> navigator.parse(
"CREATE USER john PASSWORD ;",
lexer.tokenize("CREATE USER john PASSWORD ;"),
ast,
m -> List.of()
)).isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getExpectedPossibilities()).contains("<password>");
});
}
@Test
void shouldRejectInputWhenNoRequiredAlternativeMatches() {
GroupNode ast = compiler.compile("PROCESS { FILE file_path | DIRECTORY dir_path } ;");
assertThatThrownBy(() -> navigator.parse(
"PROCESS SOCKET conn1 ;",
lexer.tokenize("PROCESS SOCKET conn1 ;"),
ast,
m -> List.of()
)).isInstanceOf(DslSyntaxException.class)
.satisfies(error -> {
DslSyntaxException ex = (DslSyntaxException) error;
assertThat(ex.getExpectedPossibilities()).containsAnyOf("FILE", "DIRECTORY");
});
}
@Test
void shouldParseDeeplyNestedAlternatives() {
GroupNode ast = compiler.compile("ROUTE { HTTP { GET | POST } | MQ { PUBLISH | SUBSCRIBE } } target ;");
ParseResult http = navigator.parse(
"ROUTE HTTP GET api_users ;",
lexer.tokenize("ROUTE HTTP GET api_users ;"),
ast,
m -> List.of()
);
ParseResult mq = navigator.parse(
"ROUTE MQ SUBSCRIBE jobs ;",
lexer.tokenize("ROUTE MQ SUBSCRIBE jobs ;"),
ast,
m -> List.of()
);
assertThat(http.hasKeyword("HTTP")).isTrue();
assertThat(http.hasKeyword("GET")).isTrue();
assertThat(http.getParameters()).extracting(ParseResult.BoundParameter::value).containsExactly("api_users");
assertThat(mq.hasKeyword("MQ")).isTrue();
assertThat(mq.hasKeyword("SUBSCRIBE")).isTrue();
assertThat(mq.getParameters()).extracting(ParseResult.BoundParameter::value).containsExactly("jobs");
}
@Test
void shouldTrackQuotedValueInsideNestedStructures() {
GroupNode ast = compiler.compile("ROUTE { HTTP { GET | POST } | MQ { PUBLISH | SUBSCRIBE } } target [ WITH payload ] ;");
ParseResult result = navigator.parse(
"ROUTE MQ PUBLISH orders WITH 'priority=high ; retry=false' ;",
lexer.tokenize("ROUTE MQ PUBLISH orders WITH 'priority=high ; retry=false' ;"),
ast,
m -> List.of()
);
assertThat(result.hasKeyword("MQ")).isTrue();
assertThat(result.hasKeyword("PUBLISH")).isTrue();
assertThat(result.getParameters())
.extracting(ParseResult.BoundParameter::name, ParseResult.BoundParameter::value)
.containsExactly(
tuple("target", "orders"),
tuple("payload", "priority=high ; retry=false")
);
}
}