Skip to content

Commit b621a44

Browse files
committed
Merge pull request skyscreamer#40 from dmackinder/master
Regex and array size verification enhancements
2 parents 28895cf + 7c096c6 commit b621a44

File tree

12 files changed

+1014
-8
lines changed

12 files changed

+1014
-8
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package org.skyscreamer.jsonassert;
2+
3+
import java.text.MessageFormat;
4+
5+
import org.json.JSONArray;
6+
import org.json.JSONException;
7+
import org.skyscreamer.jsonassert.comparator.JSONComparator;
8+
9+
/**
10+
* <p>A value matcher for arrays. This operates like STRICT_ORDER array match,
11+
* however if expected array has less elements than actual array the matching
12+
* process loops through the expected array to get expected elements for the
13+
* additional actual elements. In general the expected array will contain a
14+
* single element which is matched against each actual array element in turn.
15+
* This allows simple verification of constant array element components and
16+
* coupled with RegularExpressionValueMatcher can be used to match specific
17+
* array element components against a regular expression pattern. As a convenience to reduce syntactic complexity of expected string, if the
18+
* expected object is not an array, a one element expected array is created
19+
* containing whatever is provided as the expected value.</p>
20+
*
21+
* <p>Some examples of typical usage idioms listed below.</p>
22+
*
23+
* <p>Assuming JSON to be verified is held in String variable ARRAY_OF_JSONOBJECTS and contains:</p>
24+
*
25+
* <code>{a:[{background:white,id:1,type:row}, {background:grey,id:2,type:row}, {background:white,id:3,type:row}, {background:grey,id:4,type:row}]}</code>
26+
*
27+
* <p>then:</p>
28+
*
29+
* <p>To verify that the 'id' attribute of first element of array 'a' is '1':</p>
30+
*
31+
* <code>
32+
* JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);<br/>
33+
* Customization customization = new Customization("a", new ArrayValueMatcher&lt;Object&gt;(comparator, 0));<br/>
34+
* JSONAssert.assertEquals("{a:[{id:1}]}", ARRAY_OF_JSONOBJECTS, new CustomComparator(JSONCompareMode.LENIENT, customization));
35+
* </code>
36+
*
37+
* <p>To simplify complexity of expected JSON string, the value <code>"a:[{id:1}]}"</code> may be replaced by <code>"a:{id:1}}"</code></p>
38+
*
39+
* <p>To verify that the 'type' attribute of second and third elements of array 'a' is 'row':</p>
40+
*
41+
* <code>
42+
* JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);<br/>
43+
* Customization customization = new Customization("a", new ArrayValueMatcher&lt;Object&gt;(comparator, 1, 2));<br/>
44+
* JSONAssert.assertEquals("{a:[{type:row}]}", ARRAY_OF_JSONOBJECTS, new CustomComparator(JSONCompareMode.LENIENT, customization));
45+
* </code>
46+
*
47+
* <p>To verify that the 'type' attribute of every element of array 'a' is 'row':</p>
48+
*
49+
* <code>
50+
* JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);<br/>
51+
* Customization customization = new Customization("a", new ArrayValueMatcher&lt;Object&gt;(comparator));<br/>
52+
* JSONAssert.assertEquals("{a:[{type:row}]}", ARRAY_OF_JSONOBJECTS, new CustomComparator(JSONCompareMode.LENIENT, customization));
53+
* </code>
54+
*
55+
* <p>To verify that the 'background' attribute of every element of array 'a' alternates between 'white' and 'grey' starting with first element 'background' being 'white':</p>
56+
*
57+
* <code>
58+
* JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);<br/>
59+
* Customization customization = new Customization("a", new ArrayValueMatcher&lt;Object&gt;(comparator));<br/>
60+
* JSONAssert.assertEquals("{a:[{background:white},{background:grey}]}", ARRAY_OF_JSONOBJECTS, new CustomComparator(JSONCompareMode.LENIENT, customization));
61+
* </code>
62+
*
63+
* <p>Assuming JSON to be verified is held in String variable ARRAY_OF_JSONARRAYS and contains:</p>
64+
*
65+
* <code>{a:[[6,7,8], [9,10,11], [12,13,14], [19,20,21,22]]}</code>
66+
*
67+
* <p>then:</p>
68+
*
69+
* <p>To verify that the first three elements of JSON array 'a' are JSON arrays of length 3:</p>
70+
*
71+
* <code>
72+
* JSONComparator comparator = new ArraySizeComparator(JSONCompareMode.STRICT_ORDER);<br/>
73+
* Customization customization = new Customization("a", new ArrayValueMatcher&lt;Object&gt;(comparator, 0, 2));<br/>
74+
* JSONAssert.assertEquals("{a:[[3]]}", ARRAY_OF_JSONARRAYS, new CustomComparator(JSONCompareMode.LENIENT, customization));
75+
* </code>
76+
*
77+
* <p>NOTE: simplified expected JSON strings are not possible in this case as ArraySizeComparator does not support them.</p>
78+
*
79+
* <p>To verify that the second elements of JSON array 'a' is a JSON array whose first element has the value 9:</p>
80+
*
81+
* <code>
82+
* Customization innerCustomization = new Customization("a[1]", new ArrayValueMatcher&lt;Object&gt;(comparator, 0));<br/>
83+
* JSONComparator comparator = new CustomComparator(JSONCompareMode.LENIENT, innerCustomization);<br/>
84+
* Customization customization = new Customization("a", new ArrayValueMatcher&lt;Object&gt;(comparator, 1));<br/>
85+
* JSONAssert.assertEquals("{a:[[9]]}", ARRAY_OF_JSONARRAYS, new CustomComparator(JSONCompareMode.LENIENT, customization));
86+
* </code>
87+
*
88+
* <p>To simplify complexity of expected JSON string, the value <code>"{a:[[9]]}"</code> may be replaced by <code>"{a:[9]}"</code> or <code>"{a:9}"</code></p>
89+
*
90+
* @author Duncan Mackinder
91+
*
92+
*/
93+
public class ArrayValueMatcher<T> implements LocationAwareValueMatcher<T> {
94+
private final JSONComparator comparator;
95+
private final int from;
96+
private final int to;
97+
98+
/**
99+
* Create ArrayValueMatcher to match every element in actual array against
100+
* elements taken in sequence from expected array, repeating from start of
101+
* expected array if necessary.
102+
*
103+
* @param comparator
104+
* comparator to use to compare elements
105+
*/
106+
public ArrayValueMatcher(JSONComparator comparator) {
107+
this(comparator, 0, Integer.MAX_VALUE);
108+
}
109+
110+
/**
111+
* Create ArrayValueMatcher to match specified element in actual array
112+
* against first element of expected array.
113+
*
114+
* @param comparator
115+
* comparator to use to compare elements
116+
* @param index
117+
* index of the array element to be compared
118+
*/
119+
public ArrayValueMatcher(JSONComparator comparator, int index) {
120+
this(comparator, index, index);
121+
}
122+
123+
/**
124+
* Create ArrayValueMatcher to match every element in specified range
125+
* (inclusive) from actual array against elements taken in sequence from
126+
* expected array, repeating from start of expected array if necessary.
127+
*
128+
* @param comparator
129+
* comparator to use to compare elements
130+
* @from first element in actual array to compared
131+
* @to last element in actual array to compared
132+
*/
133+
public ArrayValueMatcher(JSONComparator comparator, int from, int to) {
134+
assert comparator != null : "comparator null";
135+
assert from >= 0 : MessageFormat.format("from({0}) < 0", from);
136+
assert to >= from : MessageFormat.format("to({0}) < from({1})", to,
137+
from);
138+
this.comparator = comparator;
139+
this.from = from;
140+
this.to = to;
141+
}
142+
143+
@Override
144+
/*
145+
* NOTE: method defined as required by ValueMatcher interface but will never
146+
* be called so defined simply to indicate match failure
147+
*/
148+
public boolean equal(T o1, T o2) {
149+
return false;
150+
}
151+
152+
@Override
153+
public boolean equal(String prefix, T actual, T expected, JSONCompareResult result) {
154+
if (!(actual instanceof JSONArray)) {
155+
throw new IllegalArgumentException("ArrayValueMatcher applied to non-array actual value");
156+
}
157+
try {
158+
JSONArray actualArray = (JSONArray) actual;
159+
JSONArray expectedArray = expected instanceof JSONArray ? (JSONArray) expected: new JSONArray(new Object[] { expected });
160+
int first = Math.max(0, from);
161+
int last = Math.min(actualArray.length() - 1, to);
162+
int expectedLen = expectedArray.length();
163+
for (int i = first; i <= last; i++) {
164+
String elementPrefix = MessageFormat.format("{0}[{1}]", prefix, i);
165+
Object actualElement = actualArray.get(i);
166+
Object expectedElement = expectedArray.get((i - first) % expectedLen);
167+
comparator.compareValues(elementPrefix, expectedElement, actualElement, result);
168+
}
169+
// any failures have already been passed to result, so return true
170+
return true;
171+
}
172+
catch (JSONException e) {
173+
return false;
174+
}
175+
}
176+
177+
}

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,50 @@ public boolean appliesToPath(String path) {
2222
return this.path.equals(path);
2323
}
2424

