Skip to content

Commit 3851ea5

Browse files
Duncan MackinderDuncan Mackinder
authored andcommitted
Regex and array size verification enhancements
Adding regular expression customization and value matchers and array size comparator enhancements.
1 parent 28895cf commit 3851ea5

File tree

9 files changed

+400
-9
lines changed

9 files changed

+400
-9
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.skyscreamer.jsonassert;
2+
3+
/**
4+
* Interface implemented by classes that determine if JSON elements require
5+
* custom comparisons when compared by CustomComparator.
6+
*
7+
* @author Duncan Mackinder
8+
*
9+
*/
10+
public interface Customizable {
11+
12+
public abstract boolean appliesToPath(String path);
13+
14+
public abstract boolean matches(Object actual, Object expected);
15+
16+
}

src/main/java/org/skyscreamer/jsonassert/Customization.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/**
44
* Associates a custom matcher to a specific jsonpath.
55
*/
6-
public final class Customization {
6+
public final class Customization implements Customizable {
77
private final String path;
88
private final ValueMatcher<Object> comparator;
99

@@ -14,15 +14,23 @@ public Customization(String path, ValueMatcher<Object> comparator) {
1414
this.comparator = comparator;
1515
}
1616

17-
public static Customization customization(String path, ValueMatcher<Object> comparator) {
17+
public static Customizable customization(String path, ValueMatcher<Object> comparator) {
1818
return new Customization(path, comparator);
1919
}
2020

21-
public boolean appliesToPath(String path) {
21+
/* (non-Javadoc)
22+
* @see org.skyscreamer.jsonassert.Customizable#appliesToPath(java.lang.String)
23+
*/
24+
@Override
25+
public boolean appliesToPath(String path) {
2226
return this.path.equals(path);
2327
}
2428

25-
public boolean matches(Object actual, Object expected) {
29+
/* (non-Javadoc)
30+
* @see org.skyscreamer.jsonassert.Customizable#matches(java.lang.Object, java.lang.Object)
31+
*/
32+
@Override
33+
public boolean matches(Object actual, Object expected) {
2634
return comparator.equal(actual, expected);
2735
}
2836
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.skyscreamer.jsonassert;
2+
3+
import java.util.regex.Pattern;
4+
5+
/**
6+
* Associates a custom matcher to a jsonpath identified by a regular expression
7+
*
8+
* @author Duncan Mackinder
9+
*/
10+
public final class RegularExpressionCustomization implements Customizable {
11+
private final Pattern path;
12+
private final ValueMatcher<Object> comparator;
13+
14+
public RegularExpressionCustomization(String path, ValueMatcher<Object> comparator) {
15+
assert path != null;
16+
assert comparator != null;
17+
this.path = Pattern.compile(path);
18+
this.comparator = comparator;
19+
}
20+
21+
public static Customizable customization(String path, ValueMatcher<Object> comparator) {
22+
return new RegularExpressionCustomization(path, comparator);
23+
}
24+
25+
/* (non-Javadoc)
26+
* @see org.skyscreamer.jsonassert.Customizable#appliesToPath(java.lang.String)
27+
*/
28+
@Override
29+
public boolean appliesToPath(String path) {
30+
return this.path.matcher(path).matches();
31+
}
32+
33+
/* (non-Javadoc)
34+
* @see org.skyscreamer.jsonassert.Customizable#matches(java.lang.Object, java.lang.Object)
35+
*/
36+
@Override
37+
public boolean matches(Object actual, Object expected) {
38+
return comparator.equal(actual, expected);
39+
}
40+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package org.skyscreamer.jsonassert;
2+
3+
import java.util.regex.Pattern;
4+
5+
import org.skyscreamer.jsonassert.ValueMatcher;
6+
7+
/**
8+
* A JSONassert value matcher that matches actual value to regular expression.
9+
* If non-null regular expression passed to constructor, then all actual values
10+
* will be compared against this pattern, ignoring any expected value passed to
11+
* equal method. If null regular expression passed to constructor, then expected
12+
* value passed to equals method will be treated as a regular expression.
13+
*
14+
* @author Duncan Mackinder
15+
*
16+
*/
17+
public class RegularExpressionValueMatcher<T> implements ValueMatcher<T> {
18+
19+
private final Pattern expectedPattern;
20+
21+
public RegularExpressionValueMatcher() {
22+
this(null);
23+
}
24+
25+
public RegularExpressionValueMatcher(String expected) {
26+
expectedPattern = expected == null ? null : Pattern.compile(expected);
27+
}
28+
29+
@Override
30+
public boolean equal(T actual, T expected) {
31+
String actualString = actual.toString();
32+
Pattern pattern = expectedPattern != null ? expectedPattern : Pattern
33+
.compile(expected.toString());
34+
return pattern.matcher(actualString).matches();
35+
}
36+
37+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.skyscreamer.jsonassert.comparator;
2+
3+
import org.json.JSONArray;
4+
import org.json.JSONException;
5+
import org.skyscreamer.jsonassert.JSONCompareMode;
6+
import org.skyscreamer.jsonassert.JSONCompareResult;
7+
8+
/**
9+
* A JSONAssert array size comparator. Expected array should consist of either 1
10+
* or 2 integer values that define maximum and minimum size of that the actual
11+
* array is expected to be. If expected array contains a single integer value
12+
* then the actual array must contain exactly that number of elements.
13+
*
14+
* @author Duncan Mackinder
15+
*
16+
*/
17+
public class ArraySizeComparator extends DefaultComparator {
18+
19+
public ArraySizeComparator(JSONCompareMode mode) {
20+
super(mode);
21+
}
22+
23+
@Override
24+
public void compareJSONArray(String prefix, JSONArray expected,
25+
JSONArray actual, JSONCompareResult result) throws JSONException {
26+
if (expected.length() < 1 || expected.length() > 2) {
27+
result.fail(prefix + ": invalid expectation, length=" + expected.length());
28+
return;
29+
}
30+
if (!(expected.get(0) instanceof Number)) {
31+
result.fail(prefix + ": min expected length not a number: " + expected.get(0));
32+
return;
33+
}
34+
if ((expected.length() == 2 && !(expected.get(1) instanceof Number))) {
35+
result.fail(prefix + ": max expected length not a number: " + expected.get(1));
36+
return;
37+
}
38+
int minExpectedLength = expected.getInt(0);
39+
if (minExpectedLength < 0) {
40+
result.fail(prefix + ": invalid expectation, invalid min expected length: " + minExpectedLength);
41+
return;
42+
}
43+
int maxExpectedLength = expected.length() == 2? expected.getInt(1): minExpectedLength;
44+
if (maxExpectedLength < minExpectedLength) {
45+
result.fail(prefix + ": invalid expectation, invalid expected length range: "+ minExpectedLength+" to " + maxExpectedLength);
46+
return;
47+
}
48+
if (actual.length() < minExpectedLength || actual.length() > maxExpectedLength) {
49+
result.fail(prefix + "[]: Expected " + minExpectedLength + (expected.length() == 2? (" to "+maxExpectedLength): "")
50+
+ " values but got " + actual.length());
51+
}
52+
}
53+
54+
}

src/main/java/org/skyscreamer/jsonassert/comparator/CustomComparator.java

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.skyscreamer.jsonassert.comparator;
22

33
import org.json.JSONException;
4+
import org.skyscreamer.jsonassert.Customizable;
45
import org.skyscreamer.jsonassert.Customization;
56
import org.skyscreamer.jsonassert.JSONCompareMode;
67
import org.skyscreamer.jsonassert.JSONCompareResult;
@@ -10,16 +11,41 @@
1011

1112
public class CustomComparator extends DefaultComparator {
1213

13-
private final Collection<Customization> customizations;
14+
private final Collection<Customizable> customizations;
1415

15-
public CustomComparator(JSONCompareMode mode, Customization... customizations) {
16+
/**
17+
* Create custom comparator from array of customizations.
18+
*
19+
* @param mode
20+
* comparison mode
21+
* @param customizations
22+
* array of customizations
23+
*/
24+
public CustomComparator(JSONCompareMode mode, Customizable... customizations) {
1625
super(mode);
1726
this.customizations = Arrays.asList(customizations);
1827
}
1928

29+
/**
30+
* Create custom comparator from array of customizations. Provides backwards
31+
* compatibility with code compiled against JSONassert 1.2.1 or earlier. Use
32+
* CustomComparator(JSONCompareMode mode, Customizable... customizations)
33+
* constructor in place of this one.
34+
*
35+
* @param mode
36+
* comparison mode
37+
* @param customizations
38+
* array of customizations
39+
*/
40+
@Deprecated
41+
public CustomComparator(JSONCompareMode mode, Customization... customizations) {
42+
super(mode);
43+
this.customizations = Arrays.asList((Customizable[])customizations);
44+
}
45+
2046
@Override
2147
public void compareValues(String prefix, Object expectedValue, Object actualValue, JSONCompareResult result) throws JSONException {
22-
Customization customization = getCustomization(prefix);
48+
Customizable customization = getCustomization(prefix);
2349
if (customization != null) {
2450
if (!customization.matches(actualValue, expectedValue)) {
2551
result.fail(prefix, expectedValue, actualValue);
@@ -29,8 +55,8 @@ public void compareValues(String prefix, Object expectedValue, Object actualValu
2955
}
3056
}
3157

32-
private Customization getCustomization(String path) {
33-
for (Customization c : customizations)
58+
private Customizable getCustomization(String path) {
59+
for (Customizable c : customizations)
3460
if (c.appliesToPath(path))
3561
return c;
3662
return null;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package org.skyscreamer.jsonassert;
2+
3+
/**
4+
* Unit tests for RegularExpressionCustomization
5+
*
6+
* @author Duncan Mackinder
7+
*
8+
*/
9+
import org.json.JSONException;
10+
import org.junit.Test;
11+
import org.skyscreamer.jsonassert.comparator.CustomComparator;
12+
import org.skyscreamer.jsonassert.comparator.JSONComparator;
13+
14+
import static org.junit.Assert.assertEquals;
15+
import static org.junit.Assert.assertTrue;
16+
import static org.skyscreamer.jsonassert.JSONCompare.compareJSON;
17+
18+
public class RegularExpressionCustomizationTest {
19+
20+
String actual = "{\"first\":\"actual\", \"second\":1, \"third\":\"actual\"}";
21+
String expected = "{\"first\":\"expected\", \"second\":1, \"third\":\"expected\"}";
22+
23+
String deepActual = "{\n" +
24+
" \"outer\":\n" +
25+
" {\n" +
26+
" \"inner\":\n" +
27+
" [\n" +
28+
" {\n" +
29+
" \"value\": \"actual\",\n" +
30+
" \"otherValue\": \"foo\"\n" +
31+
" },\n" +
32+
" {\n" +
33+
" \"value\": \"actual\",\n" +
34+
" \"otherValue\": \"bar\"\n" +
35+
" },\n" +
36+
" {\n" +
37+
" \"value\": \"actual\",\n" +
38+
" \"otherValue\": \"baz\"\n" +
39+
" }\n" +
40+
" ]\n" +
41+
" }\n" +
42+
"}";
43+
String deepExpected = "{\n" +
44+
" \"outer\":\n" +
45+
" {\n" +
46+
" \"inner\":\n" +
47+
" [\n" +
48+
" {\n" +
49+
" \"value\": \"expected\",\n" +
50+
" \"otherValue\": \"foo\"\n" +
51+
" },\n" +
52+
" {\n" +
53+
" \"value\": \"expected\",\n" +
54+
" \"otherValue\": \"bar\"\n" +
55+
" },\n" +
56+
" {\n" +
57+
" \"value\": \"expected\",\n" +
58+
" \"otherValue\": \"baz\"\n" +
59+
" }\n" +
60+
" ]\n" +
61+
" }\n" +
62+
"}";
63+
64+
int comparatorCallCount = 0;
65+
ValueMatcher<Object> comparator = new ValueMatcher<Object>() {
66+
@Override
67+
public boolean equal(Object o1, Object o2) {
68+
comparatorCallCount++;
69+
return o1.toString().equals("actual") && o2.toString().equals("expected");
70+
}
71+
};
72+
73+
@Test
74+
public void whenPathMatchesInCustomizationThenCallCustomMatcher() throws JSONException {
75+
JSONComparator jsonCmp = new CustomComparator(JSONCompareMode.STRICT, new RegularExpressionCustomization(".*i.*", comparator)); // will match first and third but not second
76+
JSONCompareResult result = compareJSON(expected, actual, jsonCmp);
77+
assertTrue(result.getMessage(), result.passed());
78+
assertEquals(2, comparatorCallCount);
79+
}
80+
81+
@Test
82+
public void whenDeepPathMatchesCallCustomMatcher() throws JSONException {
83+
JSONComparator jsonCmp = new CustomComparator(JSONCompareMode.STRICT, new RegularExpressionCustomization("outer\\.inner\\[.*\\]\\.value", comparator));
84+
JSONCompareResult result = compareJSON(deepExpected, deepActual, jsonCmp);
85+
assertTrue(result.getMessage(), result.passed());
86+
assertEquals(3, comparatorCallCount);
87+
}
88+
89+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.skyscreamer.jsonassert;
2+
3+
import org.json.JSONException;
4+
import org.junit.Test;
5+
import org.skyscreamer.jsonassert.Customization;
6+
import org.skyscreamer.jsonassert.JSONCompareMode;
7+
import org.skyscreamer.jsonassert.comparator.CustomComparator;
8+
9+
/**
10+
* Unit tests for RegularExpressionValueMatcher
11+
*
12+
* @author Duncan Mackinder
13+
*
14+
*/
15+
public class RegularExpressionValueMatcherTest {
16+
private void doTest(String jsonPath, String regex, String expectedJSON,
17+
String actualJSON) throws JSONException {
18+
JSONAssert.assertEquals(expectedJSON, actualJSON, new CustomComparator(
19+
JSONCompareMode.STRICT_ORDER, new Customization(jsonPath,
20+
new RegularExpressionValueMatcher<Object>(regex))));
21+
}
22+
23+
@Test
24+
public void fixedRegexMatchesStringAttributeInsideArray() throws JSONException {
25+
doTest("d.results[0].__metadata.uri", "http://localhost:80/Person\\('\\d+'\\)", "{d:{results:[{__metadata:{uri:X}}]}}", "{d:{results:[{__metadata:{uri:\"http://localhost:80/Person('1')\",type:Person},id:1}]}}");
26+
}
27+
28+
@Test
29+
public void fixedRegexWithSimplePathMatchsStringAttribute() throws JSONException {
30+
doTest("a", "v.", "{a:x}", "{a:v1}");
31+
}
32+
33+
@Test
34+
public void fixedRegexWithThreeLevelPathMatchsStringAttribute() throws JSONException {
35+
doTest("a.b.c", ".*Is.*", "{a:{b:{c:x}}}", "{a:{b:{c:thisIsAString}}}");
36+
}
37+
38+
@Test
39+
public void dynamicRegexWithSimplePathMatchsStringAttribute() throws JSONException {
40+
doTest("a", null, "{a:\"v.\"}", "{a:v1}");
41+
}
42+
43+
@Test
44+
public void dynamicRegexWithThreeLevelPathMatchsStringAttribute() throws JSONException {
45+
doTest("a.b.c", null, "{a:{b:{c:\".*Is.*\"}}}",
46+
"{a:{b:{c:thisIsAString}}}");
47+
}
48+
49+
}

0 commit comments

Comments
 (0)