Skip to content

Commit

Permalink
Merge 848c479 into d9a76c9
Browse files Browse the repository at this point in the history
  • Loading branch information
fslev committed Oct 3, 2022
2 parents d9a76c9 + 848c479 commit c691b4c
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 148 deletions.
22 changes: 11 additions & 11 deletions src/main/java/io/json/compare/JSONCompare.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import com.fasterxml.jackson.databind.JsonNode;
import io.json.compare.matcher.JsonMatcher;
import io.json.compare.matcher.MatcherException;
import io.json.compare.util.JsonUtils;
import org.junit.jupiter.api.AssertionFailureBuilder;

import java.io.IOException;
import java.util.List;
import java.util.Set;


Expand Down Expand Up @@ -62,11 +62,12 @@ public static void assertNotMatches(Object expected, Object actual, Set<CompareM
public static void assertMatches(Object expected, Object actual, JsonComparator comparator, Set<CompareMode> compareModes, String message) {
JsonNode expectedJson = toJson(expected);
JsonNode actualJson = toJson(actual);
try {
new JsonMatcher(expectedJson, actualJson,
comparator == null ? new DefaultJsonComparator() : comparator, compareModes).match();
} catch (MatcherException e) {
String defaultMessage = String.format("%s\n", e.getMessage());
List<String> diffs = new JsonMatcher(expectedJson, actualJson,
comparator == null ? new DefaultJsonComparator() : comparator, compareModes).match();
if (!diffs.isEmpty()) {
String defaultMessage = String.format("FOUND %s DIFFERENCE(S):\n%s\n",
diffs.size(), diffs.stream().map(diff ->
"\n_________________________DIFF__________________________\n" + diff).reduce(String::concat).get());
if (comparator == null || comparator.getClass().equals(DefaultJsonComparator.class)) {
defaultMessage += "\n\n" + ASSERTION_ERROR_HINT_MESSAGE + "\n";
}
Expand All @@ -78,13 +79,12 @@ public static void assertMatches(Object expected, Object actual, JsonComparator
public static void assertNotMatches(Object expected, Object actual, JsonComparator comparator, Set<CompareMode> compareModes, String message) {
JsonNode expectedJson = toJson(expected);
JsonNode actualJson = toJson(actual);
try {
new JsonMatcher(expectedJson, actualJson,
comparator == null ? new DefaultJsonComparator() : comparator, compareModes).match();
} catch (MatcherException e) {
List<String> diffs = new JsonMatcher(expectedJson, actualJson,
comparator == null ? new DefaultJsonComparator() : comparator, compareModes).match();
if (!diffs.isEmpty()) {
return;
}
String defaultMessage = "JSONs are equal";
String defaultMessage = "\nJSONs are equal";
AssertionFailureBuilder.assertionFailure().message(message == null ? defaultMessage : defaultMessage + "\n" + message)
.expected(prettyPrint(expectedJson)).actual(prettyPrint(actualJson))
.includeValuesInMessage(false).buildAndThrow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
import io.json.compare.DefaultJsonComparator;
import io.json.compare.JsonComparator;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Optional;
import java.util.Set;
import java.util.*;

abstract class AbstractJsonMatcher {

Expand All @@ -27,7 +24,7 @@ abstract class AbstractJsonMatcher {
this.compareModes = compareModes == null ? new HashSet<>() : compareModes;
}

protected abstract void match() throws MatcherException;
protected abstract List<String> match();

protected static UseCase getUseCase(JsonNode node) {
if (node.isValueNode()) {
Expand Down
86 changes: 42 additions & 44 deletions src/main/java/io/json/compare/matcher/JsonArrayMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
import io.json.compare.JsonComparator;
import io.json.compare.util.MessageUtil;

import java.util.HashSet;
import java.util.Set;
import java.util.*;

class JsonArrayMatcher extends AbstractJsonMatcher {

Expand All @@ -18,79 +17,78 @@ class JsonArrayMatcher extends AbstractJsonMatcher {
}

@Override
public void match() throws MatcherException {
public List<String> match() {
List<String> diffs = new ArrayList<>();

for (int i = 0; i < expected.size(); i++) {
JsonNode expElement = expected.get(i);
if (isJsonPathNode(expElement)) {
new JsonMatcher(expElement, actual, comparator, compareModes).match();
diffs.addAll(new JsonMatcher(expElement, actual, comparator, compareModes).match());
} else {
matchWithActualJsonArray(i, expElement, actual);
diffs.addAll(matchWithJsonArray(i, expElement, actual));
}
}
if (compareModes.contains(CompareMode.JSON_ARRAY_NON_EXTENSIBLE) && expected.size() < actual.size()) {
throw new MatcherException("Actual JSON ARRAY has extra elements");
diffs.add("Actual JSON ARRAY has extra elements");
}
return diffs;
}

private void matchWithActualJsonArray(int expPosition, JsonNode expElement, JsonNode actual) throws MatcherException {
private List<String> matchWithJsonArray(int expPosition, JsonNode expElement, JsonNode actualArray) {
List<String> diffs = new ArrayList<>();
UseCase useCase = getUseCase(expElement);
boolean found = false;
actualElementsLoop:
for (int j = 0; j < actual.size(); j++) {

for (int j = 0; j < actualArray.size(); j++) {
if (matchedPositions.contains(j)) {
continue;
}
if (compareModes.contains(CompareMode.JSON_ARRAY_STRICT_ORDER) && j != expPosition) {
continue;
}
List<String> elementDiffs;
switch (useCase) {
case MATCH:
JsonNode actElement = actual.get(j);
try {
new JsonMatcher(expElement, actElement, comparator, compareModes).match();
} catch (MatcherException e) {
JsonNode actElement = actualArray.get(j);
elementDiffs = new JsonMatcher(expElement, actElement, comparator, compareModes).match();
if (elementDiffs.isEmpty()) {
matchedPositions.add(j);
return Collections.emptyList();
} else {
if (compareModes.contains(CompareMode.JSON_ARRAY_STRICT_ORDER)) {
throw new MatcherException(String
.format("JSON ARRAY elements differ at position %s:\n%s", expPosition + 1,
MessageUtil.cropL(JSONCompare.prettyPrint(expElement))));
diffs.add(String.format("JSON ARRAY elements differ at position %s:\n%s\nDifferences:\n%s", expPosition + 1,
MessageUtil.cropL(JSONCompare.prettyPrint(expElement)), String.join("\n", elementDiffs)));
return diffs;
}
continue actualElementsLoop;
}
found = true;
matchedPositions.add(j);
break actualElementsLoop;
break;
case MATCH_ANY:
matchedPositions.add(j);
return;
return Collections.emptyList();
case DO_NOT_MATCH:
actElement = actual.get(j);
if (!areOfSameType(expElement, actElement)) {
continue actualElementsLoop;
}
try {
new JsonMatcher(expElement, actElement, comparator, compareModes).match();
} catch (MatcherException e) {
found = true;
break actualElementsLoop;
actElement = actualArray.get(j);
if (areOfSameType(expElement, actElement)) {
elementDiffs = new JsonMatcher(expElement, actElement, comparator, compareModes).match();
if (!elementDiffs.isEmpty()) {
diffs.add("Expected element from position " + (expPosition + 1)
+ " was FOUND:\n" + MessageUtil.cropL(JSONCompare.prettyPrint(expElement)));
return diffs;
}
}
break;
case DO_NOT_MATCH_ANY:
throw new MatcherException("Expected element from position " + (expPosition + 1)
+ " was FOUND:\n" + MessageUtil.cropL(JSONCompare.prettyPrint(expElement)));
diffs.add(String.format("Actual JSON array has extra elements.\nExpected condition %s from position %s means there" +
" should be no more actual elements other than the ones expected",
expElement, expPosition + 1));
return diffs;
}
}
if (!found && useCase == UseCase.MATCH) {
throw new MatcherException("Expected element from position " + (expPosition + 1) + " was NOT FOUND:\n"
+ MessageUtil.cropL(JSONCompare.prettyPrint(expElement)));
}
if (found && useCase == UseCase.DO_NOT_MATCH) {
throw new MatcherException("Expected element from position " + (expPosition + 1)
+ " was FOUND:\n" + MessageUtil.cropL(JSONCompare.prettyPrint(expElement)));
}
if (useCase == UseCase.MATCH_ANY) {
throw new MatcherException("Expected condition of type MATCH_ANY from position " + (expPosition + 1)
+ " was NOT MET. Actual Json Array has no extra elements:\n"
if (useCase == UseCase.MATCH) {
diffs.add("Expected element from position " + (expPosition + 1) + " was NOT FOUND:\n"
+ MessageUtil.cropL(JSONCompare.prettyPrint(expElement)));
} else if (useCase == UseCase.MATCH_ANY) {
diffs.add(String.format("Actual Json Array has no extra elements. Expected condition %s from position %s means there" +
" should be more actual elements", expElement, expPosition + 1));
}
return diffs;
}
}
20 changes: 12 additions & 8 deletions src/main/java/io/json/compare/matcher/JsonMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import io.json.compare.CompareMode;
import io.json.compare.JsonComparator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

public class JsonMatcher extends AbstractJsonMatcher {
Expand All @@ -13,20 +16,21 @@ public JsonMatcher(JsonNode expected, JsonNode actual, JsonComparator comparator
}

@Override
public void match() throws MatcherException {
public List<String> match() {
if (isJsonObject(expected) && isJsonObject(actual)) {
new JsonObjectMatcher(expected, actual, comparator, compareModes).match();
return new JsonObjectMatcher(expected, actual, comparator, compareModes).match();
} else if (isJsonArray(expected) && isJsonArray(actual)) {
new JsonArrayMatcher(expected, actual, comparator, compareModes).match();
return new JsonArrayMatcher(expected, actual, comparator, compareModes).match();
} else if (isValueNode(expected) && isValueNode(actual)) {
new JsonValueMatcher(expected, actual, comparator, compareModes).match();
return new JsonValueMatcher(expected, actual, comparator, compareModes).match();
} else if (isJsonPathNode(expected)) {
new JsonObjectMatcher(expected, actual, comparator, compareModes).match();
return new JsonObjectMatcher(expected, actual, comparator, compareModes).match();
} else if (isMissingNode(expected) && isMissingNode(actual)) {
//do nothing
return Collections.emptyList();
} else {
throw new MatcherException("Different JSON types: "
+ expected.getClass().getSimpleName() + " vs " + actual.getClass().getSimpleName());
List<String> diffs = new ArrayList<>();
diffs.add("Different JSON types: expected" + expected.getClass().getSimpleName() + " but got " + actual.getClass().getSimpleName());
return diffs;
}
}
}
59 changes: 32 additions & 27 deletions src/main/java/io/json/compare/matcher/JsonObjectMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ class JsonObjectMatcher extends AbstractJsonMatcher {
}

@Override
public void match() throws MatcherException {
public List<String> match() {
List<String> diffs = new ArrayList<>();

Iterator<Map.Entry<String, JsonNode>> it = expected.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> entry = it.next();
Expand All @@ -27,72 +29,75 @@ public void match() throws MatcherException {
Optional<String> jsonPathExpression = extractJsonPathExp(expectedSanitizedField);
List<Map.Entry<String, JsonNode>> candidateEntries = null;
if (!jsonPathExpression.isPresent()) {
candidateEntries = searchCandidateEntriesByField(expectedSanitizedField, actual);
candidateEntries = searchCandidatesByField(expectedSanitizedField, actual);
}
switch (useCase) {
case MATCH_ANY:
case MATCH:
if (!jsonPathExpression.isPresent()) {
if (candidateEntries.isEmpty()) {
throw new MatcherException(String.format("Field '%s' was not found or cannot be matched", expectedField));
diffs.add(String.format("Field '%s' was not found", expectedField));
} else {
diffs.addAll(matchWithCandidates(expectedSanitizedField, expectedValue, candidateEntries));
}
matchWithCandidateEntries(expectedSanitizedField, expectedValue, candidateEntries);
} else {
try {
new JsonPathMatcher(jsonPathExpression.get(), expectedValue, actual, comparator, compareModes).match();
diffs.addAll(new JsonPathMatcher(jsonPathExpression.get(), expectedValue, actual, comparator, compareModes).match());
} catch (PathNotFoundException e) {
throw new MatcherException(String.format("%s <- json path ('%s')", e.getMessage(), jsonPathExpression.get()));
diffs.add(String.format("json path ('%s') -> %s", jsonPathExpression.get(), e.getMessage()));
}
}
break;
case DO_NOT_MATCH_ANY:
case DO_NOT_MATCH:
if (!jsonPathExpression.isPresent()) {
if (!candidateEntries.isEmpty()) {
throw new MatcherException(String.format("Field '%s' was found", expectedField));
diffs.add(String.format("Field '%s' was found", expectedField));
}
} else {
try {
new JsonPathMatcher(jsonPathExpression.get(), expectedValue, actual, comparator, compareModes).match();
} catch (PathNotFoundException e) {
break;
}
throw new MatcherException(String.format("Json path '%s' was found", expectedField));
diffs.add(String.format("Json path '%s' was found", expectedField));
}
break;
}
}
if (compareModes.contains(CompareMode.JSON_OBJECT_NON_EXTENSIBLE) && expected.size() < actual.size()) {
throw new MatcherException("Actual JSON OBJECT has extra fields");
diffs.add("Actual JSON OBJECT has extra fields");
}
return diffs;
}

private void matchWithCandidateEntries(String expectedKey, JsonNode expectedValue, List<Map.Entry<String, JsonNode>> candidates) throws MatcherException {
private List<String> matchWithCandidates(String expectedField, JsonNode expectedValue, List<Map.Entry<String, JsonNode>> candidates) {
List<String> diffs = new ArrayList<>();

UseCase expectedValueUseCase = getUseCase(expectedValue);
for (ListIterator<Map.Entry<String, JsonNode>> it = candidates.listIterator(); it.hasNext(); ) {
Map.Entry<String, JsonNode> candidateEntry = it.next();

for (Map.Entry<String, JsonNode> candidateEntry : candidates) {
String candidateField = candidateEntry.getKey();

if (expectedValueUseCase == UseCase.MATCH_ANY) {
matchedFieldNames.add(candidateField);
break;
return Collections.emptyList();
}

JsonNode candidateValue = candidateEntry.getValue();
try {
new JsonMatcher(expectedValue, candidateValue, comparator, compareModes).match();
} catch (MatcherException e) {
if (it.hasNext()) {
continue;
} else {
throw new MatcherException(String.format("%s <- %s", e.getMessage(), expectedKey));
}
List<String> candidateDiffs = new JsonMatcher(expectedValue, candidateValue, comparator, compareModes).match();
if (candidateDiffs.isEmpty()) {
matchedFieldNames.add(candidateField);
return Collections.emptyList();
} else {
candidateDiffs.forEach(diff -> diffs.add(String.format("%s -> %s", expectedField, diff)));
}
matchedFieldNames.add(candidateField);
break;
}
return diffs;
}

private List<Map.Entry<String, JsonNode>> searchCandidateEntriesByField(String fieldName, JsonNode target) {
List<Map.Entry<String, JsonNode>> candidatesList = new ArrayList<>();
private List<Map.Entry<String, JsonNode>> searchCandidatesByField(String fieldName, JsonNode target) {
List<Map.Entry<String, JsonNode>> candidates = new ArrayList<>();
Iterator<Map.Entry<String, JsonNode>> it = target.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> entry = it.next();
Expand All @@ -101,9 +106,9 @@ private List<Map.Entry<String, JsonNode>> searchCandidateEntriesByField(String f
continue;
}
if (comparator.compareFields(fieldName, key)) {
candidatesList.add(entry);
candidates.add(entry);
}
}
return candidatesList;
return candidates;
}
}
18 changes: 9 additions & 9 deletions src/main/java/io/json/compare/matcher/JsonPathMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import io.json.compare.CompareMode;
import io.json.compare.JsonComparator;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

class JsonPathMatcher extends AbstractJsonMatcher {
Expand All @@ -26,14 +28,12 @@ class JsonPathMatcher extends AbstractJsonMatcher {
}

@Override
public void match() throws MatcherException {
JsonNode result = null;
try {
result = MAPPER.convertValue(PARSE_CONTEXT.parse(actual).read(jsonPath), JsonNode.class);
new JsonMatcher(expected, result, comparator, compareModes).match();
} catch (MatcherException e) {
throw new MatcherException(String.format("Expected json path result:\n%s\nBut got:\n%s\n\n%s <- json path ('%s')",
expected, result, e.getMessage(), jsonPath));
}
public List<String> match() {
List<String> diffs = new ArrayList<>();
JsonNode result = MAPPER.convertValue(PARSE_CONTEXT.parse(actual).read(jsonPath), JsonNode.class);
List<String> jsonPathDiffs = new JsonMatcher(expected, result, comparator, compareModes).match();
jsonPathDiffs.forEach(diff -> diffs.add(String.format("Json path ('%s') -> Expected json path result:\n%s\nBut got:\n%s\n\n%s",
jsonPath, expected, result, diff)));
return diffs;
}
}
Loading

0 comments on commit c691b4c

Please sign in to comment.