From cd8caf14d0d87b0b8eb00328c35a0988b7a02f0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:57:29 +0000 Subject: [PATCH 1/3] Initial plan From 3367a05523ba07cf5add59091a81eee696148ae6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:05:30 +0000 Subject: [PATCH 2/3] Add Changelog root element to Bjoern specifications Co-authored-by: Mehtrick <4493019+Mehtrick@users.noreply.github.com> --- README.md | 4 ++++ .../BjoernFeatureTestClassBuilder.java | 3 +++ .../mehtrick/bjoern/parser/modell/Bjoern.java | 12 +++++++++- .../bjoern/parser/modell/BjoernZGRModell.java | 17 ++++++++++++-- .../validator/validations/BjoernKeywords.java | 2 +- src/main/resources/asciidoc.ftlh | 4 ++++ .../bjoern/asciidoc/AsciiDocBuildTest.java | 22 +++++++++++++++++++ .../BjoernFeatureTestClassBuilderTest.java | 12 ++++++++++ .../bjoern/parser/BjoernValidatorTest.java | 4 ++-- src/test/resources/changelog.zgr | 10 +++++++++ 10 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/changelog.zgr diff --git a/README.md b/README.md index 4c7a550..d870564 100755 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ The optional `Reference` field links a spec to an external ticket or resource. I - In generated Java: rendered as a `@see text` Javadoc tag on the abstract class. - In generated docs: rendered as an AsciiDoc hyperlink (`link:url[text]`) below the feature title. +The optional `Changelog` field allows you to document changes, background information, or technical rationale for the specification. It accepts arbitrary free text: +- In generated Java: rendered as a `@Changelog` tag in the Javadoc of the abstract class. +- In generated docs: inserted after the introduction (version/reference), only when present. + ## Code generation Bjoern will then generate the TestClasses based on the spec diff --git a/src/main/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilder.java b/src/main/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilder.java index 8377006..fc685be 100644 --- a/src/main/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilder.java +++ b/src/main/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilder.java @@ -48,6 +48,9 @@ private void addJavaDoc(Bjoern bjoern, Builder featureClassBuilder) { if (StringUtils.isNotBlank(bjoern.getReference())) { javadoc.append("\n@see ").append(bjoern.getReferenceAsJavadoc()); } + if (StringUtils.isNotBlank(bjoern.getChangelog())) { + javadoc.append("\n@Changelog ").append(bjoern.getChangelog()); + } featureClassBuilder.addJavadoc("$L", javadoc.toString()); } diff --git a/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java b/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java index ab80c8d..0fb737a 100644 --- a/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java +++ b/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java @@ -15,6 +15,7 @@ public class Bjoern { private String feature; private String version; private String reference; + private String changelog; private BjoernBackground background; private List scenarios; private String filePath; @@ -23,6 +24,7 @@ public Bjoern(BjoernZGRModell yamlModell, String path) { setFeature(yamlModell.getFeature()); setVersion(yamlModell.getVersion()); setReference(yamlModell.getReference()); + setChangelog(yamlModell.getChangelog()); setScenarios(yamlModell.getScenarios().stream().map(BjoernScenario::new).collect(Collectors.toList())); setFilePath(path); if (yamlModell.getBackground() != null) { @@ -58,6 +60,14 @@ public void setReference(String reference) { this.reference = reference; } + public String getChangelog() { + return this.changelog; + } + + public void setChangelog(String changelog) { + this.changelog = changelog; + } + /** * Returns the reference formatted as a Javadoc hyperlink if it is a Markdown link ([text](url)) * with an allowed URL scheme (http/https), otherwise returns the plain reference text. @@ -136,6 +146,6 @@ public void setFilePath(String filePath) { public String toString() { - return "Bjoern(feature=" + this.getFeature() + ", version=" + this.getVersion() + ", reference=" + this.getReference() + ", background=" + this.getBackground() + ", scenarios=" + this.getScenarios() + ", filePath=" + this.getFilePath() + ")"; + return "Bjoern(feature=" + this.getFeature() + ", version=" + this.getVersion() + ", reference=" + this.getReference() + ", changelog=" + this.getChangelog() + ", background=" + this.getBackground() + ", scenarios=" + this.getScenarios() + ", filePath=" + this.getFilePath() + ")"; } } diff --git a/src/main/java/de/mehtrick/bjoern/parser/modell/BjoernZGRModell.java b/src/main/java/de/mehtrick/bjoern/parser/modell/BjoernZGRModell.java index 539406e..725cce6 100644 --- a/src/main/java/de/mehtrick/bjoern/parser/modell/BjoernZGRModell.java +++ b/src/main/java/de/mehtrick/bjoern/parser/modell/BjoernZGRModell.java @@ -15,7 +15,7 @@ * */ @JsonInclude(JsonInclude.Include.NON_NULL) -@JsonPropertyOrder({ "Feature", "Version", "Reference", "Scenarios" }) +@JsonPropertyOrder({ "Feature", "Version", "Reference", "Changelog", "Scenarios" }) public class BjoernZGRModell implements Serializable { @JsonProperty("Feature") @@ -27,6 +27,9 @@ public class BjoernZGRModell implements Serializable { @JsonProperty("Reference") private String reference; + @JsonProperty("Changelog") + private String changelog; + @JsonProperty("Background") private BjoernZGRBackground background; @@ -64,6 +67,16 @@ public void setReference(String reference) { this.reference = reference; } + @JsonProperty("Changelog") + public String getChangelog() { + return changelog; + } + + @JsonProperty("Changelog") + public void setChangelog(String changelog) { + this.changelog = changelog; + } + @JsonProperty("Scenarios") public List getScenarios() { return bjoernZGRScenarios; @@ -85,6 +98,6 @@ public void setBackground(BjoernZGRBackground background) { } public String toString() { - return "BjoernZGRModell(feature=" + this.getFeature() + ", version=" + this.getVersion() + ", reference=" + this.getReference() + ", background=" + this.getBackground() + ", bjoernZGRScenarios=" + this.bjoernZGRScenarios + ")"; + return "BjoernZGRModell(feature=" + this.getFeature() + ", version=" + this.getVersion() + ", reference=" + this.getReference() + ", changelog=" + this.getChangelog() + ", background=" + this.getBackground() + ", bjoernZGRScenarios=" + this.bjoernZGRScenarios + ")"; } } diff --git a/src/main/java/de/mehtrick/bjoern/parser/validator/validations/BjoernKeywords.java b/src/main/java/de/mehtrick/bjoern/parser/validator/validations/BjoernKeywords.java index b28488d..41b90b2 100644 --- a/src/main/java/de/mehtrick/bjoern/parser/validator/validations/BjoernKeywords.java +++ b/src/main/java/de/mehtrick/bjoern/parser/validator/validations/BjoernKeywords.java @@ -6,7 +6,7 @@ public enum BjoernKeywords { - GIVEN("Given:"), WHEN("When:"), THEN("Then:"), BACKGROUND("Background:"), FEATURE("Feature:"), VERSION("Version:"), REFERENCE("Reference:"), SCENARIO("- Scenario:"), SCENARIOS("Scenarios:"), STATEMENT("-"); + GIVEN("Given:"), WHEN("When:"), THEN("Then:"), BACKGROUND("Background:"), FEATURE("Feature:"), VERSION("Version:"), REFERENCE("Reference:"), CHANGELOG("Changelog:"), SCENARIO("- Scenario:"), SCENARIOS("Scenarios:"), STATEMENT("-"); public String keyword; diff --git a/src/main/resources/asciidoc.ftlh b/src/main/resources/asciidoc.ftlh index c4caacc..4c7930a 100644 --- a/src/main/resources/asciidoc.ftlh +++ b/src/main/resources/asciidoc.ftlh @@ -8,6 +8,10 @@ Version: ${version} <#if referenceAsAsciidoc?? && referenceAsAsciidoc?has_content> Reference: ${referenceAsAsciidoc} + +<#if changelog?? && changelog?has_content> +Changelog: ${changelog} + <#if gitHistory?? && gitHistory?has_content> diff --git a/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java b/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java index 6ffd386..b802df7 100644 --- a/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java +++ b/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java @@ -53,6 +53,28 @@ public void testDocGenerationWithReference() throws IOException, BjoernMissingPr assertThat(content).contains("Reference: link:https://example.com/TICKET-123[TICKET-123]"); } + @Test + @DisplayName("Test Doc Generation with Changelog") + public void testDocGenerationWithChangelog() throws IOException, BjoernMissingPropertyException, NotSupportedJunitVersionException { + BjoernDocApplication.main(new String[]{"path=src/test/resources/changelog.zgr", "docdir=src/gen/resources"}); + File generatedFile = new File("src/gen/resources/changelog.adoc"); + assertThat(generatedFile).exists(); + String content = new String(Files.readAllBytes(generatedFile.toPath()), StandardCharsets.UTF_8); + assertThat(content).contains("= Test mit Changelog"); + assertThat(content).contains("Changelog: This is a changelog entry describing what changed in this spec."); + } + + @Test + @DisplayName("Test Doc Generation without Changelog does not include changelog section") + public void testDocGenerationWithoutChangelog() throws IOException, BjoernMissingPropertyException, NotSupportedJunitVersionException { + BjoernDocApplication.main(new String[]{"path=src/test/resources/version.zgr", "docdir=src/gen/resources"}); + File generatedFile = new File("src/gen/resources/version.adoc"); + assertThat(generatedFile).exists(); + String content = new String(Files.readAllBytes(generatedFile.toPath()), StandardCharsets.UTF_8); + assertThat(content).contains("= Test mit Version"); + assertThat(content).doesNotContain("Changelog:"); + } + @Test @DisplayName("Test Generation of Empty Given") public void testGenerationOfEmptyGiven() throws BjoernMissingPropertyException, FileNotFoundException, NotSupportedJunitVersionException { diff --git a/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java b/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java index ac9e398..3a7bb7c 100644 --- a/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java +++ b/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java @@ -93,4 +93,16 @@ void testJavadocWithVersion() { Assertions.assertThat(mappedFeature.javadoc.toString()).contains("1.0.0"); } + @Test + void testJavadocWithChangelog() { + //given + bjoern = getBjoern("src/test/resources/changelog.zgr"); + //when + TypeSpec mappedFeature = new BjoernFeatureTestClassBuilder(bjoernCodeGeneratorConfig).build(bjoern).getFeatureClass(); + //then + Assertions.assertThat(mappedFeature.javadoc.toString()).contains("Test mit Changelog"); + Assertions.assertThat(mappedFeature.javadoc.toString()).contains("@Changelog"); + Assertions.assertThat(mappedFeature.javadoc.toString()).contains("This is a changelog entry describing what changed in this spec."); + } + } diff --git a/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java b/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java index 61184c0..f4b9c54 100644 --- a/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java +++ b/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java @@ -21,8 +21,8 @@ public void testInvalidKeyword() { Assertions.assertThatExceptionOfType(BjoernValidatorException.class).isThrownBy(() -> { bjoernValidator.validate(" WrongKeyword \r\n \r\n anotherWrongOne", "defaultpath"); } - ).withMessageContaining("ValidationError at line 1: The line starts with an invalid Keyword. Found \" WrongKeyword \". Allowed Keywords are: Given:,When:,Then:,Background:,Feature:,Version:,Reference:,- Scenario:,Scenarios:,-. This check is case-sensitive!") - .withMessageContaining("ValidationError at line 3: The line starts with an invalid Keyword. Found \" anotherWrongOne\". Allowed Keywords are: Given:,When:,Then:,Background:,Feature:,Version:,Reference:,- Scenario:,Scenarios:,-. This check is case-sensitive!"); + ).withMessageContaining("ValidationError at line 1: The line starts with an invalid Keyword. Found \" WrongKeyword \". Allowed Keywords are: Given:,When:,Then:,Background:,Feature:,Version:,Reference:,Changelog:,- Scenario:,Scenarios:,-. This check is case-sensitive!") + .withMessageContaining("ValidationError at line 3: The line starts with an invalid Keyword. Found \" anotherWrongOne\". Allowed Keywords are: Given:,When:,Then:,Background:,Feature:,Version:,Reference:,Changelog:,- Scenario:,Scenarios:,-. This check is case-sensitive!"); } @Test diff --git a/src/test/resources/changelog.zgr b/src/test/resources/changelog.zgr new file mode 100644 index 0000000..cb9ed9c --- /dev/null +++ b/src/test/resources/changelog.zgr @@ -0,0 +1,10 @@ +Feature: Test mit Changelog +Changelog: "This is a changelog entry describing what changed in this spec." +Scenarios: + - Scenario: Einfaches Szenario + Given: + - Ein Benutzer + When: + - Benutzer tut etwas + Then: + - Ergebnis ist korrekt From c3646ab51ae2684b437014c04bd08d0580d0cb76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:13:06 +0000 Subject: [PATCH 3/3] Sanitize changelog for AsciiDoc and support YAML multiline blocks Co-authored-by: Mehtrick <4493019+Mehtrick@users.noreply.github.com> --- .../mehtrick/bjoern/parser/modell/Bjoern.java | 11 ++++++ .../validations/InvalidKeywordValidation.java | 29 +++++++++++++++ src/main/resources/asciidoc.ftlh | 4 +-- .../bjoern/asciidoc/AsciiDocBuildTest.java | 3 +- .../BjoernFeatureTestClassBuilderTest.java | 3 +- .../bjoern/parser/BjoernValidatorTest.java | 36 +++++++++++++++++++ src/test/resources/changelog.zgr | 4 ++- 7 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java b/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java index 0fb737a..46fe449 100644 --- a/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java +++ b/src/main/java/de/mehtrick/bjoern/parser/modell/Bjoern.java @@ -1,6 +1,7 @@ package de.mehtrick.bjoern.parser.modell; import de.mehtrick.bjoern.parser.BjoernTextParser; +import de.mehtrick.bjoern.parser.replacer.AsciidocReplacer; import org.apache.commons.text.StringEscapeUtils; import java.util.List; @@ -68,6 +69,16 @@ public void setChangelog(String changelog) { this.changelog = changelog; } + /** + * Returns the changelog with AsciiDoc-specific characters escaped (e.g. pipe characters). + */ + public String getChangelogAsAsciidoc() { + if (changelog == null) { + return null; + } + return AsciidocReplacer.replace(changelog); + } + /** * Returns the reference formatted as a Javadoc hyperlink if it is a Markdown link ([text](url)) * with an allowed URL scheme (http/https), otherwise returns the plain reference text. diff --git a/src/main/java/de/mehtrick/bjoern/parser/validator/validations/InvalidKeywordValidation.java b/src/main/java/de/mehtrick/bjoern/parser/validator/validations/InvalidKeywordValidation.java index db7bdcf..2013a70 100644 --- a/src/main/java/de/mehtrick/bjoern/parser/validator/validations/InvalidKeywordValidation.java +++ b/src/main/java/de/mehtrick/bjoern/parser/validator/validations/InvalidKeywordValidation.java @@ -9,10 +9,39 @@ public InvalidKeywordValidation(String errorText) { @Override public void validate(String[] lines, int index) throws BjoernValidationsException { + if (isInsideChangelogBlock(lines, index)) { + return; + } String trimmedLine = getTrimmedLine(lines, index); boolean lineDoesNotStartWithKnownKeyword = BjoernKeywords.getKeywordValues().stream().noneMatch(trimmedLine::startsWith); if (lineDoesNotStartWithKnownKeyword) { throw new BjoernValidationsException(index, errorText, lines[index], BjoernKeywords.getKeywordsAsSingleString()); } } + + /** + * Returns true when the current line is an indented continuation of a YAML block scalar Changelog value + * (i.e. the Changelog key was followed by a block scalar indicator such as | or >). + */ + private boolean isInsideChangelogBlock(String[] lines, int index) { + if (lines[index].isEmpty() || !Character.isWhitespace(lines[index].charAt(0))) { + return false; + } + for (int i = index - 1; i >= 0; i--) { + if (lines[i].trim().isEmpty()) { + continue; + } + if (!Character.isWhitespace(lines[i].charAt(0))) { + // Found the last root-level non-empty line + int colonIndex = lines[i].indexOf(':'); + if (colonIndex < 0) { + return false; + } + String afterKey = lines[i].substring(colonIndex + 1).trim(); + return lines[i].trim().startsWith(BjoernKeywords.CHANGELOG.keyword) + && (afterKey.startsWith("|") || afterKey.startsWith(">")); + } + } + return false; + } } diff --git a/src/main/resources/asciidoc.ftlh b/src/main/resources/asciidoc.ftlh index 4c7930a..ad9ecef 100644 --- a/src/main/resources/asciidoc.ftlh +++ b/src/main/resources/asciidoc.ftlh @@ -9,8 +9,8 @@ Version: ${version} Reference: ${referenceAsAsciidoc} -<#if changelog?? && changelog?has_content> -Changelog: ${changelog} +<#if changelogAsAsciidoc?? && changelogAsAsciidoc?has_content> +Changelog: ${changelogAsAsciidoc} diff --git a/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java b/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java index b802df7..31ac13f 100644 --- a/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java +++ b/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java @@ -61,7 +61,8 @@ public void testDocGenerationWithChangelog() throws IOException, BjoernMissingPr assertThat(generatedFile).exists(); String content = new String(Files.readAllBytes(generatedFile.toPath()), StandardCharsets.UTF_8); assertThat(content).contains("= Test mit Changelog"); - assertThat(content).contains("Changelog: This is a changelog entry describing what changed in this spec."); + assertThat(content).contains("First line of the changelog."); + assertThat(content).contains("Second line with a pipe \\| character."); } @Test diff --git a/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java b/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java index 3a7bb7c..ae5cf82 100644 --- a/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java +++ b/src/test/java/de/mehtrick/bjoern/generator/builder/BjoernFeatureTestClassBuilderTest.java @@ -102,7 +102,8 @@ void testJavadocWithChangelog() { //then Assertions.assertThat(mappedFeature.javadoc.toString()).contains("Test mit Changelog"); Assertions.assertThat(mappedFeature.javadoc.toString()).contains("@Changelog"); - Assertions.assertThat(mappedFeature.javadoc.toString()).contains("This is a changelog entry describing what changed in this spec."); + Assertions.assertThat(mappedFeature.javadoc.toString()).contains("First line of the changelog."); + Assertions.assertThat(mappedFeature.javadoc.toString()).contains("Second line with a pipe | character."); } } diff --git a/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java b/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java index f4b9c54..ac51bee 100644 --- a/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java +++ b/src/test/java/de/mehtrick/bjoern/parser/BjoernValidatorTest.java @@ -159,4 +159,40 @@ public void correctsmaple() { //THEN //No Error } + + @Test + public void changelogMultilineLiteralBlock() { + String zgr = "Feature: Feature\r\n" + + "Changelog: |\r\n" + + " First line of changelog.\r\n" + + " Second line with | pipe.\r\n" + + "Scenarios: \r\n" + + " - Scenario: Scenario \r\n" + + " Given: \r\n" + + " - Ein Benutzer"; + + //WHEN + bjoernValidator.validate(zgr, "default"); + + //THEN + //No Error - multiline Changelog block scalar continuation lines must not trigger InvalidKeywordValidation + } + + @Test + public void changelogMultilineFoldedBlock() { + String zgr = "Feature: Feature\r\n" + + "Changelog: >\r\n" + + " First line of changelog.\r\n" + + " Second line.\r\n" + + "Scenarios: \r\n" + + " - Scenario: Scenario \r\n" + + " Given: \r\n" + + " - Ein Benutzer"; + + //WHEN + bjoernValidator.validate(zgr, "default"); + + //THEN + //No Error - folded block scalar continuation lines must also be accepted + } } diff --git a/src/test/resources/changelog.zgr b/src/test/resources/changelog.zgr index cb9ed9c..15ce8e7 100644 --- a/src/test/resources/changelog.zgr +++ b/src/test/resources/changelog.zgr @@ -1,5 +1,7 @@ Feature: Test mit Changelog -Changelog: "This is a changelog entry describing what changed in this spec." +Changelog: | + First line of the changelog. + Second line with a pipe | character. Scenarios: - Scenario: Einfaches Szenario Given: