Skip to content

Commit

Permalink
StopAtFirstError (#147)
Browse files Browse the repository at this point in the history
* Fixed date-time filters

* Added PHP 8.4 to tests

* Updated isMultipleOf helper

* Fixed error formatter

* Added stopAtFirstError
  • Loading branch information
sorinsarca authored Dec 29, 2024
1 parent d138972 commit ddf937c
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 36 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand All @@ -32,4 +32,4 @@ jobs:
run: composer update --no-interaction --no-progress

- name: Execute tests
run: vendor/bin/phpunit --verbose
run: composer run tests
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"Opis\\JsonSchema\\Test\\": "tests/"
}
},
"scripts": {
"tests": "./vendor/bin/phpunit --verbose --color"
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
Expand Down
8 changes: 6 additions & 2 deletions src/CompliantValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/Errors/ErrorFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

Expand Down
4 changes: 2 additions & 2 deletions src/Filters/DateTimeFilters.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

namespace Opis\JsonSchema\Filters;

use DateTime;
use DateTime, DateTimeZone;

final class DateTimeFilters
{
Expand Down Expand Up @@ -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);
}
Expand Down
75 changes: 57 additions & 18 deletions src/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
34 changes: 31 additions & 3 deletions src/Schemas/ObjectSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
);
}
}
39 changes: 33 additions & 6 deletions src/ValidationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class ValidationContext

protected int $maxErrors = 1;

protected bool $stopAtFirstError = true;

/**
* @param $data
* @param SchemaLoader $loader
Expand All @@ -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;
Expand All @@ -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],
];
Expand All @@ -101,18 +105,28 @@ 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(
Schema $sender,
?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());
Expand All @@ -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
Expand Down Expand Up @@ -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;
}

/* --------------------- */

/**
Expand Down
38 changes: 36 additions & 2 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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,
);
}

/**
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ddf937c

Please sign in to comment.