Skip to content

Commit 90a858e

Browse files
committed
feat: add proof-of-concept implementation for strictfully validating using draft-06 schema
1 parent 3763af1 commit 90a858e

22 files changed

+833
-9
lines changed

src/JsonSchema/Constraints/Constraint.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface
2121
public const CHECK_MODE_EARLY_COERCE = 0x00000040;
2222
public const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080;
2323
public const CHECK_MODE_VALIDATE_SCHEMA = 0x00000100;
24+
public const CHECK_MODE_STRICT = 0x00000200;
2425

2526
/**
2627
* Bubble down the path
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Constraints\Drafts\Draft06;
6+
7+
use JsonSchema\ConstraintError;
8+
use JsonSchema\Constraints\ConstraintInterface;
9+
use JsonSchema\Constraints\Factory;
10+
use JsonSchema\Entity\ErrorBagProxy;
11+
use JsonSchema\Entity\JsonPointer;
12+
use JsonSchema\Tool\DeepComparer;
13+
14+
class ConstConstraint implements ConstraintInterface
15+
{
16+
use ErrorBagProxy;
17+
18+
public function __construct(?Factory $factory = null)
19+
{
20+
$this->initialiseErrorBag($factory ?: new Factory());
21+
}
22+
23+
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
24+
{
25+
if (!property_exists($schema, 'const')) {
26+
return;
27+
}
28+
29+
if (DeepComparer::isEqual($value, $schema->const)) {
30+
return;
31+
}
32+
33+
$this->addError(ConstraintError::CONSTANT(), $path, ['const' => $schema->const]);
34+
}
35+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Constraints\Drafts\Draft06;
6+
7+
use JsonSchema\Constraints\Constraint;
8+
use JsonSchema\Entity\JsonPointer;
9+
10+
class Draft06Constraint extends Constraint
11+
{
12+
public function __construct()
13+
{
14+
parent::__construct(new Factory());
15+
}
16+
17+
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
18+
{
19+
// Apply defaults
20+
// Required keyword
21+
$this->checkForKeyword('type', $value, $schema, $path, $i);
22+
// Not
23+
// Dependencies
24+
// allof
25+
// anyof
26+
// oneof
27+
28+
// array
29+
// object
30+
// string
31+
$this->checkForKeyword('number', $value, $schema, $path, $i);
32+
$this->checkForKeyword('uniqueItems', $value, $schema, $path, $i);
33+
$this->checkForKeyword('minItems', $value, $schema, $path, $i);
34+
$this->checkForKeyword('minProperties', $value, $schema, $path, $i);
35+
$this->checkForKeyword('minimum', $value, $schema, $path, $i);
36+
$this->checkForKeyword('minLength', $value, $schema, $path, $i);
37+
$this->checkForKeyword('exclusiveMinimum', $value, $schema, $path, $i);
38+
$this->checkForKeyword('maxItems', $value, $schema, $path, $i);
39+
$this->checkForKeyword('enum', $value, $schema, $path, $i);
40+
$this->checkForKeyword('const', $value, $schema, $path, $i);
41+
}
42+
43+
protected function checkForKeyword(string $keyword, $value, $schema = null, ?JsonPointer $path = null, $i = null): void
44+
{
45+
$validator = $this->factory->createInstanceFor($keyword);
46+
$validator->check($value, $schema, $path, $i);
47+
48+
$this->addErrors($validator->getErrors());
49+
}
50+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Constraints\Drafts\Draft06;
6+
7+
use JsonSchema\ConstraintError;
8+
use JsonSchema\Constraints\ConstraintInterface;
9+
use JsonSchema\Constraints\Factory;
10+
use JsonSchema\Constraints\UndefinedConstraint;
11+
use JsonSchema\Entity\ErrorBagProxy;
12+
use JsonSchema\Entity\JsonPointer;
13+
use JsonSchema\Tool\DeepComparer;
14+
15+
class EnumConstraint implements ConstraintInterface
16+
{
17+
use ErrorBagProxy;
18+
19+
public function __construct(?Factory $factory = null)
20+
{
21+
$this->initialiseErrorBag($factory ?: new Factory());
22+
}
23+
24+
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
25+
{
26+
if (!property_exists($schema, 'enum')) {
27+
return;
28+
}
29+
30+
foreach ($schema->enum as $enumCase) {
31+
if (DeepComparer::isEqual($value, $enumCase)) {
32+
return;
33+
}
34+
35+
if (is_numeric($value) && is_numeric($enumCase) && DeepComparer::isEqual((float) $value, (float) $enumCase)) {
36+
return;
37+
}
38+
}
39+
40+
$this->addError(ConstraintError::ENUM(), $path, ['enum' => $schema->enum]);
41+
}
42+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Constraints\Drafts\Draft06;
6+
7+
use JsonSchema\ConstraintError;
8+
use JsonSchema\Constraints\ConstraintInterface;
9+
use JsonSchema\Constraints\Factory;
10+
use JsonSchema\Entity\ErrorBagProxy;
11+
use JsonSchema\Entity\JsonPointer;
12+
13+
class ExclusiveMinimumConstraint implements ConstraintInterface
14+
{
15+
use ErrorBagProxy;
16+
17+
public function __construct(?Factory $factory = null)
18+
{
19+
$this->initialiseErrorBag($factory ?: new Factory());
20+
}
21+
22+
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
23+
{
24+
if (!property_exists($schema, 'exclusiveMinimum')) {
25+
return;
26+
}
27+
28+
if ($value > $schema->exclusiveMinimum) {
29+
return;
30+
}
31+
32+
$this->addError(ConstraintError::EXCLUSIVE_MINIMUM(), $path, ['exclusiveMinimum' => $schema->exclusiveMinimum, 'found' => $value]);
33+
}
34+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Constraints\Drafts\Draft06;
6+
7+
class Factory extends \JsonSchema\Constraints\Factory
8+
{
9+
/**
10+
* @var array<string, class-string>
11+
*/
12+
protected $constraintMap = [
13+
'type' => TypeConstraint::class,
14+
'const' => ConstConstraint::class,
15+
'enum' => EnumConstraint::class,
16+
'number' => NumberConstraint::class,
17+
'uniqueItems' => UniqueItemsConstraint::class,
18+
'minItems' => MinItemsConstraint::class,
19+
'minProperties' => MinPropertiesConstraint::class,
20+
'minimum' => MinimumConstraint::class,
21+
'exclusiveMinimum' => ExclusiveMinimumConstraint::class,
22+
'minLength' => MinLengthConstraint::class,
23+
'maxItems' => MaxItemsConstraint::class,
24+
];
25+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Constraints\Drafts\Draft06;
6+
7+
use JsonSchema\ConstraintError;
8+
use JsonSchema\Constraints\ConstraintInterface;
9+
use JsonSchema\Constraints\Factory;
10+
use JsonSchema\Entity\ErrorBagProxy;
11+
use JsonSchema\Entity\JsonPointer;
12+
13+
class MaxItemsConstraint implements ConstraintInterface
14+
{
15+
use ErrorBagProxy;
16+
17+
public function __construct(?Factory $factory = null)
18+
{
19+
$this->initialiseErrorBag($factory ?: new Factory());
20+
}
21+
22+
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
23+
{
24+
if (!property_exists($schema, 'maxItems')) {
25+
return;
26+
}
27+
28+
if (!is_array($value)) {
29+
return;
30+
}
31+
32+
$count = count($value);
33+
if ($count <= $schema->maxItems) {
34+
return;
35+
}
36+
37+
$this->addError(ConstraintError::MAX_ITEMS(), $path, ['maxItems' => $schema->maxItems, 'found' => $count]);
38+
}
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Constraints\Drafts\Draft06;
6+
7+
use JsonSchema\ConstraintError;
8+
use JsonSchema\Constraints\ConstraintInterface;
9+
use JsonSchema\Constraints\Factory;
10+
use JsonSchema\Entity\ErrorBagProxy;
11+
use JsonSchema\Entity\JsonPointer;
12+
13+
class MinItemsConstraint implements ConstraintInterface
14+
{
15+
use ErrorBagProxy;
16+
17+
public function __construct(?Factory $factory = null)
18+
{
19+
$this->initialiseErrorBag($factory ?: new Factory());
20+
}
21+
22+
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
23+
{
24+
if (!property_exists($schema, 'minItems')) {
25+
return;
26+
}
27+
28+
if (!is_array($value)) {
29+
return;
30+
}
31+
32+
$count = count($value);
33+
if ($count >= $schema->minItems) {
34+
return;
35+
}
36+
37+
$this->addError(ConstraintError::MIN_ITEMS(), $path, ['minItems' => $schema->minItems, 'found' => $count]);
38+
}
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Constraints\Drafts\Draft06;
6+
7+
use JsonSchema\ConstraintError;
8+
use JsonSchema\Constraints\ConstraintInterface;
9+
use JsonSchema\Constraints\Factory;
10+
use JsonSchema\Entity\ErrorBagProxy;
11+
use JsonSchema\Entity\JsonPointer;
12+
13+
class MinLengthConstraint implements ConstraintInterface
14+
{
15+
use ErrorBagProxy;
16+
17+
public function __construct(?Factory $factory = null)
18+
{
19+
$this->initialiseErrorBag($factory ?: new Factory());
20+
}
21+
22+
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
23+
{
24+
if (!property_exists($schema, 'minLength')) {
25+
return;
26+
}
27+
28+
if (!is_string($value)) {
29+
return;
30+
}
31+
32+
$length = mb_strlen($value);
33+
if ($length >= $schema->minLength) {
34+
return;
35+
}
36+
37+
$this->addError(ConstraintError::LENGTH_MIN(), $path, ['minLength' => $schema->minLength, 'found' => $length]);
38+
}
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Constraints\Drafts\Draft06;
6+
7+
use JsonSchema\ConstraintError;
8+
use JsonSchema\Constraints\ConstraintInterface;
9+
use JsonSchema\Constraints\Factory;
10+
use JsonSchema\Entity\ErrorBagProxy;
11+
use JsonSchema\Entity\JsonPointer;
12+
13+
class MinPropertiesConstraint implements ConstraintInterface
14+
{
15+
use ErrorBagProxy;
16+
17+
public function __construct(?Factory $factory = null)
18+
{
19+
$this->initialiseErrorBag($factory ?: new Factory());
20+
}
21+
22+
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
23+
{
24+
if (!property_exists($schema, 'minProperties')) {
25+
return;
26+
}
27+
28+
if (!is_object($value)) {
29+
return;
30+
}
31+
32+
$count = count(get_object_vars($value));
33+
if ($count >= $schema->minProperties) {
34+
return;
35+
}
36+
37+
$this->addError(ConstraintError::PROPERTIES_MIN(), $path, ['minProperties' => $schema->minProperties, 'found' => $count]);
38+
}
39+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Constraints\Drafts\Draft06;
6+
7+
use JsonSchema\ConstraintError;
8+
use JsonSchema\Constraints\ConstraintInterface;
9+
use JsonSchema\Constraints\Factory;
10+
use JsonSchema\Entity\ErrorBagProxy;
11+
use JsonSchema\Entity\JsonPointer;
12+
13+
class MinimumConstraint implements ConstraintInterface
14+
{
15+
use ErrorBagProxy;
16+
17+
public function __construct(?Factory $factory = null)
18+
{
19+
$this->initialiseErrorBag($factory ?: new Factory());
20+
}
21+
22+
public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
23+
{
24+
if (!property_exists($schema, 'minimum')) {
25+
return;
26+
}
27+
28+
if ($value >= $schema->minimum) {
29+
return;
30+
}
31+
32+
$this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum, 'found' => $value]);
33+
}
34+
}

0 commit comments

Comments
 (0)