Skip to content

Commit b9f8324

Browse files
authored
feat: add ignore property matcher (#32)
1 parent 161e98b commit b9f8324

20 files changed

+790
-113
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ final var mixedMatcher = new CompositeJsonMatcher(
165165
| Matcher | Description |
166166
|---------|-------------|
167167
| `NullEqualsEmptyArrayMatcher` | Treats `null` and `[]` as equivalent |
168+
| `IgnoredPathMatcher` | Ignores specified fields during comparison |
168169

169170
## Treating Null as Empty Array
170171

@@ -196,6 +197,64 @@ System.out.println(diff.similarityRate()); // 100.0
196197
- This matcher only handles `null` vs empty array `[]`, not missing properties
197198
- Non-empty arrays do not match `null`
198199

200+
## Ignoring path
201+
202+
The `IgnoredPathMatcher` allows you to ignore specific fields during comparison. This is useful for fields like timestamps, IDs, or other dynamic values that you don't want to compare.
203+
204+
```java
205+
final var jsonMatcher = new CompositeJsonMatcher(
206+
new IgnoredPathMatcher("timestamp", "id"), // Must be first
207+
new LenientJsonArrayPartialMatcher(),
208+
new LenientJsonObjectPartialMatcher(),
209+
new StrictPrimitivePartialMatcher()
210+
);
211+
212+
// These will match with 100% similarity:
213+
final var diff = DiffGenerator.diff(
214+
"{\"name\": \"John\", \"timestamp\": \"2024-01-01\"}",
215+
"{\"name\": \"John\", \"timestamp\": \"2024-12-31\"}",
216+
jsonMatcher
217+
);
218+
219+
System.out.println(diff.similarityRate()); // 100.0
220+
```
221+
222+
### Path Patterns
223+
224+
The `IgnoredPathMatcher` supports various path patterns:
225+
226+
| Pattern | Description | Example |
227+
|---------|-------------|---------|
228+
| `name` | Matches field `name` at any level | Ignores `$.name`, `$.user.name`, `$.data.user.name` |
229+
| `user.name` | Matches `name` under `user` | Ignores `$.user.name`, `$.data.user.name` |
230+
| `*.name` | Wildcard for any property | Ignores `$.foo.name`, `$.bar.name` |
231+
| `items[0]` | Matches specific array index | Ignores `$.items[0]` |
232+
| `items[*]` | Wildcard for any array index | Ignores `$.items[0]`, `$.items[1]`, etc. |
233+
| `items[*].id` | Field in any array element | Ignores `$.items[0].id`, `$.items[5].id` |
234+
235+
### Examples
236+
237+
```java
238+
// Ignore a single field everywhere
239+
new IgnoredPathMatcher("createdAt")
240+
241+
// Ignore multiple fields
242+
new IgnoredPathMatcher("createdAt", "updatedAt", "id")
243+
244+
// Ignore nested field
245+
new IgnoredPathMatcher("metadata.timestamp")
246+
247+
// Ignore field in all array elements
248+
new IgnoredPathMatcher("users[*].password")
249+
250+
// Combine multiple patterns
251+
new IgnoredPathMatcher("id", "*.createdAt", "items[*].internalId")
252+
```
253+
254+
**Important:**
255+
- Place `IgnoredPathMatcher` **before** other matchers in the constructor
256+
- Patterns match against the end of the path, so `name` matches `$.user.name` as well as `$.name`
257+
199258
## Advanced Example
200259

201260
```java

src/main/java/com/deblock/jsondiff/matcher/CompositeJsonMatcher.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
import com.deblock.jsondiff.diff.*;
44
import tools.jackson.databind.JsonNode;
5-
import tools.jackson.databind.node.ArrayNode;
6-
import tools.jackson.databind.node.ObjectNode;
7-
import tools.jackson.databind.node.ValueNode;
85

96
import java.util.ArrayList;
107
import java.util.Arrays;
@@ -21,7 +18,7 @@ public CompositeJsonMatcher(PartialJsonMatcher<?> ...jsonArrayPartialMatcher) {
2118
@Override
2219
public JsonDiff diff(Path path, JsonNode expected, JsonNode received) {
2320
return this.matchers.stream()
24-
.filter(matcher -> matcher.manage(expected, received))
21+
.filter(matcher -> matcher.manage(path, received, expected))
2522
.findFirst()
2623
.map(matcher -> matcher.jsonDiff(path, expected, received, this))
2724
.orElseGet(() -> new UnMatchedPrimaryDiff(path, expected, received));
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.deblock.jsondiff.matcher;
2+
3+
import com.deblock.jsondiff.diff.JsonDiff;
4+
import com.deblock.jsondiff.diff.MatchedPrimaryDiff;
5+
import tools.jackson.databind.JsonNode;
6+
7+
import java.util.Arrays;
8+
import java.util.List;
9+
10+
public class IgnoredPathMatcher implements PartialJsonMatcher {
11+
private final List<PathMatcher> pathsToIgnore;
12+
13+
public IgnoredPathMatcher(List<String> paths) {
14+
this.pathsToIgnore = paths.stream()
15+
.map(PathMatcher::from)
16+
.toList();
17+
}
18+
19+
public IgnoredPathMatcher(String ...paths) {
20+
this(Arrays.stream(paths).toList());
21+
}
22+
23+
@Override
24+
public JsonDiff jsonDiff(Path path, JsonNode expectedJson, JsonNode receivedJson, JsonMatcher jsonMatcher) {
25+
return new MatchedPrimaryDiff(path, expectedJson);
26+
}
27+
28+
@Override
29+
public boolean manage(Path path, JsonNode expected, JsonNode received) {
30+
return pathsToIgnore.stream().anyMatch(pattern -> pattern.match(path));
31+
}
32+
}

src/main/java/com/deblock/jsondiff/matcher/LenientJsonArrayPartialMatcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public JsonDiff jsonDiff(Path path, ArrayNode expectedArrayNode, ArrayNode recie
5858
}
5959

6060
@Override
61-
public boolean manage(JsonNode expected, JsonNode received) {
61+
public boolean manage(Path path, JsonNode received, JsonNode expected) {
6262
return expected.isArray() && received.isArray();
6363
}
6464

src/main/java/com/deblock/jsondiff/matcher/LenientJsonObjectPartialMatcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public JsonDiff jsonDiff(Path path, ObjectNode expectedJson, ObjectNode received
2929
}
3030

3131
@Override
32-
public boolean manage(JsonNode expected, JsonNode received) {
32+
public boolean manage(Path path, JsonNode received, JsonNode expected) {
3333
return expected.isObject() && received.isObject();
3434
}
3535
}

src/main/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public JsonDiff jsonDiff(Path path, ValueNode expectedValue, ValueNode receivedV
3535
}
3636

3737
@Override
38-
public boolean manage(JsonNode expected, JsonNode received) {
38+
public boolean manage(Path path, JsonNode received, JsonNode expected) {
3939
return expected.isNumber() && received.isNumber();
4040
}
4141
}

src/main/java/com/deblock/jsondiff/matcher/NullEqualsEmptyArrayMatcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public JsonDiff jsonDiff(Path path, JsonNode expectedJson, JsonNode receivedJson
1919
}
2020

2121
@Override
22-
public boolean manage(JsonNode expected, JsonNode received) {
22+
public boolean manage(Path path, JsonNode received, JsonNode expected) {
2323
return (expected.isNull() && received.isArray())
2424
|| (received.isNull() && expected.isArray());
2525
}

src/main/java/com/deblock/jsondiff/matcher/PartialJsonMatcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
public interface PartialJsonMatcher<T extends JsonNode> {
77
JsonDiff jsonDiff(Path path, T expectedJson, T receivedJson, JsonMatcher jsonMatcher);
88

9-
boolean manage(JsonNode expected, JsonNode received);
9+
boolean manage(Path path, JsonNode received, JsonNode expected);
1010

1111
}

src/main/java/com/deblock/jsondiff/matcher/Path.java

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,88 @@
22

33
import java.util.Objects;
44

5+
/**
6+
* Represents a JSON path (e.g., $.property.0.subproperty).
7+
* Stored in reverse order (last element at head) for O(1) add operations
8+
* and efficient end-matching in PathMatcher.
9+
*/
510
public class Path {
611
public static final Path ROOT = new Path();
712

8-
public final PathItem property;
9-
public final Path next;
13+
private final PathItem last;
14+
private final Path previous;
1015

1116
public Path() {
1217
this(null, null);
1318
}
1419

15-
private Path(PathItem property, Path next) {
16-
this.property = property;
17-
this.next = next;
20+
private Path(PathItem last, Path previous) {
21+
this.last = last;
22+
this.previous = previous;
1823
}
1924

20-
private Path(PathItem property) {
21-
this.property = property;
22-
this.next = null;
25+
public Path add(PathItem item) {
26+
if (this.last == null) {
27+
return new Path(item, null);
28+
}
29+
return new Path(item, this);
2330
}
2431

25-
public Path add(PathItem item) {
26-
if (this.next == null) {
27-
return new Path(this.property, new Path(item));
28-
} else {
29-
return new Path(this.property, this.next.add(item));
32+
public PathItem item() {
33+
return last;
34+
}
35+
36+
/**
37+
* Returns the path without its last element.
38+
*/
39+
public Path previous() {
40+
return previous == null ? ROOT : previous;
41+
}
42+
43+
/**
44+
* Returns the path items in natural order (from root to leaf).
45+
* This is useful for traversing the path from start to end.
46+
*/
47+
public java.util.List<PathItem> toList() {
48+
java.util.List<PathItem> result = new java.util.ArrayList<>();
49+
collectItems(result);
50+
return result;
51+
}
52+
53+
private void collectItems(java.util.List<PathItem> result) {
54+
if (last == null) return;
55+
if (previous != null) {
56+
previous.collectItems(result);
3057
}
58+
result.add(last);
3159
}
3260

61+
@Override
3362
public String toString() {
34-
return ((this.property == null) ? "$" : this.property) +
35-
((this.next == null) ? "" : "." + this.next);
63+
StringBuilder sb = new StringBuilder("$");
64+
appendReversed(sb);
65+
return sb.toString();
66+
}
67+
68+
private void appendReversed(StringBuilder sb) {
69+
if (last == null) return;
70+
if (previous != null) {
71+
previous.appendReversed(sb);
72+
}
73+
sb.append(".").append(last);
3674
}
3775

3876
@Override
3977
public boolean equals(Object o) {
4078
if (this == o) return true;
4179
if (o == null || getClass() != o.getClass()) return false;
4280
Path path = (Path) o;
43-
return Objects.equals(property, path.property) && Objects.equals(next, path.next);
81+
return Objects.equals(last, path.last) && Objects.equals(previous, path.previous);
4482
}
4583

4684
@Override
4785
public int hashCode() {
48-
return Objects.hash(property, next);
86+
return Objects.hash(last, previous);
4987
}
5088

5189
public interface PathItem {

0 commit comments

Comments
 (0)