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

View File

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

View File

@@ -10,7 +10,7 @@ import de.dogfire.dqfl.error.DqflParseException;
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.
*/
public final class DqflParser
@@ -330,6 +330,10 @@ public final class DqflParser
catBuilder.usePool(child.value);
this.cursor++;
}
case "USE_CATEGORY" -> {
catBuilder.useCategory(this.unquote(child.value));
this.cursor++;
}
case "LAYOUT" -> {
final LayoutType lt = LayoutType.parse(child.value);
if(lt == null)
@@ -670,4 +674,4 @@ public final class DqflParser
+ this.keyword + (this.value != null ? " " + this.value : "");
}
}
}
}

View File

@@ -1,4 +1,3 @@
package de.dogfire.dqfl.validator;
import java.util.ArrayList;
@@ -12,8 +11,8 @@ import de.dogfire.dqfl.error.DqflValidationResult;
import de.dogfire.dqfl.model.*;
/**
* Validator für DQFL V1 Dokumente.
* Prüft alle Fachregeln aus Kapitel 10 der Spezifikation.
* Validator für DQFL V1/V1_1 Dokumente.
* Prüft alle Fachregeln aus der Spezifikation.
*/
public final class DqflValidator
{
@@ -40,25 +39,18 @@ public final class DqflValidator
errors.add(DqflError.error("FLOW is required"));
}
// 3. SOURCE-Aliase müssen eindeutig sein (bereits durch Map im Parser sichergestellt,
// aber wir prüfen trotzdem ob Sources vorhanden sind wenn Pools sie referenzieren)
// 4. POOL-Validierung
// 3. POOL-Validierung
this.validatePools(document, errors);
// 5. ROUND-Validierung
// 4. ROUND-Validierung
this.validateRounds(document, errors);
// 6. Alias-Eindeutigkeit über Objektarten
// 5. Alias-Eindeutigkeit über Objektarten
this.validateAliasUniqueness(document, errors);
return new DqflValidationResult(errors);
}
// ================================================================
// Pool-Validierung
// ================================================================
private void validatePools(final DqflDocument document, final List<DqflError> errors)
{
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)
{
if(document.getRounds().isEmpty())
@@ -119,10 +107,6 @@ public final class DqflValidator
}
}
// ================================================================
// Category-Validierung
// ================================================================
private void validateCategory(
final DqflCategory category,
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
if(category.getSize() != null && category.getLayout() == null)
{
@@ -182,10 +186,6 @@ public final class DqflValidator
}
}
// ================================================================
// Phase-Validierung
// ================================================================
private void validatePhase(
final DqflPhase phase,
final String categoryAlias,
@@ -236,19 +236,10 @@ public final class DqflValidator
}
}
// ================================================================
// Alias-Eindeutigkeit (über Objektarten)
// ================================================================
private void validateAliasUniqueness(
final DqflDocument document,
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> poolAliases = document.getPools().keySet();
@@ -262,4 +253,4 @@ public final class DqflValidator
}
}
}
}
}

View File

@@ -1,4 +1,3 @@
package de.dogfire.dqfl;
import static org.junit.jupiter.api.Assertions.*;
@@ -14,7 +13,7 @@ import de.dogfire.dqfl.error.DqflValidationResult;
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
{
@@ -75,6 +74,7 @@ class DqflTest
assertEquals("C1", c1.getAlias());
assertEquals("Geschichte", c1.getTitle());
assertEquals("HISTORY", c1.getUsePool());
assertNull(c1.getUseCategory());
assertEquals(LayoutType.GRID_2D, c1.getLayout());
assertEquals(new GridSize(6, 6), c1.getSize());
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
// ================================================================
@@ -408,4 +545,4 @@ class DqflTest
return new String(is.readAllBytes());
}
}
}
}