diff --git a/CHANGELOG.md b/CHANGELOG.md index 89193ac..fb392f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - New various path matchers +- New `LineExtensions`, `ChildLimits` and `PathSorts` helper classes (and associated builders) ### Changed +- Helpers classes `PathUtils` and `PathPredicates` removed, use `PathMatchers` instead - Filtering: now using `PathMatcher` interface instead of `Predicate` - Filtering: split into distinct directories and files filters for better control -- `PathUtils` and `PathPredicates` removed, use `PathMatchers` instead - Line extension: empty string is now permitted ### Fixed diff --git a/README.md b/README.md index 0039a24..65558f8 100644 --- a/README.md +++ b/README.md @@ -132,12 +132,12 @@ filtering/ Files and directories can be sorted using a custom comparator (default is alphabetical order). If the provided comparator considers two paths equal (i.e., returns `0`), an alphabetical comparator is applied as a tie-breaker to ensure consistent results across all systems. -The `PrettyPrintOptions.Sorts` class provides a set of basic, ready-to-use comparators. +The `PathSorts` class provides a set of basic, ready-to-use comparators, as well as a builder for creating your own tailor-made sorts. ```java // Example: Sorting.java var prettyPrinter = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.sort(PrettyPrintOptions.Sorts.DIRECTORY_FIRST)) + .customizeOptions(options -> options.sort(PathSorts.DIRECTORY_FIRST)) .build(); ``` ``` @@ -219,14 +219,13 @@ Use the `ChildLimitBuilder` and `PathMatchers` classes to help you build the lim ```java // Example: ChildLimitDynamic.java -var isNodeModuleMatcher = PathMatchers.hasName("node_modules"); -var childLimit = ChildLimitBuilder.builder() - .defaultLimit(ChildLimitBuilder.UNLIMITED) - .limit(isNodeModuleMatcher, 0) +var childLimit = ChildLimits.builder() + .setDefault(ChildLimits.UNLIMITED) // Unlimited children by default + .add(PathMatchers.hasName("node_modules"), 0) // Do NOT print any children in "node_modules" folder .build(); var prettyPrinter = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.withChildLimit(childLimit)) - .build(); + .customizeOptions(options -> options.withChildLimit(childLimit)) + .build(); ``` ``` child_limit_dynamic/ @@ -249,28 +248,23 @@ child_limit_dynamic/ You can extend each displayed path with additional information by providing a custom `Function`. This is useful to annotate your tree with comments, display file sizes, or add domain-specific notes. -The function receives the current path and returns an optional string to append. +The function receives the current path and returns an optional string to append (empty string is authorized). If the function returns `null`, nothing is added. +Use the `LineExtensions` class to help you build line extension functions. + ```java // Example: LineExtension.java var printedPath = Path.of("src/example/resources/line_extension"); -Function lineExtension = path -> { - if (PathMatchers.hasRelativePathMatchingGlob("src/main/java/api", printedPath).matches(path)) { - return "\t\t\t// All API code: controllers, etc."; - } - if (PathMatchers.hasRelativePathMatchingGlob("src/main/java/domain", printedPath).matches(path)) { - return "\t\t\t// All domain code: value objects, etc."; - } - if (PathMatchers.hasRelativePathMatchingGlob("src/main/java/infra", printedPath).matches(path)) { - return "\t\t\t// All infra code: database, email service, etc."; - } - if (PathMatchers.hasNameMatchingGlob("*.properties").matches(path)) { - return "\t// Config file"; - } - return null; -}; +Function lineExtension = LineExtensions.builder() + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/api"), "\t\t\t// All API code: controllers, etc.") + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/domain"), "\t\t\t// All domain code: value objects, etc.") + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/infra"), "\t\t\t// All infra code: database, email service, etc.") + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/api"), "\t\t\t// All API code: controllers, etc.") + .add(PathMatchers.hasNameMatchingGlob("*.properties"), "\t// Config file") + .build(); + var prettyPrinter = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.withLineExtension(lineExtension)) .build(); diff --git a/ROADMAP.md b/ROADMAP.md index a8733db..6760223 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -27,8 +27,8 @@ ## To do - [x] More `PathMatchers` functions! -- [ ] Helper class for line extension -- [ ] Helper class for sorting +- [x] Helper class for line extension +- [x] Helper class for sorting - [ ] Option: custom emojis - [ ] Option: hide number of skipped files and folders for child limit - [ ] Rework/fix Github wiki to be up to date diff --git a/assets/project-structure.png b/assets/project-structure.png index 3637503..c76a1bb 100644 Binary files a/assets/project-structure.png and b/assets/project-structure.png differ diff --git a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ChildLimitDynamic.java b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ChildLimitDynamic.java index 2bed344..b1d6f87 100644 --- a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ChildLimitDynamic.java +++ b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ChildLimitDynamic.java @@ -1,16 +1,15 @@ package io.github.computerdaddyguy.jfiletreeprettyprinter.example; -import io.github.computerdaddyguy.jfiletreeprettyprinter.ChildLimitBuilder; +import io.github.computerdaddyguy.jfiletreeprettyprinter.ChildLimits; import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter; import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers; public class ChildLimitDynamic { public static void main(String[] args) { - var isNodeModuleMatcher = PathMatchers.hasName("node_modules"); - var childLimit = ChildLimitBuilder.builder() - .defaultLimit(ChildLimitBuilder.UNLIMITED) - .limit(isNodeModuleMatcher, 0) + var childLimit = ChildLimits.builder() + .setDefault(ChildLimits.UNLIMITED) // Unlimited children by default + .add(PathMatchers.hasName("node_modules"), 0) // Do NOT print any children in "node_modules" folder .build(); var prettyPrinter = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.withChildLimit(childLimit)) diff --git a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/LineExtension.java b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/LineExtension.java index 4e882b0..fc79715 100644 --- a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/LineExtension.java +++ b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/LineExtension.java @@ -1,6 +1,7 @@ package io.github.computerdaddyguy.jfiletreeprettyprinter.example; import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter; +import io.github.computerdaddyguy.jfiletreeprettyprinter.LineExtensions; import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers; import java.nio.file.Path; import java.util.function.Function; @@ -10,21 +11,14 @@ public class LineExtension { public static void main(String[] args) { var printedPath = Path.of("src/example/resources/line_extension"); - Function lineExtension = path -> { - if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/api").matches(path)) { - return "\t\t\t// All API code: controllers, etc."; - } - if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/domain").matches(path)) { - return "\t\t\t// All domain code: value objects, etc."; - } - if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/infra").matches(path)) { - return "\t\t\t// All infra code: database, email service, etc."; - } - if (PathMatchers.hasNameMatchingGlob("*.properties").matches(path)) { - return "\t// Config file"; - } - return null; - }; + Function lineExtension = LineExtensions.builder() + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/api"), "\t\t\t// All API code: controllers, etc.") + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/domain"), "\t\t\t// All domain code: value objects, etc.") + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/infra"), "\t\t\t// All infra code: database, email service, etc.") + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/api"), "\t\t\t// All API code: controllers, etc.") + .add(PathMatchers.hasNameMatchingGlob("*.properties"), "\t// Config file") + .build(); + var prettyPrinter = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.withLineExtension(lineExtension)) .build(); diff --git a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ProjectStructure.java b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ProjectStructure.java index 25b1202..0bbf98b 100644 --- a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ProjectStructure.java +++ b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ProjectStructure.java @@ -1,9 +1,10 @@ package io.github.computerdaddyguy.jfiletreeprettyprinter.example; -import io.github.computerdaddyguy.jfiletreeprettyprinter.ChildLimitBuilder; +import io.github.computerdaddyguy.jfiletreeprettyprinter.ChildLimits; import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter; +import io.github.computerdaddyguy.jfiletreeprettyprinter.LineExtensions; import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers; -import io.github.computerdaddyguy.jfiletreeprettyprinter.PrettyPrintOptions.Sorts; +import io.github.computerdaddyguy.jfiletreeprettyprinter.PathSorts; import java.nio.file.Path; import java.util.Comparator; import java.util.function.Function; @@ -47,46 +48,38 @@ public static void main(String[] args) { var fileFilter = PathMatchers.allOf( // Hide files with names starting with "." - PathMatchers.not(PathMatchers.hasNameStartingWith(".")), - - // Inside "jfiletreeprettyprinter" folder, keep only "FileTreePrettyPrinter.java" - // Files in other folders are not restricted by this rule. - PathMatchers.ifMatchesThenElse( - /* if */ PathMatchers.hasDirectParentMatching(PathMatchers.hasName("jfiletreeprettyprinter")), - /* then */ PathMatchers.hasName("FileTreePrettyPrinter.java"), - /* else */ path -> true - ) + PathMatchers.not(PathMatchers.hasNameStartingWith(".")) ); /* * Limit the number of displayed children by directory: some content is not relevant and clutters the final result! */ - var childLimitFunction = ChildLimitBuilder.builder() + var childLimitFunction = ChildLimits.builder() // Hide all files under renderer and scanner packages - .limit(PathMatchers.hasAbsolutePathMatchingGlob("**/io/github/computerdaddyguy/jfiletreeprettyprinter/renderer"), 0) - .limit(PathMatchers.hasAbsolutePathMatchingGlob("**/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner"), 0) + .add(PathMatchers.hasAbsolutePathMatchingGlob("**/io/github/computerdaddyguy/jfiletreeprettyprinter/renderer"), 0) + .add(PathMatchers.hasAbsolutePathMatchingGlob("**/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner"), 0) + .add(PathMatchers.hasAbsolutePathMatchingGlob("**/io/github/computerdaddyguy/jfiletreeprettyprinter"), 3) .build(); /* * Add some comments on a few files and directories */ - Function lineExtension = path -> { - if (PathMatchers.hasName("project-structure.png").matches(path)) { - return "\t// This image"; - } else if (PathMatchers.hasName("FileTreePrettyPrinter.java").matches(path)) { - return "\t// Main entry point"; - } else if (PathMatchers.hasName("README.md").matches(path)) { - return "\t\t// You're reading at this!"; - } else if (PathMatchers.hasRelativePathMatchingGlob(projectFolder, "src/main/java").matches(path)) { - return ""; // Empty string: force line break in compact directory chain - } - return null; - }; + Function lineExtension = LineExtensions.builder() + .add(PathMatchers.hasName("project-structure.png"), "\t// This image") + .add(PathMatchers.hasName("FileTreePrettyPrinter.java"), "\t// Main entry point") + .add(PathMatchers.hasName("README.md"), "\t\t// You're reading at this!") + .addLineBreak(PathMatchers.hasRelativePathMatchingGlob(projectFolder, "src/main/java")) + .build(); /* - * Sort all paths by directory first (then alphabetically by default) + * Sort all paths by directory first (with highest precedence), + * then "FileTreePrettyPrinter.java" has precedence "-100". + * All other files have default precedence "0", and are then sorted alphabetically by default. */ - Comparator pathComparator = Sorts.DIRECTORY_FIRST; + Comparator pathComparator = PathSorts.builder() + .addFirst(PathMatchers.isDirectory()) + .add(PathMatchers.hasName("FileTreePrettyPrinter.java"), -100) // Default precedence is "0" + .build(); /* * Build the final FileTreePrettyPrinter @@ -115,25 +108,27 @@ public static void main(String[] args) { Expected result ================================ - πŸ“‚ JFileTreePrettyPrinter/ - β”œβ”€ πŸ“‚ assets/ - β”‚ └─ πŸ–ΌοΈ project-structure.png // This image - β”œβ”€ πŸ“‚ src/main/java/ - β”‚ └─ πŸ“‚ io/github/computerdaddyguy/jfiletreeprettyprinter/ - β”‚ β”œβ”€ πŸ“‚ renderer/ - β”‚ β”‚ └─ ... (5 files and 2 directories skipped) - β”‚ β”œβ”€ πŸ“‚ scanner/ - β”‚ β”‚ └─ ... (4 files skipped) - β”‚ └─ β˜• FileTreePrettyPrinter.java // Main entry point - β”œβ”€ πŸ—ΊοΈ CHANGELOG.md - β”œβ”€ πŸ“– CONTRIBUTING.md - β”œβ”€ πŸ“„ LICENSE - β”œβ”€ πŸ“– README.md // You're reading at this! - β”œβ”€ πŸ—ΊοΈ ROADMAP.md - β”œβ”€ πŸ›‘οΈ SECURITY.md - β”œβ”€ πŸ—οΈ pom.xml - β”œβ”€ πŸ“– release_process.md - └─ πŸ“œ runMutationTests.sh + πŸ“‚ JFileTreePrettyPrinter/ + β”œβ”€ πŸ“‚ assets/ + β”‚ └─ πŸ–ΌοΈ project-structure.png // This image + β”œβ”€ πŸ“‚ src/main/java/ + β”‚ └─ πŸ“‚ io/github/computerdaddyguy/jfiletreeprettyprinter/ + β”‚ β”œβ”€ πŸ“‚ renderer/ + β”‚ β”‚ └─ ... (5 files and 2 directories skipped) + β”‚ β”œβ”€ πŸ“‚ scanner/ + β”‚ β”‚ └─ ... (4 files skipped) + β”‚ β”œβ”€ β˜• FileTreePrettyPrinter.java // Main entry point + β”‚ └─ ... (10 files skipped) + β”œβ”€ πŸ—ΊοΈ CHANGELOG.md + β”œβ”€ πŸ“– CONTRIBUTING.md + β”œβ”€ πŸ“„ LICENSE + β”œβ”€ πŸ“– README.md // You're reading at this! + β”œβ”€ πŸ—ΊοΈ ROADMAP.md + β”œβ”€ πŸ›‘οΈ SECURITY.md + β”œβ”€ πŸ—οΈ pom.xml + β”œβ”€ πŸ“– release_process.md + └─ πŸ“œ runMutationTests.sh + */ } diff --git a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/Sorting.java b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/Sorting.java index ca2d530..5009595 100644 --- a/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/Sorting.java +++ b/src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/Sorting.java @@ -1,13 +1,13 @@ package io.github.computerdaddyguy.jfiletreeprettyprinter.example; import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter; -import io.github.computerdaddyguy.jfiletreeprettyprinter.PrettyPrintOptions; +import io.github.computerdaddyguy.jfiletreeprettyprinter.PathSorts; public class Sorting { public static void main(String[] args) { var prettyPrinter = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.sort(PrettyPrintOptions.Sorts.DIRECTORY_FIRST)) + .customizeOptions(options -> options.sort(PathSorts.DIRECTORY_FIRST)) .build(); var tree = prettyPrinter.prettyPrint("src/example/resources/sorting"); System.out.println(tree); diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitBuilder.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitBuilder.java index db57198..12090b6 100644 --- a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitBuilder.java +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitBuilder.java @@ -12,103 +12,111 @@ * A builder for creating a {@code ToIntFunction} that defines * how many child entries (files or directories) are allowed under a given path. *

