diff --git a/README.md b/README.md index e0c718b..0f93dfd 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ final var mixedMatcher = new CompositeJsonMatcher( | Matcher | Description | |---------|-------------| | `NullEqualsEmptyArrayMatcher` | Treats `null` and `[]` as equivalent | +| `IgnoredPathMatcher` | Ignores specified fields during comparison | ## Treating Null as Empty Array @@ -196,6 +197,64 @@ System.out.println(diff.similarityRate()); // 100.0 - This matcher only handles `null` vs empty array `[]`, not missing properties - Non-empty arrays do not match `null` +## Ignoring path + +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. + +```java +final var jsonMatcher = new CompositeJsonMatcher( + new IgnoredPathMatcher("timestamp", "id"), // Must be first + new LenientJsonArrayPartialMatcher(), + new LenientJsonObjectPartialMatcher(), + new StrictPrimitivePartialMatcher() +); + +// These will match with 100% similarity: +final var diff = DiffGenerator.diff( + "{\"name\": \"John\", \"timestamp\": \"2024-01-01\"}", + "{\"name\": \"John\", \"timestamp\": \"2024-12-31\"}", + jsonMatcher +); + +System.out.println(diff.similarityRate()); // 100.0 +``` + +### Path Patterns + +The `IgnoredPathMatcher` supports various path patterns: + +| Pattern | Description | Example | +|---------|-------------|---------| +| `name` | Matches field `name` at any level | Ignores `$.name`, `$.user.name`, `$.data.user.name` | +| `user.name` | Matches `name` under `user` | Ignores `$.user.name`, `$.data.user.name` | +| `*.name` | Wildcard for any property | Ignores `$.foo.name`, `$.bar.name` | +| `items[0]` | Matches specific array index | Ignores `$.items[0]` | +| `items[*]` | Wildcard for any array index | Ignores `$.items[0]`, `$.items[1]`, etc. | +| `items[*].id` | Field in any array element | Ignores `$.items[0].id`, `$.items[5].id` | + +### Examples + +```java +// Ignore a single field everywhere +new IgnoredPathMatcher("createdAt") + +// Ignore multiple fields +new IgnoredPathMatcher("createdAt", "updatedAt", "id") + +// Ignore nested field +new IgnoredPathMatcher("metadata.timestamp") + +// Ignore field in all array elements +new IgnoredPathMatcher("users[*].password") + +// Combine multiple patterns +new IgnoredPathMatcher("id", "*.createdAt", "items[*].internalId") +``` + +**Important:** +- Place `IgnoredPathMatcher` **before** other matchers in the constructor +- Patterns match against the end of the path, so `name` matches `$.user.name` as well as `$.name` + ## Advanced Example ```java diff --git a/src/main/java/com/deblock/jsondiff/matcher/CompositeJsonMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/CompositeJsonMatcher.java index e8d83ff..9ffd393 100644 --- a/src/main/java/com/deblock/jsondiff/matcher/CompositeJsonMatcher.java +++ b/src/main/java/com/deblock/jsondiff/matcher/CompositeJsonMatcher.java @@ -2,9 +2,6 @@ import com.deblock.jsondiff.diff.*; import tools.jackson.databind.JsonNode; -import tools.jackson.databind.node.ArrayNode; -import tools.jackson.databind.node.ObjectNode; -import tools.jackson.databind.node.ValueNode; import java.util.ArrayList; import java.util.Arrays; @@ -21,7 +18,7 @@ public CompositeJsonMatcher(PartialJsonMatcher ...jsonArrayPartialMatcher) { @Override public JsonDiff diff(Path path, JsonNode expected, JsonNode received) { return this.matchers.stream() - .filter(matcher -> matcher.manage(expected, received)) + .filter(matcher -> matcher.manage(path, received, expected)) .findFirst() .map(matcher -> matcher.jsonDiff(path, expected, received, this)) .orElseGet(() -> new UnMatchedPrimaryDiff(path, expected, received)); diff --git a/src/main/java/com/deblock/jsondiff/matcher/IgnoredPathMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/IgnoredPathMatcher.java new file mode 100644 index 0000000..48e72ef --- /dev/null +++ b/src/main/java/com/deblock/jsondiff/matcher/IgnoredPathMatcher.java @@ -0,0 +1,32 @@ +package com.deblock.jsondiff.matcher; + +import com.deblock.jsondiff.diff.JsonDiff; +import com.deblock.jsondiff.diff.MatchedPrimaryDiff; +import tools.jackson.databind.JsonNode; + +import java.util.Arrays; +import java.util.List; + +public class IgnoredPathMatcher implements PartialJsonMatcher { + private final List pathsToIgnore; + + public IgnoredPathMatcher(List paths) { + this.pathsToIgnore = paths.stream() + .map(PathMatcher::from) + .toList(); + } + + public IgnoredPathMatcher(String ...paths) { + this(Arrays.stream(paths).toList()); + } + + @Override + public JsonDiff jsonDiff(Path path, JsonNode expectedJson, JsonNode receivedJson, JsonMatcher jsonMatcher) { + return new MatchedPrimaryDiff(path, expectedJson); + } + + @Override + public boolean manage(Path path, JsonNode expected, JsonNode received) { + return pathsToIgnore.stream().anyMatch(pattern -> pattern.match(path)); + } +} diff --git a/src/main/java/com/deblock/jsondiff/matcher/LenientJsonArrayPartialMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/LenientJsonArrayPartialMatcher.java index d3f5dab..88c828b 100644 --- a/src/main/java/com/deblock/jsondiff/matcher/LenientJsonArrayPartialMatcher.java +++ b/src/main/java/com/deblock/jsondiff/matcher/LenientJsonArrayPartialMatcher.java @@ -58,7 +58,7 @@ public JsonDiff jsonDiff(Path path, ArrayNode expectedArrayNode, ArrayNode recie } @Override - public boolean manage(JsonNode expected, JsonNode received) { + public boolean manage(Path path, JsonNode received, JsonNode expected) { return expected.isArray() && received.isArray(); } diff --git a/src/main/java/com/deblock/jsondiff/matcher/LenientJsonObjectPartialMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/LenientJsonObjectPartialMatcher.java index b0b0d0f..86a92d8 100644 --- a/src/main/java/com/deblock/jsondiff/matcher/LenientJsonObjectPartialMatcher.java +++ b/src/main/java/com/deblock/jsondiff/matcher/LenientJsonObjectPartialMatcher.java @@ -29,7 +29,7 @@ public JsonDiff jsonDiff(Path path, ObjectNode expectedJson, ObjectNode received } @Override - public boolean manage(JsonNode expected, JsonNode received) { + public boolean manage(Path path, JsonNode received, JsonNode expected) { return expected.isObject() && received.isObject(); } } \ No newline at end of file diff --git a/src/main/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcher.java index e471359..06c8298 100644 --- a/src/main/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcher.java +++ b/src/main/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcher.java @@ -35,7 +35,7 @@ public JsonDiff jsonDiff(Path path, ValueNode expectedValue, ValueNode receivedV } @Override - public boolean manage(JsonNode expected, JsonNode received) { + public boolean manage(Path path, JsonNode received, JsonNode expected) { return expected.isNumber() && received.isNumber(); } } diff --git a/src/main/java/com/deblock/jsondiff/matcher/NullEqualsEmptyArrayMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/NullEqualsEmptyArrayMatcher.java index 35716c0..de73b5c 100644 --- a/src/main/java/com/deblock/jsondiff/matcher/NullEqualsEmptyArrayMatcher.java +++ b/src/main/java/com/deblock/jsondiff/matcher/NullEqualsEmptyArrayMatcher.java @@ -19,7 +19,7 @@ public JsonDiff jsonDiff(Path path, JsonNode expectedJson, JsonNode receivedJson } @Override - public boolean manage(JsonNode expected, JsonNode received) { + public boolean manage(Path path, JsonNode received, JsonNode expected) { return (expected.isNull() && received.isArray()) || (received.isNull() && expected.isArray()); } diff --git a/src/main/java/com/deblock/jsondiff/matcher/PartialJsonMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/PartialJsonMatcher.java index 6d04e46..a9394ff 100644 --- a/src/main/java/com/deblock/jsondiff/matcher/PartialJsonMatcher.java +++ b/src/main/java/com/deblock/jsondiff/matcher/PartialJsonMatcher.java @@ -6,6 +6,6 @@ public interface PartialJsonMatcher { JsonDiff jsonDiff(Path path, T expectedJson, T receivedJson, JsonMatcher jsonMatcher); - boolean manage(JsonNode expected, JsonNode received); + boolean manage(Path path, JsonNode received, JsonNode expected); } diff --git a/src/main/java/com/deblock/jsondiff/matcher/Path.java b/src/main/java/com/deblock/jsondiff/matcher/Path.java index e90add4..a8f1fe7 100644 --- a/src/main/java/com/deblock/jsondiff/matcher/Path.java +++ b/src/main/java/com/deblock/jsondiff/matcher/Path.java @@ -2,37 +2,75 @@ import java.util.Objects; +/** + * Represents a JSON path (e.g., $.property.0.subproperty). + * Stored in reverse order (last element at head) for O(1) add operations + * and efficient end-matching in PathMatcher. + */ public class Path { public static final Path ROOT = new Path(); - public final PathItem property; - public final Path next; + private final PathItem last; + private final Path previous; public Path() { this(null, null); } - private Path(PathItem property, Path next) { - this.property = property; - this.next = next; + private Path(PathItem last, Path previous) { + this.last = last; + this.previous = previous; } - private Path(PathItem property) { - this.property = property; - this.next = null; + public Path add(PathItem item) { + if (this.last == null) { + return new Path(item, null); + } + return new Path(item, this); } - public Path add(PathItem item) { - if (this.next == null) { - return new Path(this.property, new Path(item)); - } else { - return new Path(this.property, this.next.add(item)); + public PathItem item() { + return last; + } + + /** + * Returns the path without its last element. + */ + public Path previous() { + return previous == null ? ROOT : previous; + } + + /** + * Returns the path items in natural order (from root to leaf). + * This is useful for traversing the path from start to end. + */ + public java.util.List toList() { + java.util.List result = new java.util.ArrayList<>(); + collectItems(result); + return result; + } + + private void collectItems(java.util.List result) { + if (last == null) return; + if (previous != null) { + previous.collectItems(result); } + result.add(last); } + @Override public String toString() { - return ((this.property == null) ? "$" : this.property) + - ((this.next == null) ? "" : "." + this.next); + StringBuilder sb = new StringBuilder("$"); + appendReversed(sb); + return sb.toString(); + } + + private void appendReversed(StringBuilder sb) { + if (last == null) return; + if (previous != null) { + previous.appendReversed(sb); + } + sb.append(".").append(last); } @Override @@ -40,12 +78,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Path path = (Path) o; - return Objects.equals(property, path.property) && Objects.equals(next, path.next); + return Objects.equals(last, path.last) && Objects.equals(previous, path.previous); } @Override public int hashCode() { - return Objects.hash(property, next); + return Objects.hash(last, previous); } public interface PathItem { diff --git a/src/main/java/com/deblock/jsondiff/matcher/PathMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/PathMatcher.java new file mode 100644 index 0000000..5b61d33 --- /dev/null +++ b/src/main/java/com/deblock/jsondiff/matcher/PathMatcher.java @@ -0,0 +1,152 @@ +package com.deblock.jsondiff.matcher; + +public class PathMatcher { + public final PathMatcherItem last; + public final PathMatcher previous; + + public static PathMatcher from(String path) { + PathMatcher matcher = new PathMatcher(); + for (String part : path.split("\\.")) { + if (part.isEmpty()) { + throw new IllegalArgumentException("path matcher part can not be empty"); + } + if (part.endsWith("]")) { + String index = part.substring(part.lastIndexOf("[") + 1, part.length() - 1); + matcher = matcher.add(PathMatcherItem.ofProperty(part.substring(0, part.lastIndexOf("[")))); + matcher = matcher.add(PathMatcherItem.ofArrayIndex(index)); + } else { + matcher = matcher.add(PathMatcherItem.ofProperty(part)); + } + } + return matcher; + } + + private PathMatcher() { + this(null, null); + } + + private PathMatcher(PathMatcherItem last, PathMatcher previous) { + this.last = last; + this.previous = previous; + } + + private PathMatcher add(PathMatcherItem item) { + if (this.last == null) { + return new PathMatcher(item, null); + } + return new PathMatcher(item, this); + } + + public boolean match(Path path) { + if (this.last == null) { + return true; + } + + if (path == null || path.item() == null) { + return false; + } + + if (!this.last.match(path.item())) { + return false; + } + + if (this.previous == null) { + return true; + } + + return this.previous.match(path.previous()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("$"); + appendReversed(sb); + return sb.toString(); + } + + private void appendReversed(StringBuilder sb) { + if (last == null) return; + if (previous != null) { + previous.appendReversed(sb); + } + sb.append(".").append(last); + } + + public interface PathMatcherItem { + static PathMatcherItem ofProperty(String property) { + if ("*".equals(property)) { + return new WildcardMatcherItem(Path.PathItem.ObjectProperty.class); + } + return new ObjectProperty(property); + } + + static PathMatcherItem ofArrayIndex(String index) { + if ("*".equals(index)) { + return new WildcardMatcherItem(Path.PathItem.ArrayIndex.class); + } + return new ArrayIndex(Integer.parseInt(index)); + } + + boolean match(Path.PathItem pathItem); + + class WildcardMatcherItem implements PathMatcherItem { + private final Class type; + + public WildcardMatcherItem(Class type) { + this.type = type; + } + + @Override + public String toString() { + return "*"; + } + + @Override + public boolean match(Path.PathItem pathItem) { + return type.isAssignableFrom(pathItem.getClass()); + } + } + + class ArrayIndex implements PathMatcherItem { + public final int index; + + public ArrayIndex(int index) { + this.index = index; + } + + @Override + public String toString() { + return String.valueOf(index); + } + + @Override + public boolean match(Path.PathItem pathItem) { + if (pathItem instanceof Path.PathItem.ArrayIndex arrayIndex) { + return arrayIndex.index == this.index; + } + return false; + } + } + + class ObjectProperty implements PathMatcherItem { + public final String property; + + public ObjectProperty(String property) { + this.property = property; + } + + @Override + public String toString() { + return this.property; + } + + @Override + public boolean match(Path.PathItem pathItem) { + if (pathItem instanceof Path.PathItem.ObjectProperty objectProperty) { + return objectProperty.property.equals(this.property); + } + return false; + } + } + } +} diff --git a/src/main/java/com/deblock/jsondiff/matcher/StrictJsonArrayPartialMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/StrictJsonArrayPartialMatcher.java index 923786b..769fd81 100644 --- a/src/main/java/com/deblock/jsondiff/matcher/StrictJsonArrayPartialMatcher.java +++ b/src/main/java/com/deblock/jsondiff/matcher/StrictJsonArrayPartialMatcher.java @@ -5,13 +5,6 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - public class StrictJsonArrayPartialMatcher implements PartialJsonMatcher { @Override public JsonDiff jsonDiff(Path path, ArrayNode expectedValues, ArrayNode receivedValues, JsonMatcher jsonMatcher) { @@ -38,7 +31,7 @@ public JsonDiff jsonDiff(Path path, ArrayNode expectedValues, ArrayNode received } @Override - public boolean manage(JsonNode expected, JsonNode received) { + public boolean manage(Path path, JsonNode received, JsonNode expected) { return expected.isArray() && received.isArray(); } } diff --git a/src/main/java/com/deblock/jsondiff/matcher/StrictJsonObjectPartialMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/StrictJsonObjectPartialMatcher.java index 0103bc9..89f4341 100644 --- a/src/main/java/com/deblock/jsondiff/matcher/StrictJsonObjectPartialMatcher.java +++ b/src/main/java/com/deblock/jsondiff/matcher/StrictJsonObjectPartialMatcher.java @@ -40,7 +40,7 @@ public JsonDiff jsonDiff(Path path, ObjectNode expectedJson, ObjectNode received } @Override - public boolean manage(JsonNode expected, JsonNode received) { + public boolean manage(Path path, JsonNode received, JsonNode expected) { return expected.isObject() && received.isObject(); } } diff --git a/src/main/java/com/deblock/jsondiff/matcher/StrictPrimitivePartialMatcher.java b/src/main/java/com/deblock/jsondiff/matcher/StrictPrimitivePartialMatcher.java index c6e4601..b2b4609 100644 --- a/src/main/java/com/deblock/jsondiff/matcher/StrictPrimitivePartialMatcher.java +++ b/src/main/java/com/deblock/jsondiff/matcher/StrictPrimitivePartialMatcher.java @@ -20,7 +20,7 @@ public JsonDiff jsonDiff(Path path, ValueNode expectedValue, ValueNode receivedV } @Override - public boolean manage(JsonNode expected, JsonNode received) { + public boolean manage(Path path, JsonNode received, JsonNode expected) { return expected.isValueNode() && received.isValueNode(); } } diff --git a/src/main/java/com/deblock/jsondiff/viewer/PatchDiffViewer.java b/src/main/java/com/deblock/jsondiff/viewer/PatchDiffViewer.java index 7d62b8a..46fc67e 100644 --- a/src/main/java/com/deblock/jsondiff/viewer/PatchDiffViewer.java +++ b/src/main/java/com/deblock/jsondiff/viewer/PatchDiffViewer.java @@ -53,44 +53,54 @@ public void primaryNonMatching(Path path, JsonNode expected, JsonNode value) { public Object addPath(Object root, Path path, DiffValue diffValue) { if (path == null) { return diffValue; - } else if (path.property instanceof Path.PathItem.ArrayIndex) { - final var index = ((Path.PathItem.ArrayIndex) path.property).index; + } + + var items = path.toList(); + if (items.isEmpty()) { + return diffValue; + } + + return addPathItems(root, items, 0, diffValue); + } + + private Object addPathItems(Object root, java.util.List items, int index, DiffValue diffValue) { + if (index >= items.size()) { + return diffValue; + } + + var item = items.get(index); + boolean isLast = index == items.size() - 1; + + if (item instanceof Path.PathItem.ArrayIndex arrayItem) { + final var arrayIndex = arrayItem.index; if (root == null) { final var newRoot = new DiffValue.ArrayDiff(); - newRoot.set(index, this.addPath(null, path.next, diffValue)); + newRoot.set(arrayIndex, isLast ? diffValue : this.addPathItems(null, items, index + 1, diffValue)); return newRoot; - } else if (root instanceof DiffValue.ArrayDiff) { - final var array = (DiffValue.ArrayDiff) root; - if (array.hasIndex(index) && !(diffValue instanceof DiffValue.ExtraProperty)) { - this.addPath(array.get(index), path.next, diffValue); + } else if (root instanceof DiffValue.ArrayDiff arrayDiff) { + if (arrayDiff.hasIndex(arrayIndex) && !(diffValue instanceof DiffValue.ExtraProperty)) { + this.addPathItems(arrayDiff.get(arrayIndex), items, index + 1, diffValue); } else { - array.set(index, this.addPath(array.get(index), path.next, diffValue)); + arrayDiff.set(arrayIndex, isLast ? diffValue : this.addPathItems(arrayDiff.get(arrayIndex), items, index + 1, diffValue)); } - return array; + return arrayDiff; } else { - throw new IllegalArgumentException("The path " + path + " is not an array"); + throw new IllegalArgumentException("The path is not an array at index " + arrayIndex); } - } else if (path.property instanceof Path.PathItem.ObjectProperty) { - final var propertyName = ((Path.PathItem.ObjectProperty) path.property).property; + } else if (item instanceof Path.PathItem.ObjectProperty objectItem) { + final var propertyName = objectItem.property; if (root == null) { final var newRoot = new HashMap(); - newRoot.put(propertyName, this.addPath(null, path.next, diffValue)); + newRoot.put(propertyName, isLast ? diffValue : this.addPathItems(null, items, index + 1, diffValue)); return newRoot; - } else if (root instanceof Map) { - final var map = (Map) root; - map.put(propertyName, this.addPath(map.get(propertyName), path.next, diffValue)); + } else if (root instanceof Map map) { + map.put(propertyName, isLast ? diffValue : this.addPathItems(map.get(propertyName), items, index + 1, diffValue)); return map; } else { - throw new IllegalArgumentException("The path " + path + " is not an object"); - } - } else if (path.property == null) { - if (path.next != null) { - return this.addPath(root, path.next, diffValue); - } else { - return diff; + throw new IllegalArgumentException("The path is not an object at property " + propertyName); } } else { - throw new IllegalArgumentException("Unsupported path type " + path.property.getClass()); + throw new IllegalArgumentException("Unsupported path item type " + item.getClass()); } } @@ -99,9 +109,9 @@ public String toString() { } private String toDiff(Object diff, String indent, String startOfLine, String endOfLineExpected, String endOfLineActual) { - if (diff instanceof DiffValue.ArrayDiff) { + if (diff instanceof DiffValue.ArrayDiff arrayDiff) { final var arrayContent = new StringBuilder(); - final var allObjects = ((DiffValue.ArrayDiff) diff).allObjects(); + final var allObjects = arrayDiff.allObjects(); for (int i = 0; i < allObjects.size(); ++i) { final var object = allObjects.get(i); arrayContent @@ -115,10 +125,9 @@ private String toDiff(Object diff, String indent, String startOfLine, String end .append("\n"); } return startOfLine + " [\n" + arrayContent + indent + " ]" + endOfLineExpected; - } else if (diff instanceof Map) { + } else if (diff instanceof Map diffObject) { final var objectContent = new StringBuilder(); - final var diffObject = (Map) diff; - final var keys = new ArrayList<>(diffObject.keySet()); + final var keys = new ArrayList(diffObject.keySet()); for (int i = 0; i < keys.size(); ++i) { final var object = diffObject.get(keys.get(i)); final var isObjectNotADiff = object instanceof DiffValue.ArrayDiff || object instanceof Map; @@ -135,19 +144,18 @@ private String toDiff(Object diff, String indent, String startOfLine, String end .append("\n"); } return startOfLine + " {\n" + objectContent + indent + " }" + endOfLineExpected; - } else if (diff instanceof DiffValue.MatchingProperty) { + } else if (diff instanceof DiffValue.MatchingProperty matchingDiff) { if (endOfLineActual.equals(endOfLineExpected)) { - return " " + indent + ((DiffValue.MatchingProperty) diff).value.toString() + endOfLineActual; + return " " + indent + matchingDiff.value.toString() + endOfLineActual; } else { - return "-" + indent + ((DiffValue.MatchingProperty) diff).value.toString() + endOfLineActual + "\n" + - "+" + indent + ((DiffValue.MatchingProperty) diff).value.toString() + endOfLineExpected; + return "-" + indent + matchingDiff.value.toString() + endOfLineActual + "\n" + + "+" + indent + matchingDiff.value.toString() + endOfLineExpected; } - } else if (diff instanceof DiffValue.MissingProperty) { - return "+" + indent + ((DiffValue.MissingProperty) diff).value.toString() + endOfLineExpected; - } else if (diff instanceof DiffValue.ExtraProperty) { - return "-" + indent + ((DiffValue.ExtraProperty) diff).value.toString() + endOfLineActual; - } else if (diff instanceof DiffValue.NonMatchingProperty) { - final var value = ((DiffValue.NonMatchingProperty) diff); + } else if (diff instanceof DiffValue.MissingProperty missingPropertyDiff) { + return "+" + indent + missingPropertyDiff.value.toString() + endOfLineExpected; + } else if (diff instanceof DiffValue.ExtraProperty extraPropertyDiff) { + return "-" + indent + extraPropertyDiff.value.toString() + endOfLineActual; + } else if (diff instanceof DiffValue.NonMatchingProperty value) { return "-" + indent + value.value.toString() + endOfLineActual + "\n+" + indent + value.expected.toString() + endOfLineExpected; } else { throw new IllegalArgumentException("Unsupported diff type " + diff.getClass()); @@ -214,8 +222,8 @@ public static class ArrayDiff extends DiffValue { public final List extraProperty = new ArrayList<>(); public void set(int index, Object object) { - if (object instanceof DiffValue.ExtraProperty) { - extraProperty.add((DiffValue.ExtraProperty) object); + if (object instanceof DiffValue.ExtraProperty extraPropertyDiff) { + extraProperty.add(extraPropertyDiff); } else { while (diffs.size() <= index) { diffs.add(null); diff --git a/src/test/java/com/deblock/jsondiff/integration/IgnoredPathIntegrationTest.java b/src/test/java/com/deblock/jsondiff/integration/IgnoredPathIntegrationTest.java new file mode 100644 index 0000000..75375d3 --- /dev/null +++ b/src/test/java/com/deblock/jsondiff/integration/IgnoredPathIntegrationTest.java @@ -0,0 +1,28 @@ +package com.deblock.jsondiff.integration; + +import com.deblock.jsondiff.DiffGenerator; +import com.deblock.jsondiff.matcher.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class IgnoredPathIntegrationTest { + + private final CompositeJsonMatcher jsonMatcher = new CompositeJsonMatcher( + new IgnoredPathMatcher("foo"), + new LenientJsonArrayPartialMatcher(), + new LenientJsonObjectPartialMatcher(), + new StrictPrimitivePartialMatcher() + ); + + @Test + public void shouldIgnoreFooField() { + final var expected = "{\"foo\": \"bar\"}"; + final var received = "{\"foo\": \"foo\"}"; + + final var diff = DiffGenerator.diff(expected, received, jsonMatcher); + + assertEquals(100.0, diff.similarityRate()); + } + +} diff --git a/src/test/java/com/deblock/jsondiff/matcher/CompositeJsonMatcherTest.java b/src/test/java/com/deblock/jsondiff/matcher/CompositeJsonMatcherTest.java index bfadf03..88d79c1 100644 --- a/src/test/java/com/deblock/jsondiff/matcher/CompositeJsonMatcherTest.java +++ b/src/test/java/com/deblock/jsondiff/matcher/CompositeJsonMatcherTest.java @@ -21,12 +21,12 @@ public void shouldCallTheArrayMatcherIfTheTwoObjectAreArray() { final var objectMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); final var primitiveMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); - Mockito.when(arrayMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isArray() && ((JsonNode)inv.getArgument(1)).isArray()); - Mockito.when(objectMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isObject() && ((JsonNode)inv.getArgument(1)).isObject()); - Mockito.when(primitiveMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isValueNode() && ((JsonNode)inv.getArgument(1)).isValueNode()); + Mockito.when(arrayMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isArray() && ((JsonNode)inv.getArgument(2)).isArray()); + Mockito.when(objectMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isObject() && ((JsonNode)inv.getArgument(2)).isObject()); + Mockito.when(primitiveMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isValueNode() && ((JsonNode)inv.getArgument(2)).isValueNode()); final var compositeMatcher = new CompositeJsonMatcher( arrayMatcher, @@ -50,12 +50,12 @@ public void shouldCallTheObjectMatcherIfTheTwoObjectAreObject() { final var objectMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); final var primitiveMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); - Mockito.when(arrayMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isArray() && ((JsonNode)inv.getArgument(1)).isArray()); - Mockito.when(objectMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isObject() && ((JsonNode)inv.getArgument(1)).isObject()); - Mockito.when(primitiveMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isValueNode() && ((JsonNode)inv.getArgument(1)).isValueNode()); + Mockito.when(arrayMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isArray() && ((JsonNode)inv.getArgument(2)).isArray()); + Mockito.when(objectMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isObject() && ((JsonNode)inv.getArgument(2)).isObject()); + Mockito.when(primitiveMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isValueNode() && ((JsonNode)inv.getArgument(2)).isValueNode()); final var compositeMatcher = new CompositeJsonMatcher( arrayMatcher, @@ -79,12 +79,12 @@ public void shouldCallThePrimitiveMatcherIfTheTwoObjectAreValue() { final var objectMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); final var primitiveMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); - Mockito.when(arrayMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isArray() && ((JsonNode)inv.getArgument(1)).isArray()); - Mockito.when(objectMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isObject() && ((JsonNode)inv.getArgument(1)).isObject()); - Mockito.when(primitiveMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isValueNode() && ((JsonNode)inv.getArgument(1)).isValueNode()); + Mockito.when(arrayMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isArray() && ((JsonNode)inv.getArgument(2)).isArray()); + Mockito.when(objectMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isObject() && ((JsonNode)inv.getArgument(2)).isObject()); + Mockito.when(primitiveMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isValueNode() && ((JsonNode)inv.getArgument(2)).isValueNode()); final var compositeMatcher = new CompositeJsonMatcher( arrayMatcher, @@ -108,12 +108,12 @@ public void shouldReturnANonMatchWhenTypesAreDifferent() { final var objectMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); final var primitiveMatcher = (PartialJsonMatcher) Mockito.mock(PartialJsonMatcher.class); - Mockito.when(arrayMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isArray() && ((JsonNode)inv.getArgument(1)).isArray()); - Mockito.when(objectMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isObject() && ((JsonNode)inv.getArgument(1)).isObject()); - Mockito.when(primitiveMatcher.manage(any(), any())).thenAnswer(inv -> - ((JsonNode)inv.getArgument(0)).isValueNode() && ((JsonNode)inv.getArgument(1)).isValueNode()); + Mockito.when(arrayMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isArray() && ((JsonNode)inv.getArgument(2)).isArray()); + Mockito.when(objectMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isObject() && ((JsonNode)inv.getArgument(2)).isObject()); + Mockito.when(primitiveMatcher.manage(any(), any(), any())).thenAnswer(inv -> + ((JsonNode)inv.getArgument(1)).isValueNode() && ((JsonNode)inv.getArgument(2)).isValueNode()); final var compositeMatcher = new CompositeJsonMatcher( arrayMatcher, diff --git a/src/test/java/com/deblock/jsondiff/matcher/IgnoredPathMatcherTest.java b/src/test/java/com/deblock/jsondiff/matcher/IgnoredPathMatcherTest.java new file mode 100644 index 0000000..13ef93f --- /dev/null +++ b/src/test/java/com/deblock/jsondiff/matcher/IgnoredPathMatcherTest.java @@ -0,0 +1,172 @@ +package com.deblock.jsondiff.matcher; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import tools.jackson.databind.node.IntNode; +import tools.jackson.databind.node.StringNode; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class IgnoredPathMatcherTest { + + @Nested + class ManageMethod { + + @Test + void shouldMatchExactPath() { + var matcher = new IgnoredPathMatcher("name"); + var path = Path.ROOT.add(Path.PathItem.of("name")); + + assertTrue(matcher.manage(path, null, null)); + } + + @Test + void shouldMatchEndOfPath() { + var matcher = new IgnoredPathMatcher("name"); + var path = Path.ROOT + .add(Path.PathItem.of("user")) + .add(Path.PathItem.of("name")); + + assertTrue(matcher.manage(path, null, null)); + } + + @Test + void shouldNotMatchDifferentPath() { + var matcher = new IgnoredPathMatcher("name"); + var path = Path.ROOT.add(Path.PathItem.of("age")); + + assertFalse(matcher.manage(path, null, null)); + } + + @Test + void shouldMatchNestedPath() { + var matcher = new IgnoredPathMatcher("user.name"); + var path = Path.ROOT + .add(Path.PathItem.of("data")) + .add(Path.PathItem.of("user")) + .add(Path.PathItem.of("name")); + + assertTrue(matcher.manage(path, null, null)); + } + + @Test + void shouldNotMatchPartialNestedPath() { + var matcher = new IgnoredPathMatcher("user.name"); + var path = Path.ROOT + .add(Path.PathItem.of("name")); + + assertFalse(matcher.manage(path, null, null)); + } + + @Test + void shouldMatchWithWildcardProperty() { + var matcher = new IgnoredPathMatcher("*.name"); + var path = Path.ROOT + .add(Path.PathItem.of("user")) + .add(Path.PathItem.of("name")); + + assertTrue(matcher.manage(path, null, null)); + } + + @Test + void shouldMatchArrayIndex() { + var matcher = new IgnoredPathMatcher("items[0]"); + var path = Path.ROOT + .add(Path.PathItem.of("items")) + .add(Path.PathItem.of(0)); + + assertTrue(matcher.manage(path, null, null)); + } + + @Test + void shouldMatchArrayWildcard() { + var matcher = new IgnoredPathMatcher("items[*].id"); + var path = Path.ROOT + .add(Path.PathItem.of("items")) + .add(Path.PathItem.of(5)) + .add(Path.PathItem.of("id")); + + assertTrue(matcher.manage(path, null, null)); + } + + @Test + void shouldMatchAnyOfMultiplePatterns() { + var matcher = new IgnoredPathMatcher("name", "age", "email"); + var pathName = Path.ROOT.add(Path.PathItem.of("name")); + var pathAge = Path.ROOT.add(Path.PathItem.of("age")); + var pathEmail = Path.ROOT.add(Path.PathItem.of("email")); + var pathCity = Path.ROOT.add(Path.PathItem.of("city")); + + assertTrue(matcher.manage(pathName, null, null)); + assertTrue(matcher.manage(pathAge, null, null)); + assertTrue(matcher.manage(pathEmail, null, null)); + assertFalse(matcher.manage(pathCity, null, null)); + } + + @Test + void shouldWorkWithListConstructor() { + var matcher = new IgnoredPathMatcher(List.of("name", "age")); + var pathName = Path.ROOT.add(Path.PathItem.of("name")); + var pathAge = Path.ROOT.add(Path.PathItem.of("age")); + + assertTrue(matcher.manage(pathName, null, null)); + assertTrue(matcher.manage(pathAge, null, null)); + } + + @Test + void shouldNotMatchRootPath() { + var matcher = new IgnoredPathMatcher("name"); + + assertFalse(matcher.manage(Path.ROOT, null, null)); + } + } + + @Nested + class JsonDiffMethod { + + @Test + void shouldReturnMatchedDiffWithFullSimilarity() { + var matcher = new IgnoredPathMatcher("name"); + var path = Path.ROOT.add(Path.PathItem.of("name")); + var expected = StringNode.valueOf("John"); + var received = StringNode.valueOf("Jane"); + + var diff = matcher.jsonDiff(path, expected, received, Mockito.mock(JsonMatcher.class)); + + assertEquals(100, diff.similarityRate()); + assertEquals(path, diff.path()); + } + + @Test + void shouldReturnMatchedDiffEvenWithDifferentTypes() { + var matcher = new IgnoredPathMatcher("value"); + var path = Path.ROOT.add(Path.PathItem.of("value")); + var expected = StringNode.valueOf("100"); + var received = IntNode.valueOf(100); + + var diff = matcher.jsonDiff(path, expected, received, Mockito.mock(JsonMatcher.class)); + + assertEquals(100, diff.similarityRate()); + } + + @Test + void shouldReturnMatchedDiffForNestedPath() { + var matcher = new IgnoredPathMatcher("user.timestamp"); + var path = Path.ROOT + .add(Path.PathItem.of("user")) + .add(Path.PathItem.of("timestamp")); + var expected = StringNode.valueOf("2024-01-01"); + var received = StringNode.valueOf("2024-12-31"); + + var diff = matcher.jsonDiff(path, expected, received, Mockito.mock(JsonMatcher.class)); + + assertEquals(100, diff.similarityRate()); + new JsonDiffAsserter() + .assertPrimaryMatching(path) + .validate(diff); + } + } +} diff --git a/src/test/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcherTest.java b/src/test/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcherTest.java index 1218fd4..00a607e 100644 --- a/src/test/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcherTest.java +++ b/src/test/java/com/deblock/jsondiff/matcher/LenientNumberPrimitivePartialMatcherTest.java @@ -20,7 +20,7 @@ void manage_shouldReturnTrue_whenBothNodesAreNumbers() { final var number1 = IntNode.valueOf(10); final var number2 = DecimalNode.valueOf(BigDecimal.valueOf(20)); - assertTrue(matcher.manage(number1, number2)); + assertTrue(matcher.manage(null, number2, number1)); } @Test @@ -28,7 +28,7 @@ void manage_shouldReturnFalse_whenExpectedIsNotNumber() { final var string = StringNode.valueOf("test"); final var number = IntNode.valueOf(10); - assertFalse(matcher.manage(string, number)); + assertFalse(matcher.manage(null, number, string)); } @Test @@ -36,7 +36,7 @@ void manage_shouldReturnFalse_whenReceivedIsNotNumber() { final var number = IntNode.valueOf(10); final var string = StringNode.valueOf("test"); - assertFalse(matcher.manage(number, string)); + assertFalse(matcher.manage(null, string, number)); } @Test @@ -44,7 +44,7 @@ void manage_shouldReturnFalse_whenBothAreStrings() { final var string1 = StringNode.valueOf("test1"); final var string2 = StringNode.valueOf("test2"); - assertFalse(matcher.manage(string1, string2)); + assertFalse(matcher.manage(null, string2, string1)); } @Test @@ -52,7 +52,7 @@ void manage_shouldReturnFalse_whenBothAreBooleans() { final var bool1 = BooleanNode.TRUE; final var bool2 = BooleanNode.FALSE; - assertFalse(matcher.manage(bool1, bool2)); + assertFalse(matcher.manage(null, bool2, bool1)); } @Test diff --git a/src/test/java/com/deblock/jsondiff/matcher/NullEqualsEmptyArrayMatcherTest.java b/src/test/java/com/deblock/jsondiff/matcher/NullEqualsEmptyArrayMatcherTest.java index 56954e9..37c1f0d 100644 --- a/src/test/java/com/deblock/jsondiff/matcher/NullEqualsEmptyArrayMatcherTest.java +++ b/src/test/java/com/deblock/jsondiff/matcher/NullEqualsEmptyArrayMatcherTest.java @@ -4,9 +4,7 @@ import com.deblock.jsondiff.diff.UnMatchedPrimaryDiff; import org.junit.jupiter.api.Test; import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.NullNode; -import tools.jackson.databind.node.ObjectNode; import static org.junit.jupiter.api.Assertions.*; @@ -20,7 +18,7 @@ public void manage_shouldReturnTrue_whenExpectedIsNullAndReceivedIsArray() { final var nullNode = NullNode.getInstance(); final var arrayNode = MAPPER.createArrayNode(); - assertTrue(matcher.manage(nullNode, arrayNode)); + assertTrue(matcher.manage(null, arrayNode, nullNode)); } @Test @@ -28,7 +26,7 @@ public void manage_shouldReturnTrue_whenExpectedIsArrayAndReceivedIsNull() { final var arrayNode = MAPPER.createArrayNode(); final var nullNode = NullNode.getInstance(); - assertTrue(matcher.manage(arrayNode, nullNode)); + assertTrue(matcher.manage(null, nullNode, arrayNode)); } @Test @@ -36,7 +34,7 @@ public void manage_shouldReturnFalse_whenBothAreNull() { final var nullNode1 = NullNode.getInstance(); final var nullNode2 = NullNode.getInstance(); - assertFalse(matcher.manage(nullNode1, nullNode2)); + assertFalse(matcher.manage(null, nullNode2, nullNode1)); } @Test @@ -44,7 +42,7 @@ public void manage_shouldReturnFalse_whenBothAreArrays() { final var array1 = MAPPER.createArrayNode(); final var array2 = MAPPER.createArrayNode(); - assertFalse(matcher.manage(array1, array2)); + assertFalse(matcher.manage(null, array2, array1)); } @Test @@ -52,7 +50,7 @@ public void manage_shouldReturnFalse_whenExpectedIsNullAndReceivedIsObject() { final var nullNode = NullNode.getInstance(); final var objectNode = MAPPER.createObjectNode(); - assertFalse(matcher.manage(nullNode, objectNode)); + assertFalse(matcher.manage(null, objectNode, nullNode)); } @Test diff --git a/src/test/java/com/deblock/jsondiff/matcher/PathMatcherTest.java b/src/test/java/com/deblock/jsondiff/matcher/PathMatcherTest.java new file mode 100644 index 0000000..0851d4d --- /dev/null +++ b/src/test/java/com/deblock/jsondiff/matcher/PathMatcherTest.java @@ -0,0 +1,200 @@ +package com.deblock.jsondiff.matcher; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class PathMatcherTest { + + @Nested + class ExactMatching { + + @Test + void shouldMatchExactProperty() { + var matcher = PathMatcher.from("foo"); + var path = Path.ROOT.add(Path.PathItem.of("foo")); + + assertTrue(matcher.match(path)); + } + + @Test + void shouldMatchExactNestedPath() { + var matcher = PathMatcher.from("foo.bar"); + var path = Path.ROOT + .add(Path.PathItem.of("foo")) + .add(Path.PathItem.of("bar")); + + assertTrue(matcher.match(path)); + } + + @Test + void shouldNotMatchDifferentProperty() { + var matcher = PathMatcher.from("foo"); + var path = Path.ROOT.add(Path.PathItem.of("bar")); + + assertFalse(matcher.match(path)); + } + + @Test + void shouldMatchArrayIndex() { + var matcher = PathMatcher.from("items[0]"); + var path = Path.ROOT + .add(Path.PathItem.of("items")) + .add(Path.PathItem.of(0)); + + assertTrue(matcher.match(path)); + } + + @Test + void shouldNotMatchDifferentArrayIndex() { + var matcher = PathMatcher.from("items[0]"); + var path = Path.ROOT + .add(Path.PathItem.of("items")) + .add(Path.PathItem.of(1)); + + assertFalse(matcher.match(path)); + } + } + + @Nested + class WildcardMatching { + + @Test + void shouldMatchAnyPropertyWithWildcard() { + var matcher = PathMatcher.from("foo.*.baz"); + var path = Path.ROOT + .add(Path.PathItem.of("foo")) + .add(Path.PathItem.of("bar")) + .add(Path.PathItem.of("baz")); + + assertTrue(matcher.match(path)); + } + + @Test + void shouldMatchAnyArrayIndexWithWildcard() { + var matcher = PathMatcher.from("items[*]"); + var path0 = Path.ROOT.add(Path.PathItem.of("items")).add(Path.PathItem.of(0)); + var path5 = Path.ROOT.add(Path.PathItem.of("items")).add(Path.PathItem.of(5)); + var path99 = Path.ROOT.add(Path.PathItem.of("items")).add(Path.PathItem.of(99)); + + assertTrue(matcher.match(path0)); + assertTrue(matcher.match(path5)); + assertTrue(matcher.match(path99)); + } + + @Test + void shouldNotMatchPropertyWildcardWithArrayIndex() { + var matcher = PathMatcher.from("foo.*"); + var path = Path.ROOT + .add(Path.PathItem.of("foo")) + .add(Path.PathItem.of(0)); + + assertFalse(matcher.match(path)); + } + + @Test + void shouldNotMatchArrayWildcardWithProperty() { + var matcher = PathMatcher.from("items[*]"); + var path = Path.ROOT + .add(Path.PathItem.of("items")) + .add(Path.PathItem.of("notAnIndex")); + + assertFalse(matcher.match(path)); + } + } + + @Nested + class EndsWithMatching { + + @Test + void shouldMatchAtEndOfPath() { + var matcher = PathMatcher.from("bar"); + var path = Path.ROOT + .add(Path.PathItem.of("foo")) + .add(Path.PathItem.of("bar")); + + assertTrue(matcher.match(path)); + } + + @Test + void shouldMatchNestedPatternAtEndOfPath() { + var matcher = PathMatcher.from("foo.bar"); + var path = Path.ROOT + .add(Path.PathItem.of("prefix")) + .add(Path.PathItem.of("foo")) + .add(Path.PathItem.of("bar")); + + assertTrue(matcher.match(path)); + } + + @Test + void shouldNotMatchPropertyInMiddleOfPath() { + var matcher = PathMatcher.from("foo"); + var path = Path.ROOT + .add(Path.PathItem.of("test")) + .add(Path.PathItem.of("foo")) + .add(Path.PathItem.of("bar")); + + assertFalse(matcher.match(path)); + } + + @Test + void shouldNotMatchNestedPatternInMiddleOfPath() { + var matcher = PathMatcher.from("foo.bar"); + var path = Path.ROOT + .add(Path.PathItem.of("prefix")) + .add(Path.PathItem.of("foo")) + .add(Path.PathItem.of("bar")) + .add(Path.PathItem.of("suffix")); + + assertFalse(matcher.match(path)); + } + + @Test + void shouldNotMatchIfPatternNotInPath() { + var matcher = PathMatcher.from("notfound"); + var path = Path.ROOT + .add(Path.PathItem.of("foo")) + .add(Path.PathItem.of("bar")); + + assertFalse(matcher.match(path)); + } + + @Test + void shouldMatchArrayAtEndOfPath() { + var matcher = PathMatcher.from("items[*].name"); + var path = Path.ROOT + .add(Path.PathItem.of("data")) + .add(Path.PathItem.of("items")) + .add(Path.PathItem.of(0)) + .add(Path.PathItem.of("name")); + + assertTrue(matcher.match(path)); + } + + @Test + void shouldNotMatchWhenPathIsShorterThanMatcher() { + var matcher = PathMatcher.from("foo.bar.baz"); + var path = Path.ROOT + .add(Path.PathItem.of("bar")) + .add(Path.PathItem.of("baz")); + + assertFalse(matcher.match(path)); + } + } + + @Nested + class EdgeCases { + + @Test + void shouldThrowExceptionForEmptyPathMatching() { + assertThrows(IllegalArgumentException.class, () -> PathMatcher.from("")); + } + + @Test + void shouldThrowExceptionForEmptyPathMatchingBetweenDot() { + assertThrows(IllegalArgumentException.class, () -> PathMatcher.from("foo..bar")); + } + } +}