All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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> customMatching = new HashMap<>(); for (Entry> entry : matcherConfiguration.getCustomMatchers().entrySet()) { Object object = actual == null ? null : findBeanAt(entry.getKey(), actual); customMatching.put(object, matcherConfiguration.getCustomMatchers().get(entry.getKey())); } for (Entry> entry : customMatching.entrySet()) { Matcher matcher = entry.getValue(); Object object = entry.getKey(); if (!matcher.matches(object)) { appendFieldPath(matcher, mismatchDescription); matcher.describeMismatch(object, mismatchDescription); appendFieldJsonSnippet(object, mismatchDescription, gson); result = false; } } return result; } private void appendFieldJsonSnippet(Object actual, Description mismatchDescription, Gson gson) { JsonElement jsonTree = gson.toJsonTree(actual); if (!jsonTree.isJsonPrimitive() && !jsonTree.isJsonNull()) { mismatchDescription.appendText("\n" + gson.toJson(actual)); } } private void appendFieldPath(Matcher matcher, Description mismatchDescription) { for (Entry> entry : matcherConfiguration.getCustomMatchers().entrySet()) { if (entry.getValue().equals(matcher)) { mismatchDescription.appendText(entry.getKey()).appendText(" "); } } } @Override public JsonMatcher skipCircularReferenceCheck(Function matcher) { matcherConfiguration.addSkipCircularReferenceChecker(matcher); return this; } @SuppressWarnings({"unchecked", "varargs"}) @Override public final JsonMatcher skipCircularReferenceCheck(Function matcher, Function... matchers) { matcherConfiguration.addSkipCircularReferenceChecker(matcher); matcherConfiguration.addSkipCircularReferenceChecker(matchers); return this; } @Override public JsonMatcher sortField(Matcher fieldNamePattern) { matcherConfiguration.addPatternToSort(fieldNamePattern); return this; } @SuppressWarnings({"varargs", "unchecked"}) @SafeVarargs @Override public final JsonMatcher sortField(Matcher... fieldNamePatterns) { matcherConfiguration.addPatternToSort(fieldNamePatterns); return this; } @Override public JsonMatcher sortField(String fieldPath) { matcherConfiguration.addPathToSort(fieldPath); return this; } @Override public JsonMatcher sortField(String... fieldPaths) { matcherConfiguration.addPathToSort(fieldPaths); return this; } @Override public String toString() { if (fileNameWithPath == null) { return "JsonMatcher"; } return "JsonMatcher for " + fileStoreMatcherUtils.getApproved(fileNameWithPath); } private static class Either { private JsonElement parsedContent; private String originalContent; public Either(JsonElement parsedContent) { this.parsedContent = parsedContent; this.originalContent = null; } public Either(String originalContent) { this.originalContent = originalContent; this.parsedContent = null; } boolean isParsedJson() { return originalContent == null; } public JsonElement getParsedContent() { return parsedContent; } public String getOriginalContent() { return originalContent; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy