diff --git a/pom.xml b/pom.xml index 17e0f6c..fee8220 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ - + 4.0.0 de.dogfire.dqfl @@ -32,18 +32,18 @@ gitea - https://dein-gitea-server/api/packages/pat201290/maven + https://gitea.dogfire.de/api/packages/pat201290/maven gitea - https://dein-gitea-server/api/packages/pat201290/maven + https://gitea.dogfire.de/api/packages/pat201290/maven gitea - https://dein-gitea-server/api/packages/pat201290/maven + https://gitea.dogfire.de/api/packages/pat201290/maven diff --git a/src.zip b/src.zip new file mode 100644 index 0000000..33df3c2 Binary files /dev/null and b/src.zip differ diff --git a/src/main/java/de/dogfire/dqfl/model/DqflCategory.java b/src/main/java/de/dogfire/dqfl/model/DqflCategory.java index e77cdf0..d47ac5d 100644 --- a/src/main/java/de/dogfire/dqfl/model/DqflCategory.java +++ b/src/main/java/de/dogfire/dqfl/model/DqflCategory.java @@ -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 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 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); } } -} +} \ No newline at end of file diff --git a/src/main/java/de/dogfire/dqfl/parser/DqflParser.java b/src/main/java/de/dogfire/dqfl/parser/DqflParser.java index 5a147a3..753644d 100644 --- a/src/main/java/de/dogfire/dqfl/parser/DqflParser.java +++ b/src/main/java/de/dogfire/dqfl/parser/DqflParser.java @@ -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 : ""); } } -} +} \ No newline at end of file diff --git a/src/main/java/de/dogfire/dqfl/validator/DqflValidator.java b/src/main/java/de/dogfire/dqfl/validator/DqflValidator.java index 63e283f..e47b968 100644 --- a/src/main/java/de/dogfire/dqfl/validator/DqflValidator.java +++ b/src/main/java/de/dogfire/dqfl/validator/DqflValidator.java @@ -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 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 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 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 sourceAliases = document.getSources().keySet(); final Set poolAliases = document.getPools().keySet(); @@ -262,4 +253,4 @@ public final class DqflValidator } } } -} +} \ No newline at end of file diff --git a/src/test/java/de/dogfire/dqfl/DqflTest.java b/src/test/java/de/dogfire/dqfl/DqflTest.java index b2662ae..a01e8a7 100644 --- a/src/test/java/de/dogfire/dqfl/DqflTest.java +++ b/src/test/java/de/dogfire/dqfl/DqflTest.java @@ -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()); } } -} +} \ No newline at end of file