From 03f46cca9f024b4d2db9287ad3992025d1544870 Mon Sep 17 00:00:00 2001 From: Alexander Goussas Date: Thu, 23 Oct 2025 23:57:28 -0500 Subject: [PATCH] feat: implement markdown parser + add tests --- .gitignore | 2 + app/build.gradle.kts | 2 +- changelog-plugin/build.gradle.kts | 9 +++ .../github/aloussase/changelog/Changelog.kt | 2 +- .../aloussase/changelog/ChangelogEntry.kt | 2 +- .../aloussase/changelog/ChangelogPlugin.kt | 14 +++- .../config/ChangelogPluginExtension.kt | 2 +- .../aloussase/changelog/config/Config.kt | 4 +- .../changelog/formatter/ChangelogFormatter.kt | 10 +++ .../formatter/ChangelogFormatterFactory.kt | 14 ++++ .../changelog/formatter/MarkdownFormatter.kt | 9 +++ .../parser/ChangelogParserFactory.kt | 13 ++++ .../parser/MarkdownChangelogParser.kt | 51 ++++++++++++- .../parser/MarkdownChangelogParserTests.kt | 76 +++++++++++++++++++ 14 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/ChangelogFormatter.kt create mode 100644 changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/ChangelogFormatterFactory.kt create mode 100644 changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/MarkdownFormatter.kt create mode 100644 changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/parser/ChangelogParserFactory.kt create mode 100644 changelog-plugin/src/test/kotlin/io/github/aloussase/changelog/parser/MarkdownChangelogParserTests.kt diff --git a/.gitignore b/.gitignore index 38e0d8c..ec06a08 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ bin/ .DS_Store .idea + +CHANGELOG.md diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d4ec185..5669953 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { changelog { format = "markdown" - outputFileName = "CHANGELOG.md" + fileName = "CHANGELOG.md" git { baseBranch = "master" diff --git a/changelog-plugin/build.gradle.kts b/changelog-plugin/build.gradle.kts index 17c85d3..dead3ef 100644 --- a/changelog-plugin/build.gradle.kts +++ b/changelog-plugin/build.gradle.kts @@ -17,3 +17,12 @@ gradlePlugin { } } } + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") + testImplementation("org.hamcrest:java-hamcrest:2.0.0.0") +} + +tasks.withType().configureEach { + useJUnitPlatform() +} diff --git a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/Changelog.kt b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/Changelog.kt index 22a5fd5..971b9b2 100644 --- a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/Changelog.kt +++ b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/Changelog.kt @@ -1,5 +1,5 @@ package io.github.aloussase.changelog data class Changelog( - val entries: List + val entries: List = emptyList(), ) diff --git a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/ChangelogEntry.kt b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/ChangelogEntry.kt index 615decd..4733a14 100644 --- a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/ChangelogEntry.kt +++ b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/ChangelogEntry.kt @@ -4,5 +4,5 @@ import io.github.aloussase.changelog.git.Commit data class ChangelogEntry( val branchName: String, - val commit: Commit, + val commits: List = emptyList(), ) diff --git a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/ChangelogPlugin.kt b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/ChangelogPlugin.kt index a38e0e4..b8d9224 100644 --- a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/ChangelogPlugin.kt +++ b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/ChangelogPlugin.kt @@ -2,8 +2,11 @@ package io.github.aloussase.changelog import io.github.aloussase.changelog.config.ChangelogPluginExtension import io.github.aloussase.changelog.config.Config +import io.github.aloussase.changelog.formatter.ChangelogFormatterFactory +import io.github.aloussase.changelog.parser.ChangelogParserFactory import org.gradle.api.Plugin import org.gradle.api.Project +import java.io.File class ChangelogPlugin : Plugin { override fun apply(project: Project) { @@ -11,10 +14,19 @@ class ChangelogPlugin : Plugin { extension.gitInfo.baseBranch.convention("master") extension.format.convention("markdown") + extension.fileName.convention("CHANGELOG.md") project.tasks.register("changelog") { val config = Config.from(extension) - println(config) + val parser = ChangelogParserFactory.createParser(config.documentFormat) + val changelogFile = File(config.fileName) + if (!changelogFile.exists()) { + changelogFile.createNewFile() + } + val document = changelogFile.readText() + val changelog = parser.parse(document).getOrThrow() + val formatter = ChangelogFormatterFactory.create(config.documentFormat) + changelogFile.writeText(formatter.format(changelog)) } } } diff --git a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/config/ChangelogPluginExtension.kt b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/config/ChangelogPluginExtension.kt index 907c997..a6b60ea 100644 --- a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/config/ChangelogPluginExtension.kt +++ b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/config/ChangelogPluginExtension.kt @@ -10,7 +10,7 @@ abstract class ChangelogPluginExtension { abstract val format: Property - abstract val outputFileName: Property + abstract val fileName: Property fun git(action: Action) = action.execute(gitInfo) } diff --git a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/config/Config.kt b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/config/Config.kt index 3b5fd24..927eda3 100644 --- a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/config/Config.kt +++ b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/config/Config.kt @@ -4,7 +4,7 @@ import io.github.aloussase.changelog.formats.DocumentFormat import org.gradle.api.GradleException data class Config( - val outputFileName: String, + val fileName: String, val gitBranch: String, val documentFormat: DocumentFormat, ) { @@ -20,7 +20,7 @@ data class Config( throw GradleException("Branch name cannot be blank") } - val outputFileName = extension.outputFileName.get() + val outputFileName = extension.fileName.get() if (outputFileName.isBlank()) { throw GradleException("Output file name cannot be blank") } diff --git a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/ChangelogFormatter.kt b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/ChangelogFormatter.kt new file mode 100644 index 0000000..4ab002b --- /dev/null +++ b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/ChangelogFormatter.kt @@ -0,0 +1,10 @@ +package io.github.aloussase.changelog.formatter + +import io.github.aloussase.changelog.Changelog + +interface ChangelogFormatter { + /** + * Format a changelog into a string. + */ + fun format(changelog: Changelog): String +} diff --git a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/ChangelogFormatterFactory.kt b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/ChangelogFormatterFactory.kt new file mode 100644 index 0000000..4e0e843 --- /dev/null +++ b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/ChangelogFormatterFactory.kt @@ -0,0 +1,14 @@ +package io.github.aloussase.changelog.formatter + +import io.github.aloussase.changelog.formats.DocumentFormat + +abstract class ChangelogFormatterFactory { + + companion object { + fun create(format: DocumentFormat): ChangelogFormatter { + return when (format) { + DocumentFormat.Markdown -> MarkdownFormatter() + } + } + } +} diff --git a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/MarkdownFormatter.kt b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/MarkdownFormatter.kt new file mode 100644 index 0000000..da6cbb7 --- /dev/null +++ b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/formatter/MarkdownFormatter.kt @@ -0,0 +1,9 @@ +package io.github.aloussase.changelog.formatter + +import io.github.aloussase.changelog.Changelog + +class MarkdownFormatter : ChangelogFormatter { + override fun format(changelog: Changelog): String { + return changelog.entries.first().branchName + } +} diff --git a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/parser/ChangelogParserFactory.kt b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/parser/ChangelogParserFactory.kt new file mode 100644 index 0000000..fcb976f --- /dev/null +++ b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/parser/ChangelogParserFactory.kt @@ -0,0 +1,13 @@ +package io.github.aloussase.changelog.parser + +import io.github.aloussase.changelog.formats.DocumentFormat + +abstract class ChangelogParserFactory { + companion object { + fun createParser(documentFormat: DocumentFormat): ChangelogParser { + return when (documentFormat) { + DocumentFormat.Markdown -> MarkdownChangelogParser() + } + } + } +} diff --git a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/parser/MarkdownChangelogParser.kt b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/parser/MarkdownChangelogParser.kt index b016edf..824dd04 100644 --- a/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/parser/MarkdownChangelogParser.kt +++ b/changelog-plugin/src/main/kotlin/io/github/aloussase/changelog/parser/MarkdownChangelogParser.kt @@ -1,12 +1,61 @@ package io.github.aloussase.changelog.parser import io.github.aloussase.changelog.Changelog +import io.github.aloussase.changelog.ChangelogEntry +import io.github.aloussase.changelog.git.Commit +import org.gradle.api.GradleException /** * A ChangelogParser that assumes it's input to be a Markdown document. */ class MarkdownChangelogParser : ChangelogParser { + + companion object { + private val BRANCH_NAME_REGEX = Regex("## [\\w-]+") + private val COMMIT_REGRX = Regex("- [\\w ]+") + private val DOC_TITLE_REGEX = Regex("# (Changelog|CHANGELOG)") + } + override fun parse(doc: String): Result { - TODO("Not yet implemented") + if (doc.isEmpty()) { + return Result.success(Changelog()) + } + + val entries = arrayListOf() + + for (blk in doc.split("\n\n")) { + if (DOC_TITLE_REGEX.matches(blk)) continue + + val lines = blk.split("\n") + if (lines.isEmpty()) continue + + val branchName = lines[0] + if (!BRANCH_NAME_REGEX.matches(branchName)) { + return Result.failure( + GradleException("Expected a valid branch name, but got $branchName") + ) + } + + val commits = arrayListOf() + + for (line in lines.subList(1, lines.size)) { + if (COMMIT_REGRX.matches(line)) { + commits.add(Commit(line.dropWhile { it == '-' || it == ' ' })) + } else { + return Result.failure( + GradleException("Expected a valid commit message, but got $line") + ) + } + } + + entries.add( + ChangelogEntry( + branchName.dropWhile { it == '#' || it == ' ' }, + commits + ) + ) + } + + return Result.success(Changelog(entries)) } } diff --git a/changelog-plugin/src/test/kotlin/io/github/aloussase/changelog/parser/MarkdownChangelogParserTests.kt b/changelog-plugin/src/test/kotlin/io/github/aloussase/changelog/parser/MarkdownChangelogParserTests.kt new file mode 100644 index 0000000..76f61a0 --- /dev/null +++ b/changelog-plugin/src/test/kotlin/io/github/aloussase/changelog/parser/MarkdownChangelogParserTests.kt @@ -0,0 +1,76 @@ +package io.github.aloussase.changelog.parser + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.emptyIterable +import org.hamcrest.Matchers.equalTo +import org.hamcrest.collection.IsCollectionWithSize.hasSize +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class MarkdownChangelogParserTests { + + @Test + fun givenEmptyFileWhenParseIsInvokedThenReturnEmptyChangelog() { + val input = "" + val parser = MarkdownChangelogParser() + + val result = parser.parse(input) + + assertThat(result.isSuccess, equalTo(true)) + assertThat(result.getOrThrow().entries, emptyIterable()) + } + + @ParameterizedTest + @ValueSource(strings = ["# Changelog", "# CHANGELOG"]) + fun givenDocumentContainingOnlyHeaderWhenParseIsInvokedThenReturnEmptyChangelog(doc: String) { + val parser = MarkdownChangelogParser() + + val result = parser.parse(doc) + + assertThat(result.isSuccess, equalTo(true)) + assertThat(result.getOrThrow().entries, emptyIterable()) + } + + @Test + fun givenDocumentContainingSingleEntryWithNoCommitsWhenParseisInvokedThenReturnChangelogWithThatEntryAndNoCommits() { + val doc = "# Changelog\n\n## LOYMAR-123" + val parser = MarkdownChangelogParser() + + val result = parser.parse(doc) + + assertThat(result.isSuccess, equalTo(true)) + assertThat(result.getOrThrow().entries, hasSize(1)) + assertThat(result.getOrThrow().entries.first().branchName, equalTo("LOYMAR-123")) + assertThat(result.getOrThrow().entries.first().commits, emptyIterable()) + } + + @Test + fun givenDocumentContainingSingleEntryWithCommitsWhenParseIsInvokedThenReturnChangelogWithSingleEntryAndCommits() { + val doc = "# Changelog\n\n## LOYMAR-123\n- first commit\n- second commit" + val parser = MarkdownChangelogParser() + + val result = parser.parse(doc) + + assertThat(result.isSuccess, equalTo(true)) + assertThat(result.getOrThrow().entries, hasSize(1)) + assertThat(result.getOrThrow().entries[0].commits, hasSize(2)) + assertThat(result.getOrThrow().entries[0].commits[0].message, equalTo("first commit")) + assertThat(result.getOrThrow().entries[0].commits[1].message, equalTo("second commit")) + } + + @Test + fun givenDocumentWithTwoEntriesWhenParseIsInvokedThenReturnChangelogWithTwoEntries() { + val doc = "# Changelog\n\n## LOYMAR-123\n- first commit\n- second commit\n\n## LOYMAR-456\n- third commit" + val parser = MarkdownChangelogParser() + + val result = parser.parse(doc) + + assertThat(result.isSuccess, equalTo(true)) + assertThat(result.getOrThrow().entries, hasSize(2)) + assertThat(result.getOrThrow().entries[0].commits, hasSize(2)) + assertThat(result.getOrThrow().entries[1].commits, hasSize(1)) + assertThat(result.getOrThrow().entries[1].commits[0].message, equalTo("third commit")) + } + +} -- 2.43.0