Skip to content

refactor: replace icecave/parity with custom deep comparator #803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- only check minProperties or maxProperties on objects ([#802](https://github.com/jsonrainbow/json-schema/pull/802))
- 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))

## Changed
- replace icecave/parity with custom deep comparator ([#803](https://github.com/jsonrainbow/json-schema/pull/803))
-
## [6.2.1] - 2025-03-06
### Fixed
- allow items: true to pass validation ([#801](https://github.com/jsonrainbow/json-schema/pull/801))
Expand Down
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
"require": {
"php": "^7.2 || ^8.0",
"ext-json": "*",
"marc-mabe/php-enum":"^4.0",
"icecave/parity": "^3.0"
"marc-mabe/php-enum":"^4.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.3.0",
Expand Down
8 changes: 4 additions & 4 deletions src/JsonSchema/Constraints/ConstConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

namespace JsonSchema\Constraints;

use Icecave\Parity\Parity;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Tool\DeepComparer;

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

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

if (Parity::isEqualTo($element, $const)) {
if (DeepComparer::isEqual($element, $const)) {
return;
}

Expand Down
9 changes: 5 additions & 4 deletions src/JsonSchema/Constraints/EnumConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

namespace JsonSchema\Constraints;

use Icecave\Parity\Parity;
use JsonSchema\ConstraintError;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Tool\DeepComparer;

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

foreach ($schema->enum as $enum) {
$enumType = gettype($enum);
if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type == 'array' && $enumType == 'object') {
if (Parity::isEqualTo((object) $element, $enum)) {

if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type === 'array' && $enumType === 'object') {
if (DeepComparer::isEqual((object) $element, $enum)) {
return;
}
}

if ($type === gettype($enum)) {
if (Parity::isEqualTo($element, $enum)) {
if (DeepComparer::isEqual($element, $enum)) {
return;
}
}
Expand Down
58 changes: 58 additions & 0 deletions src/JsonSchema/Tool/DeepComparer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Tool;

class DeepComparer
{
/**
* @param mixed $left
* @param mixed $right
*/
public static function isEqual($left, $right): bool
{
$isLeftScalar = is_scalar($left);
$isRightScalar = is_scalar($right);

if ($isLeftScalar && $isRightScalar) {
return $left === $right;
}

if ($isLeftScalar !== $isRightScalar) {
return false;
}

if (is_array($left) && is_array($right)) {
return self::isArrayEqual($left, $right);
}

if ($left instanceof \stdClass && $right instanceof \stdClass) {
return self::isArrayEqual((array) $left, (array) $right);
}

return false;
}

/**
* @param array<string|int, mixed> $left
* @param array<string|int, mixed> $right
*/
private static function isArrayEqual(array $left, array $right): bool
{
if (count($left) !== count($right)) {
return false;
}
foreach ($left as $key => $value) {
if (!array_key_exists($key, $right)) {
return false;
}

if (!self::isEqual($value, $right[$key])) {
return false;
}
}

return true;
}
}
94 changes: 94 additions & 0 deletions tests/Tool/DeepComparerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Tests\Tool;

use JsonSchema\Tool\DeepComparer;
use PHPUnit\Framework\TestCase;

class DeepComparerTest extends TestCase
{
/**
* @dataProvider equalDataProvider
*/
public function testComparesDeepEqualForEqualLeftAndRight($left, $right): void
{
self::assertTrue(DeepComparer::isEqual($left, $right));
}

/**
* @dataProvider notEqualDataProvider
*/
public function testComparesDeepEqualForNotEqualLeftAndRight($left, $right): void
{
self::assertFalse(DeepComparer::isEqual($left, $right));
}

public function equalDataProvider(): \Generator
{
yield 'Boolean true' => [true, true];
yield 'Boolean false' => [false, false];

yield 'Integer one' => [1, 1];
yield 'Integer INT MIN' => [PHP_INT_MIN, PHP_INT_MIN];
yield 'Integer INT MAX' => [PHP_INT_MAX, PHP_INT_MAX];

yield 'Float PI' => [M_PI, M_PI];

yield 'String' => ['hello world!', 'hello world!'];

yield 'array of integer' => [[1, 2, 3], [1, 2, 3]];
yield 'object of integer' => [(object) [1, 2, 3], (object) [1, 2, 3]];

yield 'nested objects of integers' => [
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60)],
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60)],
];
}

public function notEqualDataProvider(): \Generator
{
yield 'Boolean true/false' => [true, false];

yield 'Integer one/two' => [1, 2];
yield 'Integer INT MIN/MAX' => [PHP_INT_MIN, PHP_INT_MAX];

yield 'Float PI/' => [M_PI, M_E];

yield 'String' => ['hello world!', 'hell0 w0rld!'];

yield 'array of integer with smaller left side' => [[1, 3], [1, 2, 3]];
yield 'array of integer with smaller right side' => [[1, 2, 3], [1, 3]];
yield 'object of integer with smaller left side' => [(object) [1, 3], (object) [1, 2, 3]];
yield 'object of integer with smaller right side' => [(object) [1, 2, 3], (object) [1, 3]];

yield 'nested objects of integers with different left hand side' => [
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60, 2)],
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60)],
];
yield 'nested objects of integers with different right hand side' => [
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60)],
(object) [1 => (object) range(1, 10), 2 => (object) range(50, 60, 2)],
];

$options = [
'boolean' => true,
'integer' => 42,
'float' => M_PI,
'string' => 'hello world!',
'array' => [1, 2, 3],
'object' => (object) [1, 2, 3],
];

foreach ($options as $leftType => $leftValue) {
foreach ($options as $rightType => $rightValue) {
if ($leftType === $rightType) {
continue;
}

yield sprintf('%s vs. %s', $leftType, $rightType) => [$leftValue, $rightValue];
}
}
}
}