- * The resulting function evaluates added rules in the order they were defined. - * The first matching rule determines the child limit for a given path. + * Rules are evaluated in the order they are added (first-match-wins). + * The first rule that matches a path determines the limit. * If no rules match, the default limit is applied. *

* *

- * A limit of {@link #UNLIMITED} ({@code -1}) means no restriction. - * A limit of {@code 0} means the directory cannot have any children. + * Special values: + *

    + *
  • {@link #UNLIMITED} ({@code -1}) β€” no restriction on children.
  • + *
  • {@code 0} β€” directory cannot contain any children.
  • + *
  • Any positive number β€” maximum number of children allowed.
  • + *
*

* *

Example usage:

*
{@code
- * var childLimit = ChildLimitBuilder.builder()
- *     .defaultLimit(ChildLimit.UNLIMITED)   // unlimited unless specified
- *     .limit(PathMatchers.hasName("bigDir"), 10)  // max 10 children in "bigDir"
- *     .limit(PathMatchers.hasName("emptyDir"), 0) // disallow children in "emptyDir"
+ * var childLimit = ChildLimitBuilder.newInstance()
+ *     .setDefault(ChildLimitBuilder.UNLIMITED)     // unlimited unless specified
+ *     .add(PathMatchers.hasName("bigDir"), 10)     // max 10 children in "bigDir"
+ *     .add(PathMatchers.hasName("emptyDir"), 0)    // disallow children in "emptyDir"
  *     .build();
- *
  * }
*/ @NullMarked public class ChildLimitBuilder { - /** - * Unlimited children. - */ - public static final int UNLIMITED = -1; - - private static final ChildControl UNLIMITED_CONTROL = new ChildControl(p -> true, UNLIMITED); - - private List controls; - private ChildControl defaultControl; - - /** - * Creates a new builder. - */ - private ChildLimitBuilder() { - this.controls = new ArrayList<>(); - this.defaultControl = UNLIMITED_CONTROL; - } - - private record ChildControl(PathMatcher pathMatcher, int limit) { - - } + private List> limits; + private int defaultLimit; - public static ChildLimitBuilder builder() { - return new ChildLimitBuilder(); + /* package */ ChildLimitBuilder(int defaultLimit) { + this.limits = new ArrayList<>(); + this.defaultLimit = defaultLimit; } /** - * Builds the child limit function based on the configured rules. + * Builds the final child limit function based on the configured rules. *

- * Rules are tested in the order they were added. The first matching rule - * provides the limit. If no rule matches, the default limit is used. + * Rules are evaluated in insertion order. The first rule that matches a path + * determines its child limit. If no rules match, the default limit is returned. *

* * @return a function mapping a {@link Path} to its maximum number of children */ public ToIntFunction build() { - var immutControls = List.copyOf(controls); - var immutDefaultControl = this.defaultControl; - return p -> immutControls.stream() - .filter(control -> control.pathMatcher().matches(p)) - .findFirst() - .orElse(immutDefaultControl) - .limit(); + var immutLimits = List.copyOf(limits); + return path -> { + int result = defaultLimit; + for (var rule : immutLimits) { + result = rule.applyAsInt(path); + if (result >= 0) { + break; + } + } + return result; + }; } /** * Sets the default child limit to apply when no specific rule matches. * * @param limit the default limit (use {@link #UNLIMITED} for no restriction) - * * @return this builder for chaining */ - public ChildLimitBuilder defaultLimit(int limit) { - this.defaultControl = new ChildControl(p -> true, limit); + public ChildLimitBuilder setDefault(int limit) { + this.defaultLimit = limit; + return this; + } + + /** + * Adds a custom rule expressed as a {@link ToIntFunction}. + *

+ * This function should return: + *

    + *
  • {@link #UNLIMITED} ({@code -1}) if it does not apply
  • + *
  • Any non-negative integer as the effective child limit if it applies
  • + *
+ *

+ * + * @param childLimit the function mapping a path to a child limit + * @return this builder for chaining + * @throws NullPointerException if {@code childLimit} is null + */ + public ChildLimitBuilder add(ToIntFunction childLimit) { + Objects.requireNonNull(childLimit, "childLimit is null"); + this.limits.add(childLimit); return this; } /** * Adds a child limit rule for paths matching the given matcher. *

- * Rules are evaluated in the order they are added. The first matching rule wins. + * If the path matches, this rule applies the given limit. + * Otherwise, it yields {@link #UNLIMITED} so that subsequent rules may apply. + * Because rules are evaluated in insertion order, the first matching rule wins. *

* - * @param pathMatcher the condition for paths - * @param limit the maximum number of children (use {@link #UNLIMITED} for no restriction) - * + * @param pathMatcher the matcher used to test paths (non-null) + * @param limit the maximum number of children for matching paths + * (use {@link #UNLIMITED} for no restriction) * @return this builder for chaining - * * @throws NullPointerException if {@code pathMatcher} is null */ - public ChildLimitBuilder limit(PathMatcher pathMatcher, int limit) { + public ChildLimitBuilder add(PathMatcher pathMatcher, int limit) { Objects.requireNonNull(pathMatcher, "pathMatcher is null"); - this.controls.add(new ChildControl(pathMatcher, limit)); - return this; + return add(path -> pathMatcher.matches(path) ? limit : ChildLimits.UNLIMITED); } } diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimits.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimits.java new file mode 100644 index 0000000..b640440 --- /dev/null +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimits.java @@ -0,0 +1,60 @@ +package io.github.computerdaddyguy.jfiletreeprettyprinter; + +/** + * Utility class providing constants and factory methods for creating + * {@link ChildLimitBuilder} instances. + *

+ * A child limit defines how many entries (files or directories) are allowed + * under a given directory path when building or rendering a file tree. + *

+ * + *

+ * This class also exposes the {@link #UNLIMITED} constant, representing + * an unrestricted number of children. + *

+ * + *

Example usage:

+ *
{@code
+ * var limits = ChildLimits.builder()
+ *     .add(PathMatchers.hasName("bin"), 0)      // Hide "bin" folder contents
+ *     .add(PathMatchers.hasName("src"), 20)     // Limit to 20 children
+ *     .build();
+ * }
+ * + * @see ChildLimitBuilder + */ +public final class ChildLimits { + + /** + * Special value indicating unlimited children ({@code -1}). + */ + public static final int UNLIMITED = -1; + + private ChildLimits() { + // Helper class + } + + /** + * Returns a new {@link ChildLimitBuilder} with the default limit + * set to {@link #UNLIMITED}. + * + * @return a fresh builder instance + */ + public static ChildLimitBuilder builder() { + return builder(UNLIMITED); + } + + /** + * Returns a new {@link ChildLimitBuilder} with the given default limit. + *

+ * The default limit applies when no specific rule matches a given path. + *

+ * + * @param defaultLimit the default limit (use {@link #UNLIMITED} for no restriction) + * @return a fresh builder instance + */ + public static ChildLimitBuilder builder(int defaultLimit) { + return new ChildLimitBuilder(defaultLimit); + } + +} diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FileTreePrettyPrinterBuilder.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FileTreePrettyPrinterBuilder.java index 888d24a..682250a 100644 --- a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FileTreePrettyPrinterBuilder.java +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FileTreePrettyPrinterBuilder.java @@ -31,7 +31,11 @@ @NullMarked public class FileTreePrettyPrinterBuilder { - private PrettyPrintOptions options = PrettyPrintOptions.createDefault(); + private PrettyPrintOptions options; + + /* package */ FileTreePrettyPrinterBuilder() { + options = PrettyPrintOptions.createDefault(); + } /** * Builds the pretty printer using the configured options. diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionBuilder.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionBuilder.java new file mode 100644 index 0000000..b10cb13 --- /dev/null +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionBuilder.java @@ -0,0 +1,116 @@ +package io.github.computerdaddyguy.jfiletreeprettyprinter; + +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import org.jspecify.annotations.NullMarked; + +/** + * A builder for constructing functions that provide optional line extensions + * (such as comments or formatting markers) when pretty-printing file trees. + *

+ * A {@code LineExtensionBuilder} allows you to add rules in the form of + * {@link Function} objects that map a {@link Path} to an extension string. + * When the resulting function is applied to a path, rules are evaluated + * in insertion order, and the first non-{@code null} result is used. + *

    + *
  • {@code null} means no extension (line is printed normally).
  • + *
  • An empty string means "force line break" in compact directory chains.
  • + *
  • Any non-empty string is appended after the path (e.g. a comment).
  • + *
+ * + *

Example usage: + *

{@code
+ * var lineExtension = LineExtensionBuilder.newInstance()
+ *     .add(PathMatchers.hasName("README.md"), " // Project documentation")
+ *     .addLineBreak(PathMatchers.hasRelativePathMatchingGlob(root, "src/main/java"))
+ *     .build();
+ * }
+ * + * The returned {@code Function} can then be passed to + * {@link PrettyPrintOptions#withLineExtension(Function)}. + * + * @see PrettyPrintOptions + */ +@NullMarked +public class LineExtensionBuilder { + + private List> extensions; + + /* package */ LineExtensionBuilder() { + this.extensions = new ArrayList<>(); + } + + /** + * Builds the final function mapping a {@link Path} to an extension string. + *

+ * The function applies the registered rules in insertion order. + * The first rule returning a non-{@code null} value determines the extension. + * If none match, the function returns {@code null}. + * + * @return a function mapping paths to extensions + */ + public Function build() { + var immutExtensions = List.copyOf(extensions); + return path -> { + String result = LineExtensions.NO_EXTENSION; + for (var rule : immutExtensions) { + result = rule.apply(path); + if (!Objects.equals(result, LineExtensions.NO_EXTENSION)) { + break; + } + } + return result; + }; + } + + /** + * Adds a custom line extension rule. + *

+ * The function should return either: + *

    + *
  • {@code null} to indicate "no extension".
  • + *
  • an empty string to force a line break.
  • + *
  • a non-empty string to append after the path.
  • + *
+ * + * @param lineExtension a function mapping paths to extensions (non-null) + * @return this builder (for chaining) + * @throws NullPointerException if {@code lineExtension} is null + */ + public LineExtensionBuilder add(Function lineExtension) { + Objects.requireNonNull(lineExtension, "lineExtension is null"); + this.extensions.add(lineExtension); + return this; + } + + /** + * Adds a rule that appends the given extension when the matcher matches. + *

+ * If the matcher does not match, the rule returns {@code null}. + * + * @param pathMatcher the matcher to test paths against (non-null) + * @param extension the extension string to return when matched + * @return this builder (for chaining) + * @throws NullPointerException if {@code pathMatcher} is null + */ + public LineExtensionBuilder add(PathMatcher pathMatcher, String extension) { + Objects.requireNonNull(pathMatcher, "pathMatcher is null"); + return add(path -> pathMatcher.matches(path) ? extension : LineExtensions.NO_EXTENSION); + } + + /** + * Adds a rule that forces a line break (instead of appending text) + * whenever the given matcher matches. + * + * @param pathMatcher the matcher to test paths against (non-null) + * @return this builder (for chaining) + */ + public LineExtensionBuilder addLineBreak(PathMatcher pathMatcher) { + return add(pathMatcher, LineExtensions.LINE_BREAK_EXTENSION); + } + +} diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensions.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensions.java new file mode 100644 index 0000000..efce6d4 --- /dev/null +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensions.java @@ -0,0 +1,53 @@ +package io.github.computerdaddyguy.jfiletreeprettyprinter; + +/** + * Utility class providing constants and factory methods for creating + * {@link LineExtensionBuilder} instances used to define per-path line extensions + * in pretty-printed file trees. + *

+ * Line extensions are optional text fragments appended to specific lines + * in the printed tree. They can be used to add comments, annotations, or + * formatting cues. + *

+ * + *

+ * Two special constants are provided: + *

+ *
    + *
  • {@link #NO_EXTENSION} β€” indicates no extension for a given path.
  • + *
  • {@link #LINE_BREAK_EXTENSION} β€” indicates a forced line break, + * useful for splitting compact directory chains.
  • + *
+ * + *

Example usage:

+ *
{@code
+ * var lineExt = LineExtensions.builder()
+ *     .add(PathMatchers.hasName("README.md"), "\t// Project documentation")
+ *     .addLineBreak(PathMatchers.hasRelativePathMatchingGlob(".", "src/main/java"))
+ *     .build();
+ * }
+ * + * @see LineExtensionBuilder + */ +public final class LineExtensions { + + /** Indicates that no extension should be applied to the line ({@code null}). */ + public static final String NO_EXTENSION = null; + + /** Indicates a forced line break (empty string). */ + public static final String LINE_BREAK_EXTENSION = ""; + + private LineExtensions() { + // Helper class + } + + /** + * Returns a new {@link LineExtensionBuilder}. + * + * @return a fresh builder instance + */ + public static LineExtensionBuilder builder() { + return new LineExtensionBuilder(); + } + +} diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathSortBuilder.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathSortBuilder.java new file mode 100644 index 0000000..0745e40 --- /dev/null +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathSortBuilder.java @@ -0,0 +1,138 @@ +package io.github.computerdaddyguy.jfiletreeprettyprinter; + +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.ToIntFunction; +import org.jspecify.annotations.NullMarked; + +/** + * A builder for creating a {@link Comparator Comparator<Path>} that defines + * a custom sorting order for file system paths based on rule precedence. + *

+ * Each rule assigns an integer "precedence" value to paths. The first rule + * that matches a path determines its precedence. Paths are then sorted by + * ascending precedence value (lower values come first), followed by an + * alphabetical fallback comparison. + *

+ * + *

+ * Predefined precedence constants: + *

+ *
    + *
  • {@link #HIGHEST_PRECEDENCE} ({@code Integer.MIN_VALUE}) β€” top priority
  • + *
  • {@link #DEFAULT_PRECEDENCE} ({@code 0}) β€” default order
  • + *
  • {@link #LOWEST_PRECEDENCE} ({@code Integer.MAX_VALUE}) β€” last priority
  • + *
+ * + *

Example usage:

+ *
{@code
+ * var customSort = PathSortBuilder.newInstance()
+ *     .addFirst(PathMatchers.hasName("README.md"))     // always first
+ *     .addLast(PathMatchers.hasName("target"))         // always last
+ *     .add(path -> path.toString().contains("core") ? -10 : 0) // custom priority rule
+ *     .build();
+ *
+ * var printer = FileTreePrettyPrinter.builder()
+ *     .customizeOptions(options -> options.sort(customSort))
+ *     .build();
+ * }
+ */ +@NullMarked +public class PathSortBuilder { + + /** Highest possible precedence β€” items appear first. */ + public static final int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; + + /** Default precedence (neutral value). */ + public static final int DEFAULT_PRECEDENCE = 0; + + /** Lowest possible precedence β€” items appear last. */ + public static final int LOWEST_PRECEDENCE = Integer.MAX_VALUE; + + private List> orders; + + /* package */ PathSortBuilder() { + this.orders = new ArrayList<>(); + } + + /** + * Builds the final {@link Comparator Comparator<Path>} based on the configured rules. + *

+ * Rules are tested in the order they were added. The first matching rule + * having a result different than {@link DEFAULT_PRECEDENCE} (meaning, {@code 0}) + * determines the precedence value for a given path. Paths are sorted by + * this precedence, and then alphabetically as a tiebreaker. + *

+ * + * @return a comparator defining the final path order + */ + public Comparator build() { + var immutOrders = List.copyOf(orders); + Function finalFunction = path -> { + int result = DEFAULT_PRECEDENCE; + for (var rule : immutOrders) { + result = rule.applyAsInt(path); + if (result != DEFAULT_PRECEDENCE) { + break; + } + } + return result; + }; + return Comparator.comparing(finalFunction).thenComparing(PathSorts.ALPHABETICAL); + } + + /** + * Adds a custom rule function defining a precedence for a path. + * + * @param order a function returning a precedence value + * @return this builder for chaining + * + * @throws NullPointerException if {@code order} is null + */ + public PathSortBuilder add(ToIntFunction order) { + Objects.requireNonNull(order, "order is null"); + this.orders.add(order); + return this; + } + + /** + * Adds a rule that assigns a precedence value to all paths matching + * the specified {@link PathMatcher}. + * + * @param pathMatcher the matcher to test paths + * @param order the precedence value to assign + * @return this builder for chaining + * + * @throws NullPointerException if {@code pathMatcher} is null + */ + public PathSortBuilder add(PathMatcher pathMatcher, int order) { + Objects.requireNonNull(pathMatcher, "pathMatcher is null"); + return add(path -> pathMatcher.matches(path) ? order : DEFAULT_PRECEDENCE); + } + + /** + * Adds a rule that forces matching paths to appear first in the sort order. + * + * @param pathMatcher the matcher identifying high-priority paths + * @return this builder for chaining + */ + public PathSortBuilder addFirst(PathMatcher pathMatcher) { + return add(pathMatcher, HIGHEST_PRECEDENCE); + } + + /** + * Adds a rule that forces matching paths to appear last in the sort order. + * + * @param pathMatcher the matcher identifying low-priority paths + * @return this builder for chaining + */ + public PathSortBuilder addLast(PathMatcher pathMatcher) { + return add(pathMatcher, LOWEST_PRECEDENCE); + } + +} diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathSorts.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathSorts.java new file mode 100644 index 0000000..4d8fd6e --- /dev/null +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathSorts.java @@ -0,0 +1,139 @@ +package io.github.computerdaddyguy.jfiletreeprettyprinter; + +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Optional; + +public final class PathSorts { + + private PathSorts() { + // Helper class + } + + /** + * Default alphabetical comparator based on the file name. + */ + public static final Comparator ALPHABETICAL = Comparator.comparing( + (Path path) -> Optional.ofNullable(path.getFileName()) + .map(Path::toString) + .orElse("") + ); + + /** + * Comparator that sorts paths by file size in ascending order. + * Directories are treated as having a size of {@code 0}, so they appear before regular files. + */ + public static final Comparator BY_FILE_SIZE = Comparator.comparing( + (Path path) -> PathMatchers.isDirectory().matches(path) ? 0 : path.toFile().length() + ); + + /** + * Comparator that places directories before files. + * Paths of the same type (both directories or both files) are considered equal. + */ + public static final Comparator DIRECTORY_FIRST = (Path path1, Path path2) -> { + var isP1Dir = PathMatchers.isDirectory().matches(path1); + var isP2Dir = PathMatchers.isDirectory().matches(path2); + if (isP1Dir == isP2Dir) { + return 0; + } + return isP1Dir ? -1 : 1; + }; + + /** + * Comparator that places files before directories. + * Paths of the same type (both directories or both files) are considered equal. + */ + public static final Comparator DIRECTORY_LAST = DIRECTORY_FIRST.reversed(); + + /** + * Comparator that sort files by their extension (= the part after the last '.' character in the name). + * In case of several extensions (e.g. ".tar.gz"), files are firstly ordered by the "gz", then by "tar.gz": + * - ccc.gz + * - ddd.gz + * - .tar.gz + * - aaa.tar.gz + * - bbb.tar.gz + */ + + /** + * Comparator that sorts files by their extension(s), defined as the substring(s) after each '.' in the name. + *

+ * Examples of ordering: + *

    + *
  • ccc.gz
  • + *
  • ddd.gz
  • + *
  • aaa.tar.gz
  • + *
  • bbb.tar.gz
  • + *
  • zzz.txt
  • + *
+ *

+ * Rules: + *

    + *
  • If a file has no extension, it is sorted before files with extensions (e.g., README comes before aaa.txt).
  • + *
  • For multi-part extensions (like .tar.gz), comparison is hierarchical: + * first by the last extension (gz), then by the preceding extension (tar.gz), etc.
  • + *
  • If extensions are equal, names are compared alphabetically as a tie-breaker.
  • + *
+ */ + public static final Comparator BY_EXTENSION = (p1, p2) -> { + var isDir1 = PathMatchers.isDirectory().matches(p1); + var isDir2 = PathMatchers.isDirectory().matches(p2); + + // Directories come first + if (isDir1) { + return isDir2 ? 0 : -1; + } + if (isDir2) { + return 1; + } + + String name1 = p1.getFileName().toString(); + String name2 = p2.getFileName().toString(); + + String[] parts1 = name1.split("\\."); + String[] parts2 = name2.split("\\."); + + // Handle files without extensions + if (parts1.length == 1 && parts2.length == 1) { + return name1.compareToIgnoreCase(name2); + } + if (parts1.length == 1) { + return -1; // p1 has no extension --> comes first + } + if (parts2.length == 1) { + return 1; // p2 has no extension --> comes first + } + + // Compare extensions starting from the last part + int i1 = parts1.length - 1; + int i2 = parts2.length - 1; + while (i1 >= 1 && i2 >= 1) { + int cmp = parts1[i1].compareToIgnoreCase(parts2[i2]); + if (cmp != 0) { + return cmp; + } + i1--; + i2--; + } + + // If one has more extension parts, it comes after + if (i1 > i2) + return 1; + if (i2 > i1) + return -1; + + // Files have same extension (will be sorted by the next comparator - the alphabetical tie-breaker comparator by default) + return 0; + }; + + /** + * Returns a new {@link PathSortBuilder}. + * + * @return a fresh builder instance + */ + public static PathSortBuilder builder() { + return new PathSortBuilder(); + } + +} diff --git a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PrettyPrintOptions.java b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PrettyPrintOptions.java index 2f52829..58d500c 100644 --- a/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PrettyPrintOptions.java +++ b/src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PrettyPrintOptions.java @@ -27,7 +27,7 @@ public static PrettyPrintOptions createDefault() { // ---------- Child limit function ---------- - private ToIntFunction childLimit = p -> -1; + private ToIntFunction childLimit = p -> ChildLimits.UNLIMITED; @Override public ToIntFunction getChildLimit() { @@ -50,6 +50,9 @@ public PrettyPrintOptions withChildLimit(int childLimit) { * Default is no limit. * * @param childLimitFunction The dynamic limitation function, cannot be null. A negative computed value means no limit. + * + * @see ChildLimits + * @see ChildLimitBuilder */ public PrettyPrintOptions withChildLimit(ToIntFunction childLimitFunction) { this.childLimit = Objects.requireNonNull(childLimitFunction, "childLimitFunction is null"); @@ -152,129 +155,7 @@ public PrettyPrintOptions withMaxDepth(int maxDepth) { // ---------- File sort ---------- - public static final class Sorts { - - private Sorts() { - // Helper class - } - - /** - * Comparator that sort paths alphabetically by name. - */ - public static final Comparator BY_NAME = Comparator.comparing((Path path) -> path.getFileName().toString()); - - /** - * Comparator that sorts paths by file size in ascending order. - * Directories are treated as having a size of {@code 0}, so they appear before regular files. - */ - public static final Comparator BY_FILE_SIZE = Comparator.comparing((Path path) -> { - var file = path.toFile(); - return file.isDirectory() ? 0 : file.length(); - }); - - /** - * Comparator that places directories before files. - * Paths of the same type (both directories or both files) are considered equal. - */ - public static final Comparator DIRECTORY_FIRST = (Path path1, Path path2) -> { - var isP1Dir = path1.toFile().isDirectory(); - var isP2Dir = path2.toFile().isDirectory(); - if (isP1Dir == isP2Dir) { - return 0; - } - return isP1Dir ? -1 : 1; - }; - - /** - * Comparator that places files before directories. - * Paths of the same type (both directories or both files) are considered equal. - */ - public static final Comparator DIRECTORY_LAST = DIRECTORY_FIRST.reversed(); - - /** - * Comparator that sort files by their extension (= the part after the last '.' character in the name). - * In case of several extensions (e.g. ".tar.gz"), files are firstly ordered by the "gz", then by "tar.gz": - * - ccc.gz - * - ddd.gz - * - .tar.gz - * - aaa.tar.gz - * - bbb.tar.gz - */ - - /** - * Comparator that sorts files by their extension(s), defined as the substring(s) after each '.' in the name. - *

- * Examples of ordering: - *

    - *
  • ccc.gz
  • - *
  • ddd.gz
  • - *
  • aaa.tar.gz
  • - *
  • bbb.tar.gz
  • - *
  • zzz.txt
  • - *
- *

- * Rules: - *

    - *
  • If a file has no extension, it is sorted before files with extensions (e.g., README comes before aaa.txt).
  • - *
  • For multi-part extensions (like .tar.gz), comparison is hierarchical: - * first by the last extension (gz), then by the preceding extension (tar.gz), etc.
  • - *
  • If extensions are equal, names are compared alphabetically as a tie-breaker.
  • - *
- */ - public static final Comparator BY_EXTENSION = (p1, p2) -> { - boolean isDir1 = p1.toFile().isDirectory(); - boolean isDir2 = p2.toFile().isDirectory(); - - // Directories come first - if (isDir1) { - return isDir2 ? 0 : -1; - } - if (isDir2) { - return 1; - } - - String name1 = p1.getFileName().toString(); - String name2 = p2.getFileName().toString(); - - String[] parts1 = name1.split("\\."); - String[] parts2 = name2.split("\\."); - - // Handle files without extensions - if (parts1.length == 1 && parts2.length == 1) { - return name1.compareToIgnoreCase(name2); - } - if (parts1.length == 1) { - return -1; // p1 has no extension --> comes first - } - if (parts2.length == 1) { - return 1; // p2 has no extension --> comes first - } - - // Compare extensions starting from the last part - int i1 = parts1.length - 1; - int i2 = parts2.length - 1; - while (i1 >= 1 && i2 >= 1) { - int cmp = parts1[i1].compareToIgnoreCase(parts2[i2]); - if (cmp != 0) { - return cmp; - } - i1--; - i2--; - } - - // If one has more extension parts, it comes after - if (i1 > i2) - return 1; - if (i2 > i1) - return -1; - - // Files have same extension (will be sorted by the next comparator - the alphabetical tie-breaker comparator by default) - return 0; - }; - - } - - private Comparator pathComparator = Sorts.BY_NAME; + private Comparator pathComparator = PathSorts.ALPHABETICAL; @Override public Comparator pathComparator() { @@ -289,9 +170,12 @@ public Comparator pathComparator() { * to ensure consistent ordering across all systems. * * @param pathComparator The custom comparator + * + * @see PathSorts + * @see PathSortBuilder */ public PrettyPrintOptions sort(Comparator pathComparator) { - this.pathComparator = Objects.requireNonNull(pathComparator, "pathComparator is null").thenComparing(Sorts.BY_NAME); + this.pathComparator = Objects.requireNonNull(pathComparator, "pathComparator is null").thenComparing(PathSorts.ALPHABETICAL); return this; } @@ -311,6 +195,8 @@ public PathMatcher pathFilter() { * Directories that do not pass this filter will not be displayed. * * @param matcher The filter to apply on directories, cannot be null + * + * @see PathMatchers */ public PrettyPrintOptions filterDirectories(@Nullable PathMatcher matcher) { this.dirMatcher = Objects.requireNonNull(matcher, "matcher is null"); @@ -323,6 +209,8 @@ public PrettyPrintOptions filterDirectories(@Nullable PathMatcher matcher) { * Files that do not pass this filter will not be displayed. * * @param matcher The filter to apply on files, cannot be null + * + * @see PathMatchers */ public PrettyPrintOptions filterFiles(@Nullable PathMatcher matcher) { this.fileMatcher = Objects.requireNonNull(matcher, "matcher is null"); @@ -351,6 +239,10 @@ public Function getLineExtension() { * If the function returns {@code null}, nothing is added. * * @param lineExtension the custom line extension function, or {@code null} to disable + * + * + * @see LineExtensions + * @see LineExtensionBuilder */ public PrettyPrintOptions withLineExtension(@Nullable Function lineExtension) { this.lineExtension = lineExtension; diff --git a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitDynamicTest.java b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitDynamicTest.java index 41ba666..30e1928 100644 --- a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitDynamicTest.java +++ b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitDynamicTest.java @@ -18,10 +18,10 @@ class ChildLimitDynamicTest { // @formatter:off options -> options.withChildLimit( - ChildLimitBuilder.builder() - .limit(PathMatchers.hasName("limit_1"), 1) - .limit(PathMatchers.hasName("limit_3"), 3) - .defaultLimit(ChildLimitBuilder.UNLIMITED) + ChildLimits.builder() + .add(PathMatchers.hasName("limit_1"), 1) + .add(PathMatchers.hasName("limit_3"), 3) + .setDefault(ChildLimits.UNLIMITED) .build() ) // @formatter:on diff --git a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FilteringTest.java b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FilteringTest.java index f31dc1f..da30094 100644 --- a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FilteringTest.java +++ b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/FilteringTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import io.github.computerdaddyguy.jfiletreeprettyprinter.PrettyPrintOptions.Sorts; import io.github.computerdaddyguy.jfiletreeprettyprinter.util.FileStructureCreator; import java.nio.file.Path; import org.junit.jupiter.api.Test; @@ -60,7 +59,7 @@ void example_and_sorting() { var filter = PathMatchers.hasExtension("java"); FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.sort(Sorts.BY_NAME.reversed())) + .customizeOptions(options -> options.sort(PathSorts.ALPHABETICAL.reversed())) .customizeOptions(options -> options.filterFiles(filter)) .build(); diff --git a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionTest.java b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionTest.java index 75c1566..d145a3a 100644 --- a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionTest.java +++ b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/LineExtensionTest.java @@ -31,21 +31,12 @@ void example_dir_match() { var printedPath = Path.of("src/example/resources/line_extension"); - Function lineExtension = path -> { - if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/api").matches(path)) { - return "\t\t\t// All API code: controllers, etc."; - } - if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/domain").matches(path)) { - return "\t\t\t// All domain code: value objects, etc."; - } - if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/infra").matches(path)) { - return "\t\t\t// All infra code: database, email service, etc."; - } - if (PathMatchers.hasNameMatchingGlob("*.properties").matches(path)) { - return "\t// Config file"; - } - return null; - }; + Function lineExtension = LineExtensions.builder() + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/api"), "\t\t\t// All API code: controllers, etc.") + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/domain"), "\t\t\t// All domain code: value objects, etc.") + .add(PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/infra"), "\t\t\t// All infra code: database, email service, etc.") + .add(PathMatchers.hasNameMatchingGlob("*.properties"), "\t// Config file") + .build(); var printer = FileTreePrettyPrinter.builder() .customizeOptions(options -> options.withLineExtension(lineExtension)) @@ -71,12 +62,9 @@ void example_dir_match() { @Test void compact_dir_first_dir() { - Function lineExtension = p -> { - if (PathMatchers.hasName("dirA").matches(p)) { - return " // 1"; - } - return null; - }; + Function lineExtension = LineExtensions.builder() + .add(PathMatchers.hasName("dirA"), " // 1") + .build(); var expected = """ targetPath/ @@ -91,14 +79,11 @@ void compact_dir_first_dir() { } @Test - void compact_dir_empty_string_workds() { + void compact_dir_empty_string_creates_line_break() { - Function lineExtension = p -> { - if (PathMatchers.hasName("dirA").matches(p)) { - return ""; - } - return null; - }; + Function lineExtension = LineExtensions.builder() + .addLineBreak(PathMatchers.hasName("dirA")) + .build(); var expected = """ targetPath/ @@ -115,12 +100,9 @@ void compact_dir_empty_string_workds() { @Test void compact_dir_middle_dir() { - Function lineExtension = p -> { - if (PathMatchers.hasName("dirB").matches(p)) { - return " // 2"; - } - return null; - }; + Function lineExtension = LineExtensions.builder() + .add(PathMatchers.hasName("dirB"), " // 2") + .build(); var expected = """ targetPath/ @@ -137,12 +119,9 @@ void compact_dir_middle_dir() { @Test void compact_dir_last_dir() { - Function lineExtension = p -> { - if (PathMatchers.hasName("dirC").matches(p)) { - return " // 3"; - } - return null; - }; + Function lineExtension = LineExtensions.builder() + .add(PathMatchers.hasName("dirC"), " // 3") + .build(); var expected = """ targetPath/ diff --git a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathSortBuilderTest.java b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathSortBuilderTest.java new file mode 100644 index 0000000..1d91e63 --- /dev/null +++ b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/PathSortBuilderTest.java @@ -0,0 +1,114 @@ +package io.github.computerdaddyguy.jfiletreeprettyprinter; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; +import java.util.Comparator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class PathSortBuilderTest { + + private static void assertCompare(Comparator comparator, Path pathFirst, Path pathSecond) { + assertThat(comparator.compare(pathFirst, pathSecond)).isNegative(); + assertThat(comparator.compare(pathSecond, pathFirst)).isPositive(); + assertThat(comparator.compare(pathFirst, pathFirst)).isZero(); + assertThat(comparator.compare(pathSecond, pathSecond)).isZero(); + } + + @Nested + class AddFunction { + + @Test + void when_compare_equal_then_alphabetical() { + var comparator = PathSorts.builder() + .add(p -> 10) + .build(); + + var pathA = Path.of("A"); + var pathB = Path.of("B"); + + assertCompare(comparator, pathA, pathB); + } + + @Test + void nominal() { + var comparator = PathSorts.builder() + .add(p -> p.getFileName().toString().equals("A") ? 2 : 1) + .build(); + + var pathA = Path.of("A"); + var pathB = Path.of("B"); + + assertCompare(comparator, pathB, pathA); + } + + } + + @Nested + class AddMatcher { + + @Test + void when_compare_equal_then_alphabetical() { + var comparator = PathSorts.builder() + .add(p -> true, 10) + .build(); + + var pathA = Path.of("A"); + var pathB = Path.of("B"); + + assertCompare(comparator, pathA, pathB); + } + + @Test + void nominal() { + var comparator = PathSorts.builder() + .add(p -> p.getFileName().toString().equals("A"), 2) + .add(p -> p.getFileName().toString().equals("B"), 1) + .build(); + + var pathA = Path.of("A"); + var pathB = Path.of("B"); + + assertCompare(comparator, pathB, pathA); + } + + } + + @Nested + class AddFirst { + + @Test + void when_compare_equal_then_alphabetical() { + var comparator = PathSorts.builder() + .addFirst(p -> p.getFileName().toString().equals("A")) + .addFirst(p -> p.getFileName().toString().equals("B")) + .build(); + + var pathA = Path.of("A"); + var pathB = Path.of("B"); + + assertCompare(comparator, pathA, pathB); + } + + } + + @Nested + class AddLast { + + @Test + void when_compare_equal_then_alphabetical() { + var comparator = PathSorts.builder() + .addFirst(p -> p.getFileName().toString().equals("A")) + .addFirst(p -> p.getFileName().toString().equals("B")) + .build(); + + var pathA = Path.of("A"); + var pathB = Path.of("B"); + + assertCompare(comparator, pathA, pathB); + } + + } + +} diff --git a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/SortingTest.java b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/SortingTest.java index c996f85..90f8c31 100644 --- a/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/SortingTest.java +++ b/src/test/java/io/github/computerdaddyguy/jfiletreeprettyprinter/SortingTest.java @@ -16,7 +16,7 @@ class SortingTest { void example() { FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.sort(PrettyPrintOptions.Sorts.DIRECTORY_FIRST)) + .customizeOptions(options -> options.sort(PathSorts.DIRECTORY_FIRST)) .build(); var result = printer.prettyPrint("src/example/resources/sorting"); @@ -80,7 +80,7 @@ void defaultOrderIsAlphabetical() { void alphabetical_reversed() { FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.sort(PrettyPrintOptions.Sorts.BY_NAME.reversed())) + .customizeOptions(options -> options.sort(PathSorts.ALPHABETICAL.reversed())) .build(); var result = printer.prettyPrint(buildDefaultPath()); @@ -107,7 +107,7 @@ void alphabetical_reversed() { void fileSize() { FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.sort(PrettyPrintOptions.Sorts.BY_FILE_SIZE)) + .customizeOptions(options -> options.sort(PathSorts.BY_FILE_SIZE)) .build(); var result = printer.prettyPrint("src/example/resources/sorting"); @@ -151,7 +151,7 @@ private Path build_directoryFirstOrLast_paths() { void directoriesFirst() { FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.sort(PrettyPrintOptions.Sorts.DIRECTORY_FIRST)) + .customizeOptions(options -> options.sort(PathSorts.DIRECTORY_FIRST)) .build(); var result = printer.prettyPrint(build_directoryFirstOrLast_paths()); @@ -174,7 +174,7 @@ void directoriesFirst() { void directoriesLast() { FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.sort(PrettyPrintOptions.Sorts.DIRECTORY_LAST)) + .customizeOptions(options -> options.sort(PathSorts.DIRECTORY_LAST)) .build(); var result = printer.prettyPrint(build_directoryFirstOrLast_paths()); @@ -217,7 +217,7 @@ private Path build_extension_paths() { void byExtension() { FileTreePrettyPrinter printer = FileTreePrettyPrinter.builder() - .customizeOptions(options -> options.sort(PrettyPrintOptions.Sorts.BY_EXTENSION)) + .customizeOptions(options -> options.sort(PathSorts.BY_EXTENSION)) .build(); var result = printer.prettyPrint(build_extension_paths());