25+
/**
26+
* Return true if actual value matches expected value using this
27+
* Customization's comparator. Calls to this method should be replaced by
28+
* calls to matches(String prefix, Object actual, Object expected,
29+
* JSONCompareResult result).
30+
*
31+
* @param actual
32+
* JSON value being tested
33+
* @param expected
34+
* expected JSON value
35+
* @return true if actual value matches expected value
36+
*/
37+
@Deprecated
2538
public boolean matches(Object actual, Object expected) {
2639
return comparator.equal(actual, expected);
2740
}
41+
42+
/**
43+
* Return true if actual value matches expected value using this
44+
* Customization's comparator. The equal method used for comparison depends
45+
* on type of comparator.
46+
*
47+
* @param prefix
48+
* JSON path of the JSON item being tested (only used if
49+
* comparator is a LocationAwareValueMatcher)
50+
* @param actual
51+
* JSON value being tested
52+
* @param expected
53+
* expected JSON value
54+
* @param result
55+
* JSONCompareResult to which match failure may be passed (only
56+
* used if comparator is a LocationAwareValueMatcher)
57+
* @return true if expected and actual equal or any difference has already
58+
* been passed to specified result instance, false otherwise.
59+
* @throws ValueMatcherException
60+
* if expected and actual values not equal and ValueMatcher
61+
* needs to override default comparison failure message that
62+
* would be generated if this method returned false.
63+
*/
64+
public boolean matches(String prefix, Object actual, Object expected,
65+
JSONCompareResult result) throws ValueMatcherException {
66+
if (comparator instanceof LocationAwareValueMatcher) {
67+
return ((LocationAwareValueMatcher<Object>)comparator).equal(prefix, actual, expected, result);
68+
}
69+
return comparator.equal(actual, expected);
70+
}
2871
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ public JSONCompareResult fail(String field, Object expected, Object actual) {
127127
return this;
128128
}
129129

