Skip to content

Commit cd30df3

Browse files
refactor: replace icecave/parity with custom deep comparator (#803)
After considering the options and rethinking the problem we are trying to solve I came top the conclusion we don't need a external library which can handle PHP class equality. Since we are working with JSON we only have scalar types (boolean, float, integer and string) and two collections types array and `stdClass`. This made it simple to write our own deep comparison. Fixes #753
1 parent 848c9ee commit cd30df3

File tree

6 files changed

+165
-10
lines changed

6 files changed

+165
-10
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- only check minProperties or maxProperties on objects ([#802](https://github.com/jsonrainbow/json-schema/pull/802))
1111
- replace filter_var for uri and uri-reference to userland code to be RFC 3986 compliant ([#800](https://github.com/jsonrainbow/json-schema/pull/800))
1212

13+
## Changed
14+
- replace icecave/parity with custom deep comparator ([#803](https://github.com/jsonrainbow/json-schema/pull/803))
15+
-
1316
## [6.2.1] - 2025-03-06
1417
### Fixed
1518
- allow items: true to pass validation ([#801](https://github.com/jsonrainbow/json-schema/pull/801))

composer.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@
2929
"require": {
3030
"php": "^7.2 || ^8.0",
3131
"ext-json": "*",
32-
"marc-mabe/php-enum":"^4.0",
33-
"icecave/parity": "^3.0"
32+
"marc-mabe/php-enum":"^4.0"
3433
},
3534
"require-dev": {
3635
"friendsofphp/php-cs-fixer": "3.3.0",

src/JsonSchema/Constraints/ConstConstraint.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
namespace JsonSchema\Constraints;
1313

14-
use Icecave\Parity\Parity;
1514
use JsonSchema\ConstraintError;
1615
use JsonSchema\Entity\JsonPointer;
16+
use JsonSchema\Tool\DeepComparer;
1717

1818
/**
1919
* The ConstConstraint Constraints, validates an element against a constant value
@@ -36,13 +36,13 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
3636
$type = gettype($element);
3737
$constType = gettype($const);
3838

39-
if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type == 'array' && $constType == 'object') {
40-
if (Parity::isEqualTo((object) $element, $const)) {
39+
if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type === 'array' && $constType === 'object') {
40+
if (DeepComparer::isEqual((object) $element, $const)) {
4141
return;
4242
}
4343
}
4444

45-
if (Parity::isEqualTo($element, $const)) {
45+
if (DeepComparer::isEqual($element, $const)) {
4646
return;
4747
}
4848

src/JsonSchema/Constraints/EnumConstraint.php

+5-4
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
namespace JsonSchema\Constraints;
1313

14-
use Icecave\Parity\Parity;
1514
use JsonSchema\ConstraintError;
1615
use JsonSchema\Entity\JsonPointer;
16+
use JsonSchema\Tool\DeepComparer;
1717

1818
/**
1919
* The EnumConstraint Constraints, validates an element against a given set of possibilities
@@ -36,14 +36,15 @@ public function check(&$element, $schema = null, ?JsonPointer $path = null, $i =
3636

3737
foreach ($schema->enum as $enum) {
3838
$enumType = gettype($enum);
39-
if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type == 'array' && $enumType == 'object') {
40-
if (Parity::isEqualTo((object) $element, $enum)) {
39+
40+
if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type === 'array' && $enumType === 'object') {
41+
if (DeepComparer::isEqual((object) $element, $enum)) {
4142
return;
4243
}
4344
}
4445

4546
if ($type === gettype($enum)) {
46-
if (Parity::isEqualTo($element, $enum)) {
47+
if (DeepComparer::isEqual($element, $enum)) {
4748
return;
4849
}
4950
}

src/JsonSchema/Tool/DeepComparer.php

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Tool;
6+
7+
class DeepComparer
8+
{
9+
/**
10+
* @param mixed $left
11+
* @param mixed $right
12+
*/
13+
public static function isEqual($left, $right): bool
14+
{
15+
$isLeftScalar = is_scalar($left);
16+
$isRightScalar = is_scalar($right);
17+
18+
if ($isLeftScalar && $isRightScalar) {
19+
return $left === $right;
20+
}
21+
22+
if ($isLeftScalar !== $isRightScalar) {
23+
return false;
24+
}
25+
26+
if (is_array($left) && is_array($right)) {
27+
return self::isArrayEqual($left, $right);
28+
}
29+
30+
if ($left instanceof \stdClass && $right instanceof \stdClass) {
31+
return self::isArrayEqual((array) $left, (array) $right);
32+
}
33+
34+
return false;
35+
}
36+
37+
/**
38+
* @param array<string|int, mixed> $left
39+
* @param array<string|int, mixed> $right
40+
*/
41+
private static function isArrayEqual(array $left, array $right): bool
42+
{
43+
if (count($left) !== count($right)) {
44+
return false;
45+
}
46+
foreach ($left as $key => $value) {
47+
if (!array_key_exists($key, $right)) {
48+
return false;
49+
}
50+
51+
if (!self::isEqual($value, $right[$key])) {
52+
return false;
53+
}
54+
}
55+
56+
return true;
57+
}
58+
}

tests/Tool/DeepComparerTest.php

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Tests\Tool;
6+
7+
use JsonSchema\Tool\DeepComparer;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class DeepComparerTest extends TestCase
11+
{
12+
/**
13+
* @dataProvider equalDataProvider
14+
*/
15+
public function testComparesDeepEqualForEqualLeftAndRight($left, $right): void
16+
{
17+
self::assertTrue(DeepComparer::isEqual($left, $right));
18+
}
19+
20+
/**
21+
* @dataProvider notEqualDataProvider
22+
*/
23+
public function testComparesDeepEqualForNotEqualLeftAndRight($left, $right): void
24+
{
25+
self::assertFalse(DeepComparer::isEqual($left, $right));
26+
}
27+
28+
public function equalDataProvider(): \Generator
29+
{
30+
yield 'Boolean true' => [true, true];
31+
yield 'Boolean false' => [false, false];
32+
33+
yield 'Integer one' => [1, 1];
34+
yield 'Integer INT MIN' => [PHP_INT_MIN, PHP_INT_MIN];
35+
yield 'Integer INT MAX' => [PHP_INT_MAX, PHP_INT_MAX];
36+
37+
yield 'Float PI' => [M_PI, M_PI];
38+
39+
yield 'String' => ['hello world!', 'hello world!'];
40+
41+
yield 'array of integer' => [[1, 2, 3], [1, 2, 3]];
42+
yield 'object of integer' => [(object) [1, 2, 3], (object) [1, 2, 3]];
43+
44+
yield 'nested objects of integers' => [
45+
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60)],
46+
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60)],
47+
];
48+
}
49+
50+
public function notEqualDataProvider(): \Generator
51+
{
52+
yield 'Boolean true/false' => [true, false];
53+
54+
yield 'Integer one/two' => [1, 2];
55+
yield 'Integer INT MIN/MAX' => [PHP_INT_MIN, PHP_INT_MAX];
56+
57+
yield 'Float PI/' => [M_PI, M_E];
58+
59+
yield 'String' => ['hello world!', 'hell0 w0rld!'];
60+
61+
yield 'array of integer with smaller left side' => [[1, 3], [1, 2, 3]];
62+
yield 'array of integer with smaller right side' => [[1, 2, 3], [1, 3]];
63+
yield 'object of integer with smaller left side' => [(object) [1, 3], (object) [1, 2, 3]];
64+
yield 'object of integer with smaller right side' => [(object) [1, 2, 3], (object) [1, 3]];
65+
66+
yield 'nested objects of integers with different left hand side' => [
67+
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60, 2)],
68+
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60)],
69+
];
70+
yield 'nested objects of integers with different right hand side' => [
71+
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60)],
72+
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60, 2)],
73+
];
74+
75+
$options = [
76+
'boolean' => true,
77+
'integer' => 42,
78+
'float' => M_PI,
79+
'string' => 'hello world!',
80+
'array' => [1, 2, 3],
81+
'object' => (object) [1, 2, 3],
82+
];
83+
84+
foreach ($options as $leftType => $leftValue) {
85+
foreach ($options as $rightType => $rightValue) {
86+
if ($leftType === $rightType) {
87+
continue;
88+
}
89+
90+
yield sprintf('%s vs. %s', $leftType, $rightType) => [$leftValue, $rightValue];
91+
}
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)