Skip to content

Commit

Permalink
Resolves dhis2#52 - JsonValue equality checks
Browse files Browse the repository at this point in the history
  • Loading branch information
jbee committed Feb 3, 2024
1 parent 298c557 commit 88288cf
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 3 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# ChangeLog

## [Unreleased] v1.1

**Features**
* Added: _json-patch_ support, see `JsonPatch`, `JsonPointer`
* Added: node level bulk modification API, see `JsonNode.Add`, `JsonNode.Remove`
* Added: test for same information `JsonValue#equivalentTo`
* Added: test for same definition (ignoring formatting) `JsonValue#identicalTo`

**Breaking Changes**

**Bugfixes**
4 changes: 1 addition & 3 deletions src/main/java/org/hisp/dhis/jsontree/JsonAbstractObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,7 @@ default Stream<Map.Entry<String, E>> entries() {
* @throws JsonTreeException in case this value is not an JSON object
*/
default List<String> names() {
List<String> names = new ArrayList<>();
keys().forEach( names::add );
return names;
return keys().toList();
}

/**
Expand Down
50 changes: 50 additions & 0 deletions src/main/java/org/hisp/dhis/jsontree/JsonValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;

Expand Down Expand Up @@ -295,6 +296,55 @@ default <T, V extends JsonValue> List<T> toListFromVarargs( Class<V> elementType
: List.of( toElement.apply( as( elementType ) ) );
}

/**
* The same information does not imply the value is identically defined.
* There can be differences in formatting, the order of object members or leading or tailing zeros for numbers.
* <p>
* Equivalence is always symmetric; if A is equivalent to B then B must also be equivalent to A.
*
* @param other the value to compare with
* @return true, if this value represents the same information, else false
* @since 1.1
*/
default boolean equivalentTo(JsonValue other) {
return equivalentTo( this, other, JsonValue::equivalentTo );
}

/**
* The two values only differ in formatting (whitespace outside of values).
* <p>
* All values that are identical are also {@link #equivalentTo(JsonValue)}.
* <p>
* Identical is always symmetric; if A is identical to B then B must also be identical to A.
*
* @param other the value to compare with
* @return true, if this value only differs in formatting from the other value, otherwise false
* @since 1.1
*/
default boolean identicalTo(JsonValue other) {
if (!equivalentTo( this, other, JsonValue::identicalTo )) return false;
if (isNumber()) return toJson().equals( other.toJson() );
if (!isObject()) return true;
// keys must be in same order
return asObject().names().equals( other.asObject().names() );
}

private static boolean equivalentTo(JsonValue a, JsonValue b, BiPredicate<JsonValue, JsonValue> compare ) {
if (a.type() != b.type()) return false;
if (a.isUndefined()) return true; // includes null
if (a.isString()) return a.as( JsonString.class ).string().equals( b.as( JsonString.class ).string() );
if (a.isBoolean()) return a.as(JsonBoolean.class).booleanValue() == b.as( JsonBoolean.class ).booleanValue();
if (a.isNumber()) return a.as( JsonNumber.class ).doubleValue() == b.as( JsonNumber.class ).doubleValue();
if (a.isArray()) {
JsonArray ar = a.as( JsonArray.class );
JsonArray br = b.as( JsonArray.class );
return ar.size() == br.size() && ar.indexes().allMatch( i -> compare.test( ar.get( i ), br.get( i ) ));
}
JsonObject ao = a.asObject();
JsonObject bo = b.asObject();
return ao.size() == bo.size() && ao.keys().allMatch( key -> compare.test( ao.get( key ), bo.get( key ) ) );
}

/**
* Access the node in the JSON document. This can be the low level API that is concerned with extraction by path.
* <p>
Expand Down
148 changes: 148 additions & 0 deletions src/test/java/org/hisp/dhis/jsontree/JsonValueIsEquivalentTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package org.hisp.dhis.jsontree;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Tests the {@link JsonValue#equivalentTo(JsonValue)} method.
*/
class JsonValueIsEquivalentTest {

@Test
void testEquivalentTo_Undefined_Undefined() {
JsonMixed root = JsonMixed.of( "{}" );
assertEquivalent( root.get( "foo" ), root.get( "bar" ) );
}

@Test
void testEquivalentTo_Undefined_NonUndefined() {
JsonValue undefined = JsonMixed.of( "{}" ).get( "foo" );
assertNotEquivalent( undefined, JsonValue.of( "null" ) );
assertNotEquivalent( undefined, JsonValue.of( "true" ) );
assertNotEquivalent( undefined, JsonValue.of( "false" ) );
assertNotEquivalent( undefined, JsonValue.of( "1" ) );
assertNotEquivalent( undefined, JsonValue.of( "\"1\"" ) );
assertNotEquivalent( undefined, JsonValue.of( "[]" ) );
assertNotEquivalent( undefined, JsonValue.of( "{}" ) );
}

@Test
void testEquivalentTo_Null_Null() {
assertEquivalent(JsonValue.of( "null" ), JsonValue.of( "null" ));
}

@Test
void testEquivalentTo_Null_NonNull() {
JsonValue nil = Json.ofNull();
assertNotEquivalent( nil, JsonMixed.of( "{}" ).get( 0 ) );
assertNotEquivalent( nil, JsonValue.of( "true" ) );
assertNotEquivalent( nil, JsonValue.of( "false" ) );
assertNotEquivalent( nil, JsonValue.of( "1" ) );
assertNotEquivalent( nil, JsonValue.of( "\"1\"" ) );
assertNotEquivalent( nil, JsonValue.of( "[]" ) );
assertNotEquivalent( nil, JsonValue.of( "{}" ) );
}

@Test
void testEquivalentTo_String_String() {
assertEquivalent(JsonValue.of( "\"hello\"" ), JsonValue.of( "\"hello\"" ));
assertNotEquivalent(JsonValue.of( "\"hello you\"" ), JsonValue.of( "\"hello\"" ));
}

@Test
void testEquivalentTo_String_NonString() {
assertNotEquivalent(JsonValue.of( "\"null\"" ), JsonValue.of( "null" ));
assertNotEquivalent(JsonValue.of( "\"true\"" ), JsonValue.of( "true" ));
assertNotEquivalent(JsonValue.of( "\"false\"" ), JsonValue.of( "false" ));
}

@Test
void testEquivalentTo_Boolean_Boolean() {
assertEquivalent(JsonValue.of( "true" ), JsonValue.of( "true" ));
assertEquivalent(JsonValue.of( "false" ), JsonValue.of( "false" ));
assertNotEquivalent(JsonValue.of( "true" ), JsonValue.of( "false" ));
}

@Test
void testEquivalentTo_Number_Number() {
assertEquivalent(JsonValue.of( "1" ), JsonValue.of( "1" ));
assertEquivalent(JsonValue.of( "1.0" ), JsonValue.of( "1.0" ));
assertEquivalent(JsonValue.of( "1" ), JsonValue.of( "1.0" ));
assertNotEquivalent(JsonValue.of( "2" ), JsonValue.of( "2.5" ));
}

@Test
void testEquivalentTo_Array_Array() {
assertEquivalent(JsonValue.of( "[]" ), JsonValue.of( "[ ]" ));
assertEquivalent(JsonValue.of( "[1]" ), JsonValue.of( "[ 1 ]" ));
assertEquivalent(JsonValue.of( "[1,2]" ), JsonValue.of( "[ 1,2 ]" ));
assertEquivalent(JsonValue.of( "[1,[2]]" ), JsonValue.of( "[ 1,[ 2] ]" ));
assertNotEquivalent(JsonValue.of( "[2,1]" ), JsonValue.of( "[1,2 ]" ));
}

@Test
void testEquivalentTo_Object_Object() {
assertEquivalent( JsonValue.of("""
{}"""), JsonValue.of("""
{}""" ));
assertEquivalent( JsonValue.of("""
{"a": "b", "c": 4}"""), JsonValue.of("""
{"c": 4, "a":"b"}""" ));
assertNotEquivalent( JsonValue.of("""
{"a": "b", "c": 4}"""), JsonValue.of("""
{"c": 3, "a":"b"}""" ));
assertNotEquivalent( JsonValue.of("""
{"a": "b", "c": 4}"""), JsonValue.of("""
{"a":"b", "c": 4, "d": null}""" ));
}

@Test
void testEquivalentTo_Mixed() {
assertEquivalent( JsonValue.of("""
{"x": 10, "c": [4, {"foo": "bar", "y": 20}]}"""), JsonValue.of("""
{"c": [4, {"y":20, "foo": "bar"}], "x":10}""" ));
}

@Test
void testIdenticalTo_Number() {
assertIdentical( JsonValue.of( "1"), JsonValue.of( "1") );
assertIdentical( JsonValue.of( "1.0"), JsonValue.of( "1.0") );
assertEquivalentButNotIdentical( JsonValue.of( "1"), JsonValue.of( "1.0") );
}

@Test
void testIdenticalTo_Object() {
assertIdentical( JsonValue.of("""
{"a": "b", "c": 4}"""), JsonValue.of("""
{"a":"b","c":4}""" ));
assertEquivalentButNotIdentical( JsonValue.of("""
{"a": "b", "c": 4}"""), JsonValue.of("""
{"c":4, "a":"b"}""" ));
assertEquivalentButNotIdentical( JsonValue.of("""
{"a": "b", "c": [{},{"x": 1, "y": 1}]}"""), JsonValue.of("""
{"a": "b", "c": [{},{"y": 1, "x": 1}]}""" ));
}

private static void assertEquivalent(JsonValue a, JsonValue b) {
assertTrue( a.equivalentTo( b ) );
assertTrue( b.equivalentTo( a ) );
}

private static void assertNotEquivalent(JsonValue a, JsonValue b) {
assertFalse( a.equivalentTo( b ) );
assertFalse( b.equivalentTo( a ) );
}

private static void assertIdentical(JsonValue a, JsonValue b) {
assertTrue( a.identicalTo( b ) );
assertTrue( b.identicalTo( a ) );
}

private static void assertEquivalentButNotIdentical(JsonValue a, JsonValue b) {
assertEquivalent( a, b );
assertFalse( a.identicalTo( b ) );
assertFalse( b.identicalTo( a ) );
}
}

0 comments on commit 88288cf

Please sign in to comment.