From 26616553abbab2984d6c40339a55cd3d20defdce Mon Sep 17 00:00:00 2001 From: Sorin Sarca Date: Sun, 29 Dec 2024 18:46:37 +0200 Subject: [PATCH 1/5] Fixed date-time filters --- src/Filters/DateTimeFilters.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Filters/DateTimeFilters.php b/src/Filters/DateTimeFilters.php index fe599fc..a43eaaa 100644 --- a/src/Filters/DateTimeFilters.php +++ b/src/Filters/DateTimeFilters.php @@ -17,7 +17,7 @@ namespace Opis\JsonSchema\Filters; -use DateTime; +use DateTime, DateTimeZone; final class DateTimeFilters { @@ -91,7 +91,7 @@ public static function MaxTime(string $time, array $args): bool private static function CreateDate(string $value, ?string $timezone = null, bool $time = true): DateTime { - $date = new DateTime($value, $timezone); + $date = new DateTime($value, $timezone ? new DateTimeZone($timezone) : null); if (!$time) { return $date->setTime(0, 0, 0, 0); } From 7a1e9a59f3962141744420a3ffdd67b37844b96f Mon Sep 17 00:00:00 2001 From: Sorin Sarca Date: Sun, 29 Dec 2024 21:27:21 +0200 Subject: [PATCH 2/5] Added PHP 8.4 to tests --- .github/workflows/tests.yml | 4 ++-- composer.json | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ddcf073..37b06f9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: true matrix: - php: [7.4, 8.0, 8.1, 8.2, 8.3] + php: [7.4, 8.0, 8.1, 8.2, 8.3, 8.4] name: PHP ${{ matrix.php }} @@ -32,4 +32,4 @@ jobs: run: composer update --no-interaction --no-progress - name: Execute tests - run: vendor/bin/phpunit --verbose \ No newline at end of file + run: composer run tests \ No newline at end of file diff --git a/composer.json b/composer.json index a436624..6bb70ba 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,9 @@ "Opis\\JsonSchema\\Test\\": "tests/" } }, + "scripts": { + "tests": "./vendor/bin/phpunit --verbose --color" + }, "extra": { "branch-alias": { "dev-master": "2.x-dev" From 1138e26f5c6ec6bbeb710ef42f0395f9fdd28fed Mon Sep 17 00:00:00 2001 From: Sorin Sarca Date: Sun, 29 Dec 2024 21:30:00 +0200 Subject: [PATCH 3/5] Updated isMultipleOf helper --- src/Helper.php | 75 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/src/Helper.php b/src/Helper.php index 6818804..a9861ba 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -235,40 +235,79 @@ public static function equals($a, $b): bool return false; } + + /** + * @var bool|null True if bcmath extension is available + */ + private static ?bool $hasBCMath = null; + + /** + * @var bool True to use bcmath + */ + public static bool $useBCMath = true; + + /** + * @var int Number scale to used when using comparisons + */ + public static int $numberScale = 14; + /** * @param $number * @param $divisor - * @param int $scale + * @param int|null $scale * @return bool */ - public static function isMultipleOf($number, $divisor, int $scale = 14): bool + public static function isMultipleOf($number, $divisor, ?int $scale = null): bool { - static $bcMath = null; - if ($bcMath === null) { - $bcMath = extension_loaded('bcmath'); + if ($number == $divisor) { + return true; } + if ($divisor == 0) { return $number == 0; } - if ($bcMath) { - $number = number_format($number, $scale, '.', ''); - $divisor = number_format($divisor, $scale, '.', ''); + if ($divisor == 1 && !is_string($number)) { + return is_int($number) || !fmod($number, 1); + } + + // maybe we get lucky + if (!fmod($number, $divisor)) { + return true; + } + + // int mod + if (is_int($number) && is_int($divisor)) { + return !($number % $divisor); + } + + // Use global scale if null + $scale ??= self::$numberScale; + + if ( + !self::$useBCMath || + !(self::$hasBCMath ??= extension_loaded('bcmath')) + ) { + // use an approximation + $div = $number / $divisor; + return abs($div - round($div)) < (10 ** -$scale); + } + + // use bcmath - /** @noinspection PhpComposerExtensionStubsInspection */ - $x = bcdiv($number, $divisor, 0); - /** @noinspection PhpComposerExtensionStubsInspection */ - $x = bcmul($divisor, $x, $scale); - /** @noinspection PhpComposerExtensionStubsInspection */ - $x = bcsub($number, $x, $scale); + $number = number_format($number, $scale, '.', ''); + $divisor = number_format($divisor, $scale, '.', ''); - /** @noinspection PhpComposerExtensionStubsInspection */ - return 0 === bccomp($x, 0, $scale); + // number can be zero after formatting + if (!(float)$divisor) { + return $number === $divisor; } - $div = $number / $divisor; + $x = bcdiv($number, $divisor, 0); + $x = bcmul($divisor, $x, $scale); + $x = bcsub($number, $x, $scale); - return $div == (int)$div; + return 0 === bccomp($x, 0, $scale); } /** From c8d2b40ea51f8a3d5a9bdc78fe4e74e3116d2a5f Mon Sep 17 00:00:00 2001 From: Sorin Sarca Date: Sun, 29 Dec 2024 22:44:21 +0200 Subject: [PATCH 4/5] Fixed error formatter --- src/Errors/ErrorFormatter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Errors/ErrorFormatter.php b/src/Errors/ErrorFormatter.php index 7db2879..8a994e0 100644 --- a/src/Errors/ErrorFormatter.php +++ b/src/Errors/ErrorFormatter.php @@ -218,7 +218,7 @@ public function formatErrorMessage(ValidationError $error, ?string $message = nu return preg_replace_callback( '~{([^}]+)}~imu', static function (array $m) use ($args) { - if (!isset($args[$m[1]])) { + if (!array_key_exists($m[1], $args)) { return $m[0]; } From 6f92cb444fbbfaed0c8e638bcf62ba06f786e857 Mon Sep 17 00:00:00 2001 From: Sorin Sarca Date: Mon, 30 Dec 2024 00:29:45 +0200 Subject: [PATCH 5/5] Added stopAtFirstError --- src/CompliantValidator.php | 8 ++++++-- src/Schemas/ObjectSchema.php | 34 ++++++++++++++++++++++++++++--- src/ValidationContext.php | 39 ++++++++++++++++++++++++++++++------ src/Validator.php | 38 +++++++++++++++++++++++++++++++++-- 4 files changed, 106 insertions(+), 13 deletions(-) diff --git a/src/CompliantValidator.php b/src/CompliantValidator.php index e7273e6..a979b1b 100644 --- a/src/CompliantValidator.php +++ b/src/CompliantValidator.php @@ -38,9 +38,13 @@ class CompliantValidator extends Validator 'keepAdditionalItemsKeyword' => false, ]; - public function __construct(?SchemaLoader $loader = null, int $max_errors = 1) + public function __construct( + ?SchemaLoader $loader = null, + int $max_errors = 1, + bool $stop_at_first_error = true + ) { - parent::__construct($loader, $max_errors); + parent::__construct($loader, $max_errors, $stop_at_first_error); // Set parser options $parser = $this->parser(); diff --git a/src/Schemas/ObjectSchema.php b/src/Schemas/ObjectSchema.php index 1cc11be..1b5da2c 100644 --- a/src/Schemas/ObjectSchema.php +++ b/src/Schemas/ObjectSchema.php @@ -18,7 +18,7 @@ namespace Opis\JsonSchema\Schemas; use Opis\JsonSchema\{Helper, Keyword, ValidationContext, KeywordValidator}; -use Opis\JsonSchema\Info\SchemaInfo; +use Opis\JsonSchema\Info\{DataInfo, SchemaInfo}; use Opis\JsonSchema\Errors\ValidationError; use Opis\JsonSchema\KeywordValidators\CallbackKeywordValidator; @@ -109,12 +109,40 @@ public function doValidate(ValidationContext $context): ?ValidationError */ protected function applyKeywords(array $keywords, ValidationContext $context): ?ValidationError { + if ($context->stopAtFirstError()) { + foreach ($keywords as $keyword) { + if ($error = $keyword->validate($context, $this)) { + return $error; + } + } + return null; + } + + /** @var null|ValidationError[] $error_list */ + $error_list = null; + foreach ($keywords as $keyword) { if ($error = $keyword->validate($context, $this)) { - return $error; + $error_list ??= []; + $error_list[] = $error; } } - return null; + if (!$error_list) { + return null; + } + + if (count($error_list) === 1) { + return $error_list[0]; + } + + return new ValidationError( + '', + $this, + DataInfo::fromContext($context), + 'Data must match schema', + [], + $error_list + ); } } \ No newline at end of file diff --git a/src/ValidationContext.php b/src/ValidationContext.php index e55470e..9e22b3e 100644 --- a/src/ValidationContext.php +++ b/src/ValidationContext.php @@ -54,6 +54,8 @@ class ValidationContext protected int $maxErrors = 1; + protected bool $stopAtFirstError = true; + /** * @param $data * @param SchemaLoader $loader @@ -70,7 +72,8 @@ public function __construct( ?Schema $sender = null, array $globals = [], ?array $slots = null, - int $max_errors = 1 + int $max_errors = 1, + bool $stop_at_first_error = true ) { $this->sender = $sender; $this->rootData = $data; @@ -79,6 +82,7 @@ public function __construct( $this->globals = $globals; $this->slots = null; $this->maxErrors = $max_errors; + $this->stopAtFirstError = $stop_at_first_error; $this->currentData = [ [$data, false], ]; @@ -101,10 +105,19 @@ public function newInstance( ?Schema $sender, ?array $globals = null, ?array $slots = null, - ?int $max_errors = null + ?int $max_errors = null, + ?bool $stop_at_first_error = null ): self { - return new self($data, $this->loader, $this, $sender, $globals ?? $this->globals, $slots ?? $this->slots, - $max_errors ?? $this->maxErrors); + return new self( + $data, + $this->loader, + $this, + $sender, + $globals ?? $this->globals, + $slots ?? $this->slots, + $max_errors ?? $this->maxErrors, + $stop_at_first_error ?? $this->stopAtFirstError + ); } public function create( @@ -112,7 +125,8 @@ public function create( ?Variables $mapper = null, ?Variables $globals = null, ?array $slots = null, - ?int $maxErrors = null + ?int $maxErrors = null, + ?bool $stop_at_first_error = null ): self { if ($globals) { $globals = $globals->resolve($this->rootData(), $this->currentDataPath()); @@ -131,7 +145,7 @@ public function create( } return new self($data, $this->loader, $this, $sender, $globals, $slots ?? $this->slots, - $maxErrors ?? $this->maxErrors); + $maxErrors ?? $this->maxErrors, $stop_at_first_error ?? $this->stopAtFirstError); } public function sender(): ?Schema @@ -359,6 +373,19 @@ public function setMaxErrors(int $max): self return $this; } + + public function stopAtFirstError(): bool + { + return $this->stopAtFirstError; + } + + public function setStopAtFirstError(bool $stop): self + { + $this->stopAtFirstError = $stop; + + return $this; + } + /* --------------------- */ /** diff --git a/src/Validator.php b/src/Validator.php index 73ae717..f80a70b 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -26,15 +26,22 @@ class Validator { protected SchemaLoader $loader; protected int $maxErrors = 1; + protected bool $stopAtFirstError = true; /** * @param SchemaLoader|null $loader * @param int $max_errors + * @param bool $stop_at_first_error */ - public function __construct(?SchemaLoader $loader = null, int $max_errors = 1) + public function __construct( + ?SchemaLoader $loader = null, + int $max_errors = 1, + bool $stop_at_first_error = true + ) { $this->loader = $loader ?? new SchemaLoader(new SchemaParser(), new SchemaResolver(), true); $this->maxErrors = $max_errors; + $this->stopAtFirstError = $stop_at_first_error; } /** @@ -170,7 +177,16 @@ public function createContext($data, ?array $globals = null, ?array $slots = nul $slots = $this->parseSlots($slots); } - return new ValidationContext($data, $this->loader, null, null, $globals ?? [], $slots, $this->maxErrors); + return new ValidationContext( + $data, + $this->loader, + null, + null, + $globals ?? [], + $slots, + $this->maxErrors, + $this->stopAtFirstError, + ); } /** @@ -249,6 +265,24 @@ public function setMaxErrors(int $max_errors): self return $this; } + /** + * @return bool + */ + public function getStopAtFirstError(): bool + { + return $this->stopAtFirstError; + } + + /** + * @param bool $stop + * @return $this + */ + public function setStopAtFirstError(bool $stop): self + { + $this->stopAtFirstError = $stop; + return $this; + } + /** * @param array $slots * @return array