Skip to content

Commit

Permalink
Merge pull request #28 from systopia/add-keyword-noIntersect
Browse files Browse the repository at this point in the history
Add `noIntersect` keyword
  • Loading branch information
dontub committed Apr 4, 2024
2 parents 0733459 + e833ca8 commit 799dd2d
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The following additional keywords are provided:
* `evaluate`
* `maxDate`
* `minDate`
* `noIntersect` An array must not contain intersecting intervals.
* `$order` Order arrays. (Only performed, if array has no violations.)
* `precision`
* `$tag` Tagged data can be fetched from a data container after validation.
Expand Down
1 change: 1 addition & 0 deletions messages/de.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
'evaluate.resolve' => 'Die Auswertung einer Berechnung war nicht möglich, da nicht alle Variablen aufgelöst werden konnten.',
'maxDate' => 'Das Datum darf nicht nach dem {maxDateTimestamp, date} sein.',
'minDate' => 'Das Datum darf nicht vor dem {minDateTimestamp, date} sein.',
'noIntersect' => 'Die Intervalle dürfen sich nicht überschneiden.',
'precision' => '{precision, plural, {
=1 {Die Zahl darf nicht mehr als eine Dezimalstelle haben.}
other {Die Zahl darf nicht mehr als # Dezimalstellen haben.}
Expand Down
65 changes: 65 additions & 0 deletions src/Keywords/NoIntersectKeyword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php
/*
* Copyright 2024 SYSTOPIA GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

declare(strict_types=1);

namespace Systopia\JsonSchema\Keywords;

use Opis\JsonSchema\Errors\ValidationError;
use Opis\JsonSchema\Keyword;
use Opis\JsonSchema\Keywords\ErrorTrait;
use Opis\JsonSchema\Schema;
use Opis\JsonSchema\ValidationContext;
use Systopia\JsonSchema\Errors\ErrorCollectorUtil;

final class NoIntersectKeyword implements Keyword
{
use ErrorTrait;

private string $beginPropertyName;

private string $endPropertyName;

public function __construct(string $beginPropertyName, string $endPropertyName)
{
$this->beginPropertyName = $beginPropertyName;
$this->endPropertyName = $endPropertyName;
}

public function validate(ValidationContext $context, Schema $schema): ?ValidationError
{
if (!ErrorCollectorUtil::getErrorCollector($context)->hasErrorAt($context->currentDataPath())) {
/** @var list<\stdClass> $array */
$array = $context->currentData();
usort(
$array,
fn ($a, $b) => ($a->{$this->beginPropertyName} ?? null) <=> ($b->{$this->beginPropertyName} ?? null)
);

$count = \count($array);
for ($i = 1; $i < $count; ++$i) {
$begin = $array[$i]->{$this->beginPropertyName} ?? null;
$previousEnd = $array[$i - 1]->{$this->endPropertyName} ?? null;
if (($begin <=> $previousEnd) <= 0) {
return $this->error($schema, $context, 'noIntersect', 'The intervals must not intersect.');
}
}
}

return null;
}
}
59 changes: 59 additions & 0 deletions src/Parsers/Keywords/NoIntersectKeywordParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/*
* Copyright 2024 SYSTOPIA GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

declare(strict_types=1);

namespace Systopia\JsonSchema\Parsers\Keywords;

use Opis\JsonSchema\Info\SchemaInfo;
use Opis\JsonSchema\Keyword;
use Opis\JsonSchema\Parsers\KeywordParser;
use Opis\JsonSchema\Parsers\SchemaParser;
use Systopia\JsonSchema\Keywords\NoIntersectKeyword;
use Systopia\JsonSchema\Parsers\EnsurePropertyTrait;

final class NoIntersectKeywordParser extends KeywordParser
{
use EnsurePropertyTrait;

public function __construct(string $keyword = 'noIntersect')
{
parent::__construct($keyword);
}

public function type(): string
{
return self::TYPE_ARRAY;
}

public function parse(SchemaInfo $info, SchemaParser $parser, object $shared): ?Keyword
{
if (!$this->keywordExists($info)) {
return null;
}

$noIntersect = $this->keywordValue($info);
if (!$noIntersect instanceof \stdClass) {
throw $this->keywordException('{keyword} must contain an object with "begin" and "end"', $info);
}

$this->assertPropertyExists($noIntersect, 'begin', $info);
$this->assertPropertyExists($noIntersect, 'end', $info);

return new NoIntersectKeyword($noIntersect->begin, $noIntersect->end);
}
}
2 changes: 2 additions & 0 deletions src/Parsers/SystopiaVocabulary.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Systopia\JsonSchema\Parsers\Keywords\EvaluateKeywordParser;
use Systopia\JsonSchema\Parsers\Keywords\MaxDateKeywordParser;
use Systopia\JsonSchema\Parsers\Keywords\MinDateKeywordParser;
use Systopia\JsonSchema\Parsers\Keywords\NoIntersectKeywordParser;
use Systopia\JsonSchema\Parsers\Keywords\OrderKeywordParser;
use Systopia\JsonSchema\Parsers\Keywords\PrecisionKeywordParser;
use Systopia\JsonSchema\Parsers\Keywords\ValidationsKeywordParser;
Expand All @@ -50,6 +51,7 @@ public function __construct(array $keywords = [], array $keywordValidators = [],
new EvaluateKeywordParser(),
new MaxDateKeywordParser(),
new MinDateKeywordParser(),
new NoIntersectKeywordParser(),
new PrecisionKeywordParser(),
new ValidationsKeywordParser(),
new OrderKeywordParser(),
Expand Down
181 changes: 181 additions & 0 deletions tests/NoIntersectTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php
/*
* Copyright 2024 SYSTOPIA GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

/** @noinspection PhpUnhandledExceptionInspection */

declare(strict_types=1);

namespace Systopia\JsonSchema\Test;

use Opis\JsonSchema\Exceptions\InvalidKeywordException;
use PHPUnit\Framework\TestCase;
use Systopia\JsonSchema\SystopiaValidator;

/**
* @covers \Systopia\JsonSchema\Keywords\NoIntersectKeyword
* @covers \Systopia\JsonSchema\Parsers\Keywords\NoIntersectKeywordParser
*/
final class NoIntersectTest extends TestCase
{
use AssertValidationErrorTrait;

public function testNumber(): void
{
$schema = <<<'JSON'
{
"type": "array",
"items": {
"type": ["object"],
"properties": {
"from": { "type": "number" },
"to": { "type": "number" }
}
},
"noIntersect": { "begin": "from", "end": "to" }
}
JSON;

$validator = new SystopiaValidator();

self::assertTrue($validator->validate([], $schema)->isValid());
self::assertTrue($validator->validate([(object) ['from' => 3, 'to' => 3]], $schema)->isValid());

$data = [
(object) ['from' => 3, 'to' => 5],
(object) ['from' => 2, 'to' => 2],
(object) ['from' => 6, 'to' => 9],
];
self::assertTrue($validator->validate($data, $schema)->isValid());

$data = [
(object) ['from' => 3, 'to' => 5],
(object) ['from' => 1, 'to' => 3],
(object) ['from' => 6, 'to' => 9],
];
$result = $validator->validate($data, $schema);
$error = $result->error();
self::assertNotNull($error);
self::assertErrorKeyword('noIntersect', $error);
self::assertFormattedErrorMessage('The intervals must not intersect.', $error);
}

public function testString(): void
{
$schema = <<<'JSON'
{
"type": "array",
"items": {
"type": ["object"],
"properties": {
"from": { "type": "string" },
"to": { "type": "string" }
}
},
"noIntersect": { "begin": "from", "end": "to" }
}
JSON;

$validator = new SystopiaValidator();

self::assertTrue($validator->validate([], $schema)->isValid());
self::assertTrue($validator->validate([(object) ['from' => 'a', 'to' => 'b']], $schema)->isValid());

$data = [
(object) ['from' => 'x', 'to' => 'y'],
(object) ['from' => 'c', 'to' => 'd'],
(object) ['from' => 'e', 'to' => 'k'],
];
self::assertTrue($validator->validate($data, $schema)->isValid());

$data = [
(object) ['from' => 'k', 'to' => 'y'],
(object) ['from' => 'c', 'to' => 'c'],
(object) ['from' => 'd', 'to' => 'k'],
];
$result = $validator->validate($data, $schema);
$error = $result->error();
self::assertNotNull($error);
self::assertErrorKeyword('noIntersect', $error);
self::assertFormattedErrorMessage('The intervals must not intersect.', $error);
}

public function testInvalidKeywordNoObject(): void
{
$schema = <<<'JSON'
{
"type": "array",
"items": {
"type": ["object"],
"properties": {
"from": { "type": "number" },
"to": { "type": "number" }
}
},
"noIntersect": true
}
JSON;

$validator = new SystopiaValidator();
self::expectException(InvalidKeywordException::class);
self::expectExceptionMessage('noIntersect must contain an object with "begin" and "end"');
$validator->validate((object) ['array' => []], $schema);
}

public function testInvalidKeywordBeginMissing(): void
{
$schema = <<<'JSON'
{
"type": "array",
"items": {
"type": ["object"],
"properties": {
"from": { "type": "number" },
"to": { "type": "number" }
}
},
"noIntersect": { "end": "from" }
}
JSON;

$validator = new SystopiaValidator();
self::expectException(InvalidKeywordException::class);
self::expectExceptionMessage('noIntersect entries must contain property "begin"');
$validator->validate((object) ['array' => []], $schema);
}

public function testInvalidKeywordEndMissing(): void
{
$schema = <<<'JSON'
{
"type": "array",
"items": {
"type": ["object"],
"properties": {
"from": { "type": "number" },
"to": { "type": "number" }
}
},
"noIntersect": { "begin": "from" }
}
JSON;

$validator = new SystopiaValidator();
self::expectException(InvalidKeywordException::class);
self::expectExceptionMessage('noIntersect entries must contain property "end"');
$validator->validate((object) ['array' => []], $schema);
}
}

0 comments on commit 799dd2d

Please sign in to comment.