Skip to content

Commit 48db655

Browse files
Feature/rework path matchers (#8)
1 parent 60fe8a8 commit 48db655

File tree

21 files changed

+1494
-1482
lines changed

21 files changed

+1494
-1482
lines changed

CHANGELOG.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
## [0.0.5] - Unreleased
1010

1111
### Added
12-
- New path predicates: `hasParentMatching`, `hasAncestorMatching`, `hasDirectChildMatching`, `hasDescendantMatching`, `hasSiblingMatching`, `hasFullPathMatchingGlob`, `hasFullPathMatching`, `hasNameMatchingGlob`, `hasNameStartingWith`
12+
- New path matchers:
13+
`hasParentMatching`, `hasAncestorMatching`, `hasDirectChildMatching`, `hasDescendantMatching`, `hasSiblingMatching`,
14+
`hasAbsolutePathMatchingGlob`, `hasAbsolutePathMatching`,
15+
`hasRelativePathMatchingGlob`, `hasRelativePathMatching`,
16+
`hasNameMatchingGlob`, `hasNameStartingWith`
1317

1418
### Changed
15-
- `PathUtils` removed, `PathPredicates` rework
16-
- Line extension: empty string is permitted
17-
- Filtering: split into distinct directories and files filters
19+
- Filtering: now using `PathMatcher` instead of `Predicate<Path>`
20+
- Filtering: split into distinct directories and files filters for better control
21+
- `PathUtils` and `PathPredicates` removed, use `PathMatchers` instead
22+
- Line extension: empty string is permitted to force line break in compact paths
1823

1924
### Fixed
2025
- The folder name is properly displayed at root when calling `prettyPrint(".")` (instead of "./")

README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,14 @@ child_limit_static/
178178
Or you can also set a limitation function, to dynamically choose the number of children displayed in each directory.
179179
It avoids cluttering the whole console with known large folders (e.g. `node_modules`) but continue to pretty print normally other folders.
180180

181-
Use the `ChildLimitBuilder` and `PathPredicates` classes to help you build the limit function that fits your needs.
181+
Use the `ChildLimitBuilder` and `PathMatchers` classes to help you build the limit function that fits your needs.
182182

183183
```java
184184
// Example: ChildLimitDynamic.java
185-
var isNodeModulePredicate = PathPredicates.builder().hasName("node_modules").build();
185+
var isNodeModuleMatcher = PathMatchers.hasName("node_modules");
186186
var childLimit = ChildLimitBuilder.builder()
187187
.defaultLimit(ChildLimitBuilder.UNLIMITED)
188-
.limit(isNodeModulePredicate, 0)
188+
.limit(isNodeModuleMatcher, 0)
189189
.build();
190190
var prettyPrinter = FileTreePrettyPrinter.builder()
191191
.customizeOptions(options -> options.withChildLimit(childLimit))
@@ -274,23 +274,23 @@ sorting/
274274
```
275275

276276
## Filtering
277-
Files and directories can be selectively included or excluded using a custom `Predicate<Path>`.
277+
Files and directories can be selectively included or excluded using a custom `PathMatcher`.
278278

279279
Filtering is independant for files & directories. Files are filtered only if their parent directory pass the directory filter.
280280
If none of some directory's children match, the directory is still displayed.
281281

282-
The `PathPredicates` class provides several ready-to-use methods for creating common predicates, as well as a builder for creating more advanced predicates.
282+
The `PathMatchers` class provides several ready-to-use methods for creating and combining common matchers.
283283

284284
```java
285285
// Example: Filtering.java
286-
Predicate<Path> excludeDirWithNoJavaFiles = dir -> !PathPredicates.hasNameEndingWith(dir, "no_java_file");
287-
var isJavaFilePredicate = PathPredicates.builder().hasExtension("java").build();
286+
var excludeDirWithNoJavaFiles = PathMatchers.not(PathMatchers.hasNameEndingWith("no_java_file"));
287+
var hasJavaExtension = PathMatchers.hasExtension("java");
288288

289289
var prettyPrinter = FileTreePrettyPrinter.builder()
290290
.customizeOptions(
291291
options -> options
292292
.filterDirectories(excludeDirWithNoJavaFiles)
293-
.filterFiles(hasJavaExtensionPredicate)
293+
.filterFiles(hasJavaExtension)
294294
)
295295
.build();
296296
```
@@ -315,17 +315,19 @@ If the function returns `null`, nothing is added.
315315

316316
```java
317317
// Example: LineExtension.java
318+
var printedPath = Path.of("src/example/resources/line_extension");
319+
318320
Function<Path, String> lineExtension = path -> {
319-
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/api")) {
321+
if (PathMatchers.hasRelativePathMatchingGlob("src/main/java/api", printedPath).matches(path)) {
320322
return "\t\t\t// All API code: controllers, etc.";
321323
}
322-
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/domain")) {
324+
if (PathMatchers.hasRelativePathMatchingGlob("src/main/java/domain", printedPath).matches(path)) {
323325
return "\t\t\t// All domain code: value objects, etc.";
324326
}
325-
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/infra")) {
327+
if (PathMatchers.hasRelativePathMatchingGlob("src/main/java/infra", printedPath).matches(path)) {
326328
return "\t\t\t// All infra code: database, email service, etc.";
327329
}
328-
if (PathPredicates.hasNameMatchingGlob(path, "*.properties")) {
330+
if (PathMatchers.hasNameMatchingGlob("*.properties").matches(path)) {
329331
return "\t// Config file";
330332
}
331333
return null;

ROADMAP.md

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
11
# Roadmap
22

33
## Done
4-
- [x] Option: tree format Unicode box drawing / classic ASCII
5-
- [x] Option: use emojis
6-
- [x] Option: children limit (static & dynamic)
7-
- [x] Option: compact directories display
8-
- [x] Option: max directory depth
9-
- [x] Add examples & README
10-
- [x] Use JSpecify annotations
11-
- [x] Unit tests, using @TempDir
12-
- [x] Mutation testing
13-
- [x] Pre-defined Path predicates
14-
- [x] Publish on Maven Central!
15-
- [x] Child limitation function helper
16-
- [x] More default emojis
17-
- [x] Option: Filtering
18-
- [x] Option: Ordering
19-
- [x] Use Github wiki to document options instead of readme
20-
- [x] Jacoco coverage report
21-
- [x] Option: Line extension (=additional text after the file name)
4+
- [x] **Features**
5+
- [x] Option: filtering
6+
- [x] Option: ordering
7+
- [x] Option: emojis
8+
- [x] Option: compact directories display
9+
- [x] Option: line extension (=additional text after the file name)
10+
- [x] Option: children limit (static & dynamic)
11+
- [x] Option: tree format Unicode box drawing / classic ASCII
12+
- [x] Option: max directory depth
13+
- [x] **Documentation**
14+
- [x] Add examples & README
15+
- [x] Use Github wiki to document options instead of readme
16+
- [x] **Code style**
17+
- [x] Use JSpecify annotations
18+
- [x] **Testing**
19+
- [x] Unit tests, using @TempDir
20+
- [x] Jacoco coverage report
21+
- [x] Mutation testing
22+
- [x] SonarCloud integration
23+
- [x] **Workflows**
24+
- [x] Github actions
25+
- [x] Publish on Maven Central!
2226

2327
## To do
24-
- [x] More `PathPredicates` functions!
28+
- [x] More `PathMatchers` functions!
29+
- [ ] Helper class for line extension
2530
- [ ] Option: custom emojis
31+
- [ ] Rework/fix Github wiki to be up to date
2632

2733
## Backlog / To analyze / To implement if requested
2834
- [ ] Option: custom tree format

src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/ChildLimitDynamic.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
import io.github.computerdaddyguy.jfiletreeprettyprinter.ChildLimitBuilder;
44
import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
5-
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
5+
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers;
66

77
public class ChildLimitDynamic {
88

99
public static void main(String[] args) {
10-
var isNodeModulePredicate = PathPredicates.builder().hasName("node_modules").build();
10+
var isNodeModuleMatcher = PathMatchers.hasName("node_modules");
1111
var childLimit = ChildLimitBuilder.builder()
1212
.defaultLimit(ChildLimitBuilder.UNLIMITED)
13-
.limit(isNodeModulePredicate, 0)
13+
.limit(isNodeModuleMatcher, 0)
1414
.build();
1515
var prettyPrinter = FileTreePrettyPrinter.builder()
1616
.customizeOptions(options -> options.withChildLimit(childLimit))

src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/CompleteExample.java

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import io.github.computerdaddyguy.jfiletreeprettyprinter.ChildLimitBuilder;
44
import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
5-
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
5+
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers;
66
import io.github.computerdaddyguy.jfiletreeprettyprinter.PrettyPrintOptions.Sorts;
77
import java.nio.file.Path;
88
import java.util.function.Function;
@@ -11,39 +11,40 @@ public class CompleteExample {
1111

1212
public static void main(String[] args) {
1313

14-
var filterDir = PathPredicates.builder()
15-
.pathTest(path -> !PathPredicates.hasName(path, ".git"))
16-
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.git"))
17-
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.github"))
18-
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.settings"))
19-
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/src/example"))
20-
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/src/test"))
21-
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/target"))
22-
.build();
14+
var jFileTreePrettyPrintFolder = Path.of(".");
2315

24-
var filterFiles = PathPredicates.builder()
25-
.pathTest(path -> !PathPredicates.hasNameStartingWith(path, "."))
26-
.pathTest(path -> {
27-
if (PathPredicates.hasParentMatching(path, parent -> PathPredicates.hasName(parent, "jfiletreeprettyprinter"))) {
28-
return PathPredicates.hasName(path, "FileTreePrettyPrinter.java");
29-
}
30-
return true;
31-
})
32-
.build();
16+
var filterDir = PathMatchers.noneOf(
17+
PathMatchers.hasName(".git"),
18+
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, ".git"),
19+
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, ".github"),
20+
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, ".settings"),
21+
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, "src/example"),
22+
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, "src/test"),
23+
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, "target")
24+
);
25+
26+
var filterFiles = PathMatchers.allOf(
27+
PathMatchers.not(PathMatchers.hasNameStartingWith(".")),
28+
PathMatchers.ifMatchesThenElse(
29+
PathMatchers.hasDirectParentMatching(PathMatchers.hasName("jfiletreeprettyprinter")), // if
30+
PathMatchers.hasName("FileTreePrettyPrinter.java"), // then
31+
p -> true // else
32+
)
33+
);
3334

3435
var childLimitFunction = ChildLimitBuilder.builder()
35-
.limit(path -> PathPredicates.hasFullPathMatchingGlob(path, "**/io/github/computerdaddyguy/jfiletreeprettyprinter/renderer"), 0)
36-
.limit(path -> PathPredicates.hasFullPathMatchingGlob(path, "**/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner"), 0)
36+
.limit(PathMatchers.hasAbsolutePathMatchingGlob("**/io/github/computerdaddyguy/jfiletreeprettyprinter/renderer"), 0)
37+
.limit(PathMatchers.hasAbsolutePathMatchingGlob("**/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner"), 0)
3738
.build();
3839

3940
Function<Path, String> lineExtension = path -> {
40-
if (PathPredicates.hasName(path, "JfileTreePrettyPrinter-structure.png")) {
41+
if (PathMatchers.hasName("JfileTreePrettyPrinter-structure.png").matches(path)) {
4142
return "\t// This image";
42-
} else if (PathPredicates.hasName(path, "FileTreePrettyPrinter.java")) {
43+
} else if (PathMatchers.hasName("FileTreePrettyPrinter.java").matches(path)) {
4344
return "\t// Main entry point";
44-
} else if (PathPredicates.hasName(path, "README.md")) {
45+
} else if (PathMatchers.hasName("README.md").matches(path)) {
4546
return "\t\t// You're reading at this!";
46-
} else if (PathPredicates.hasName(path, "java")) {
47+
} else if (PathMatchers.hasName("java").matches(path)) {
4748
return "";
4849
}
4950
return null;
@@ -61,7 +62,8 @@ public static void main(String[] args) {
6162
.sort(Sorts.DIRECTORY_FIRST)
6263
)
6364
.build();
64-
var tree = prettyPrinter.prettyPrint(".");
65+
66+
var tree = prettyPrinter.prettyPrint(jFileTreePrettyPrintFolder);
6567
System.out.println(tree);
6668
}
6769

src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/Filtering.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
package io.github.computerdaddyguy.jfiletreeprettyprinter.example;
22

33
import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
4-
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
5-
import java.nio.file.Path;
6-
import java.util.function.Predicate;
4+
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers;
75

86
public class Filtering {
97

108
public static void main(String[] args) {
11-
Predicate<Path> excludeDirWithNoJavaFiles = dir -> !PathPredicates.hasNameEndingWith(dir, "no_java_file");
12-
var hasJavaExtensionPredicate = PathPredicates.builder().hasExtension("java").build();
9+
var excludeDirWithNoJavaFiles = PathMatchers.not(PathMatchers.hasNameEndingWith("no_java_file"));
10+
var hasJavaExtension = PathMatchers.hasExtension("java");
1311

1412
var prettyPrinter = FileTreePrettyPrinter.builder()
1513
.customizeOptions(
1614
options -> options
1715
.filterDirectories(excludeDirWithNoJavaFiles)
18-
.filterFiles(hasJavaExtensionPredicate)
16+
.filterFiles(hasJavaExtension)
1917
)
2018
.build();
2119

src/example/java/io/github/computerdaddyguy/jfiletreeprettyprinter/example/LineExtension.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
11
package io.github.computerdaddyguy.jfiletreeprettyprinter.example;
22

33
import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
4-
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
4+
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers;
55
import java.nio.file.Path;
66
import java.util.function.Function;
77

88
public class LineExtension {
99

1010
public static void main(String[] args) {
11+
var printedPath = Path.of("src/example/resources/line_extension");
12+
1113
Function<Path, String> lineExtension = path -> {
12-
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/api")) {
14+
if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/api").matches(path)) {
1315
return "\t\t\t// All API code: controllers, etc.";
1416
}
15-
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/domain")) {
17+
if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/domain").matches(path)) {
1618
return "\t\t\t// All domain code: value objects, etc.";
1719
}
18-
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/infra")) {
20+
if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/infra").matches(path)) {
1921
return "\t\t\t// All infra code: database, email service, etc.";
2022
}
21-
if (PathPredicates.hasNameMatchingGlob(path, "*.properties")) {
23+
if (PathMatchers.hasNameMatchingGlob("*.properties").matches(path)) {
2224
return "\t// Config file";
2325
}
2426
return null;
2527
};
2628
var prettyPrinter = FileTreePrettyPrinter.builder()
2729
.customizeOptions(options -> options.withLineExtension(lineExtension))
2830
.build();
29-
var tree = prettyPrinter.prettyPrint("src/example/resources/line_extension");
31+
var tree = prettyPrinter.prettyPrint(printedPath);
3032
System.out.println(tree);
3133
}
3234

src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/ChildLimitBuilder.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package io.github.computerdaddyguy.jfiletreeprettyprinter;
22

33
import java.nio.file.Path;
4+
import java.nio.file.PathMatcher;
45
import java.util.ArrayList;
56
import java.util.List;
67
import java.util.Objects;
7-
import java.util.function.Predicate;
88
import java.util.function.ToIntFunction;
99
import org.jspecify.annotations.NullMarked;
1010

@@ -26,8 +26,8 @@
2626
* <pre>{@code
2727
* var childLimit = ChildLimitBuilder.builder()
2828
* .defaultLimit(ChildLimit.UNLIMITED) // unlimited unless specified
29-
* .limit(path -> PathPredicates.hasName(path, "bigDir"), 10) // max 10 children in "bigDir"
30-
* .limit(path -> PathPredicates.hasName(path, "emptyDir"), 0) // disallow children in "emptyDir"
29+
* .limit(PathMatchers.hasName("bigDir"), 10) // max 10 children in "bigDir"
30+
* .limit(PathMatchers.hasName("emptyDir"), 0) // disallow children in "emptyDir"
3131
* .build();
3232
*
3333
* }</pre>
@@ -53,7 +53,7 @@ private ChildLimitBuilder() {
5353
this.defaultControl = UNLIMITED_CONTROL;
5454
}
5555

56-
private record ChildControl(Predicate<Path> pathPredicate, int limit) {
56+
private record ChildControl(PathMatcher pathMatcher, int limit) {
5757

5858
}
5959

@@ -74,7 +74,7 @@ public ToIntFunction<Path> build() {
7474
var immutControls = List.copyOf(controls);
7575
var immutDefaultControl = this.defaultControl;
7676
return p -> immutControls.stream()
77-
.filter(control -> control.pathPredicate().test(p))
77+
.filter(control -> control.pathMatcher().matches(p))
7878
.findFirst()
7979
.orElse(immutDefaultControl)
8080
.limit();
@@ -93,21 +93,21 @@ public ChildLimitBuilder defaultLimit(int limit) {
9393
}
9494

9595
/**
96-
* Adds a child limit rule for paths matching the given predicate.
96+
* Adds a child limit rule for paths matching the given matcher.
9797
* <p>
9898
* Rules are evaluated in the order they are added. The first matching rule wins.
9999
* </p>
100100
*
101-
* @param pathPredicate the condition for paths
101+
* @param pathMatcher the condition for paths
102102
* @param limit the maximum number of children (use {@link #UNLIMITED} for no restriction)
103103
*
104104
* @return this builder for chaining
105105
*
106-
* @throws NullPointerException if {@code pathPredicate} is null
106+
* @throws NullPointerException if {@code pathMatcher} is null
107107
*/
108-
public ChildLimitBuilder limit(Predicate<Path> pathPredicate, int limit) {
109-
Objects.requireNonNull(pathPredicate, "pathPredicate is null");
110-
this.controls.add(new ChildControl(pathPredicate, limit));
108+
public ChildLimitBuilder limit(PathMatcher pathMatcher, int limit) {
109+
Objects.requireNonNull(pathMatcher, "pathMatcher is null");
110+
this.controls.add(new ChildControl(pathMatcher, limit));
111111
return this;
112112
}
113113

src/main/java/io/github/computerdaddyguy/jfiletreeprettyprinter/DefaultFileTreePrettyPrinter.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ public DefaultFileTreePrettyPrinter(PathToTreeScanner scanner, TreeEntryRenderer
2020
@Override
2121
public String prettyPrint(Path path) {
2222
Objects.requireNonNull(path, "path cannot be null");
23-
var cleanedPath = path.normalize().toAbsolutePath(); // required to avoid "./" at root when calling prettyPrint(".")
24-
var tree = scanner.scan(cleanedPath);
23+
var tree = scanner.scan(path);
2524
return renderer.renderTree(tree);
2625
}
2726

0 commit comments

Comments
 (0)