diff --git a/README.md b/README.md index 91be678..114ee68 100755 --- a/README.md +++ b/README.md @@ -101,6 +101,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..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; @@ -15,6 +16,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 +25,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 +61,24 @@ public void setReference(String reference) { this.reference = reference; } + public String getChangelog() { + return this.changelog; + } + + 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. @@ -136,6 +157,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/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 c4caacc..ad9ecef 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 changelogAsAsciidoc?? && changelogAsAsciidoc?has_content> +Changelog: ${changelogAsAsciidoc} + <#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..31ac13f 100644 --- a/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java +++ b/src/test/java/de/mehtrick/bjoern/asciidoc/AsciiDocBuildTest.java @@ -53,6 +53,29 @@ 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("First line of the changelog."); + assertThat(content).contains("Second line with a pipe \\| character."); + } + + @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..ae5cf82 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,17 @@ 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("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 61184c0..ac51bee 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 @@ -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 new file mode 100644 index 0000000..15ce8e7 --- /dev/null +++ b/src/test/resources/changelog.zgr @@ -0,0 +1,12 @@ +Feature: Test mit Changelog +Changelog: | + First line of the changelog. + Second line with a pipe | character. +Scenarios: + - Scenario: Einfaches Szenario + Given: + - Ein Benutzer + When: + - Benutzer tut etwas + Then: + - Ergebnis ist korrekt