Skip to content

StopAtFirstError #147

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 5 commits into from
Dec 29, 2024
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
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
Loading