130+
/**
131+
* Identify that the comparison failed
132+
* @param field Which field failed
133+
* @param exception exception containing details of match failure
134+
*/
135+
public JSONCompareResult fail(String field, ValueMatcherException exception) {
136+
fail(field + ": " + exception.getMessage(), exception.getExpected(), exception.getActual());
137+
return this;
138+
}
139+
130140
private String formatFailureMessage(String field, Object expected, Object actual) {
131141
return field
132142
+ "\nExpected: "
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
*
3+
*/
4+
package org.skyscreamer.jsonassert;
5+
6+
/**
7+
* A ValueMatcher extension that provides location in form of prefix to the equals method.
8+
*
9+
* @author Duncan Mackinder
10+
*
11+
*/
12+
public interface LocationAwareValueMatcher<T> extends ValueMatcher<T> {
13+
14+
/**
15+
* Match actual value with expected value. If match fails any of the
16+
* following may occur, return false, pass failure details to specified
17+
* JSONCompareResult and return true, or throw ValueMatcherException
18+
* containing failure details. Passing failure details to JSONCompareResult
19+
* or returning via ValueMatcherException enables more useful failure
20+
* description for cases where expected value depends entirely or in part on
21+
* configuration of the ValueMatcher and therefore expected value passed to
22+
* this method will not give a useful indication of expected value.
23+
*
24+
* @param prefix
25+
* JSON path of the JSON item being tested
26+
* @param actual
27+
* JSON value being tested
28+
* @param expected
29+
* expected JSON value
30+
* @param result
31+
* JSONCompareResult to which match failure may be passed
32+
* @return true if expected and actual equal or any difference has already
33+
* been passed to specified result instance, false otherwise.
34+
* @throws ValueMatcherException
35+
* if expected and actual values not equal and ValueMatcher
36+
* needs to override default comparison failure message that
37+
* would be generated if this method returned false.
38+
*/
39+
boolean equal(String prefix, T actual, T expected, JSONCompareResult result) throws ValueMatcherException;
40+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.skyscreamer.jsonassert;
2+
3+
import java.util.regex.Pattern;
4+
import java.util.regex.PatternSyntaxException;
5+
6+
import org.skyscreamer.jsonassert.ValueMatcher;
7+
8+
/**
9+
* A JSONassert value matcher that matches actual value to regular expression.
10+
* If non-null regular expression passed to constructor, then all actual values
11+
* will be compared against this constant pattern, ignoring any expected value
12+
* passed to equal method. If null regular expression passed to constructor,
13+
* then expected value passed to equals method will be used to dynamically
14+
* specify regular expression pattern that actual value must match.
15+
*
16+
* @author Duncan Mackinder
17+
*
18+
*/
19+
public class RegularExpressionValueMatcher<T> implements ValueMatcher<T> {
20+
21+
private final Pattern expectedPattern;
22+
23+
/**
24+
* Create RegularExpressionValueMatcher in which the pattern the actual
25+
* value must match with be specified dynamically from the expected string
26+
* passed to this matcher in the equals method.
27+
*/
28+
public RegularExpressionValueMatcher() {
29+
this(null);
30+
}
31+
32+
/**
33+
* Create RegularExpressionValueMatcher with specified pattern. If pattern
34+
* is not null, it must be a valid regular expression that defines a
35+
* constant expected pattern that every actual value must match (in this
36+
* case the expected value passed to equal method will be ignored). If
37+
* pattern is null, the pattern the actual value must match with be
38+
* specified dynamically from the expected string passed to this matcher in
39+
* the equals method.
40+
*
41+
* @param pattern
42+
* if non null, regular expression pattern which all actual
43+
* values this matcher is applied to must match. If null, this
44+
* matcher will apply pattern specified dynamically via the
45+
* expected parameter to the equal method.
46+
* @throws IllegalArgumentException
47+
* if pattern is non-null and not a valid regular expression.
48+
*/
49+
public RegularExpressionValueMatcher(String pattern) throws IllegalArgumentException {
50+
try {
51+
expectedPattern = pattern == null ? null : Pattern.compile(pattern);
52+
}
53+
catch (PatternSyntaxException e) {
54+
throw new IllegalArgumentException("Constant expected pattern invalid: " + e.getMessage(), e);
55+
}
56+
}
57+
58+
@Override
59+
public boolean equal(T actual, T expected) {
60+
String actualString = actual.toString();
61+
String expectedString = expected.toString();
62+
try {
63+
Pattern pattern = isStaticPattern() ? expectedPattern : Pattern
64+
.compile(expectedString);
65+
if (!pattern.matcher(actualString).matches()) {
66+
throw new ValueMatcherException(getPatternType() + " expected pattern did not match value", pattern.toString(), actualString);
67+
}
68+
}
69+
catch (PatternSyntaxException e) {
70+
throw new ValueMatcherException(getPatternType() + " expected pattern invalid: " + e.getMessage(), e, expectedString, actualString);
71+
}
72+
return true;
73+
}
74+
75+
private boolean isStaticPattern() {
76+
return expectedPattern != null;
77+
}
78+
79+
private String getPatternType() {
80+
return isStaticPattern()? "Constant": "Dynamic";
81+
}
82+
}

0 commit comments

Comments
 (0)