diff --git a/.github/workflows/java-ea-maven.yml b/.github/workflows/java-ea-maven.yml index 3d86d3a..6ec751a 100644 --- a/.github/workflows/java-ea-maven.yml +++ b/.github/workflows/java-ea-maven.yml @@ -32,6 +32,6 @@ jobs: cache: 'maven' - name: Build and (headless) test with Maven - uses: GabrielBB/xvfb-action@v1 + uses: smithki/xvfb-action@v1.1.2 with: run: mvn -U -B -ntp package diff --git a/.github/workflows/java8-maven.yml b/.github/workflows/java8-maven.yml index 9b53d72..5f26664 100644 --- a/.github/workflows/java8-maven.yml +++ b/.github/workflows/java8-maven.yml @@ -29,7 +29,7 @@ jobs: cache: 'maven' - name: Build and (headless) test with Maven - uses: GabrielBB/xvfb-action@v1 + uses: smithki/xvfb-action@v1.1.2 with: run: mvn -U -B -ntp package @@ -59,7 +59,7 @@ jobs: cache: 'maven' - name: Deploy snapshot with Maven if settings defined - run: test ! -f ci.settings.xml || mvn -B -ntp deploy -DskipTests=true -s ci.settings.xml -P base-deploy,snapshot-deploy,!non-deployable-modules + run: test ! -f ci.settings.xml || mvn -B -ntp deploy -DskipTests -s ci.settings.xml -P base-deploy,snapshot-deploy,!non-deployable-modules env: OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} @@ -67,13 +67,22 @@ jobs: SIGN_KEY_PASS: ${{ secrets.MAVEN_GPG_PASSPHRASE }} - name: Dryrun release assets with Maven - run: mvn -B -ntp install -DskipTests=true -P full-release -Djreleaser.dry.run=true + run: mvn -B -ntp install -DskipTests -P full-release -Djreleaser.output.directory=$PWD/out/jreleaser -Djreleaser.dry.run env: JRELEASER_GITHUB_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }} JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.MAVEN_GPG_PUBLIC_KEY }} JRELEASER_GPG_SECRET_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} JRELEASER_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + - name: Upload JReleaser output + if: always() + uses: actions/upload-artifact@v3 + with: + name: assets-snapshot-log + path: | + out/jreleaser/trace.log + out/jreleaser/output.properties + release-job: needs: build-and-test-job if: startsWith(github.repository, 'nbbrd/') && startsWith(github.ref, 'refs/tags/v') @@ -100,7 +109,7 @@ jobs: cache: 'maven' - name: Deploy with Maven if settings defined - run: test ! -f ci.settings.xml || mvn -B -ntp deploy -DskipTests=true -s ci.settings.xml -P base-deploy,release-deploy,!non-deployable-modules + run: test ! -f ci.settings.xml || mvn -B -ntp deploy -DskipTests -s ci.settings.xml -P base-deploy,release-deploy,!non-deployable-modules env: OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} @@ -110,9 +119,18 @@ jobs: MAVEN_OPTS: "--add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED" - name: Release assets with Maven - run: mvn -B -ntp install -DskipTests=true -P full-release + run: mvn -B -ntp install -DskipTests -P full-release -Djreleaser.output.directory=$PWD/out/jreleaser env: JRELEASER_GITHUB_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }} JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.MAVEN_GPG_PUBLIC_KEY }} JRELEASER_GPG_SECRET_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} JRELEASER_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + + - name: Upload JReleaser output + if: always() + uses: actions/upload-artifact@v3 + with: + name: assets-release-log + path: | + out/jreleaser/trace.log + out/jreleaser/output.properties diff --git a/CHANGELOG.md b/CHANGELOG.md index 19665ed..07aa8c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.6.0] - 2023-06-20 + +This release improves extension points and also aligns features of Maven plugin and CLI. + +### Added + +- Add extension point for scan formatting [#119](https://github.com/nbbrd/heylogs/issues/119) +- Add scan mojo [#120](https://github.com/nbbrd/heylogs/issues/120) +- Add list command and mojo [#120](https://github.com/nbbrd/heylogs/issues/120) + +### Changed + +- Refactor extension points [#119](https://github.com/nbbrd/heylogs/issues/119) +- Merge old list command into extract command [#120](https://github.com/nbbrd/heylogs/issues/120) +- Improve output of errors in check mojo [#119](https://github.com/nbbrd/heylogs/issues/119) + ## [0.5.0] - 2022-11-29 ### Added @@ -67,7 +83,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Initial release -[Unreleased]: https://github.com/nbbrd/heylogs/compare/v0.5.0...HEAD +[Unreleased]: https://github.com/nbbrd/heylogs/compare/v0.6.0...HEAD +[0.6.0]: https://github.com/nbbrd/heylogs/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/nbbrd/heylogs/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/nbbrd/heylogs/compare/v0.3.2...v0.4.0 [0.3.2]: https://github.com/nbbrd/heylogs/compare/v0.3.1...v0.3.2 diff --git a/heylogs-api/pom.xml b/heylogs-api/pom.xml index 571e3fd..fe0d1d0 100644 --- a/heylogs-api/pom.xml +++ b/heylogs-api/pom.xml @@ -7,7 +7,7 @@ com.github.nbbrd.heylogs heylogs-parent - 0.5.0 + 0.6.0 heylogs-api @@ -50,7 +50,12 @@ org.semver4j semver4j - 3.0.0 + 4.3.0 + + + com.github.nbbrd.java-io-util + java-io-base + 0.0.23 diff --git a/heylogs-api/src/main/java/internal/heylogs/ExtendedRules.java b/heylogs-api/src/main/java/internal/heylogs/ExtendedRules.java index 775c1b0..9ff546b 100644 --- a/heylogs-api/src/main/java/internal/heylogs/ExtendedRules.java +++ b/heylogs-api/src/main/java/internal/heylogs/ExtendedRules.java @@ -3,13 +3,15 @@ import com.vladsch.flexmark.ast.Link; import com.vladsch.flexmark.ast.LinkNodeBase; import com.vladsch.flexmark.util.ast.Node; +import nbbrd.design.MightBeGenerated; import nbbrd.design.VisibleForTesting; import nbbrd.heylogs.Failure; -import nbbrd.heylogs.Rule; -import nbbrd.heylogs.RuleBatch; +import nbbrd.heylogs.spi.Rule; +import nbbrd.heylogs.spi.RuleBatch; +import nbbrd.io.text.Parser; import nbbrd.service.ServiceProvider; +import org.jetbrains.annotations.NotNull; -import java.net.MalformedURLException; import java.net.URL; import java.util.Locale; import java.util.stream.Stream; @@ -18,19 +20,19 @@ public enum ExtendedRules implements Rule { HTTPS { @Override - public Failure validate(Node node) { - return node instanceof LinkNodeBase ? validateHttps((LinkNodeBase) node) : null; + public Failure validate(@NotNull Node node) { + return node instanceof LinkNodeBase ? validateHttps((LinkNodeBase) node) : NO_PROBLEM; } }, GITHUB_ISSUE_REF { @Override - public Failure validate(Node node) { - return node instanceof Link ? validateGitHubIssueRef((Link) node) : null; + public Failure validate(@NotNull Node node) { + return node instanceof Link ? validateGitHubIssueRef((Link) node) : NO_PROBLEM; } }; @Override - public String getName() { + public @NotNull String getId() { return name().toLowerCase(Locale.ROOT).replace('_', '-'); } @@ -41,49 +43,61 @@ public boolean isAvailable() { @VisibleForTesting static Failure validateHttps(LinkNodeBase link) { - try { - if (new URL(link.getUrl().toString()).getProtocol().equals("http")) { - return Failure.of(HTTPS, "Expecting HTTPS protocol", link); - } - } catch (MalformedURLException e) { - } - return null; + return Parser + .onURL() + .parseValue(link.getUrl()) + .filter(url -> !url.getProtocol().equals("https")) + .map(ignore -> Failure + .builder() + .rule(HTTPS) + .message("Expecting HTTPS protocol") + .location(link) + .build()) + .orElse(NO_PROBLEM); } @VisibleForTesting static Failure validateGitHubIssueRef(Link link) { int expected = getGitHubIssueRefFromURL(link); int found = getGitHubIssueRefFromText(link); - return expected != -1 && found != -1 && expected != found - ? Failure.of(GITHUB_ISSUE_REF, "Expecting GitHub issue ref " + expected + ", found " + found, link) - : null; + return expected != NO_ISSUE_REF && found != NO_ISSUE_REF && expected != found + ? Failure + .builder() + .rule(GITHUB_ISSUE_REF) + .message("Expecting GitHub issue ref " + expected + ", found " + found) + .location(link) + .build() + : NO_PROBLEM; } private static int getGitHubIssueRefFromURL(Link link) { - try { - URL url = new URL(link.getUrl().toString()); - if (url.getHost().equals("github.com")) { - int index = url.getPath().indexOf("/issues/"); - if (index != -1) { - return Integer.parseInt(url.getPath().substring(index + 8)); - } + URL url = Parser.onURL().parse(link.getUrl()); + if (url != null && url.getHost().equals("github.com")) { + int index = url.getPath().indexOf("/issues/"); + if (index != -1) { + return Parser + .onInteger() + .parseValue(url.getPath().substring(index + 8)) + .orElse(NO_ISSUE_REF); } - } catch (MalformedURLException | NumberFormatException ex) { } - return -1; + return NO_ISSUE_REF; } private static int getGitHubIssueRefFromText(Link link) { - try { - String text = link.getText().toString(); - if (text.startsWith("#")) { - return Integer.parseInt(text.substring(1)); - } - } catch (NumberFormatException ex) { + String text = link.getText().toString(); + if (text.startsWith("#")) { + return Parser + .onInteger() + .parseValue(text.substring(1)) + .orElse(NO_ISSUE_REF); } - return -1; + return NO_ISSUE_REF; } + private static final int NO_ISSUE_REF = -1; + + @MightBeGenerated @ServiceProvider public static final class Batch implements RuleBatch { diff --git a/heylogs-api/src/main/java/internal/heylogs/GuidingPrinciples.java b/heylogs-api/src/main/java/internal/heylogs/GuidingPrinciples.java index 5a275c2..c8c5b45 100644 --- a/heylogs-api/src/main/java/internal/heylogs/GuidingPrinciples.java +++ b/heylogs-api/src/main/java/internal/heylogs/GuidingPrinciples.java @@ -6,8 +6,11 @@ import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Document; import com.vladsch.flexmark.util.ast.Node; +import nbbrd.design.MightBeGenerated; import nbbrd.design.VisibleForTesting; import nbbrd.heylogs.*; +import nbbrd.heylogs.spi.Rule; +import nbbrd.heylogs.spi.RuleBatch; import nbbrd.service.ServiceProvider; import org.jetbrains.annotations.NotNull; @@ -19,49 +22,43 @@ public enum GuidingPrinciples implements Rule { FOR_HUMANS { @Override - public Failure validate(Node node) { - return node instanceof Document ? validateForHumans((Document) node) : null; + public Failure validate(@NotNull Node node) { + return node instanceof Document ? validateForHumans((Document) node) : NO_PROBLEM; } }, ENTRY_FOR_EVERY_VERSIONS { @Override - public Failure validate(Node node) { - return node instanceof Heading ? validateEntryForEveryVersions((Heading) node) : null; + public Failure validate(@NotNull Node node) { + return node instanceof Heading ? validateEntryForEveryVersions((Heading) node) : NO_PROBLEM; } }, TYPE_OF_CHANGES_GROUPED { @Override - public Failure validate(Node node) { - return node instanceof Heading ? validateTypeOfChangesGrouped((Heading) node) : null; + public Failure validate(@NotNull Node node) { + return node instanceof Heading ? validateTypeOfChangesGrouped((Heading) node) : NO_PROBLEM; } }, LINKABLE { @Override - public Failure validate(Node node) { - return node instanceof Heading ? validateLinkable((Heading) node) : null; + public Failure validate(@NotNull Node node) { + return node instanceof Heading ? validateLinkable((Heading) node) : NO_PROBLEM; } }, LATEST_VERSION_FIRST { @Override - public Failure validate(Node node) { - return node instanceof Document ? validateLatestVersionFirst((Document) node) : null; + public Failure validate(@NotNull Node node) { + return node instanceof Document ? validateLatestVersionFirst((Document) node) : NO_PROBLEM; } }, DATE_DISPLAYED { @Override - public Failure validate(Node node) { - return null; - } - }, - SEMVER { - @Override - public Failure validate(Node node) { - return null; + public Failure validate(@NotNull Node node) { + return NO_PROBLEM; } }; @Override - public String getName() { + public @NotNull String getId() { return name().toLowerCase(Locale.ROOT).replace('_', '-'); } @@ -79,49 +76,74 @@ static Failure validateForHumans(@NotNull Document document) { switch (headings.size()) { case 0: - return Failure.of(FOR_HUMANS, "Missing Changelog heading", document); + return Failure + .builder() + .rule(FOR_HUMANS) + .message("Missing Changelog heading") + .location(document) + .build(); case 1: try { Changelog.parse(headings.get(0)); - return null; + return NO_PROBLEM; } catch (IllegalArgumentException ex) { - return Failure.of(FOR_HUMANS, ex.getMessage(), document); + return Failure + .builder() + .rule(FOR_HUMANS) + .message(ex.getMessage()) + .location(document) + .build(); } default: - return Failure.of(FOR_HUMANS, "Too many Changelog headings", document); + return Failure + .builder() + .rule(FOR_HUMANS) + .message("Too many Changelog headings") + .location(document) + .build(); } } @VisibleForTesting static Failure validateEntryForEveryVersions(@NotNull Heading heading) { if (!Version.isVersionLevel(heading)) { - return null; + return NO_PROBLEM; } try { Version.parse(heading); } catch (IllegalArgumentException ex) { - return Failure.of(ENTRY_FOR_EVERY_VERSIONS, ex.getMessage(), heading); - } - return null; + return Failure + .builder() + .rule(ENTRY_FOR_EVERY_VERSIONS) + .message(ex.getMessage()) + .location(heading) + .build(); + } + return NO_PROBLEM; } @VisibleForTesting static Failure validateTypeOfChangesGrouped(@NotNull Heading heading) { if (!TypeOfChange.isTypeOfChangeLevel(heading)) { - return null; + return NO_PROBLEM; } try { TypeOfChange.parse(heading); } catch (IllegalArgumentException ex) { - return Failure.of(TYPE_OF_CHANGES_GROUPED, ex.getMessage(), heading); - } - return null; + return Failure + .builder() + .rule(TYPE_OF_CHANGES_GROUPED) + .message(ex.getMessage()) + .location(heading) + .build(); + } + return NO_PROBLEM; } @VisibleForTesting static Failure validateLinkable(@NotNull Heading heading) { if (!Version.isVersionLevel(heading)) { - return null; + return NO_PROBLEM; } try { @@ -132,10 +154,15 @@ static Failure validateLinkable(@NotNull Heading heading) { Reference reference = repository.get(normalizeRef); return reference == null - ? Failure.of(LINKABLE, "Missing reference '" + version.getRef() + "'", heading) - : null; + ? Failure + .builder() + .rule(LINKABLE) + .message("Missing reference '" + version.getRef() + "'") + .location(heading) + .build() + : NO_PROBLEM; } catch (IllegalArgumentException ex) { - return null; + return NO_PROBLEM; } } @@ -145,7 +172,14 @@ static Failure validateLatestVersionFirst(@NotNull Document doc) { Comparator comparator = Comparator.comparing((VersionNode item) -> item.getVersion().getDate()).reversed(); VersionNode unsortedItem = getFirstUnsortedItem(versions, comparator); - return unsortedItem != null ? Failure.of(LATEST_VERSION_FIRST, "Versions not sorted", unsortedItem.getNode()) : null; + return unsortedItem != null + ? Failure + .builder() + .rule(LATEST_VERSION_FIRST) + .message("Versions not sorted") + .location(unsortedItem.getNode()) + .build() + : NO_PROBLEM; } @lombok.Value @@ -187,6 +221,7 @@ private static T getFirstUnsortedItem(List list, Comparator comparator return null; } + @MightBeGenerated @ServiceProvider public static final class Batch implements RuleBatch { diff --git a/heylogs-api/src/main/java/internal/heylogs/SemverRule.java b/heylogs-api/src/main/java/internal/heylogs/SemverRule.java index 6a23e6a..ad45011 100644 --- a/heylogs-api/src/main/java/internal/heylogs/SemverRule.java +++ b/heylogs-api/src/main/java/internal/heylogs/SemverRule.java @@ -4,44 +4,52 @@ import com.vladsch.flexmark.util.ast.Node; import nbbrd.design.VisibleForTesting; import nbbrd.heylogs.Failure; -import nbbrd.heylogs.Rule; import nbbrd.heylogs.Version; +import nbbrd.heylogs.spi.Rule; import nbbrd.service.ServiceProvider; +import org.jetbrains.annotations.NotNull; import org.semver4j.Semver; @ServiceProvider public final class SemverRule implements Rule { @Override - public String getName() { + public @NotNull String getId() { return "semver"; } @Override - public Failure validate(Node node) { - return node instanceof Heading ? validateSemVer((Heading) node) : null; + public Failure validate(@NotNull Node node) { + return node instanceof Heading ? validateSemVer((Heading) node) : NO_PROBLEM; } @Override public boolean isAvailable() { - return Rule.isEnabled(System.getProperties(), getName()); + return Rule.isEnabled(System.getProperties(), getId()); } @VisibleForTesting Failure validateSemVer(Heading heading) { if (!Version.isVersionLevel(heading)) { - return null; + return NO_PROBLEM; } try { Version version = Version.parse(heading); if (version.isUnreleased()) { - return null; + return NO_PROBLEM; } String ref = version.getRef(); - return Semver.isValid(ref) ? null : Failure.of(this, "Invalid semver format: '" + ref + "'", heading); + return Semver.isValid(ref) + ? NO_PROBLEM + : Failure + .builder() + .rule(this) + .message("Invalid semver format: '" + ref + "'") + .location(heading) + .build(); } catch (IllegalArgumentException ex) { - return null; + return NO_PROBLEM; } } } diff --git a/heylogs-api/src/main/java/internal/heylogs/StylishFormat.java b/heylogs-api/src/main/java/internal/heylogs/StylishFormat.java new file mode 100644 index 0000000..6680c9b --- /dev/null +++ b/heylogs-api/src/main/java/internal/heylogs/StylishFormat.java @@ -0,0 +1,87 @@ +package internal.heylogs; + +import lombok.NonNull; +import nbbrd.design.MightBePromoted; +import nbbrd.heylogs.Failure; +import nbbrd.heylogs.Status; +import nbbrd.heylogs.spi.Format; +import nbbrd.service.ServiceProvider; + +import java.io.IOException; +import java.util.List; + +import static java.lang.System.lineSeparator; +import static java.util.Locale.ROOT; + +// https://eslint.org/docs/latest/user-guide/formatters/#stylish +@ServiceProvider +public final class StylishFormat implements Format { + + public static final String ID = "stylish"; + + @Override + public @NonNull String getId() { + return ID; + } + + @Override + public void formatFailures(@NonNull Appendable appendable, @NonNull String source, @NonNull List failures) throws IOException { + appendable + .append(source) + .append(lineSeparator()); + + int l = failures.stream().mapToInt(failure -> getNumberOfDigits(failure.getLine())).max().orElse(0); + int c = failures.stream().mapToInt(failure -> getNumberOfDigits(failure.getColumn())).max().orElse(0); + int m = failures.stream().mapToInt(failure -> failure.getMessage().length()).max().orElse(0); + + for (Failure x : failures) { + appendable + .append(String.format(ROOT, " %" + l + "d:%-" + c + "d error %-" + m + "s %s", x.getLine(), x.getColumn(), x.getMessage(), x.getRuleId())) + .append(lineSeparator()); + } + + appendable.append(lineSeparator()); + switch (failures.size()) { + case 0: + appendable.append(" No problem"); + break; + case 1: + appendable.append(" 1 problem"); + break; + default: + appendable.append(String.format(ROOT, " %d problems", failures.size())); + break; + } + appendable.append(lineSeparator()); + } + + @MightBePromoted + private static int getNumberOfDigits(int number) { + return (int) (Math.log10(number) + 1); + } + + @Override + public void formatStatus(@NonNull Appendable appendable, @NonNull String source, @NonNull Status status) throws IOException { + appendable.append(source); + appendable.append(lineSeparator()); + if (status.getReleaseCount() == 0) { + appendable.append(" No release found"); + appendable.append(lineSeparator()); + } else { + appendable.append(String.format(ROOT, " Found %d releases", status.getReleaseCount())); + appendable.append(lineSeparator()); + appendable.append(String.format(ROOT, " Ranging from %s to %s", status.getTimeRange().getFrom(), status.getTimeRange().getTo())); + appendable.append(lineSeparator()); + + if (status.isCompatibleWithSemver()) { + appendable.append(" Compatible with Semantic Versioning").append(status.getSemverDetails()); + appendable.append(lineSeparator()); + } else { + appendable.append(" Not compatible with Semantic Versioning"); + appendable.append(lineSeparator()); + } + } + appendable.append(status.isHasUnreleasedSection() ? " Has an unreleased version" : " Has no unreleased version"); + appendable.append(lineSeparator()); + } +} diff --git a/heylogs-api/src/main/java/internal/heylogs/StylishFormatter.java b/heylogs-api/src/main/java/internal/heylogs/StylishFormatter.java deleted file mode 100644 index 2654907..0000000 --- a/heylogs-api/src/main/java/internal/heylogs/StylishFormatter.java +++ /dev/null @@ -1,55 +0,0 @@ -package internal.heylogs; - -import nbbrd.heylogs.Failure; -import nbbrd.heylogs.FailureFormatter; -import nbbrd.service.ServiceProvider; - -import java.io.IOException; -import java.util.List; - -import static java.lang.System.lineSeparator; - -// https://eslint.org/docs/latest/user-guide/formatters/#stylish -@ServiceProvider -public final class StylishFormatter implements FailureFormatter { - - @Override - public String getName() { - return "stylish"; - } - - @Override - public void format(Appendable appendable, String source, List failures) throws IOException { - appendable - .append(source) - .append(lineSeparator()); - - int l = failures.stream().mapToInt(failure -> getNumberOfDigits(failure.getLine())).max().orElse(0); - int c = failures.stream().mapToInt(failure -> getNumberOfDigits(failure.getColumn())).max().orElse(0); - int m = failures.stream().mapToInt(failure -> failure.getMessage().length()).max().orElse(0); - - for (Failure failure : failures) { - appendable - .append(String.format(" %" + l + "d:%-" + c + "d error %-" + m + "s %s", failure.getLine(), failure.getColumn(), failure.getMessage(), failure.getRule())) - .append(lineSeparator()); - } - - appendable.append(lineSeparator()); - switch (failures.size()) { - case 0: - appendable.append(" No problem"); - break; - case 1: - appendable.append(" 1 problem"); - break; - default: - appendable.append(String.format(" %d problems", failures.size())); - break; - } - appendable.append(lineSeparator()); - } - - private static int getNumberOfDigits(int number) { - return (int) (Math.log10(number) + 1); - } -} diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/BaseSection.java b/heylogs-api/src/main/java/nbbrd/heylogs/BaseSection.java index 9a3ba2f..0dac8c8 100644 --- a/heylogs-api/src/main/java/nbbrd/heylogs/BaseSection.java +++ b/heylogs-api/src/main/java/nbbrd/heylogs/BaseSection.java @@ -1,6 +1,7 @@ package nbbrd.heylogs; import com.vladsch.flexmark.ast.Heading; +import lombok.NonNull; import nbbrd.design.SealedType; @SealedType({ @@ -10,5 +11,5 @@ }) public interface BaseSection { - Heading toHeading(); + @NonNull Heading toHeading(); } diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/Changelog.java b/heylogs-api/src/main/java/nbbrd/heylogs/Changelog.java index d9f1e3d..35ad679 100644 --- a/heylogs-api/src/main/java/nbbrd/heylogs/Changelog.java +++ b/heylogs-api/src/main/java/nbbrd/heylogs/Changelog.java @@ -3,15 +3,19 @@ import com.vladsch.flexmark.ast.Heading; import com.vladsch.flexmark.ast.Text; import com.vladsch.flexmark.util.sequence.BasedSequence; +import lombok.NonNull; import nbbrd.design.RepresentableAs; +import nbbrd.design.StaticFactoryMethod; @RepresentableAs(Heading.class) public enum Changelog implements BaseSection { + INSTANCE; private static final int HEADING_LEVEL = 1; - public static Changelog parse(Heading heading) { + @StaticFactoryMethod + public static @NonNull Changelog parse(@NonNull Heading heading) { if (!isChangelogLevel(heading)) { throw new IllegalArgumentException("Invalid heading level"); } @@ -22,7 +26,7 @@ public static Changelog parse(Heading heading) { } @Override - public Heading toHeading() { + public @NonNull Heading toHeading() { Heading result = new Heading(); result.setOpeningMarker(BasedSequence.repeatOf("#", HEADING_LEVEL)); result.setLevel(HEADING_LEVEL); diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/Checker.java b/heylogs-api/src/main/java/nbbrd/heylogs/Checker.java new file mode 100644 index 0000000..ad6b84a --- /dev/null +++ b/heylogs-api/src/main/java/nbbrd/heylogs/Checker.java @@ -0,0 +1,61 @@ +package nbbrd.heylogs; + +import com.vladsch.flexmark.util.ast.Document; +import com.vladsch.flexmark.util.ast.Node; +import lombok.NonNull; +import nbbrd.design.StaticFactoryMethod; +import nbbrd.heylogs.spi.Format; +import nbbrd.heylogs.spi.FormatLoader; +import nbbrd.heylogs.spi.Rule; +import nbbrd.heylogs.spi.RuleLoader; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@lombok.Value +@lombok.Builder(toBuilder = true) +public class Checker { + + @StaticFactoryMethod + public static @NonNull Checker ofServiceLoader() { + return Checker + .builder() + .rules(RuleLoader.load()) + .formats(FormatLoader.load()) + .build(); + } + + @NonNull + @lombok.Singular + List rules; + + @NonNull + @lombok.Singular + List formats; + + @NonNull + @lombok.Builder.Default + String formatId = FIRST_FORMAT_AVAILABLE; + + public @NonNull List validate(@NonNull Document doc) { + return Stream.concat(Stream.of(doc), Nodes.of(Node.class).descendants(doc)) + .flatMap(node -> rules.stream().map(rule -> rule.validate(node)).filter(Objects::nonNull)) + .collect(Collectors.toList()); + } + + public void formatFailures(@NonNull Appendable appendable, @NonNull String source, @NonNull List failures) throws IOException { + getFormatById().formatFailures(appendable, source, failures); + } + + private Format getFormatById() throws IOException { + return formats.stream() + .filter(format -> formatId.equals(FIRST_FORMAT_AVAILABLE) || format.getId().equals(formatId)) + .findFirst() + .orElseThrow(() -> new IOException("Cannot find format '" + formatId + "'")); + } + + private static final String FIRST_FORMAT_AVAILABLE = ""; +} diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/VersionFilter.java b/heylogs-api/src/main/java/nbbrd/heylogs/Extractor.java similarity index 78% rename from heylogs-api/src/main/java/nbbrd/heylogs/VersionFilter.java rename to heylogs-api/src/main/java/nbbrd/heylogs/Extractor.java index 94b0e8b..4632c1f 100644 --- a/heylogs-api/src/main/java/nbbrd/heylogs/VersionFilter.java +++ b/heylogs-api/src/main/java/nbbrd/heylogs/Extractor.java @@ -5,6 +5,7 @@ import com.vladsch.flexmark.ast.Reference; import com.vladsch.flexmark.util.ast.Document; import com.vladsch.flexmark.util.ast.Node; +import lombok.NonNull; import java.time.LocalDate; import java.time.Year; @@ -14,10 +15,11 @@ import java.util.regex.Pattern; @lombok.Value -@lombok.Builder -public class VersionFilter { +@lombok.Builder(toBuilder = true) +public class Extractor { + + public static final Extractor DEFAULT = Extractor.builder().build(); - public static final VersionFilter DEFAULT = VersionFilter.builder().build(); @lombok.NonNull @lombok.Builder.Default String ref = ""; @@ -33,23 +35,26 @@ public class VersionFilter { @lombok.Builder.Default int limit = Integer.MAX_VALUE; + @lombok.Builder.Default + boolean ignoreContent = false; + private boolean isUnreleasedPattern() { return unreleasedPattern.asPredicate().test(ref); } - private boolean containsRef(Version version) { + private boolean containsRef(@NonNull Version version) { return (isUnreleasedPattern() && version.isUnreleased()) || version.getRef().contains(ref); } - public boolean contains(Heading heading) { - return contains(Version.parse(heading)); + public boolean contains(@NonNull Version version) { + return containsRef(version) && timeRange.contains(version.getDate()); } - public boolean contains(Version version) { - return containsRef(version) && timeRange.contains(version.getDate()); + public boolean contains(@NonNull Heading heading) { + return contains(Version.parse(heading)); } - public void apply(Document root) { + public void extract(@NonNull Document root) { int found = 0; boolean keep = false; @@ -58,7 +63,10 @@ public void apply(Document root) { for (Node current : root.getChildren()) { - if (current instanceof Heading && Version.isVersionLevel((Heading) current)) { + boolean versionHeading = current instanceof Heading + && Version.isVersionLevel((Heading) current); + + if (versionHeading) { if (found >= getLimit() || !contains((Heading) current)) { keep = false; } else { @@ -72,6 +80,9 @@ public void apply(Document root) { .descendants(current) .map(node -> node.getReference().toString()) .forEach(refNodes::add); + if (versionHeading && ignoreContent) { + keep = false; + } } else { if (current instanceof Reference) { references.add((Reference) current); diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/Failure.java b/heylogs-api/src/main/java/nbbrd/heylogs/Failure.java index a625bd1..e1ea34c 100644 --- a/heylogs-api/src/main/java/nbbrd/heylogs/Failure.java +++ b/heylogs-api/src/main/java/nbbrd/heylogs/Failure.java @@ -1,19 +1,15 @@ package nbbrd.heylogs; -import com.vladsch.flexmark.util.ast.Document; import com.vladsch.flexmark.util.ast.Node; -import lombok.AccessLevel; +import lombok.NonNull; +import nbbrd.heylogs.spi.Rule; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@lombok.Value(staticConstructor = "of") +@lombok.Value +@lombok.Builder public class Failure { @lombok.NonNull - String rule; + String ruleId; @lombok.NonNull String message; @@ -22,17 +18,15 @@ public class Failure { int column; - public static Failure of(Rule rule, String message, Node node) { - return new Failure(rule.getName(), message, node.getStartLineNumber() + 1, node.lineColumnAtStart().getSecond() + 1); - } + public static final class Builder { - public static Failure of(Rule rule, String message, int line, int column) { - return new Failure(rule.getName(), message, line, column); - } + public @NonNull Builder rule(@NonNull Rule rule) { + return ruleId(rule.getId()); + } - public static List allOf(Document doc, List rules) { - return Stream.concat(Stream.of(doc), Nodes.of(Node.class).descendants(doc)) - .flatMap(node -> rules.stream().map(rule -> rule.validate(node)).filter(Objects::nonNull)) - .collect(Collectors.toList()); + public @NonNull Builder location(@NonNull Node location) { + return line(location.getStartLineNumber() + 1) + .column(location.lineColumnAtStart().getSecond() + 1); + } } } diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/FailureFormatter.java b/heylogs-api/src/main/java/nbbrd/heylogs/FailureFormatter.java deleted file mode 100644 index e277412..0000000 --- a/heylogs-api/src/main/java/nbbrd/heylogs/FailureFormatter.java +++ /dev/null @@ -1,18 +0,0 @@ -package nbbrd.heylogs; - -import nbbrd.service.Quantifier; -import nbbrd.service.ServiceDefinition; - -import java.io.IOException; -import java.util.List; - -@ServiceDefinition( - quantifier = Quantifier.MULTIPLE, - batch = true -) -public interface FailureFormatter { - - String getName(); - - void format(Appendable appendable, String source, List failures) throws IOException; -} diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/Scan.java b/heylogs-api/src/main/java/nbbrd/heylogs/Scan.java deleted file mode 100644 index ea7eb1e..0000000 --- a/heylogs-api/src/main/java/nbbrd/heylogs/Scan.java +++ /dev/null @@ -1,68 +0,0 @@ -package nbbrd.heylogs; - -import com.vladsch.flexmark.ast.Heading; -import com.vladsch.flexmark.util.ast.Node; -import org.semver4j.Semver; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toList; -import static nbbrd.heylogs.TimeRange.toTimeRange; - -@lombok.Value -public class Scan { - - public static Scan of(Node document) { - Map> versionByType = Nodes.of(Heading.class) - .descendants(document) - .filter(Version::isVersionLevel) - .map(Scan::parseVersionOrNull) - .filter(Objects::nonNull) - .collect(Collectors.partitioningBy(Version::isUnreleased)); - - boolean compatibleWithSemver = isCompatibleWithSemver(versionByType.get(false)); - - return new Scan( - versionByType.get(false).size(), - versionByType.get(false).stream().map(Version::getDate).collect(toTimeRange()).orElse(TimeRange.ALL), - compatibleWithSemver, - compatibleWithSemver ? getDetails(versionByType.get(false)) : "", - versionByType.containsKey(true) - ); - } - - private static Version parseVersionOrNull(Heading heading) { - try { - return Version.parse(heading); - } catch (IllegalArgumentException ex) { - return null; - } - } - - int releaseCount; - TimeRange timeRange; - boolean compatibleWithSemver; - String semverDetails; - boolean hasUnreleasedSection; - - private static boolean isCompatibleWithSemver(List releases) { - return releases.stream().map(Version::getRef).allMatch(Semver::isValid); - } - - private static String getDetails(List releases) { - List semvers = releases.stream().map(Version::getRef).map(Semver::parse).collect(toList()); - - SortedMap> diffs = IntStream.range(1, semvers.size()) - .mapToObj(i -> semvers.get(i).diff(semvers.get(i - 1))) - .collect(groupingBy((Semver.VersionDiff o) -> o, TreeMap::new, toList())); - - return diffs - .entrySet() - .stream() - .map(entry -> entry.getValue().size() + " " + entry.getKey().toString()) - .collect(Collectors.joining(", ", " (", ")")); - } -} diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/Scanner.java b/heylogs-api/src/main/java/nbbrd/heylogs/Scanner.java new file mode 100644 index 0000000..e68d9db --- /dev/null +++ b/heylogs-api/src/main/java/nbbrd/heylogs/Scanner.java @@ -0,0 +1,101 @@ +package nbbrd.heylogs; + +import com.vladsch.flexmark.ast.Heading; +import com.vladsch.flexmark.util.ast.Node; +import lombok.NonNull; +import nbbrd.design.StaticFactoryMethod; +import nbbrd.heylogs.spi.Format; +import nbbrd.heylogs.spi.FormatLoader; +import org.semver4j.Semver; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; +import static nbbrd.heylogs.TimeRange.toTimeRange; + +@lombok.Value +@lombok.Builder(toBuilder = true) +public class Scanner { + + @StaticFactoryMethod + public static @NonNull Scanner ofServiceLoader() { + return builder() + .formats(FormatLoader.load()) + .build(); + } + + @NonNull + @lombok.Singular + List formats; + + @NonNull + @lombok.Builder.Default + String formatId = FIRST_FORMAT_AVAILABLE; + + public @NonNull Status scan(@NonNull Node document) { + Map> versionByType = Nodes.of(Heading.class) + .descendants(document) + .filter(Version::isVersionLevel) + .map(Scanner::parseVersionOrNull) + .filter(Objects::nonNull) + .collect(Collectors.partitioningBy(Version::isUnreleased)); + + boolean compatibleWithSemver = isCompatibleWithSemver(versionByType.get(false)); + + return Status + .builder() + .releaseCount(versionByType.get(false).size()) + .timeRange(versionByType.get(false).stream().map(Version::getDate).collect(toTimeRange()).orElse(TimeRange.ALL)) + .compatibleWithSemver(compatibleWithSemver) + .semverDetails(compatibleWithSemver ? getDetails(versionByType.get(false)) : "") + .hasUnreleasedSection(versionByType.containsKey(true)) + .build(); + } + + private static Version parseVersionOrNull(Heading heading) { + try { + return Version.parse(heading); + } catch (IllegalArgumentException ex) { + return null; + } + } + + private static boolean isCompatibleWithSemver(List releases) { + return releases.stream().map(Version::getRef).allMatch(Semver::isValid); + } + + private static String getDetails(List releases) { + List semvers = releases.stream().map(Version::getRef).map(Semver::parse).collect(toList()); + + TreeMap> diffs = IntStream.range(1, semvers.size()) + .mapToObj(i -> semvers.get(i).diff(semvers.get(i - 1))) + .collect(groupingBy((Semver.VersionDiff o) -> o, TreeMap::new, toList())); + + return diffs + .descendingMap() + .entrySet() + .stream() + .map(entry -> entry.getValue().size() + " " + entry.getKey().toString()) + .collect(Collectors.joining(", ", " (", ")")); + } + + public void formatStatus(@NonNull Appendable appendable, @NonNull String source, @NonNull Status status) throws IOException { + getFormatById().formatStatus(appendable, source, status); + } + + private Format getFormatById() throws IOException { + return formats.stream() + .filter(format -> formatId.equals(FIRST_FORMAT_AVAILABLE) || format.getId().equals(formatId)) + .findFirst() + .orElseThrow(() -> new IOException("Cannot find format '" + formatId + "'")); + } + + private static final String FIRST_FORMAT_AVAILABLE = ""; +} diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/Status.java b/heylogs-api/src/main/java/nbbrd/heylogs/Status.java new file mode 100644 index 0000000..2d9ed60 --- /dev/null +++ b/heylogs-api/src/main/java/nbbrd/heylogs/Status.java @@ -0,0 +1,21 @@ +package nbbrd.heylogs; + +@lombok.Value +@lombok.Builder +public class Status { + + @lombok.Builder.Default + int releaseCount = 0; + + @lombok.Builder.Default + TimeRange timeRange = TimeRange.ALL; + + @lombok.Builder.Default + boolean compatibleWithSemver = false; + + @lombok.Builder.Default + String semverDetails = ""; + + @lombok.Builder.Default + boolean hasUnreleasedSection = false; +} diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/TypeOfChange.java b/heylogs-api/src/main/java/nbbrd/heylogs/TypeOfChange.java index fa3df35..527bc1b 100644 --- a/heylogs-api/src/main/java/nbbrd/heylogs/TypeOfChange.java +++ b/heylogs-api/src/main/java/nbbrd/heylogs/TypeOfChange.java @@ -3,10 +3,14 @@ import com.vladsch.flexmark.ast.Heading; import com.vladsch.flexmark.ast.Text; import com.vladsch.flexmark.util.sequence.BasedSequence; +import lombok.NonNull; +import nbbrd.design.RepresentableAs; +import nbbrd.design.StaticFactoryMethod; import java.util.stream.Stream; @lombok.AllArgsConstructor +@RepresentableAs(Heading.class) public enum TypeOfChange implements BaseSection { ADDED("Added"), @@ -22,7 +26,7 @@ public enum TypeOfChange implements BaseSection { final String label; @Override - public Heading toHeading() { + public @NonNull Heading toHeading() { Heading result = new Heading(); result.setOpeningMarker(BasedSequence.repeatOf("#", HEADING_LEVEL)); result.setLevel(HEADING_LEVEL); @@ -30,7 +34,8 @@ public Heading toHeading() { return result; } - public static TypeOfChange parse(Heading heading) { + @StaticFactoryMethod + public static @NonNull TypeOfChange parse(@NonNull Heading heading) { if (!isTypeOfChangeLevel(heading)) { throw new IllegalArgumentException("Invalid heading level"); } diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/Version.java b/heylogs-api/src/main/java/nbbrd/heylogs/Version.java index 5156675..fc62998 100644 --- a/heylogs-api/src/main/java/nbbrd/heylogs/Version.java +++ b/heylogs-api/src/main/java/nbbrd/heylogs/Version.java @@ -5,12 +5,16 @@ import com.vladsch.flexmark.ast.Text; import com.vladsch.flexmark.util.ast.Node; import com.vladsch.flexmark.util.sequence.BasedSequence; +import lombok.NonNull; +import nbbrd.design.RepresentableAs; +import nbbrd.design.StaticFactoryMethod; import java.time.LocalDate; import java.time.format.DateTimeParseException; import java.util.Iterator; -@lombok.Value +@lombok.Value(staticConstructor = "of") +@RepresentableAs(Heading.class) public class Version implements BaseSection { private static final String UNRELEASED_KEYWORD = "unreleased"; @@ -27,7 +31,7 @@ public boolean isUnreleased() { } @Override - public Heading toHeading() { + public @NonNull Heading toHeading() { Heading result = new Heading(); result.setOpeningMarker(BasedSequence.repeatOf("#", HEADING_LEVEL)); result.setLevel(HEADING_LEVEL); @@ -47,7 +51,8 @@ public Heading toHeading() { return result; } - public static Version parse(Heading heading) { + @StaticFactoryMethod + public static @NonNull Version parse(@NonNull Heading heading) { if (!isVersionLevel(heading)) { throw new IllegalArgumentException("Invalid heading level"); } @@ -89,10 +94,6 @@ private static String parseRef(Node firstPart) throws IllegalArgumentException { } private static LocalDate parseDate(Node secondPart) throws IllegalArgumentException { - if (!(secondPart instanceof Text)) { - throw new IllegalArgumentException("Invalid date type"); - } - BasedSequence date = secondPart.getChars(); if (!date.trimStart().startsWith("-")) { diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/spi/Format.java b/heylogs-api/src/main/java/nbbrd/heylogs/spi/Format.java new file mode 100644 index 0000000..be997a4 --- /dev/null +++ b/heylogs-api/src/main/java/nbbrd/heylogs/spi/Format.java @@ -0,0 +1,23 @@ +package nbbrd.heylogs.spi; + +import lombok.NonNull; +import nbbrd.heylogs.Failure; +import nbbrd.heylogs.Status; +import nbbrd.service.Quantifier; +import nbbrd.service.ServiceDefinition; + +import java.io.IOException; +import java.util.List; + +@ServiceDefinition( + quantifier = Quantifier.MULTIPLE, + batch = true +) +public interface Format { + + @NonNull String getId(); + + void formatFailures(@NonNull Appendable appendable, @NonNull String source, @NonNull List failures) throws IOException; + + void formatStatus(@NonNull Appendable appendable, @NonNull String source, @NonNull Status status) throws IOException; +} diff --git a/heylogs-api/src/main/java/nbbrd/heylogs/Rule.java b/heylogs-api/src/main/java/nbbrd/heylogs/spi/Rule.java similarity index 69% rename from heylogs-api/src/main/java/nbbrd/heylogs/Rule.java rename to heylogs-api/src/main/java/nbbrd/heylogs/spi/Rule.java index d82ee32..a6bbea0 100644 --- a/heylogs-api/src/main/java/nbbrd/heylogs/Rule.java +++ b/heylogs-api/src/main/java/nbbrd/heylogs/spi/Rule.java @@ -1,14 +1,15 @@ -package nbbrd.heylogs; +package nbbrd.heylogs.spi; import com.vladsch.flexmark.util.ast.Node; import lombok.NonNull; +import nbbrd.heylogs.Failure; import nbbrd.service.Quantifier; import nbbrd.service.ServiceDefinition; import nbbrd.service.ServiceFilter; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Arrays; import java.util.Properties; -import java.util.stream.Stream; @ServiceDefinition( quantifier = Quantifier.MULTIPLE, @@ -16,17 +17,19 @@ ) public interface Rule { - String getName(); + @NonNull String getId(); - Failure validate(Node node); + @Nullable Failure validate(@NonNull Node node); @ServiceFilter boolean isAvailable(); + Failure NO_PROBLEM = null; + String ENABLE_KEY = "heylogs.rule.enable"; - static boolean isEnabled(@NonNull Properties properties, @NonNull String ruleName) { + static boolean isEnabled(@NonNull Properties properties, @NonNull String ruleId) { String list = properties.getProperty(ENABLE_KEY); - return list != null && Arrays.asList(list.split(",", -1)).contains(ruleName); + return list != null && Arrays.asList(list.split(",", -1)).contains(ruleId); } } diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/Sample.java b/heylogs-api/src/test/java/_test/Sample.java similarity index 91% rename from heylogs-api/src/test/java/nbbrd/heylogs/Sample.java rename to heylogs-api/src/test/java/_test/Sample.java index a593aa1..1b28534 100644 --- a/heylogs-api/src/test/java/nbbrd/heylogs/Sample.java +++ b/heylogs-api/src/test/java/_test/Sample.java @@ -1,4 +1,4 @@ -package nbbrd.heylogs; +package _test; import com.vladsch.flexmark.ast.Heading; import com.vladsch.flexmark.formatter.Formatter; @@ -7,6 +7,7 @@ import com.vladsch.flexmark.util.sequence.BasedSequence; import java.io.*; +import java.nio.charset.StandardCharsets; public class Sample { @@ -18,7 +19,7 @@ public static Document using(String name) { if (stream == null) { throw new IllegalArgumentException("Missing resource '" + name + "'"); } - try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { return PARSER.parseReader(reader); } } catch (IOException ex) { diff --git a/heylogs-api/src/test/java/internal/heylogs/ExtendedRulesTest.java b/heylogs-api/src/test/java/internal/heylogs/ExtendedRulesTest.java index f2de4f7..2a6583e 100644 --- a/heylogs-api/src/test/java/internal/heylogs/ExtendedRulesTest.java +++ b/heylogs-api/src/test/java/internal/heylogs/ExtendedRulesTest.java @@ -3,10 +3,9 @@ import com.vladsch.flexmark.ast.Link; import com.vladsch.flexmark.ast.LinkNodeBase; import com.vladsch.flexmark.util.ast.Node; -import internal.heylogs.ExtendedRules; import nbbrd.heylogs.Failure; import nbbrd.heylogs.Nodes; -import nbbrd.heylogs.Sample; +import _test.Sample; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -15,7 +14,7 @@ import static internal.heylogs.ExtendedRules.GITHUB_ISSUE_REF; import static internal.heylogs.ExtendedRules.HTTPS; import static nbbrd.heylogs.Nodes.of; -import static nbbrd.heylogs.Sample.using; +import static _test.Sample.using; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.data.Index.atIndex; @@ -23,7 +22,7 @@ public class ExtendedRulesTest { @Test public void test() { - Node sample = Sample.using("Main.md"); + Node sample = Sample.using("/Main.md"); for (ExtendedRules rule : ExtendedRules.values()) { Assertions.assertThat(Nodes.of(Node.class).descendants(sample).map(rule::validate).filter(Objects::nonNull)) .isEmpty(); @@ -32,34 +31,34 @@ public void test() { @Test public void testValidateHttps() { - assertThat(of(LinkNodeBase.class).descendants(using("Main.md"))) + assertThat(of(LinkNodeBase.class).descendants(using("/Main.md"))) .map(ExtendedRules::validateHttps) .isNotEmpty() .filteredOn(Objects::nonNull) .isEmpty(); - assertThat(of(LinkNodeBase.class).descendants(using("NonHttps.md"))) + assertThat(of(LinkNodeBase.class).descendants(using("/NonHttps.md"))) .map(ExtendedRules::validateHttps) .isNotEmpty() .filteredOn(Objects::nonNull) - .contains(Failure.of(HTTPS, "Expecting HTTPS protocol", 1, 1), atIndex(0)) - .contains(Failure.of(HTTPS, "Expecting HTTPS protocol", 2, 7), atIndex(1)) + .contains(Failure.builder().rule(HTTPS).message("Expecting HTTPS protocol").line(1).column(1).build(), atIndex(0)) + .contains(Failure.builder().rule(HTTPS).message("Expecting HTTPS protocol").line(2).column(7).build(), atIndex(1)) .hasSize(2); // FIXME: should be 3 } @Test public void testValidateGitHubIssueRef() { - assertThat(of(Link.class).descendants(using("Main.md"))) + assertThat(of(Link.class).descendants(using("/Main.md"))) .map(ExtendedRules::validateGitHubIssueRef) .isNotEmpty() .filteredOn(Objects::nonNull) .isEmpty(); - assertThat(of(Link.class).descendants(using("InvalidGitHubIssueRef.md"))) + assertThat(of(Link.class).descendants(using("/InvalidGitHubIssueRef.md"))) .map(ExtendedRules::validateGitHubIssueRef) .isNotEmpty() .filteredOn(Objects::nonNull) - .contains(Failure.of(GITHUB_ISSUE_REF, "Expecting GitHub issue ref 172, found 173", 2, 1), atIndex(0)) + .contains(Failure.builder().rule(GITHUB_ISSUE_REF).message("Expecting GitHub issue ref 172, found 173").line(2).column(1).build(), atIndex(0)) .hasSize(1); } } diff --git a/heylogs-api/src/test/java/internal/heylogs/GuidingPrinciplesTest.java b/heylogs-api/src/test/java/internal/heylogs/GuidingPrinciplesTest.java index b4d58d1..3c4dd09 100644 --- a/heylogs-api/src/test/java/internal/heylogs/GuidingPrinciplesTest.java +++ b/heylogs-api/src/test/java/internal/heylogs/GuidingPrinciplesTest.java @@ -2,7 +2,6 @@ import com.vladsch.flexmark.ast.Heading; import com.vladsch.flexmark.util.ast.Node; -import internal.heylogs.GuidingPrinciples; import nbbrd.heylogs.Failure; import org.junit.jupiter.api.Test; @@ -10,7 +9,7 @@ import static internal.heylogs.GuidingPrinciples.*; import static nbbrd.heylogs.Nodes.of; -import static nbbrd.heylogs.Sample.using; +import static _test.Sample.using; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.data.Index.atIndex; @@ -18,7 +17,7 @@ public class GuidingPrinciplesTest { @Test public void testSample() { - Node sample = using("Main.md"); + Node sample = using("/Main.md"); for (GuidingPrinciples rule : GuidingPrinciples.values()) { assertThat(of(Node.class).descendants(sample).map(rule::validate).filter(Objects::nonNull)) .isEmpty(); @@ -27,85 +26,85 @@ public void testSample() { @Test public void testValidateForHumans() { - assertThat(validateForHumans(using("Main.md"))) + assertThat(validateForHumans(using("/Main.md"))) .isNull(); - assertThat(validateForHumans(using("Empty.md"))) - .isEqualTo(Failure.of(FOR_HUMANS, "Missing Changelog heading", 1, 1)); + assertThat(validateForHumans(using("/Empty.md"))) + .isEqualTo(Failure.builder().rule(FOR_HUMANS).message("Missing Changelog heading").line(1).column(1).build()); - assertThat(validateForHumans(using("NoChangelog.md"))) - .isEqualTo(Failure.of(FOR_HUMANS, "Invalid text", 1, 1)); + assertThat(validateForHumans(using("/NoChangelog.md"))) + .isEqualTo(Failure.builder().rule(FOR_HUMANS).message("Invalid text").line(1).column(1).build()); - assertThat(validateForHumans(using("TooManyChangelog.md"))) - .isEqualTo(Failure.of(FOR_HUMANS, "Too many Changelog headings", 1, 1)); + assertThat(validateForHumans(using("/TooManyChangelog.md"))) + .isEqualTo(Failure.builder().rule(FOR_HUMANS).message("Too many Changelog headings").line(1).column(1).build()); } @Test public void testValidateEntryForEveryVersions() { - assertThat(of(Heading.class).descendants(using("Main.md"))) + assertThat(of(Heading.class).descendants(using("/Main.md"))) .map(GuidingPrinciples::validateEntryForEveryVersions) .isNotEmpty() .filteredOn(Objects::nonNull) .isEmpty(); - assertThat(of(Heading.class).descendants(using("InvalidVersion.md"))) + assertThat(of(Heading.class).descendants(using("/InvalidVersion.md"))) .map(GuidingPrinciples::validateEntryForEveryVersions) .isNotEmpty() .filteredOn(Objects::nonNull) - .contains(Failure.of(ENTRY_FOR_EVERY_VERSIONS, "Invalid date format", 2, 1), atIndex(0)) - .contains(Failure.of(ENTRY_FOR_EVERY_VERSIONS, "Missing date part", 3, 1), atIndex(1)) - .contains(Failure.of(ENTRY_FOR_EVERY_VERSIONS, "Missing ref link", 4, 1), atIndex(2)) + .contains(Failure.builder().rule(ENTRY_FOR_EVERY_VERSIONS).message("Invalid date format").line(2).column(1).build(), atIndex(0)) + .contains(Failure.builder().rule(ENTRY_FOR_EVERY_VERSIONS).message("Missing date part").line(3).column(1).build(), atIndex(1)) + .contains(Failure.builder().rule(ENTRY_FOR_EVERY_VERSIONS).message("Missing ref link").line(4).column(1).build(), atIndex(2)) .hasSize(3); } @Test public void testValidateTypeOfChangesGrouped() { - assertThat(of(Heading.class).descendants(using("Main.md"))) + assertThat(of(Heading.class).descendants(using("/Main.md"))) .map(GuidingPrinciples::validateTypeOfChangesGrouped) .isNotEmpty() .filteredOn(Objects::nonNull) .isEmpty(); - assertThat(of(Heading.class).descendants(using("InvalidTypeOfChange.md"))) + assertThat(of(Heading.class).descendants(using("/InvalidTypeOfChange.md"))) .map(GuidingPrinciples::validateTypeOfChangesGrouped) .isNotEmpty() .filteredOn(Objects::nonNull) - .contains(Failure.of(TYPE_OF_CHANGES_GROUPED, "Cannot parse 'Stuff'", 7, 1), atIndex(0)) + .contains(Failure.builder().rule(TYPE_OF_CHANGES_GROUPED).message("Cannot parse 'Stuff'").line(7).column(1).build(), atIndex(0)) .hasSize(1); } @Test public void testValidateLinkable() { - assertThat(of(Heading.class).descendants(using("Main.md"))) + assertThat(of(Heading.class).descendants(using("/Main.md"))) .map(GuidingPrinciples::validateLinkable) .isNotEmpty() .filteredOn(Objects::nonNull) .isEmpty(); - assertThat(of(Heading.class).descendants(using("MissingReference.md"))) + assertThat(of(Heading.class).descendants(using("/MissingReference.md"))) .map(GuidingPrinciples::validateLinkable) .isNotEmpty() .filteredOn(Objects::nonNull) - .contains(Failure.of(LINKABLE, "Missing reference '1.1.0'", 5, 1), atIndex(0)) + .contains(Failure.builder().rule(LINKABLE).message("Missing reference '1.1.0'").line(5).column(1).build(), atIndex(0)) .hasSize(1); } @Test public void testValidateLatestVersionFirst() { - assertThat(validateLatestVersionFirst(using("Main.md"))) + assertThat(validateLatestVersionFirst(using("/Main.md"))) .isNull(); - assertThat(validateLatestVersionFirst(using("Empty.md"))) + assertThat(validateLatestVersionFirst(using("/Empty.md"))) .isNull(); - assertThat(validateLatestVersionFirst(using("NotLatestVersionFirst.md"))) - .isEqualTo(Failure.of(LATEST_VERSION_FIRST, "Versions not sorted", 3, 1)); + assertThat(validateLatestVersionFirst(using("/NotLatestVersionFirst.md"))) + .isEqualTo(Failure.builder().rule(LATEST_VERSION_FIRST).message("Versions not sorted").line(3).column(1).build()); - assertThat(validateLatestVersionFirst(using("UnsortedVersion.md"))) - .isEqualTo(Failure.of(LATEST_VERSION_FIRST, "Versions not sorted", 3, 1)); + assertThat(validateLatestVersionFirst(using("/UnsortedVersion.md"))) + .isEqualTo(Failure.builder().rule(LATEST_VERSION_FIRST).message("Versions not sorted").line(3).column(1).build()); - assertThat(validateLatestVersionFirst(using("InvalidVersion.md"))) - .isEqualTo(Failure.of(LATEST_VERSION_FIRST, "Versions not sorted", 5, 1)); + assertThat(validateLatestVersionFirst(using("/InvalidVersion.md"))) + .isEqualTo(Failure.builder().rule(LATEST_VERSION_FIRST).message("Versions not sorted").line(5).column(1).build()); } } diff --git a/heylogs-api/src/test/java/internal/heylogs/SemverRuleTest.java b/heylogs-api/src/test/java/internal/heylogs/SemverRuleTest.java index 7946ab9..b92b2ec 100644 --- a/heylogs-api/src/test/java/internal/heylogs/SemverRuleTest.java +++ b/heylogs-api/src/test/java/internal/heylogs/SemverRuleTest.java @@ -2,14 +2,13 @@ import com.vladsch.flexmark.ast.Heading; import com.vladsch.flexmark.util.ast.Node; -import internal.heylogs.SemverRule; import nbbrd.heylogs.Failure; import org.junit.jupiter.api.Test; import java.util.Objects; import static nbbrd.heylogs.Nodes.of; -import static nbbrd.heylogs.Sample.using; +import static _test.Sample.using; import static org.assertj.core.api.Assertions.assertThat; public class SemverRuleTest { @@ -18,7 +17,7 @@ public class SemverRuleTest { public void testSample() { SemverRule x = new SemverRule(); - assertThat(of(Node.class).descendants(using("Main.md"))) + assertThat(of(Node.class).descendants(using("/Main.md"))) .map(x::validate) .filteredOn(Objects::nonNull) .isEmpty(); @@ -28,20 +27,20 @@ public void testSample() { public void testValidateSemVer() { SemverRule x = new SemverRule(); - assertThat(of(Heading.class).descendants(using("Main.md"))) + assertThat(of(Heading.class).descendants(using("/Main.md"))) .map(x::validateSemVer) .filteredOn(Objects::nonNull) .isEmpty(); - assertThat(of(Heading.class).descendants(using("Empty.md"))) + assertThat(of(Heading.class).descendants(using("/Empty.md"))) .map(x::validateSemVer) .filteredOn(Objects::nonNull) .isEmpty(); - assertThat(of(Heading.class).descendants(using("InvalidSemver.md"))) + assertThat(of(Heading.class).descendants(using("/InvalidSemver.md"))) .map(x::validateSemVer) .filteredOn(Objects::nonNull) .hasSize(1) - .contains(Failure.of(x, "Invalid semver format: '.1.0'", 2, 1)); + .contains(Failure.builder().rule(x).message("Invalid semver format: '.1.0'").line(2).column(1).build()); } } diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/AboutTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/AboutTest.java new file mode 100644 index 0000000..a56dee7 --- /dev/null +++ b/heylogs-api/src/test/java/nbbrd/heylogs/AboutTest.java @@ -0,0 +1,14 @@ +package nbbrd.heylogs; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AboutTest { + + @Test + public void testVersion() { + assertThat(About.VERSION) + .isEqualTo("unknown"); + } +} diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/ChangelogTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/ChangelogTest.java index b7ecadc..edd41f3 100644 --- a/heylogs-api/src/test/java/nbbrd/heylogs/ChangelogTest.java +++ b/heylogs-api/src/test/java/nbbrd/heylogs/ChangelogTest.java @@ -1,43 +1,42 @@ package nbbrd.heylogs; +import _test.Sample; import com.vladsch.flexmark.ast.Heading; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; -import java.time.LocalDate; - +import static _test.Sample.using; +import static nbbrd.heylogs.Changelog.parse; +import static _test.Sample.asHeading; import static org.assertj.core.api.Assertions.*; public class ChangelogTest { @Test - public void testParseHeading() { - assertThat(parsingHeading("# Changelog")) + public void testParse() { + //noinspection DataFlowIssue + assertThatNullPointerException() + .isThrownBy(() -> parse(null)); + + assertThat(parse(asHeading("# Changelog"))) .isEqualTo(Changelog.INSTANCE); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("## Changelog")) + .isThrownBy(() -> parse(asHeading("## Changelog"))) .withMessageContaining("Invalid heading level"); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("# hello")) + .isThrownBy(() -> parse(asHeading("# hello"))) .withMessageContaining("Invalid text"); - assertThat(Nodes.of(Heading.class).descendants(Sample.using("Main.md")).filter(Changelog::isChangelogLevel).map(Changelog::parse)) + assertThat(Nodes.of(Heading.class).descendants(using("/Main.md")).filter(Changelog::isChangelogLevel).map(Changelog::parse)) .hasSize(1) .contains(Changelog.INSTANCE, atIndex(0)); } @Test - public void testFormatHeading() { + public void testToHeading() { assertThat(Changelog.INSTANCE.toHeading()) - .extracting(Sample::asText) - .asString() + .extracting(Sample::asText, STRING) .isEqualTo("# Changelog"); } - - @NotNull - private Changelog parsingHeading(String text) { - return Changelog.parse(Sample.asHeading(text)); - } } diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/CheckerTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/CheckerTest.java new file mode 100644 index 0000000..fd92946 --- /dev/null +++ b/heylogs-api/src/test/java/nbbrd/heylogs/CheckerTest.java @@ -0,0 +1,64 @@ +package nbbrd.heylogs; + +import internal.heylogs.SemverRule; +import nbbrd.heylogs.spi.Rule; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static _test.Sample.using; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.InstanceOfAssertFactories.list; + +public class CheckerTest { + + @Test + public void testFactories() { + assertThat(Checker.builder().build()) + .returns(0, checker -> checker.getRules().size()) + .returns(0, checker -> checker.getFormats().size()); + + assertThat(Checker.ofServiceLoader()) + .extracting(Checker::getRules, list(Rule.class)) + .hasSizeGreaterThan(1) + .map(Rule::getId) + .doesNotContain("semver"); + + assertThat(Checker.ofServiceLoader().toBuilder().rule(new SemverRule()).build()) + .extracting(Checker::getRules, list(Rule.class)) + .hasSizeGreaterThan(1) + .map(Rule::getId) + .contains("semver"); + } + + @Test + public void testValidate() { + assertThat(Checker.builder().build().validate(using("/InvalidVersion.md"))) + .isEmpty(); + + assertThat(Checker.ofServiceLoader().validate(using("/InvalidVersion.md"))) + .isNotEmpty(); + } + + @Test + public void testFormatFailures() throws IOException { + assertThatIOException() + .isThrownBy(() -> Checker.builder().build().formatFailures(new StringBuilder(), "", emptyList())); + + assertThatIOException() + .isThrownBy(() -> Checker.ofServiceLoader().toBuilder().formatId("other").build().formatFailures(new StringBuilder(), "", emptyList())); + + StringBuilder output = new StringBuilder(); + Checker.ofServiceLoader().formatFailures(output, "file1", asList(Failure.builder().ruleId("rule1").message("some message").line(10).column(20).build())); + assertThat(output.toString()) + .isEqualToIgnoringNewLines( + "file1\n" + + " 10:20 error some message rule1\n" + + "\n" + + " 1 problem\n" + ); + } +} diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/ExtractorTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/ExtractorTest.java new file mode 100644 index 0000000..4ae25a4 --- /dev/null +++ b/heylogs-api/src/test/java/nbbrd/heylogs/ExtractorTest.java @@ -0,0 +1,171 @@ +package nbbrd.heylogs; + +import _test.Sample; +import com.vladsch.flexmark.util.ast.Document; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.function.Function; + +import static _test.Sample.using; +import static nbbrd.heylogs.Extractor.builder; +import static nbbrd.heylogs.Extractor.parseLocalDate; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.InstanceOfAssertFactories.STRING; + +public class ExtractorTest { + + @Test + public void testRef() { + assertThat(builder().build()) + .describedAs("Empty reference") + .is(containing(unreleased)) + .is(containing(v1_1_0)) + .is(containing(v1_0_0)); + + assertThat(builder().ref("Unreleased").build()) + .describedAs("Full reference") + .is(containing(unreleased)) + .isNot(containing(v1_1_0)) + .isNot(containing(v1_0_0)); + + assertThat(builder().ref("1.1.0").build()) + .describedAs("Full reference") + .isNot(containing(unreleased)) + .is(containing(v1_1_0)) + .isNot(containing(v1_0_0)); + + assertThat(builder().ref("rel").build()) + .describedAs("Partial reference") + .is(containing(unreleased)) + .isNot(containing(v1_1_0)) + .isNot(containing(v1_0_0)); + + assertThat(builder().ref("1.").build()) + .describedAs("Partial reference") + .isNot(containing(unreleased)) + .is(containing(v1_1_0)) + .is(containing(v1_0_0)); + + assertThat(builder().ref("other").build()) + .describedAs("Unknown reference") + .isNot(containing(unreleased)) + .isNot(containing(v1_1_0)) + .isNot(containing(v1_0_0)); + + assertThat(builder().ref("other-SNAPSHOT").build()) + .describedAs("Matching unreleased pattern reference") + .is(containing(unreleased)) + .isNot(containing(v1_1_0)) + .isNot(containing(v1_0_0)); + } + + @Test + public void testTimeRange() { + Function onTimeRange = o -> builder().timeRange(o).build(); + + assertThat(TimeRange.ALL) + .extracting(onTimeRange) + .is(containing(unreleased)) + .is(containing(v1_1_0)) + .is(containing(v1_0_0)); + + assertThat(TimeRange.of(v1_0_0.getDate(), v1_1_0.getDate())) + .extracting(onTimeRange) + .isNot(containing(unreleased)) + .is(containing(v1_1_0)) + .is(containing(v1_0_0)); + + assertThat(TimeRange.of(v1_0_0.getDate(), v1_0_0.getDate())) + .extracting(onTimeRange) + .isNot(containing(unreleased)) + .isNot(containing(v1_1_0)) + .is(containing(v1_0_0)); + + assertThat(TimeRange.of(LocalDate.MIN, v1_0_0.getDate())) + .extracting(onTimeRange) + .isNot(containing(unreleased)) + .isNot(containing(v1_1_0)) + .is(containing(v1_0_0)); + + assertThat(TimeRange.of(v1_1_0.getDate(), v1_1_0.getDate())) + .extracting(onTimeRange) + .isNot(containing(unreleased)) + .is(containing(v1_1_0)) + .isNot(containing(v1_0_0)); + + assertThat(TimeRange.of(v1_1_0.getDate(), LocalDate.MAX)) + .extracting(onTimeRange) + .is(containing(unreleased)) + .is(containing(v1_1_0)) + .isNot(containing(v1_0_0)); + } + + + @Test + public void testExtract() { + Function usingMain = extractor -> { + Document doc = using("/Main.md"); + extractor.extract(doc); + return Sample.FORMATTER.render(doc); + }; + + assertThat(builder().ref("1.1.0").build()) + .extracting(usingMain, STRING) + .isEqualTo( + "## [1.1.0] - 2019-02-15\n" + + "\n" + + "### Added\n" + + "\n" + + "- Danish translation from [@frederikspang](https://github.com/frederikspang).\n" + + "- Georgian translation from [@tatocaster](https://github.com/tatocaster).\n" + + "- Changelog inconsistency section in Bad Practices\n" + + "\n" + + "### Changed\n" + + "\n" + + "- Fixed typos in Italian translation from [@lorenzo-arena](https://github.com/lorenzo-arena).\n" + + "- Fixed typos in Indonesian translation from [@ekojs](https://github.com/ekojs).\n" + + "\n" + + "[1.1.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v1.0.0...v1.1.0\n" + + "\n"); + + assertThat(builder().ref("1.1.0").ignoreContent(true).build()) + .extracting(usingMain, STRING) + .isEqualTo( + "## [1.1.0] - 2019-02-15\n" + + "\n" + + "[1.1.0]: https://github.com/olivierlacan/keep-a-changelog/compare/v1.0.0...v1.1.0\n" + + "\n"); + + assertThat(builder().ref("zzz").build()) + .extracting(usingMain, STRING) + .isEmpty(); + } + + @Test + public void testParseLocalDate() { + assertThatNullPointerException() + .isThrownBy(() -> parseLocalDate(null)); + + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> parseLocalDate("")); + + assertThat(parseLocalDate("2010")) + .isEqualTo("2010-01-01"); + + assertThat(parseLocalDate("2010-02")) + .isEqualTo("2010-02-01"); + + assertThat(parseLocalDate("2010-02-03")) + .isEqualTo("2010-02-03"); + } + + private static Condition containing(Version version) { + return new Condition<>(parent -> parent.contains(version), "Must contain %s", version); + } + + private final Version unreleased = Version.of("Unreleased", LocalDate.MAX); + private final Version v1_1_0 = Version.of("1.1.0", LocalDate.parse("2019-02-15")); + private final Version v1_0_0 = Version.of("1.0.0", LocalDate.parse("2017-06-20")); +} diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/ScanTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/ScanTest.java deleted file mode 100644 index d2a7dd5..0000000 --- a/heylogs-api/src/test/java/nbbrd/heylogs/ScanTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package nbbrd.heylogs; - -import org.junit.jupiter.api.Test; - -import java.time.LocalDate; - -import static nbbrd.heylogs.Sample.using; -import static org.assertj.core.api.Assertions.assertThat; - -class ScanTest { - - @Test - void of() { - assertThat(Scan.of(using("Empty.md"))) - .isEqualTo(new Scan( - 0, - TimeRange.ALL, - true, " ()", - true - )); - - assertThat(Scan.of(using("Main.md"))) - .isEqualTo(new Scan( - 13, - TimeRange.of(LocalDate.of(2014, 5, 31), LocalDate.of(2019, 2, 15)), - true, " (1 MAJOR, 4 MINOR, 7 PATCH)", - true - )); - - assertThat(Scan.of(using("InvalidSemver.md"))) - .isEqualTo(new Scan( - 2, - TimeRange.of(LocalDate.of(2019, 2, 15), LocalDate.of(2019, 2, 15)), - false, "", - true - )); - } -} \ No newline at end of file diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/ScannerTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/ScannerTest.java new file mode 100644 index 0000000..f96d252 --- /dev/null +++ b/heylogs-api/src/test/java/nbbrd/heylogs/ScannerTest.java @@ -0,0 +1,75 @@ +package nbbrd.heylogs; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.time.LocalDate; + +import static _test.Sample.using; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; + +class ScannerTest { + + @Test + void testScan() { + Scanner x = Scanner.ofServiceLoader(); + + assertThat(x.scan(using("/Empty.md"))) + .isEqualTo(new Status( + 0, + TimeRange.ALL, + true, " ()", + true + )); + + assertThat(x.scan(using("/Main.md"))) + .isEqualTo(new Status( + 13, + TimeRange.of(LocalDate.of(2014, 5, 31), LocalDate.of(2019, 2, 15)), + true, " (1 MAJOR, 4 MINOR, 7 PATCH)", + true + )); + + assertThat(x.scan(using("/InvalidSemver.md"))) + .isEqualTo(new Status( + 2, + TimeRange.of(LocalDate.of(2019, 2, 15), LocalDate.of(2019, 2, 15)), + false, "", + true + )); + + assertThat(x.scan(using("/InvalidVersion.md"))) + .isEqualTo(new Status( + 1, + TimeRange.of(LocalDate.of(2019, 2, 15), LocalDate.of(2019, 2, 15)), + true, " ()", + true + )); + } + + @Test + public void testFormatStatus() throws IOException { + assertThatIOException() + .isThrownBy(() -> Scanner.builder().build().formatStatus(new StringBuilder(), "", Status.builder().build())); + + assertThatIOException() + .isThrownBy(() -> Scanner.ofServiceLoader().toBuilder().formatId("other").build().formatStatus(new StringBuilder(), "", Status.builder().build())); + + StringBuilder output = new StringBuilder(); + Scanner.ofServiceLoader().formatStatus(output, "file1", new Status( + 1, + TimeRange.of(LocalDate.of(2019, 2, 15), LocalDate.of(2019, 2, 15)), + true, " ()", + true + )); + assertThat(output.toString()) + .isEqualToIgnoringNewLines( + "file1\n" + + " Found 1 releases\n" + + " Ranging from 2019-02-15 to 2019-02-15\n" + + " Compatible with Semantic Versioning ()\n" + + " Has an unreleased version\n" + ); + } +} \ No newline at end of file diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/TypeOfChangeTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/TypeOfChangeTest.java index b2303ac..ebd732c 100644 --- a/heylogs-api/src/test/java/nbbrd/heylogs/TypeOfChangeTest.java +++ b/heylogs-api/src/test/java/nbbrd/heylogs/TypeOfChangeTest.java @@ -1,45 +1,46 @@ package nbbrd.heylogs; +import _test.Sample; import com.vladsch.flexmark.ast.Heading; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; +import static _test.Sample.asHeading; +import static _test.Sample.using; +import static nbbrd.heylogs.TypeOfChange.parse; import static org.assertj.core.api.Assertions.*; public class TypeOfChangeTest { @Test - public void testParseHeading() { - assertThat(parsingHeading("### Added")) + public void testParse() { + //noinspection DataFlowIssue + assertThatNullPointerException() + .isThrownBy(() -> parse(null)); + + assertThat(parse(asHeading("### Added"))) .isEqualTo(TypeOfChange.ADDED); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("## Added")) + .isThrownBy(() -> parse(asHeading("## Added"))) .withMessageContaining("Invalid heading level"); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("#### Added")) + .isThrownBy(() -> parse(asHeading("#### Added"))) .withMessageContaining("Invalid heading level"); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("### hello")) + .isThrownBy(() -> parse(asHeading("### hello"))) .withMessageContaining("Cannot parse"); - assertThat(Nodes.of(Heading.class).descendants(Sample.using("Main.md")).filter(TypeOfChange::isTypeOfChangeLevel).map(TypeOfChange::parse)) + assertThat(Nodes.of(Heading.class).descendants(using("/Main.md")).filter(TypeOfChange::isTypeOfChangeLevel).map(TypeOfChange::parse)) .hasSize(24) .contains(TypeOfChange.ADDED, atIndex(0)); } @Test - public void testFormatHeading() { + public void testToHeading() { assertThat(TypeOfChange.ADDED.toHeading()) - .extracting(Sample::asText) - .asString() + .extracting(Sample::asText, STRING) .isEqualTo("### Added"); } - - @NotNull - private TypeOfChange parsingHeading(String text) { - return TypeOfChange.parse(Sample.asHeading(text)); - } } diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/VersionFilterTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/VersionFilterTest.java deleted file mode 100644 index 6cb3502..0000000 --- a/heylogs-api/src/test/java/nbbrd/heylogs/VersionFilterTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package nbbrd.heylogs; - -import org.assertj.core.api.Condition; -import org.junit.jupiter.api.Test; - -import java.time.LocalDate; - -import static org.assertj.core.api.Assertions.assertThat; - -public class VersionFilterTest { - - @Test - public void testRef() { - assertThat(VersionFilter.builder().build()) - .describedAs("Empty reference") - .is(containing(unreleased)) - .is(containing(v1_1_1)); - - assertThat(VersionFilter.builder().ref("Unreleased").build()) - .describedAs("Full reference") - .is(containing(unreleased)) - .isNot(containing(v1_1_1)); - - assertThat(VersionFilter.builder().ref("1.1.0").build()) - .describedAs("Full reference") - .isNot(containing(unreleased)) - .is(containing(v1_1_1)); - - assertThat(VersionFilter.builder().ref("rel").build()) - .describedAs("Partial reference") - .is(containing(unreleased)) - .isNot(containing(v1_1_1)); - - assertThat(VersionFilter.builder().ref("other").build()) - .describedAs("Unknown reference") - .isNot(containing(unreleased)) - .isNot(containing(v1_1_1)); - - assertThat(VersionFilter.builder().ref("other-SNAPSHOT").build()) - .describedAs("Matching unreleased pattern reference") - .is(containing(unreleased)) - .isNot(containing(v1_1_1)); - } - - private static Condition containing(Version version) { - return new Condition<>(parent -> parent.contains(version), "Must contain %s", version); - } - - private final Version unreleased = new Version("Unreleased", LocalDate.MAX); - private final Version v1_1_1 = new Version("1.1.0", LocalDate.parse("2019-02-15")); -} diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/VersionTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/VersionTest.java index 8fcb5a6..c031f1a 100644 --- a/heylogs-api/src/test/java/nbbrd/heylogs/VersionTest.java +++ b/heylogs-api/src/test/java/nbbrd/heylogs/VersionTest.java @@ -1,89 +1,98 @@ package nbbrd.heylogs; +import _test.Sample; import com.vladsch.flexmark.ast.Heading; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import java.time.LocalDate; +import static _test.Sample.asHeading; +import static _test.Sample.using; +import static nbbrd.heylogs.Version.parse; import static org.assertj.core.api.Assertions.*; public class VersionTest { @Test - public void testParseHeading() { - assertThat(parsingHeading("## [Unreleased]")) - .isEqualTo(new Version("Unreleased", LocalDate.MAX)); + public void testParse() { + //noinspection DataFlowIssue + assertThatNullPointerException() + .isThrownBy(() -> parse(null)); - assertThat(parsingHeading("## [Unreleased ]")) - .isEqualTo(new Version("Unreleased", LocalDate.MAX)); + assertThat(parse(asHeading("## [Unreleased]"))) + .isEqualTo(Version.of("Unreleased", LocalDate.MAX)); + + assertThat(parse(asHeading("## [Unreleased ]"))) + .isEqualTo(Version.of("Unreleased", LocalDate.MAX)); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("## [Unreleased] - 2019-02-15")) + .isThrownBy(() -> parse(asHeading("## [Unreleased] - 2019-02-15"))) .withMessageContaining("Unexpected additional part"); - assertThat(parsingHeading("## [1.1.0] - 2019-02-15")) - .isEqualTo(new Version("1.1.0", d20190215)); + assertThat(parse(asHeading("## [1.1.0] - 2019-02-15"))) + .isEqualTo(Version.of("1.1.0", d20190215)); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("# [1.1.0] - 2019-02-15")) + .isThrownBy(() -> parse(asHeading("# [1.1.0] - 2019-02-15"))) .withMessageContaining("Invalid heading level"); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("### [1.1.0] - 2019-02-15")) + .isThrownBy(() -> parse(asHeading("### [1.1.0] - 2019-02-15"))) .withMessageContaining("Invalid heading level"); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("## Unreleased")) + .isThrownBy(() -> parse(asHeading("##"))) + .withMessageContaining("Missing ref part"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> parse(asHeading("## Unreleased"))) .withMessageContaining("Missing ref link"); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("## 1.1.0 - 2019-02-15")) + .isThrownBy(() -> parse(asHeading("## 1.1.0 - 2019-02-15"))) .withMessageContaining("Missing ref link"); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("## [1.1.0](https://localhost) - 2019-02-15")) + .isThrownBy(() -> parse(asHeading("## [1.1.0](https://localhost) - 2019-02-15"))) .withMessageContaining("Missing ref link"); assertThatIllegalArgumentException() - .isThrownBy(() -> Version.parse(Sample.asHeading("## [1.1.0] - "))) - .withMessageContaining("Invalid date"); + .isThrownBy(() -> parse(asHeading("## [1.1.0] 2019-02-15"))) + .withMessageContaining("Missing date prefix"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> parse(asHeading("## [1.1.0] - "))) + .withMessageContaining("Invalid date format"); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("## [1.1.0] - 2019-02")) - .withMessageContaining("Invalid date"); + .isThrownBy(() -> parse(asHeading("## [1.1.0] - 2019-02"))) + .withMessageContaining("Invalid date format"); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("## - 2019-02-15")) + .isThrownBy(() -> parse(asHeading("## - 2019-02-15"))) .withMessageContaining("Missing ref link"); assertThatIllegalArgumentException() - .isThrownBy(() -> parsingHeading("## [1.1.0] - 2019-02-15 [hello]")) + .isThrownBy(() -> parse(asHeading("## [1.1.0] - 2019-02-15 [hello]"))) .withMessageContaining("Unexpected additional part"); - assertThat(Nodes.of(Heading.class).descendants(Sample.using("Main.md")).filter(Version::isVersionLevel).map(Version::parse)) + assertThat(Nodes.of(Heading.class).descendants(using("/Main.md")).filter(Version::isVersionLevel).map(Version::parse)) .hasSize(14) - .contains(new Version("Unreleased", LocalDate.MAX), atIndex(0)) - .contains(new Version("1.1.0", d20190215), atIndex(1)); + .contains(Version.of("Unreleased", LocalDate.MAX), atIndex(0)) + .contains(Version.of("1.1.0", d20190215), atIndex(1)); } @Test - public void testFormatHeading() { - assertThat(new Version("Unreleased", LocalDate.MAX).toHeading()) + public void testToHeading() { + assertThat(Version.of("Unreleased", LocalDate.MAX).toHeading()) .extracting(Sample::asText) .asString() .isEqualTo("## [Unreleased]"); - assertThat(new Version("1.1.0", d20190215).toHeading()) - .extracting(Sample::asText) - .asString() + assertThat(Version.of("1.1.0", d20190215).toHeading()) + .extracting(Sample::asText, STRING) .isEqualTo("## [1.1.0] - 2019-02-15"); } - @NotNull - private Version parsingHeading(String text) { - return Version.parse(Sample.asHeading(text)); - } - private final LocalDate d20190215 = LocalDate.parse("2019-02-15"); } diff --git a/heylogs-api/src/test/java/nbbrd/heylogs/RuleTest.java b/heylogs-api/src/test/java/nbbrd/heylogs/spi/RuleTest.java similarity index 90% rename from heylogs-api/src/test/java/nbbrd/heylogs/RuleTest.java rename to heylogs-api/src/test/java/nbbrd/heylogs/spi/RuleTest.java index 874c0cc..0d881b4 100644 --- a/heylogs-api/src/test/java/nbbrd/heylogs/RuleTest.java +++ b/heylogs-api/src/test/java/nbbrd/heylogs/spi/RuleTest.java @@ -1,10 +1,11 @@ -package nbbrd.heylogs; +package nbbrd.heylogs.spi; +import nbbrd.heylogs.spi.Rule; import org.junit.jupiter.api.Test; import java.util.Properties; -import static nbbrd.heylogs.Rule.isEnabled; +import static nbbrd.heylogs.spi.Rule.isEnabled; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/Empty.md b/heylogs-api/src/test/resources/Empty.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/Empty.md rename to heylogs-api/src/test/resources/Empty.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/InvalidGitHubIssueRef.md b/heylogs-api/src/test/resources/InvalidGitHubIssueRef.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/InvalidGitHubIssueRef.md rename to heylogs-api/src/test/resources/InvalidGitHubIssueRef.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/InvalidHeadingLevel.md b/heylogs-api/src/test/resources/InvalidHeadingLevel.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/InvalidHeadingLevel.md rename to heylogs-api/src/test/resources/InvalidHeadingLevel.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/InvalidSemver.md b/heylogs-api/src/test/resources/InvalidSemver.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/InvalidSemver.md rename to heylogs-api/src/test/resources/InvalidSemver.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/InvalidTypeOfChange.md b/heylogs-api/src/test/resources/InvalidTypeOfChange.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/InvalidTypeOfChange.md rename to heylogs-api/src/test/resources/InvalidTypeOfChange.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/InvalidVersion.md b/heylogs-api/src/test/resources/InvalidVersion.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/InvalidVersion.md rename to heylogs-api/src/test/resources/InvalidVersion.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/Main.md b/heylogs-api/src/test/resources/Main.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/Main.md rename to heylogs-api/src/test/resources/Main.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/MissingReference.md b/heylogs-api/src/test/resources/MissingReference.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/MissingReference.md rename to heylogs-api/src/test/resources/MissingReference.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/NoChangelog.md b/heylogs-api/src/test/resources/NoChangelog.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/NoChangelog.md rename to heylogs-api/src/test/resources/NoChangelog.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/NonHttps.md b/heylogs-api/src/test/resources/NonHttps.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/NonHttps.md rename to heylogs-api/src/test/resources/NonHttps.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/NotLatestVersionFirst.md b/heylogs-api/src/test/resources/NotLatestVersionFirst.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/NotLatestVersionFirst.md rename to heylogs-api/src/test/resources/NotLatestVersionFirst.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/TooManyChangelog.md b/heylogs-api/src/test/resources/TooManyChangelog.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/TooManyChangelog.md rename to heylogs-api/src/test/resources/TooManyChangelog.md diff --git a/heylogs-api/src/test/resources/nbbrd/heylogs/UnsortedVersion.md b/heylogs-api/src/test/resources/UnsortedVersion.md similarity index 100% rename from heylogs-api/src/test/resources/nbbrd/heylogs/UnsortedVersion.md rename to heylogs-api/src/test/resources/UnsortedVersion.md diff --git a/heylogs-bom/pom.xml b/heylogs-bom/pom.xml new file mode 100644 index 0000000..c204fbe --- /dev/null +++ b/heylogs-bom/pom.xml @@ -0,0 +1,161 @@ + + + 4.0.0 + + + com.github.nbbrd.heylogs + heylogs-parent + 0.6.0 + + + heylogs-bom + pom + + heylogs-bom + Keep-a-changelog tool - Bill of Materials + https://github.com/nbbrd/heylogs + + + + + heylogs-api + ${project.groupId} + ${project.version} + + + heylogs-cli + ${project.groupId} + ${project.version} + + + heylogs-maven-plugin + ${project.groupId} + ${project.version} + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.5.0 + + bom + ${project.build.directory} + + + + flatten + process-resources + + flatten + + + + + + + + + com.github.nbbrd.heylogs + heylogs-maven-plugin + ${project.version} + + + changelog + + check + extract + scan + list + + + ${project.parent.basedir}/CHANGELOG.md + true + + + + + + + + + + + + full-release + + + + + org.jreleaser + jreleaser-maven-plugin + 1.5.1 + + + release-assets + install + + full-release + + + + + + true + + master + + ${project.build.directory}/CHANGELOG.md + + + + + ALWAYS + + true + + + + + SINGLE_JAR + + + + ${project.parent.basedir}/heylogs-cli/target/heylogs-cli-${project.version}-bin.jar + + + + + nbbrd.heylogs.cli.HeylogsCommand + heylogs-cli + 8 + + + RELEASE + + + RELEASE + + true + + + + RELEASE + + + + + + + + + + + + + \ No newline at end of file diff --git a/heylogs-cli/pom.xml b/heylogs-cli/pom.xml index 6b56e14..0a6de69 100644 --- a/heylogs-cli/pom.xml +++ b/heylogs-cli/pom.xml @@ -7,7 +7,7 @@ com.github.nbbrd.heylogs heylogs-parent - 0.5.0 + 0.6.0 heylogs-cli @@ -17,10 +17,6 @@ Keep-a-changelog tool - CLI https://github.com/nbbrd/heylogs - - nbbrd.heylogs.cli.MainCommand - - @@ -63,6 +59,12 @@ com.github.nbbrd.java-console-properties java-console-properties 1.4.0 + + + com.github.nbbrd.java-io-util + java-io-base + + @@ -110,7 +112,7 @@ implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> - ${project.x.mainClass} + nbbrd.heylogs.cli.HeylogsCommand true @@ -118,103 +120,6 @@ false - - - - - com.github.nbbrd.heylogs - heylogs-maven-plugin - ${project.version} - - - extract-changelog - - check - extract - - - ${project.parent.basedir}/CHANGELOG.md - true - - - - - - - - full-release - - - - org.jreleaser - jreleaser-maven-plugin - 1.3.1 - - - install - - full-release - - - - - - - - true - false - - master - - ${project.build.directory}/CHANGELOG.md - - - - - true - - - ALWAYS - - true - - - - - SINGLE_JAR - - - - ${project.build.directory}/${project.artifactId}-${project.version}-bin.jar - - - - - ${project.x.mainClass} - ${project.artifactId} - 8 - - - RELEASE - - - RELEASE - - true - - - - RELEASE - - - - - - - - - - \ No newline at end of file diff --git a/heylogs-cli/src/main/java/internal/heylogs/cli/FailureFormatOptions.java b/heylogs-cli/src/main/java/internal/heylogs/cli/FailureFormatOptions.java deleted file mode 100644 index c27a7bc..0000000 --- a/heylogs-cli/src/main/java/internal/heylogs/cli/FailureFormatOptions.java +++ /dev/null @@ -1,46 +0,0 @@ -package internal.heylogs.cli; - -import nbbrd.heylogs.FailureFormatter; -import nbbrd.heylogs.FailureFormatterLoader; -import picocli.CommandLine; - -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; - -@lombok.Getter -@lombok.Setter -public class FailureFormatOptions { - - @CommandLine.Option( - names = {"-f", "--formatter"}, - paramLabel = "", - defaultValue = "stylish", - description = "Specify the formatter used to control the appearance of the result. Valid values: ${COMPLETION-CANDIDATES}.", - completionCandidates = FailureFormatters.class, - converter = FailureFormatters.class - ) - private FailureFormatter formatter; - - public static final class FailureFormatters implements Iterable, CommandLine.ITypeConverter { - - private final List formatters = FailureFormatterLoader.load(); - - @Override - public Iterator iterator() { - return formatters - .stream() - .map(FailureFormatter::getName) - .iterator(); - } - - @Override - public FailureFormatter convert(String value) throws Exception { - return formatters - .stream() - .filter(formatter -> formatter.getName().equals(value)) - .findFirst() - .orElseThrow(NoSuchElementException::new); - } - } -} diff --git a/heylogs-cli/src/main/java/internal/heylogs/cli/FormatCandidates.java b/heylogs-cli/src/main/java/internal/heylogs/cli/FormatCandidates.java new file mode 100644 index 0000000..99e7dd8 --- /dev/null +++ b/heylogs-cli/src/main/java/internal/heylogs/cli/FormatCandidates.java @@ -0,0 +1,17 @@ +package internal.heylogs.cli; + +import nbbrd.heylogs.spi.Format; +import nbbrd.heylogs.spi.FormatLoader; + +import java.util.Iterator; + +public final class FormatCandidates implements Iterable { + + @Override + public Iterator iterator() { + return FormatLoader.load() + .stream() + .map(Format::getId) + .iterator(); + } +} diff --git a/heylogs-cli/src/main/java/internal/heylogs/cli/RuleSetOptions.java b/heylogs-cli/src/main/java/internal/heylogs/cli/RuleSetOptions.java deleted file mode 100644 index 76bf9a8..0000000 --- a/heylogs-cli/src/main/java/internal/heylogs/cli/RuleSetOptions.java +++ /dev/null @@ -1,26 +0,0 @@ -package internal.heylogs.cli; - -import nbbrd.heylogs.Rule; -import nbbrd.heylogs.RuleLoader; -import picocli.CommandLine; - -import java.util.List; - -@lombok.Getter -@lombok.Setter -public class RuleSetOptions { - - @CommandLine.Option( - names = {"-s", "--semver"}, - defaultValue = "false", - description = "Mention if this changelog follows Semantic Versioning." - ) - private boolean semver; - - public List getRules() { - if (semver) { - System.setProperty(Rule.ENABLE_KEY, "semver"); - } - return RuleLoader.load(); - } -} diff --git a/heylogs-cli/src/main/java/internal/heylogs/cli/VersionFilterOptions.java b/heylogs-cli/src/main/java/internal/heylogs/cli/VersionFilterOptions.java deleted file mode 100644 index 49ebc5b..0000000 --- a/heylogs-cli/src/main/java/internal/heylogs/cli/VersionFilterOptions.java +++ /dev/null @@ -1,67 +0,0 @@ -package internal.heylogs.cli; - -import nbbrd.heylogs.TimeRange; -import nbbrd.heylogs.VersionFilter; -import picocli.CommandLine; - -import java.time.LocalDate; -import java.util.regex.Pattern; - -@lombok.Getter -@lombok.Setter -public class VersionFilterOptions { - - @CommandLine.Option( - names = {"-r", "--ref"}, - paramLabel = "", - description = "Filter versions by name." - ) - private String ref = VersionFilter.DEFAULT.getRef(); - - @CommandLine.Option( - names = {"-u", "--unreleased"}, - paramLabel = "", - description = "Assume that versions that match this pattern are unreleased." - ) - private Pattern unreleasedPattern = VersionFilter.DEFAULT.getUnreleasedPattern(); - - @CommandLine.Option( - names = {"-f", "--from"}, - paramLabel = "", - description = "Filter versions by min date (included).", - converter = LenientDateConverter.class - ) - private LocalDate from = VersionFilter.DEFAULT.getTimeRange().getFrom(); - - @CommandLine.Option( - names = {"-t", "--to"}, - paramLabel = "", - description = "Filter versions by max date (included).", - converter = LenientDateConverter.class - ) - private LocalDate to = VersionFilter.DEFAULT.getTimeRange().getTo(); - - @CommandLine.Option( - names = {"-l", "--limit"}, - description = "Limit the number of versions." - ) - private int limit = VersionFilter.DEFAULT.getLimit(); - - public VersionFilter get() { - return VersionFilter - .builder() - .ref(ref) - .unreleasedPattern(unreleasedPattern) - .timeRange(TimeRange.of(from, to)) - .limit(limit) - .build(); - } - - private static final class LenientDateConverter implements CommandLine.ITypeConverter { - - @Override - public LocalDate convert(String value) { - return VersionFilter.parseLocalDate(value); - } - } -} diff --git a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/CheckCommand.java b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/CheckCommand.java index bbed2c7..1b80f55 100644 --- a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/CheckCommand.java +++ b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/CheckCommand.java @@ -1,19 +1,17 @@ package nbbrd.heylogs.cli; -import internal.heylogs.cli.FailureFormatOptions; +import internal.heylogs.SemverRule; +import internal.heylogs.StylishFormat; +import internal.heylogs.cli.FormatCandidates; import internal.heylogs.cli.MarkdownInputSupport; -import internal.heylogs.cli.RuleSetOptions; import nbbrd.console.picocli.FileOutputOptions; import nbbrd.console.picocli.MultiFileInputOptions; -import nbbrd.heylogs.Failure; -import nbbrd.heylogs.FailureFormatter; -import nbbrd.heylogs.Rule; +import nbbrd.heylogs.Checker; import picocli.CommandLine; import picocli.CommandLine.Command; import java.io.Writer; import java.nio.file.Path; -import java.util.List; import java.util.concurrent.Callable; import static internal.heylogs.cli.MarkdownInputSupport.newMarkdownInputSupport; @@ -28,29 +26,48 @@ public final class CheckCommand implements Callable { @CommandLine.Mixin private FileOutputOptions output; - @CommandLine.Mixin - private RuleSetOptions ruleSet; + @CommandLine.Option( + names = {"-s", "--semver"}, + defaultValue = "false", + description = "Mention if this changelog follows Semantic Versioning." + ) + private boolean semver; - @CommandLine.Mixin - private FailureFormatOptions format; + @CommandLine.Option( + names = {"-f", "--format"}, + paramLabel = "", + defaultValue = StylishFormat.ID, + description = "Specify the format used to control the appearance of the result. Valid values: ${COMPLETION-CANDIDATES}.", + completionCandidates = FormatCandidates.class + ) + private String formatId; @Override public Void call() throws Exception { try (Writer writer = newTextOutputSupport().newBufferedWriter(output.getFile())) { - List rules = ruleSet.getRules(); - FailureFormatter formatter = format.getFormatter(); + Checker checker = getChecker(); MarkdownInputSupport markdown = newMarkdownInputSupport(); for (Path file : input.getAllFiles(markdown::accept)) { - formatter.format( + checker.formatFailures( writer, markdown.getName(file), - Failure.allOf(markdown.readDocument(file), rules) + checker.validate(markdown.readDocument(file)) ); } } return null; } + + private Checker getChecker() { + Checker.Builder result = Checker.ofServiceLoader() + .toBuilder() + .formatId(formatId); + if (semver) { + result.rule(new SemverRule()); + } + return result.build(); + } } diff --git a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/DebugCommand.java b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/DebugCommand.java deleted file mode 100644 index 2995f6b..0000000 --- a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/DebugCommand.java +++ /dev/null @@ -1,21 +0,0 @@ -package nbbrd.heylogs.cli; - -import nbbrd.heylogs.FailureFormatter; -import nbbrd.heylogs.FailureFormatterLoader; -import nbbrd.heylogs.Rule; -import nbbrd.heylogs.RuleLoader; -import picocli.CommandLine.Command; - -import java.util.concurrent.Callable; -import java.util.stream.Collectors; - -@Command(name = "debug", hidden = true) -public final class DebugCommand implements Callable { - - @Override - public Void call() { - System.out.println("Rules: " + RuleLoader.load().stream().map(Rule::getName).collect(Collectors.joining(", "))); - System.out.println("Formatters: " + FailureFormatterLoader.load().stream().map(FailureFormatter::getName).collect(Collectors.joining(", "))); - return null; - } -} diff --git a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ExtractCommand.java b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ExtractCommand.java index 901bf74..4b83793 100644 --- a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ExtractCommand.java +++ b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ExtractCommand.java @@ -1,14 +1,17 @@ package nbbrd.heylogs.cli; import com.vladsch.flexmark.util.ast.Document; -import internal.heylogs.cli.VersionFilterOptions; import nbbrd.console.picocli.FileInputParameters; import nbbrd.console.picocli.FileOutputOptions; +import nbbrd.heylogs.Extractor; +import nbbrd.heylogs.TimeRange; import picocli.CommandLine; import picocli.CommandLine.Command; import java.io.IOException; +import java.time.LocalDate; import java.util.concurrent.Callable; +import java.util.regex.Pattern; import static internal.heylogs.cli.MarkdownInputSupport.newMarkdownInputSupport; import static internal.heylogs.cli.MarkdownOutputSupport.newMarkdownOutputSupport; @@ -22,8 +25,47 @@ public final class ExtractCommand implements Callable { @CommandLine.Mixin private FileOutputOptions output; - @CommandLine.ArgGroup(heading = "%nFilters:%n", exclusive = false) - private final VersionFilterOptions filter = new VersionFilterOptions(); + @CommandLine.Option( + names = {"-r", "--ref"}, + paramLabel = "", + description = "Filter versions by name." + ) + private String ref = Extractor.DEFAULT.getRef(); + + @CommandLine.Option( + names = {"-u", "--unreleased"}, + paramLabel = "", + description = "Assume that versions that match this pattern are unreleased." + ) + private Pattern unreleasedPattern = Extractor.DEFAULT.getUnreleasedPattern(); + + @CommandLine.Option( + names = {"-f", "--from"}, + paramLabel = "", + description = "Filter versions by min date (included).", + converter = LenientDateConverter.class + ) + private LocalDate from = Extractor.DEFAULT.getTimeRange().getFrom(); + + @CommandLine.Option( + names = {"-t", "--to"}, + paramLabel = "", + description = "Filter versions by max date (included).", + converter = LenientDateConverter.class + ) + private LocalDate to = Extractor.DEFAULT.getTimeRange().getTo(); + + @CommandLine.Option( + names = {"-l", "--limit"}, + description = "Limit the number of versions." + ) + private int limit = Extractor.DEFAULT.getLimit(); + + @CommandLine.Option( + names = "--ignore-content", + description = "Ignore versions content, keep headers only." + ) + private boolean ignoreContent = false; @Override public Void call() throws Exception { @@ -36,11 +78,30 @@ private Document load() throws IOException { } private Document extract(Document document) { - filter.get().apply(document); + getExtractor().extract(document); return document; } private void store(Document document) throws IOException { newMarkdownOutputSupport().writeDocument(output.getFile(), document); } + + public Extractor getExtractor() { + return Extractor + .builder() + .ref(ref) + .unreleasedPattern(unreleasedPattern) + .timeRange(TimeRange.of(from, to)) + .limit(limit) + .ignoreContent(ignoreContent) + .build(); + } + + private static final class LenientDateConverter implements CommandLine.ITypeConverter { + + @Override + public LocalDate convert(String value) { + return Extractor.parseLocalDate(value); + } + } } diff --git a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/MainCommand.java b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/HeylogsCommand.java similarity index 88% rename from heylogs-cli/src/main/java/nbbrd/heylogs/cli/MainCommand.java rename to heylogs-cli/src/main/java/nbbrd/heylogs/cli/HeylogsCommand.java index 938e863..588e9d4 100644 --- a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/MainCommand.java +++ b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/HeylogsCommand.java @@ -14,7 +14,7 @@ @Command( name = About.NAME, - versionProvider = MainCommand.ManifestVersionProvider.class, + versionProvider = HeylogsCommand.ManifestVersionProvider.class, scope = CommandLine.ScopeType.INHERIT, sortOptions = false, mixinStandardHelpOptions = true, @@ -25,17 +25,16 @@ headerHeading = "%n", subcommands = { ScanCommand.class, - ListCommand.class, CheckCommand.class, ExtractCommand.class, - DebugCommand.class + ListCommand.class }, description = { "Set of tools to deal with the @|bold keep-a-changelog|@ format.", "%nMore info at https://github.com/nbbrd/heylogs" } ) -public final class MainCommand implements Callable { +public final class HeylogsCommand implements Callable { public static void main(String[] args) { SpecialProperties specialProperties = SpecialProperties.parse(args); @@ -53,10 +52,10 @@ private static int execMain(SpecialProperties specialProperties, Properties prop specialProperties.apply(System.getProperties()); try (AnsiConsole ignore = AnsiConsole.windowsInstall()) { - CommandLine cmd = new CommandLine(new MainCommand()); + CommandLine cmd = new CommandLine(new HeylogsCommand()); cmd.setCaseInsensitiveEnumValuesAllowed(true); cmd.setDefaultValueProvider(new CommandLine.PropertiesDefaultProvider(properties)); - cmd.setExecutionExceptionHandler(new PrintAndLogExceptionHandler(MainCommand.class, specialProperties.isDebugRequired())); + cmd.setExecutionExceptionHandler(new PrintAndLogExceptionHandler(HeylogsCommand.class, specialProperties.isDebugRequired())); return cmd.execute(args); } } diff --git a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ListCommand.java b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ListCommand.java index d7c26b4..5c1d684 100644 --- a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ListCommand.java +++ b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ListCommand.java @@ -1,62 +1,40 @@ package nbbrd.heylogs.cli; -import com.vladsch.flexmark.ast.Heading; -import com.vladsch.flexmark.util.ast.Document; -import com.vladsch.flexmark.util.ast.Node; -import internal.heylogs.cli.VersionFilterOptions; -import nbbrd.console.picocli.FileInputParameters; -import nbbrd.console.picocli.FileOutputOptions; -import nbbrd.heylogs.Nodes; -import nbbrd.heylogs.Version; +import internal.heylogs.SemverRule; +import nbbrd.heylogs.Checker; +import nbbrd.heylogs.spi.Format; +import nbbrd.heylogs.spi.Rule; import picocli.CommandLine; import picocli.CommandLine.Command; -import java.io.BufferedWriter; -import java.io.IOException; -import java.util.List; import java.util.concurrent.Callable; -import java.util.stream.Collectors; -import static internal.heylogs.cli.MarkdownInputSupport.newMarkdownInputSupport; -import static nbbrd.console.picocli.text.TextOutputSupport.newTextOutputSupport; +import static java.util.stream.Collectors.joining; -@Command(name = "list", description = "List versions from changelog.") +@Command(name = "list", description = "List available resources.") public final class ListCommand implements Callable { - @CommandLine.Mixin - private FileInputParameters input; - - @CommandLine.Mixin - private FileOutputOptions output; - - @CommandLine.ArgGroup(heading = "%nFilters:%n", exclusive = false) - private final VersionFilterOptions filter = new VersionFilterOptions(); + @CommandLine.Option( + names = {"-s", "--semver"}, + defaultValue = "false", + description = "Mention if this changelog follows Semantic Versioning." + ) + private boolean semver; @Override - public Void call() throws Exception { - store(list(load())); + public Void call() { + Checker checker = getChecker(); + System.out.println("Rules: " + checker.getRules().stream().map(Rule::getId).collect(joining(", "))); + System.out.println("Formats: " + checker.getFormats().stream().map(Format::getId).collect(joining(", "))); return null; } - private Document load() throws IOException { - return newMarkdownInputSupport().readDocument(input.getFile()); - } - - private List list(Node document) { - return Nodes.of(Heading.class) - .descendants(document) - .filter(Version::isVersionLevel) - .filter(filter.get()::contains) - .limit(filter.getLimit()) - .collect(Collectors.toList()); - } - - private void store(List list) throws IOException { - try (BufferedWriter writer = newTextOutputSupport().newBufferedWriter(output.getFile())) { - for (Heading item : list) { - writer.append(item.getChars()); - writer.newLine(); - } + private Checker getChecker() { + Checker.Builder result = Checker.ofServiceLoader() + .toBuilder(); + if (semver) { + result.rule(new SemverRule()); } + return result.build(); } } diff --git a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ScanCommand.java b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ScanCommand.java index 537772a..2ca7500 100644 --- a/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ScanCommand.java +++ b/heylogs-cli/src/main/java/nbbrd/heylogs/cli/ScanCommand.java @@ -1,14 +1,15 @@ package nbbrd.heylogs.cli; +import internal.heylogs.StylishFormat; +import internal.heylogs.cli.FormatCandidates; import internal.heylogs.cli.MarkdownInputSupport; import nbbrd.console.picocli.FileOutputOptions; import nbbrd.console.picocli.MultiFileInputOptions; -import nbbrd.heylogs.Scan; +import nbbrd.heylogs.Scanner; import picocli.CommandLine; import picocli.CommandLine.Command; import java.io.BufferedWriter; -import java.io.IOException; import java.nio.file.Path; import java.util.concurrent.Callable; @@ -24,45 +25,37 @@ public final class ScanCommand implements Callable { @CommandLine.Mixin private FileOutputOptions output; + @CommandLine.Option( + names = {"-f", "--format"}, + paramLabel = "", + defaultValue = StylishFormat.ID, + description = "Specify the format used to control the appearance of the result. Valid values: ${COMPLETION-CANDIDATES}.", + completionCandidates = FormatCandidates.class + ) + private String formatId; + @Override public Void call() throws Exception { try (BufferedWriter writer = newTextOutputSupport().newBufferedWriter(output.getFile())) { + Scanner scanner = getScanner(); MarkdownInputSupport markdown = newMarkdownInputSupport(); for (Path file : input.getAllFiles(markdown::accept)) { - write( + scanner.formatStatus( writer, markdown.getName(file), - Scan.of(markdown.readDocument(file))); + scanner.scan(markdown.readDocument(file))); } } return null; } - private static void write(BufferedWriter writer, String source, Scan scan) throws IOException { - writer.write(source); - writer.newLine(); - if (scan.getReleaseCount() == 0) { - writer.append(" No release found"); - writer.newLine(); - } else { - writer.append(String.format(" Found %d releases", scan.getReleaseCount())); - writer.newLine(); - writer.append(String.format(" Ranging from %s to %s", scan.getTimeRange().getFrom(), scan.getTimeRange().getTo())); - writer.newLine(); - - if (scan.isCompatibleWithSemver()) { - writer.append(" Compatible with Semantic Versioning" + scan.getSemverDetails()); - writer.newLine(); - } else { - writer.append(" Not compatible with Semantic Versioning"); - writer.newLine(); - } - } - writer.append(scan.isHasUnreleasedSection() ? " Has an unreleased version" : " Has no unreleased version"); - writer.newLine(); - writer.newLine(); + private Scanner getScanner() { + return Scanner.ofServiceLoader() + .toBuilder() + .formatId(formatId) + .build(); } } diff --git a/heylogs-maven-plugin/pom.xml b/heylogs-maven-plugin/pom.xml index d4059fe..cb30d4d 100644 --- a/heylogs-maven-plugin/pom.xml +++ b/heylogs-maven-plugin/pom.xml @@ -7,7 +7,7 @@ com.github.nbbrd.heylogs heylogs-parent - 0.5.0 + 0.6.0 heylogs-maven-plugin @@ -18,10 +18,21 @@ https://github.com/nbbrd/heylogs + - heylogs-api - ${project.groupId} - ${project.version} + org.checkerframework + checker-qual + provided + + + org.projectlombok + lombok + provided + + + com.github.nbbrd.java-design-util + java-design-processor + provided org.apache.maven @@ -38,9 +49,16 @@ org.apache.maven.plugin-tools maven-plugin-annotations - 3.7.0 + 3.9.0 provided + + + + heylogs-api + ${project.groupId} + ${project.version} + @@ -49,7 +67,7 @@ org.apache.maven.plugins maven-plugin-plugin - 3.7.0 + 3.9.0 diff --git a/heylogs-maven-plugin/src/main/java/internal/heylogs/maven/plugin/MojoFunction.java b/heylogs-maven-plugin/src/main/java/internal/heylogs/maven/plugin/MojoFunction.java new file mode 100644 index 0000000..98cad4c --- /dev/null +++ b/heylogs-maven-plugin/src/main/java/internal/heylogs/maven/plugin/MojoFunction.java @@ -0,0 +1,36 @@ +package internal.heylogs.maven.plugin; + +import lombok.NonNull; +import nbbrd.design.StaticFactoryMethod; +import nbbrd.heylogs.Extractor; +import org.apache.maven.plugin.MojoExecutionException; + +import java.time.LocalDate; +import java.util.function.Function; +import java.util.regex.Pattern; + +@FunctionalInterface +public interface MojoFunction { + + Y applyWithMojo(X x) throws MojoExecutionException; + + static @NonNull MojoFunction of(@NonNull Function function, @NonNull String errorMessage) { + return x -> { + try { + return function.apply(x); + } catch (IllegalArgumentException ex) { + throw new MojoExecutionException(errorMessage, ex); + } + }; + } + + @StaticFactoryMethod + static @NonNull MojoFunction onPattern(@NonNull String errorMessage) { + return of(Pattern::compile, errorMessage); + } + + @StaticFactoryMethod + static @NonNull MojoFunction onLocalDate(@NonNull String errorMessage) { + return of(Extractor::parseLocalDate, errorMessage); + } +} diff --git a/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/CheckMojo.java b/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/CheckMojo.java index 6e9f440..c82f750 100644 --- a/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/CheckMojo.java +++ b/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/CheckMojo.java @@ -1,35 +1,36 @@ package nbbrd.heylogs.maven.plugin; -import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Document; +import internal.heylogs.SemverRule; +import internal.heylogs.StylishFormat; +import nbbrd.heylogs.Checker; import nbbrd.heylogs.Failure; -import nbbrd.heylogs.Rule; -import nbbrd.heylogs.RuleLoader; -import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.semver4j.Semver; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.Reader; -import java.nio.file.Files; +import java.io.StringReader; import java.util.List; +import static java.util.Locale.ROOT; + @Mojo(name = "check", defaultPhase = LifecyclePhase.VALIDATE, threadSafe = true) -public final class CheckMojo extends AbstractMojo { +public final class CheckMojo extends HeylogsMojo { @Parameter(defaultValue = "${project.basedir}/CHANGELOG.md", property = "heylogs.input.file") private File inputFile; - @Parameter(defaultValue = "false", property = "heylogs.skip") - private boolean skip; - @Parameter(defaultValue = "false", property = "heylogs.semver") private boolean semver; + @Parameter(defaultValue = StylishFormat.ID, property = "heylogs.format.id") + private String formatId; + @Parameter(defaultValue = "${project.version}", readonly = true) private String projectVersion; @@ -44,70 +45,56 @@ public void execute() throws MojoExecutionException { } if (semver) { - enableSemanticVersioning(); + checkSemanticVersioning(); } if (inputFile.exists()) { - validateFile(); + check(loadChecker()); } else { - if (isRootProject()) { - raiseErrorMissingFile(); + if (isRootProject(projectBaseDir)) { + raiseErrorMissingChangelog(); } else { - notifyMissingFile(); + notifyMissingChangelog(); } } } - private void enableSemanticVersioning() throws MojoExecutionException { + private void checkSemanticVersioning() throws MojoExecutionException { getLog().info("Using Semantic Versioning specification"); if (Semver.isValid(projectVersion)) { getLog().info("Valid project version"); - System.setProperty(Rule.ENABLE_KEY, "semver"); } else { - getLog().error(String.format("Invalid project version: '%s' must follow Semantic Versioning specification (https://semver.org/)", projectVersion)); + getLog().error(String.format(ROOT, "Invalid project version: '%s' must follow Semantic Versioning specification (https://semver.org/)", projectVersion)); throw new MojoExecutionException("Invalid project version. See above for details."); } } - private void validateFile() throws MojoExecutionException { - try { - getLog().info("Reading " + inputFile); - Document changelog = read(); - - List failures = Failure.allOf(changelog, RuleLoader.load()); - if (!failures.isEmpty()) { - getLog().error("Invalid changelog"); - failures.forEach(failure -> getLog().error(failure.toString())); - throw new MojoExecutionException("Invalid changelog"); - } - getLog().info("Valid changelog"); - } catch (IOException ex) { - throw new MojoExecutionException("Error while checking changelog", ex); + private Checker loadChecker() { + Checker.Builder result = Checker.ofServiceLoader() + .toBuilder() + .formatId(formatId); + if (semver) { + result.rule(new SemverRule()); } + return result.build(); } - private boolean isRootProject() { - File parentDir = projectBaseDir.getParentFile(); - if (parentDir != null) { - File parentPom = new File(parentDir, "pom.xml"); - return !parentPom.exists(); + private void check(Checker checker) throws MojoExecutionException { + Document changelog = readChangelog(inputFile); + List failures = checker.validate(changelog); + writeFailures(checker, failures); + if (!failures.isEmpty()) { + throw new MojoExecutionException("Invalid changelog"); } - return true; } - private void raiseErrorMissingFile() throws MojoExecutionException { - getLog().error("Missing changelog"); - throw new MojoExecutionException("Missing changelog"); - } - - private void notifyMissingFile() { - getLog().info("Changelog not found"); - } - - public Document read() throws IOException { - Parser parser = Parser.builder().build(); - try (Reader reader = Files.newBufferedReader(inputFile.toPath())) { - return parser.parseReader(reader); + private void writeFailures(Checker checker, List failures) throws MojoExecutionException { + try { + StringBuilder text = new StringBuilder(); + checker.formatFailures(text, inputFile.toString(), failures); + new BufferedReader(new StringReader(text.toString())).lines().forEach(!failures.isEmpty() ? getLog()::error : getLog()::info); + } catch (IOException ex) { + throw new MojoExecutionException("Error while writing failures", ex); } } } diff --git a/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/ExtractMojo.java b/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/ExtractMojo.java index 057c253..8cd6e1d 100644 --- a/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/ExtractMojo.java +++ b/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/ExtractMojo.java @@ -1,29 +1,21 @@ package nbbrd.heylogs.maven.plugin; -import com.vladsch.flexmark.formatter.Formatter; -import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Document; +import nbbrd.heylogs.Extractor; import nbbrd.heylogs.TimeRange; -import nbbrd.heylogs.VersionFilter; -import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import java.io.File; -import java.io.IOException; -import java.io.Reader; -import java.io.Writer; -import java.nio.file.Files; -import java.time.LocalDate; -import java.time.format.DateTimeParseException; import java.util.Objects; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; + +import static internal.heylogs.maven.plugin.MojoFunction.onLocalDate; +import static internal.heylogs.maven.plugin.MojoFunction.onPattern; @Mojo(name = "extract", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true) -public final class ExtractMojo extends AbstractMojo { +public final class ExtractMojo extends HeylogsMojo { @Parameter(defaultValue = "${project.basedir}/CHANGELOG.md", property = "heylogs.input.file") private File inputFile; @@ -43,12 +35,12 @@ public final class ExtractMojo extends AbstractMojo { @Parameter(defaultValue = "0x7fffffff", property = "heylogs.limit") private int limit; - @Parameter(defaultValue = "false", property = "heylogs.skip") - private boolean skip; - @Parameter(defaultValue = "^.*-SNAPSHOT$", property = "heylogs.unreleased.pattern") private String unreleasedPattern; + @Parameter(defaultValue = "false", property = "heylogs.ignore.content") + private boolean ignoreContent; + @Override public void execute() throws MojoExecutionException { if (skip) { @@ -61,70 +53,29 @@ public void execute() throws MojoExecutionException { throw new MojoExecutionException("Changelog not found"); } - getLog().info("Reading " + inputFile); - Document changelog = read(); - - VersionFilter filter = getFilter(); - - getLog().info("Extracting with " + filter); - filter.apply(changelog); - - getLog().info("Writing " + outputFile); - write(changelog); + extract(loadExtractor()); } - private VersionFilter getFilter() throws MojoExecutionException { - return VersionFilter + private Extractor loadExtractor() throws MojoExecutionException { + return Extractor .builder() .ref(Objects.toString(ref, "")) - .unreleasedPattern(fetchUnreleasedPattern()) - .timeRange(TimeRange.of(fetchFrom(), fetchTo())) + .unreleasedPattern(onPattern("Invalid unreleased pattern").applyWithMojo(unreleasedPattern)) + .timeRange(TimeRange.of( + onLocalDate("Invalid format for 'from' parameter").applyWithMojo(from), + onLocalDate("Invalid format for 'to' parameter").applyWithMojo(to)) + ) .limit(limit) + .ignoreContent(ignoreContent) .build(); } - private Pattern fetchUnreleasedPattern() throws MojoExecutionException { - try { - return Pattern.compile(unreleasedPattern); - } catch (PatternSyntaxException ex) { - throw new MojoExecutionException("Invalid unreleased pattern", ex); - } - } + private void extract(Extractor extractor) throws MojoExecutionException { + Document changelog = readChangelog(inputFile); - private LocalDate fetchFrom() throws MojoExecutionException { - try { - return VersionFilter.parseLocalDate(from); - } catch (DateTimeParseException ex) { - throw new MojoExecutionException("Invalid format for 'from' parameter", ex); - } - } - - private LocalDate fetchTo() throws MojoExecutionException { - try { - return VersionFilter.parseLocalDate(to); - } catch (DateTimeParseException ex) { - throw new MojoExecutionException("Invalid format for 'to' parameter", ex); - } - } + getLog().info("Extracting with " + extractor); + extractor.extract(changelog); - public Document read() throws MojoExecutionException { - Parser parser = Parser.builder().build(); - try (Reader reader = Files.newBufferedReader(inputFile.toPath())) { - return parser.parseReader(reader); - } catch (IOException ex) { - throw new MojoExecutionException("Failed to read file", ex); - } - } - - public void write(Document document) throws MojoExecutionException { - try { - Files.createDirectories(outputFile.getParentFile().toPath()); - Formatter formatter = Formatter.builder().build(); - try (Writer writer = Files.newBufferedWriter(outputFile.toPath())) { - formatter.render(document, writer); - } - } catch (IOException ex) { - throw new MojoExecutionException("Failed to write file", ex); - } + writeChangelog(changelog, outputFile); } } diff --git a/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/HeylogsMojo.java b/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/HeylogsMojo.java new file mode 100644 index 0000000..aa6b61f --- /dev/null +++ b/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/HeylogsMojo.java @@ -0,0 +1,61 @@ +package nbbrd.heylogs.maven.plugin; + +import com.vladsch.flexmark.formatter.Formatter; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.ast.Document; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Parameter; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; + +abstract class HeylogsMojo extends AbstractMojo { + + @Parameter(defaultValue = "false", property = "heylogs.skip") + protected boolean skip; + + protected void raiseErrorMissingChangelog() throws MojoExecutionException { + getLog().error("Missing changelog"); + throw new MojoExecutionException("Missing changelog"); + } + + protected void notifyMissingChangelog() { + getLog().info("Changelog not found"); + } + + protected Document readChangelog(File inputFile) throws MojoExecutionException { + getLog().info("Reading changelog " + inputFile); + Parser parser = Parser.builder().build(); + try (Reader reader = Files.newBufferedReader(inputFile.toPath())) { + return parser.parseReader(reader); + } catch (IOException ex) { + throw new MojoExecutionException("Failed to read changelog", ex); + } + } + + protected void writeChangelog(Document document, File outputFile) throws MojoExecutionException { + getLog().info("Writing changelog " + outputFile); + Formatter formatter = Formatter.builder().build(); + try { + Files.createDirectories(outputFile.getParentFile().toPath()); + try (Writer writer = Files.newBufferedWriter(outputFile.toPath())) { + formatter.render(document, writer); + } + } catch (IOException ex) { + throw new MojoExecutionException("Failed to write changelog", ex); + } + } + + protected static boolean isRootProject(File projectBaseDir) { + File parentDir = projectBaseDir.getParentFile(); + if (parentDir != null) { + File parentPom = new File(parentDir, "pom.xml"); + return !parentPom.exists(); + } + return true; + } +} diff --git a/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/ListMojo.java b/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/ListMojo.java new file mode 100644 index 0000000..75fa44c --- /dev/null +++ b/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/ListMojo.java @@ -0,0 +1,43 @@ +package nbbrd.heylogs.maven.plugin; + +import internal.heylogs.SemverRule; +import nbbrd.heylogs.Checker; +import nbbrd.heylogs.spi.Format; +import nbbrd.heylogs.spi.Rule; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import static java.util.stream.Collectors.joining; + +@Mojo(name = "list", defaultPhase = LifecyclePhase.VALIDATE, threadSafe = true) +public final class ListMojo extends HeylogsMojo { + + @Parameter(defaultValue = "false", property = "heylogs.semver") + private boolean semver; + + @Override + public void execute() { + if (skip) { + getLog().info("Listing has been skipped."); + return; + } + + list(loadChecker()); + } + + private Checker loadChecker() { + Checker.Builder result = Checker.ofServiceLoader() + .toBuilder(); + if (semver) { + result.rule(new SemverRule()); + } + return result.build(); + } + + private void list(Checker checker) { + getLog().info("Rules: " + checker.getRules().stream().map(Rule::getId).collect(joining(", "))); + getLog().info("Formats: " + checker.getFormats().stream().map(Format::getId).collect(joining(", "))); + } +} diff --git a/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/ScanMojo.java b/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/ScanMojo.java new file mode 100644 index 0000000..3eaa535 --- /dev/null +++ b/heylogs-maven-plugin/src/main/java/nbbrd/heylogs/maven/plugin/ScanMojo.java @@ -0,0 +1,69 @@ +package nbbrd.heylogs.maven.plugin; + +import com.vladsch.flexmark.util.ast.Document; +import internal.heylogs.StylishFormat; +import nbbrd.heylogs.Scanner; +import nbbrd.heylogs.Status; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.StringReader; + +@Mojo(name = "scan", defaultPhase = LifecyclePhase.VALIDATE, threadSafe = true) +public final class ScanMojo extends HeylogsMojo { + + @Parameter(defaultValue = "${project.basedir}/CHANGELOG.md", property = "heylogs.input.file") + private File inputFile; + + @Parameter(defaultValue = StylishFormat.ID, property = "heylogs.format.id") + private String formatId; + + @Parameter(defaultValue = "${project.basedir}", readonly = true) + private File projectBaseDir; + + @Override + public void execute() throws MojoExecutionException { + if (skip) { + getLog().info("Scanning has been skipped."); + return; + } + + if (inputFile.exists()) { + scan(loadScanner()); + } else { + if (isRootProject(projectBaseDir)) { + raiseErrorMissingChangelog(); + } else { + notifyMissingChangelog(); + } + } + } + + private Scanner loadScanner() { + return Scanner.ofServiceLoader() + .toBuilder() + .formatId(formatId) + .build(); + } + + private void scan(Scanner scanner) throws MojoExecutionException { + Document changelog = readChangelog(inputFile); + Status status = scanner.scan(changelog); + writeStatus(status, scanner); + } + + private void writeStatus(Status status, Scanner scanner) throws MojoExecutionException { + try { + StringBuilder text = new StringBuilder(); + scanner.formatStatus(text, inputFile.toString(), status); + new BufferedReader(new StringReader(text.toString())).lines().forEach(getLog()::info); + } catch (IOException ex) { + throw new MojoExecutionException("Error while writing status", ex); + } + } +} diff --git a/pom.xml b/pom.xml index acd6edb..bb04fc2 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.nbbrd.heylogs heylogs-parent - 0.5.0 + 0.6.0 pom heylogs @@ -42,21 +42,21 @@ org.checkerframework checker-qual - 3.27.0 + 3.35.0 org.junit junit-bom - 5.9.1 + 5.9.3 pom import org.assertj assertj-core - 3.23.1 + 3.24.2 @@ -73,17 +73,17 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.1 + 3.11.0 org.apache.maven.plugins maven-deploy-plugin - 3.0.0 + 3.1.1 org.apache.maven.plugins maven-install-plugin - 3.1.0 + 3.1.1 org.apache.maven.plugins @@ -93,7 +93,7 @@ org.apache.maven.plugins maven-resources-plugin - 3.3.0 + 3.3.1 org.apache.maven.plugins @@ -103,7 +103,13 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M7 + 3.1.2 + + + + de.thetaphi + forbiddenapis + 3.5.1 @@ -121,6 +127,32 @@ + + de.thetaphi + forbiddenapis + + false + + jdk-unsafe + jdk-deprecated + jdk-internal + jdk-non-portable + jdk-reflection + + + javax.annotation.processing.Generated + lombok.Generated + + + + + + check + testCheck + + + + @@ -128,6 +160,7 @@ heylogs-api heylogs-cli heylogs-maven-plugin + heylogs-bom @@ -166,10 +199,10 @@ - 1.18.24 - 1.5.2 - 1.3.1 - 4.7.0 + 1.18.28 + 1.6.1 + 1.4.0 + 4.7.4 @@ -336,17 +369,17 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.1.0 + 3.3.0 org.kordamp.maven pomchecker-enforcer-rules - 1.4.0 + 1.9.0 org.codehaus.mojo extra-enforcer-rules - 1.6.1 + 1.7.0 @@ -409,7 +442,7 @@ org.gaul modernizer-maven-plugin - 2.5.0 + 2.6.0 1.8 @@ -441,7 +474,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 + 0.8.10 @@ -474,7 +507,7 @@ com.amashchenko.maven.plugin gitflow-maven-plugin - 1.19.0 + 1.20.0 v @@ -493,7 +526,7 @@ org.apache.maven.plugins maven-source-plugin - 3.2.1 + 3.3.0 attach-sources @@ -506,7 +539,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.4.1 + 3.5.0 attach-empty-javadocs