![JAR search and dependency download from the Maven repository](/logo.png)
com.github.karsaig.approvalcrest.matcher.JsonMatcher Maven / Gradle / Ivy
The newest version!
package com.github.karsaig.approvalcrest.matcher;
import com.github.karsaig.approvalcrest.FileMatcherConfig;
import com.github.karsaig.approvalcrest.MatcherConfiguration;
import com.github.karsaig.approvalcrest.matcher.file.AbstractDiagnosingFileMatcher;
import com.github.karsaig.approvalcrest.matcher.file.FileStoreMatcherUtils;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.json.JSONException;
import org.skyscreamer.jsonassert.JSONAssert;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.github.karsaig.approvalcrest.BeanFinder.findBeanAt;
import static com.github.karsaig.approvalcrest.CyclicReferenceDetector.getClassesWithCircularReferences;
import static com.github.karsaig.approvalcrest.FieldsIgnorer.MARKER;
import static com.github.karsaig.approvalcrest.FieldsIgnorer.applySorting;
import static com.github.karsaig.approvalcrest.FieldsIgnorer.findPaths;
import static com.github.karsaig.approvalcrest.FieldsIgnorer.sortJsonFields;
/**
*
* Matcher for asserting expected DTOs. Searches for an approved JSON file in
* the same directory as the test file:
*
* - If found, the matcher will assert the contents of the JSON file to the actual object,
* which is serialized to a JSON String.
* - If not found, a non-approved JSON file is created, that must be
* verified and renamed to "*-approved.json" by the developer.
*
* The files and directories are hashed with SHA-1 algorithm by default to avoid too long file
* and path names.
* These are generated in the following way:
*
* - the directory name is the first {@value #NUM_OF_HASH_CHARS} characters of the hashed class name.
* - the file name is the first {@value #NUM_OF_HASH_CHARS} characters of the hashed test method name.
*
*
* This default behaviour can be overridden by using the {@link #withFileName(String)} for
* custom file name and {@link #withPathName(String)} for custom path.
*
*
* @author Andras_Gyuro
*/
public class JsonMatcher extends AbstractDiagnosingFileMatcher> implements CustomisableMatcher> {
private static final Pattern MARKER_PATTERN = Pattern.compile(MARKER);
private final MatcherConfiguration matcherConfiguration = new MatcherConfiguration();
private final Set> circularReferenceTypes = new HashSet<>();
private Either expected;
private GsonConfiguration configuration;
public JsonMatcher(TestMetaInformation testMetaInformation, FileMatcherConfig fileMatcherConfig) {
super(testMetaInformation, fileMatcherConfig, new FileStoreMatcherUtils("json", fileMatcherConfig));
}
@Override
public void describeTo(Description description) {
Gson gson = GsonProvider.gson(matcherConfiguration, circularReferenceTypes, configuration);
if (expected.isParsedJson()) {
description.appendText(filterJson(gson, expected.getParsedContent(), true));
} else {
description.appendText(expected.getOriginalContent());
}
for (String fieldPath : matcherConfiguration.getCustomMatchers().keySet()) {
description.appendText("\nand ").appendText(fieldPath).appendText(" ")
.appendDescriptionOf(matcherConfiguration.getCustomMatchers().get(fieldPath));
}
}
@Override
public JsonMatcher ignoring(String fieldPath) {
matcherConfiguration.addPathToIgnore(fieldPath);
return this;
}
@Override
public JsonMatcher ignoring(Class> clazz) {
matcherConfiguration.addTypeToIgnore(clazz);
return this;
}
@Override
public JsonMatcher ignoring(Matcher fieldNamePattern) {
matcherConfiguration.addPatternToIgnore(fieldNamePattern);
return this;
}
@SuppressWarnings({"varargs", "unchecked"})
@SafeVarargs
@Override
public final JsonMatcher ignoring(Matcher... fieldNamePatterns) {
matcherConfiguration.addPatternToIgnore(fieldNamePatterns);
return this;
}
@Override
public JsonMatcher with(String fieldPath, Matcher matcher) {
ignoring(fieldPath);
matcherConfiguration.addCustomMatcher(fieldPath, matcher);
return this;
}
@Override
public JsonMatcher withGsonConfiguration(GsonConfiguration configuration) {
this.configuration = configuration;
return this;
}
@Override
protected boolean matches(Object actual, Description mismatchDescription) {
boolean matches = false;
circularReferenceTypes.addAll(getClassesWithCircularReferences(actual, matcherConfiguration));
init();
Gson gson = GsonProvider.gson(matcherConfiguration, circularReferenceTypes, configuration);
if (createNotApprovedFileIfNotExists(actual, gson)
&& fileMatcherConfig.isPassOnCreateEnabled()) {
return true;
}
initExpectedFromFile();
if (areCustomMatchersMatching(actual, mismatchDescription, gson)) {
String expectedJson = expected.getOriginalContent();
if (expected.isParsedJson()) {
expectedJson = filterJson(gson, expected.getParsedContent(), fileMatcherConfig.isSortInputFile());
}
JsonElement actualJsonElement = getAsJsonElement(gson, actual);
if (actual == null) {
matches = appendMismatchDescription(mismatchDescription, expectedJson, "null", "actual was null");
} else {
String actualJson = filterJson(gson, actualJsonElement, true);
matches = assertEquals(expectedJson, actualJson, mismatchDescription);
if (!matches) {
matches = handleInPlaceOverwrite(actual, gson);
}
}
} else {
matches = handleInPlaceOverwrite(actual, gson);
}
return matches;
}
@Override
public JsonMatcher ignoring(String... fieldPaths) {
matcherConfiguration.addPathToIgnore(fieldPaths);
return this;
}
@Override
public JsonMatcher ignoring(Class>... clazzs) {
matcherConfiguration.addTypeToIgnore(clazzs);
return this;
}
private boolean handleInPlaceOverwrite(Object actual, Gson gson) {
if (fileMatcherConfig.isOverwriteInPlaceEnabled()) {
overwriteApprovedFile(actual, gson);
return true;
}
return false;
}
private JsonElement getAsJsonElement(Gson gson, Object object) {
JsonElement result;
if (object instanceof String) {
result = JsonParser.parseString((String) object);
} else {
result = gson.toJsonTree(object);
}
return result;
}
private void initExpectedFromFile() {
expected = getExpectedFromFile(fileContent -> {
try {
return new Either(JsonParser.parseString(fileContent));
} catch (Exception e) {
return new Either(fileContent);
}
});
}
private String filterJson(Gson gson, JsonElement jsonElement, boolean sortFile) {
Set set = new HashSet<>(matcherConfiguration.getPathsToIgnore());
JsonElement filteredJson = findPaths(jsonElement, set);
filterByFieldMatchers(filteredJson, matcherConfiguration.getPatternsToIgnore());
sortJsonFields(filteredJson, sortFile);
applySorting(filteredJson, matcherConfiguration.getPathsToSort(), matcherConfiguration.getPatternsToSort(), sortFile);
return removeSetMarker(gson.toJson(filteredJson));
}
private void filterByFieldMatchers(JsonElement jsonElement, List> matchers) {
if (jsonElement != null && !matchers.isEmpty() && !jsonElement.isJsonNull()) {
filterFieldsByFieldMatchers(jsonElement, matchers);
}
}
private void filterFieldsByFieldMatchers(JsonElement jsonElement, List> matchers) {
if (jsonElement.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
List fieldsToRemove = getFieldsToRemove(jsonObject.keySet(), matchers);
fieldsToRemove.forEach(jsonObject::remove);
jsonObject.entrySet().forEach(je -> filterFieldsByFieldMatchers(je.getValue(), matchers));
} else if (jsonElement.isJsonArray()) {
JsonArray jsonArray = jsonElement.getAsJsonArray();
Iterator iterator = jsonArray.iterator();
while (iterator.hasNext()) {
filterFieldsByFieldMatchers(iterator.next(), matchers);
}
}
}
private List getFieldsToRemove(Set fieldNames, List> matchers) {
return fieldNames.stream().filter(fn -> anyMatchesFieldName(fn, matchers)).collect(Collectors.toList());
}
private boolean anyMatchesFieldName(String fieldName, List> matchers) {
for (Matcher actual : matchers) {
if (actual.matches(fieldName)) {
return true;
}
}
return false;
}
private boolean assertEquals(String expectedJson, String actualJson,
Description mismatchDescription) {
try {
JSONAssert.assertEquals(expectedJson, actualJson, true);
} catch (AssertionError e) {
return appendMismatchDescription(mismatchDescription, expectedJson, actualJson, getAssertMessage(fileStoreMatcherUtils, e));
} catch (JSONException e) {
return appendMismatchDescription(mismatchDescription, expectedJson, actualJson, getAssertMessage(fileStoreMatcherUtils, e));
}
return true;
}
private String removeSetMarker(String json) {
return MARKER_PATTERN.matcher(json).replaceAll("");
}
private boolean createNotApprovedFileIfNotExists(Object toApprove, Gson gson) {
return createNotApprovedFileIfNotExists(toApprove, () -> serializeToJson(toApprove, gson));
}
private void overwriteApprovedFile(Object actual, Gson gson) {
overwriteApprovedFile(actual, () -> serializeToJson(actual, gson));
}
private String serializeToJson(Object toApprove, Gson gson) {
JsonElement actualJsonElement = getAsJsonElement(gson, toApprove);
return filterJson(gson, actualJsonElement, true);
}
private boolean areCustomMatchersMatching(Object actual, Description mismatchDescription,
Gson gson) {
boolean result = true;
Map
© 2015 - 2025 Weber Informatics LLC | Privacy Policy