Skip to content

Commit

Permalink
Merge pull request #4 from ekino/hotfix/arrays_simple_values_with_val…
Browse files Browse the repository at this point in the history
…idators

[FIX] Handle validators in arrays with simple values
  • Loading branch information
leomillon authored Aug 5, 2019
2 parents a54849d + 000b122 commit 921a80a
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 0 deletions.
201 changes: 201 additions & 0 deletions jcv-core/src/main/java/com/ekino/oss/jcv/core/JsonComparator.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@
*/
package com.ekino.oss.jcv.core;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import com.ekino.oss.jcv.core.validator.ValidatorTemplateManager;
import org.json.JSONArray;
Expand All @@ -24,6 +31,7 @@
import org.skyscreamer.jsonassert.comparator.DefaultComparator;
import org.skyscreamer.jsonassert.comparator.JSONComparator;

import static java.util.stream.Collectors.*;
import static org.skyscreamer.jsonassert.comparator.JSONCompareUtil.*;

/**
Expand Down Expand Up @@ -139,4 +147,197 @@ private static boolean isUsableAsUniqueKey(String candidate, JSONArray array) th
}
return true;
}

@Override
protected void compareJSONArrayOfSimpleValues(String key, JSONArray expected, JSONArray actual, JSONCompareResult result) throws JSONException {
List<Object> expectedElements = jsonArrayToList(expected);
List<Object> actualElements = jsonArrayToList(actual);

List<ExpectedElement> parsedExpectedElements = parseExpectedElements(key, expectedElements);

if (parsedExpectedElements.stream().noneMatch(ExpectedElement::hasCustomization)) {
super.compareJSONArrayOfSimpleValues(key, expected, actual, result);
return;
}

Set<Integer> actualValueMatchedIndexes = new HashSet<>();
Map<ExpectedElement, Optional<ActualElement>> matchingByValue = getMatchingByValue(parsedExpectedElements, actualElements, actualValueMatchedIndexes);
Map<ExpectedElement, Collection<ActualElement>> matchingByValidator = getExpectedElementCollectionMap(parsedExpectedElements, key, actualElements, actualValueMatchedIndexes);

long totalMatched = matchingByValue.values().stream().filter(Optional::isPresent).count()
+ matchingByValidator.values().stream().flatMap(Collection::stream).distinct().count();

if (totalMatched != actualElements.size()) {

Map<ExpectedElement, Collection<ActualElement>> allMatches = Stream.concat(
matchingByValue.entrySet().stream()
.map(entry -> new AbstractMap.SimpleEntry<ExpectedElement, Collection<ActualElement>>(entry.getKey(), entry.getValue().map(Collections::singletonList).orElseGet(Collections::emptyList)))
.map(it -> (Map.Entry<ExpectedElement, Collection<ActualElement>>) it),
matchingByValidator.entrySet().stream()
)
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));

Set<Integer> allMatchedActualIndexes = allMatches.values().stream()
.flatMap(Collection::stream)
.map(ActualElement::getIndex)
.collect(toSet());

IntStream.range(0, actualElements.size())
.filter(it -> !allMatchedActualIndexes.contains(it))
.forEachOrdered(actualIndex -> result.unexpected(key + "[" + actualIndex + "]", actualElements.get(actualIndex)));

String detailedMatchingDebugMessage = allMatches
.entrySet()
.stream()
.sorted(Comparator.comparing(entry -> entry.getKey().getIndex()))
.map(entry -> {
ExpectedElement expectedElt = entry.getKey();
String matchedElements = entry.getValue().stream()
.map(it -> "[" + it.getIndex() + "] -> " + it.getValue())
.collect(joining(",", "[", "]"));
return key + "[" + expectedElt.getIndex() + "] -> " + expectedElt.getKey() + " matched with: " + matchedElements;
})
.collect(joining("\n"));
result.fail(detailedMatchingDebugMessage);
}
}

private List<ExpectedElement> parseExpectedElements(String key, List<Object> expectedElements) {
List<ExpectedElement> parsedExpectedElements = new ArrayList<>();
for (int i = 0; i < expectedElements.size(); i++) {
Object expectedElement = expectedElements.get(i);
parsedExpectedElements.add(new ExpectedElement(
i,
expectedElement,
validators.stream()
.filter(it -> it.getContextMatcher().matches(key, expectedElement, null))
.findFirst()
.map(toMatcher())
.map(it -> new Customization(IGNORED_PATH, it))
.orElse(null)
));
}
return parsedExpectedElements;
}

private Map<ExpectedElement, Optional<ActualElement>> getMatchingByValue(List<ExpectedElement> parsedExpectedElements,
List<Object> actualElements,
Set<Integer> actualValueMatchedIndexes) {
return parsedExpectedElements.stream()
.filter(it -> !it.hasCustomization())
.map(expectedElement -> {
for (int i = 0; i < actualElements.size(); i++) {
if (actualValueMatchedIndexes.contains(i)) {
continue;
}
Object actualElement = actualElements.get(i);
if (expectedElement.getKey().equals(actualElement)) {
actualValueMatchedIndexes.add(i);
return new AbstractMap.SimpleEntry<>(expectedElement, Optional.of(new ActualElement(i, actualElement)));
}
}
return new AbstractMap.SimpleEntry<>(expectedElement, Optional.<ActualElement>empty());
})
.collect(toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
}

private Map<ExpectedElement, Collection<ActualElement>> getExpectedElementCollectionMap(List<ExpectedElement> parsedExpectedElements,
String key,
List<Object> actualElements,
Set<Integer> actualValueMatchedIndexes) {
return parsedExpectedElements.stream()
.filter(ExpectedElement::hasCustomization)
.map(expectedElement -> {
Set<ActualElement> matched = new HashSet<>();
for (int i = 0; i < actualElements.size(); i++) {
if (actualValueMatchedIndexes.contains(i)) {
continue;
}
Object actualElement = actualElements.get(i);
try {
if (expectedElement.getCustomization().matches(key, actualElement, expectedElement.getKey(), new JSONCompareResult())) {
matched.add(new ActualElement(i, actualElement));
}
} catch (ValueMatcherException e) {
// Do nothing
}
}
return new AbstractMap.SimpleEntry<>(expectedElement, matched);
})
.collect(toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
}

private static class ExpectedElement {
private final Integer index;
private final Object key;
private final Customization customization;

private ExpectedElement(Integer index, Object key, Customization customization) {
this.index = index;
this.key = key;
this.customization = customization;
}

Integer getIndex() {
return index;
}

Object getKey() {
return key;
}

Customization getCustomization() {
return customization;
}

boolean hasCustomization() {
return customization != null;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExpectedElement that = (ExpectedElement) o;
return Objects.equals(index, that.index) &&
Objects.equals(key, that.key);
}

@Override
public int hashCode() {
return Objects.hash(index, key);
}
}

private static class ActualElement {
private final Integer index;
private final Object value;

ActualElement(Integer index, Object value) {
this.index = index;
this.value = value;
}

Integer getIndex() {
return index;
}

Object getValue() {
return value;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ActualElement that = (ActualElement) o;
return Objects.equals(index, that.index) &&
Objects.equals(value, that.value);
}

@Override
public int hashCode() {
return Objects.hash(index, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import assertk.assertions.startsWith
import assertk.tableOf
import com.ekino.oss.jcv.core.validator.Validators
import org.apache.commons.io.IOUtils
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.skyscreamer.jsonassert.JSONCompare
import org.skyscreamer.jsonassert.JSONCompareMode
Expand Down Expand Up @@ -325,4 +326,97 @@ Expected: {#date_time_format:iso_instant#}
}
}
}

/**
* Tests about array with simple values assertions with validators as expected elements in non-strict order.
*/
@Nested
inner class ArrayWithSimpleValuesTest {

@Test
fun `should handle validators in arrays with simple values`() {

compare(
loadJson("array_with_simple_values/test_actual.json"),
loadJson("array_with_simple_values/test_expected.json")
) {
assertAll {
assertThat(it.passed()).isTrue()
assertThat(it.message).isNullOrEmpty()
}
}
}

@Test
fun `should throw an error if element count does not match between the two arrays`() {

compare(
"""
{
"some_array": [
"value_1",
"value_2",
"value_3"
]
}
""".trimIndent(),
"""
{
"some_array": [
"{#contains:value_#}"
]
}
""".trimIndent()
) {
assertAll {
assertThat(it.passed()).isFalse()
assertThat(it.message).isEqualTo("some_array[]: Expected 1 values but got 3")
}
}
}

@Test
fun `should throw a detailed error if some elements did not match`() {

compare(
"""
{
"some_array": [
"cd820a36-aa32-42ea-879d-293ba5f3c1e5",
"value_1",
"hello",
"value_3",
"839ceac0-2e60-4405-b27c-db2ac753d809"
]
}
""".trimIndent(),
"""
{
"some_array": [
"value_1",
"{#uuid#}",
"{#uuid#}",
"value_2",
"{#contains:value_#}"
]
}
""".trimIndent()
) {
assertAll {
assertThat(it.passed()).isFalse()
assertThat(it.message).isEqualTo(
"""
some_array[2]
Unexpected: hello
; some_array[0] -> value_1 matched with: [[1] -> value_1]
some_array[1] -> {#uuid#} matched with: [[0] -> cd820a36-aa32-42ea-879d-293ba5f3c1e5,[4] -> 839ceac0-2e60-4405-b27c-db2ac753d809]
some_array[2] -> {#uuid#} matched with: [[0] -> cd820a36-aa32-42ea-879d-293ba5f3c1e5,[4] -> 839ceac0-2e60-4405-b27c-db2ac753d809]
some_array[3] -> value_2 matched with: []
some_array[4] -> {#contains:value_#} matched with: [[3] -> value_3]
""".trimIndent()
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"array_1": [
"value_1",
"value_2",
"value_3"
],
"array_2": [
"cd820a36-aa32-42ea-879d-293ba5f3c1e5",
"value_1",
"hello",
"value_3",
"839ceac0-2e60-4405-b27c-db2ac753d809"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"array_1": [
"value_3",
"value_1",
"value_2"
],
"array_2": [
"value_1",
"{#uuid#}",
"{#uuid#}",
"{#starts_with:hel#}",
"{#contains:value_#}"
]
}

0 comments on commit 921a80a

Please sign in to comment.