Anpassung an DQFL Specification V 1.1

This commit is contained in:
2026-04-05 15:21:15 +02:00
parent 4b770fecc3
commit 7cdb676dee
6 changed files with 201 additions and 54 deletions

12
pom.xml
View File

@@ -1,6 +1,6 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="https://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>de.dogfire.dqfl</groupId> <groupId>de.dogfire.dqfl</groupId>
@@ -32,18 +32,18 @@
<repositories> <repositories>
<repository> <repository>
<id>gitea</id> <id>gitea</id>
<url>https://dein-gitea-server/api/packages/pat201290/maven</url> <url>https://gitea.dogfire.de/api/packages/pat201290/maven</url>
</repository> </repository>
</repositories> </repositories>
<distributionManagement> <distributionManagement>
<repository> <repository>
<id>gitea</id> <id>gitea</id>
<url>https://dein-gitea-server/api/packages/pat201290/maven</url> <url>https://gitea.dogfire.de/api/packages/pat201290/maven</url>
</repository> </repository>
<snapshotRepository> <snapshotRepository>
<id>gitea</id> <id>gitea</id>
<url>https://dein-gitea-server/api/packages/pat201290/maven</url> <url>https://gitea.dogfire.de/api/packages/pat201290/maven</url>
</snapshotRepository> </snapshotRepository>
</distributionManagement> </distributionManagement>

BIN
src.zip Normal file

Binary file not shown.

View File

@@ -1,4 +1,3 @@
package de.dogfire.dqfl.model; package de.dogfire.dqfl.model;
import java.util.ArrayList; import java.util.ArrayList;
@@ -14,18 +13,20 @@ public final class DqflCategory
private final String alias; private final String alias;
private final String title; private final String title;
private final String usePool; private final String usePool;
private final String useCategory;
private final LayoutType layout; private final LayoutType layout;
private final GridSize size; private final GridSize size;
private final List<DqflPhase> phases; private final List<DqflPhase> phases;
private DqflCategory(final Builder builder) private DqflCategory(final Builder builder)
{ {
this.alias = Objects.requireNonNull(builder.alias, "alias must not be null"); this.alias = Objects.requireNonNull(builder.alias, "alias must not be null");
this.title = builder.title; this.title = builder.title;
this.usePool = builder.usePool; this.usePool = builder.usePool;
this.layout = builder.layout; this.useCategory = builder.useCategory;
this.size = builder.size; this.layout = builder.layout;
this.phases = Collections.unmodifiableList(new ArrayList<>(builder.phases)); this.size = builder.size;
this.phases = Collections.unmodifiableList(new ArrayList<>(builder.phases));
} }
public String getAlias() public String getAlias()
@@ -44,6 +45,12 @@ public final class DqflCategory
return this.usePool; return this.usePool;
} }
/** Kann null sein — USE_CATEGORY ist optional (V1_1). */
public String getUseCategory()
{
return this.useCategory;
}
public LayoutType getLayout() public LayoutType getLayout()
{ {
return this.layout; return this.layout;
@@ -66,6 +73,7 @@ public final class DqflCategory
return "CATEGORY " + this.alias return "CATEGORY " + this.alias
+ " [title=" + this.title + " [title=" + this.title
+ ", pool=" + this.usePool + ", pool=" + this.usePool
+ ", category=" + this.useCategory
+ ", phases=" + this.phases.size() + "]"; + ", phases=" + this.phases.size() + "]";
} }
@@ -83,6 +91,7 @@ public final class DqflCategory
private final String alias; private final String alias;
private String title; private String title;
private String usePool; private String usePool;
private String useCategory;
private LayoutType layout; private LayoutType layout;
private GridSize size; private GridSize size;
private final List<DqflPhase> phases = new ArrayList<>(); private final List<DqflPhase> phases = new ArrayList<>();
@@ -104,6 +113,12 @@ public final class DqflCategory
return this; return this;
} }
public Builder useCategory(final String useCategory)
{
this.useCategory = useCategory;
return this;
}
public Builder layout(final LayoutType layout) public Builder layout(final LayoutType layout)
{ {
this.layout = layout; this.layout = layout;

View File

@@ -10,7 +10,7 @@ import de.dogfire.dqfl.error.DqflParseException;
import de.dogfire.dqfl.model.*; import de.dogfire.dqfl.model.*;
/** /**
* Parser für DQFL V1 Dokumente. * Parser für DQFL V1/V1_1 Dokumente.
* Liest ein einrückungssensitives Textformat und erzeugt einen {@link DqflDocument} AST. * Liest ein einrückungssensitives Textformat und erzeugt einen {@link DqflDocument} AST.
*/ */
public final class DqflParser public final class DqflParser
@@ -330,6 +330,10 @@ public final class DqflParser
catBuilder.usePool(child.value); catBuilder.usePool(child.value);
this.cursor++; this.cursor++;
} }
case "USE_CATEGORY" -> {
catBuilder.useCategory(this.unquote(child.value));
this.cursor++;
}
case "LAYOUT" -> { case "LAYOUT" -> {
final LayoutType lt = LayoutType.parse(child.value); final LayoutType lt = LayoutType.parse(child.value);
if(lt == null) if(lt == null)

View File

@@ -1,4 +1,3 @@
package de.dogfire.dqfl.validator; package de.dogfire.dqfl.validator;
import java.util.ArrayList; import java.util.ArrayList;
@@ -12,8 +11,8 @@ import de.dogfire.dqfl.error.DqflValidationResult;
import de.dogfire.dqfl.model.*; import de.dogfire.dqfl.model.*;
/** /**
* Validator für DQFL V1 Dokumente. * Validator für DQFL V1/V1_1 Dokumente.
* Prüft alle Fachregeln aus Kapitel 10 der Spezifikation. * Prüft alle Fachregeln aus der Spezifikation.
*/ */
public final class DqflValidator public final class DqflValidator
{ {
@@ -40,25 +39,18 @@ public final class DqflValidator
errors.add(DqflError.error("FLOW is required")); errors.add(DqflError.error("FLOW is required"));
} }
// 3. SOURCE-Aliase müssen eindeutig sein (bereits durch Map im Parser sichergestellt, // 3. POOL-Validierung
// aber wir prüfen trotzdem ob Sources vorhanden sind wenn Pools sie referenzieren)
// 4. POOL-Validierung
this.validatePools(document, errors); this.validatePools(document, errors);
// 5. ROUND-Validierung // 4. ROUND-Validierung
this.validateRounds(document, errors); this.validateRounds(document, errors);
// 6. Alias-Eindeutigkeit über Objektarten // 5. Alias-Eindeutigkeit über Objektarten
this.validateAliasUniqueness(document, errors); this.validateAliasUniqueness(document, errors);
return new DqflValidationResult(errors); return new DqflValidationResult(errors);
} }
// ================================================================
// Pool-Validierung
// ================================================================
private void validatePools(final DqflDocument document, final List<DqflError> errors) private void validatePools(final DqflDocument document, final List<DqflError> errors)
{ {
for(final DqflPool pool : document.getPools().values()) for(final DqflPool pool : document.getPools().values())
@@ -82,10 +74,6 @@ public final class DqflValidator
} }
} }
// ================================================================
// Round-Validierung
// ================================================================
private void validateRounds(final DqflDocument document, final List<DqflError> errors) private void validateRounds(final DqflDocument document, final List<DqflError> errors)
{ {
if(document.getRounds().isEmpty()) if(document.getRounds().isEmpty())
@@ -119,10 +107,6 @@ public final class DqflValidator
} }
} }
// ================================================================
// Category-Validierung
// ================================================================
private void validateCategory( private void validateCategory(
final DqflCategory category, final DqflCategory category,
final DqflDocument document, final DqflDocument document,
@@ -153,6 +137,26 @@ public final class DqflValidator
} }
} }
// USE_CATEGORY Validierung (V1_1)
if(category.getUseCategory() != null)
{
// USE_CATEGORY ohne USE_POOL ist ein Syntaxfehler
if(category.getUsePool() == null || category.getUsePool().isBlank())
{
errors.add(DqflError.error(
"CATEGORY '" + category.getAlias()
+ "' has USE_CATEGORY without USE_POOL"));
}
// Wert darf nicht leer sein
if(category.getUseCategory().isBlank())
{
errors.add(DqflError.error(
"CATEGORY '" + category.getAlias()
+ "' USE_CATEGORY must not be empty"));
}
}
// SIZE nur gültig wenn LAYOUT vorhanden // SIZE nur gültig wenn LAYOUT vorhanden
if(category.getSize() != null && category.getLayout() == null) if(category.getSize() != null && category.getLayout() == null)
{ {
@@ -182,10 +186,6 @@ public final class DqflValidator
} }
} }
// ================================================================
// Phase-Validierung
// ================================================================
private void validatePhase( private void validatePhase(
final DqflPhase phase, final DqflPhase phase,
final String categoryAlias, final String categoryAlias,
@@ -236,19 +236,10 @@ public final class DqflValidator
} }
} }
// ================================================================
// Alias-Eindeutigkeit (über Objektarten)
// ================================================================
private void validateAliasUniqueness( private void validateAliasUniqueness(
final DqflDocument document, final DqflDocument document,
final List<DqflError> errors) final List<DqflError> errors)
{ {
// SOURCE-Aliase eindeutig (Map-Struktur prüft bereits implizit,
// aber bei Duplikaten im Dokument wurde der erste überschrieben)
// POOL-Aliase eindeutig (gleiche Map-Logik)
// Wir prüfen explizit ob Pool-Aliase und Source-Aliase kollidieren (optional, sauber)
final Set<String> sourceAliases = document.getSources().keySet(); final Set<String> sourceAliases = document.getSources().keySet();
final Set<String> poolAliases = document.getPools().keySet(); final Set<String> poolAliases = document.getPools().keySet();

View File

@@ -1,4 +1,3 @@
package de.dogfire.dqfl; package de.dogfire.dqfl;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -14,7 +13,7 @@ import de.dogfire.dqfl.error.DqflValidationResult;
import de.dogfire.dqfl.model.*; import de.dogfire.dqfl.model.*;
/** /**
* Tests für die DQFL V1 Library: Parser + Validator. * Tests für die DQFL V1/V1_1 Library: Parser + Validator.
*/ */
class DqflTest class DqflTest
{ {
@@ -75,6 +74,7 @@ class DqflTest
assertEquals("C1", c1.getAlias()); assertEquals("C1", c1.getAlias());
assertEquals("Geschichte", c1.getTitle()); assertEquals("Geschichte", c1.getTitle());
assertEquals("HISTORY", c1.getUsePool()); assertEquals("HISTORY", c1.getUsePool());
assertNull(c1.getUseCategory());
assertEquals(LayoutType.GRID_2D, c1.getLayout()); assertEquals(LayoutType.GRID_2D, c1.getLayout());
assertEquals(new GridSize(6, 6), c1.getSize()); assertEquals(new GridSize(6, 6), c1.getSize());
assertEquals(2, c1.getPhases().size()); assertEquals(2, c1.getPhases().size());
@@ -322,6 +322,143 @@ class DqflTest
} }
} }
// ================================================================
// V1_1 USE_CATEGORY Tests
// ================================================================
@Nested
@DisplayName("V1_1 USE_CATEGORY")
class UseCategoryTests
{
@Test
@DisplayName("Parst USE_CATEGORY korrekt")
void parseUseCategory() throws Exception
{
final String input = """
DQFL_VERSION V1_1
FLOW "Test"
SOURCE SRC1
TYPE REST
CONNECTION CONN1
POOL GEO
SOURCE SRC1
REMOTE_ID "ZX82LM550000009321"
ROUND R1
CATEGORY C1
USE_POOL GEO
USE_CATEGORY "Hauptstädte"
LAYOUT GRID_2D
SIZE 4x4
PHASE NORMAL
QUESTION_SET NORMAL
CYCLES 2
TURNS_PER_CYCLE 4
TURN_ORDER ROTATING_START
PICK_MODE FREE_UNANSWERED
TEAM_MODE SOLO
SCORING SOLO_PLUS1_NO_PENALTY
""";
final DqflDocument doc = Dqfl.parseAndValidate(input);
final DqflCategory c1 = doc.getRounds().get(0).getCategories().get(0);
assertEquals("V1_1", doc.getDqflVersion());
assertEquals("GEO", c1.getUsePool());
assertEquals("Hauptstädte", c1.getUseCategory());
}
@Test
@DisplayName("USE_CATEGORY ohne USE_POOL wird als Fehler erkannt")
void useCategoryWithoutUsePool() throws Exception
{
final String input = """
DQFL_VERSION V1_1
FLOW "Test"
SOURCE SRC1
TYPE REST
CONNECTION CONN1
POOL P1
SOURCE SRC1
REMOTE_ID "AB12CD340000004711"
ROUND R1
CATEGORY C1
USE_CATEGORY "Test"
PHASE NORMAL
TEAM_MODE SOLO
SCORING SOLO_PLUS1_NO_PENALTY
""";
final DqflDocument doc = Dqfl.parse(input);
final DqflValidationResult result = Dqfl.validate(doc);
assertFalse(result.isValid());
assertTrue(result.getErrorsOnly().stream()
.anyMatch(e -> e.getMessage().contains("USE_CATEGORY without USE_POOL")));
}
@Test
@DisplayName("Category ohne USE_CATEGORY bleibt null (V1 Abwärtskompatibilität)")
void noCategoryFilterReturnsNull() throws Exception
{
final DqflDocument doc = loadReferenceDocument();
final DqflCategory c1 = doc.getRounds().get(0).getCategories().get(0);
assertNull(c1.getUseCategory());
}
@Test
@DisplayName("V1_1 Dokument mit und ohne USE_CATEGORY gemischt")
void mixedCategoriesWithAndWithout() throws Exception
{
final String input = """
DQFL_VERSION V1_1
FLOW "Mixed Test"
SOURCE SRC1
TYPE REST
CONNECTION CONN1
POOL HISTORY
SOURCE SRC1
REMOTE_ID "AB12CD340000004711"
POOL GEO
SOURCE SRC1
REMOTE_ID "ZX82LM550000009321"
ROUND R1
CATEGORY C1
USE_POOL HISTORY
LAYOUT GRID_2D
SIZE 6x6
PHASE NORMAL
QUESTION_SET NORMAL
CYCLES 3
TURNS_PER_CYCLE 4
TURN_ORDER ROTATING_START
PICK_MODE FREE_UNANSWERED
TEAM_MODE SOLO
SCORING SOLO_PLUS1_NO_PENALTY
CATEGORY C2
USE_POOL GEO
USE_CATEGORY "Hauptstädte"
LAYOUT GRID_2D
SIZE 4x4
PHASE NORMAL
QUESTION_SET NORMAL
CYCLES 2
TURNS_PER_CYCLE 4
TURN_ORDER ROTATING_START
PICK_MODE FREE_UNANSWERED
TEAM_MODE SOLO
SCORING SOLO_PLUS1_NO_PENALTY
""";
final DqflDocument doc = Dqfl.parseAndValidate(input);
final var categories = doc.getAllCategoriesFlat();
assertEquals(2, categories.size());
assertNull(categories.get(0).getUseCategory());
assertEquals("Hauptstädte", categories.get(1).getUseCategory());
}
}
// ================================================================ // ================================================================
// GridSize Tests // GridSize Tests
// ================================================================ // ================================================================