Skip to content

Commit

Permalink
Add If-Then-Else schema validation.
Browse files Browse the repository at this point in the history
  • Loading branch information
niconoe- committed Feb 19, 2024
1 parent d382e52 commit 7fc2e93
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 1 deletion.
8 changes: 8 additions & 0 deletions src/JsonSchema/ConstraintError.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ class ConstraintError extends Enum
const ADDITIONAL_ITEMS = 'additionalItems';
const ADDITIONAL_PROPERTIES = 'additionalProp';
const ALL_OF = 'allOf';
const ALWAYS_FAILS = 'alwaysFails';
const ANY_OF = 'anyOf';
const CONDITIONAL_IF = 'if';
const CONDITIONAL_THEN = 'then';
const CONDITIONAL_ELSE = 'else';
const DEPENDENCIES = 'dependencies';
const DISALLOW = 'disallow';
const DIVISIBLE_BY = 'divisibleBy';
Expand Down Expand Up @@ -59,12 +63,16 @@ public function getMessage()
self::ADDITIONAL_ITEMS => 'The item %s[%s] is not defined and the definition does not allow additional items',
self::ADDITIONAL_PROPERTIES => 'The property %s is not defined and the definition does not allow additional properties',
self::ALL_OF => 'Failed to match all schemas',
self::ALWAYS_FAILS => 'Schema always fails validation',
self::ANY_OF => 'Failed to match at least one schema',
self::DEPENDENCIES => '%s depends on %s, which is missing',
self::DISALLOW => 'Disallowed value was matched',
self::DIVISIBLE_BY => 'Is not divisible by %d',
self::ENUM => 'Does not have a value in the enumeration %s',
self::CONSTANT => 'Does not have a value equal to %s',
self::CONDITIONAL_IF => 'The keyword "if" must be a boolean or an object',
self::CONDITIONAL_THEN => 'The keyword "then" must be a boolean or an object',
self::CONDITIONAL_ELSE => 'The keyword "else" must be a boolean or an object',
self::EXCLUSIVE_MINIMUM => 'Must have a minimum value greater than %d',
self::EXCLUSIVE_MAXIMUM => 'Must have a maximum value less than %d',
self::FORMAT_COLOR => 'Invalid color',
Expand Down
30 changes: 29 additions & 1 deletion src/JsonSchema/Constraints/UndefinedConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function check(&$value, $schema = null, JsonPointer $path = null, $i = nu
// check special properties
$this->validateCommonProperties($value, $schema, $path, $i);

// check allOf, anyOf, and oneOf properties
// check allOf, anyOf, oneOf, if, then, and else properties
$this->validateOfProperties($value, $schema, $path, '');

// check known types
Expand Down Expand Up @@ -372,6 +372,34 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i
$this->errors = $startErrors;
}
}

if (isset($schema->if)) {
if (!is_bool($schema->if) && !is_object($schema->if)) {
$this->addError(ConstraintError::CONDITIONAL_IF(), $path);
}
$validator = new Validator();

Check failure on line 380 in src/JsonSchema/Constraints/UndefinedConstraint.php

View workflow job for this annotation

GitHub Actions / PHPStan

Instantiated class JsonSchema\Constraints\Validator not found.
if ($schema->if !== false && Validator::ERROR_NONE === $validator->validate($value, $schema->if)) {

Check failure on line 381 in src/JsonSchema/Constraints/UndefinedConstraint.php

View workflow job for this annotation

GitHub Actions / PHPStan

Access to constant ERROR_NONE on an unknown class JsonSchema\Constraints\Validator.

Check failure on line 381 in src/JsonSchema/Constraints/UndefinedConstraint.php

View workflow job for this annotation

GitHub Actions / PHPStan

Call to method validate() on an unknown class JsonSchema\Constraints\Validator.
if (isset($schema->then)) {
if (!is_bool($schema->then) && !is_object($schema->then)) {
$this->addError(ConstraintError::CONDITIONAL_THEN(), $path);
}
if ($schema->then === false) {
$this->addError(ConstraintError::ALWAYS_FAILS(), $path);
} else {
$this->check($value, $schema->then);
}
}
} elseif (isset($schema->else)) {
if (!is_bool($schema->else) && !is_object($schema->else)) {
$this->addError(ConstraintError::CONDITIONAL_ELSE(), $path);
}
if ($schema->else === false) {
$this->addError(ConstraintError::ALWAYS_FAILS(), $path);
} else {
$this->check($value, $schema->else);
}
}
}
}

/**
Expand Down
231 changes: 231 additions & 0 deletions tests/Constraints/IfThenElseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
<?php

/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace JsonSchema\Tests\Constraints;

class IfThenElseTest extends BaseTestCase
{
protected $validateSchema = true;

public function getInvalidTests()
{
return array(
// If "foo" === "bar", then "bar" must be defined, else Validation Failed.
// But "foo" === "bar" and "bar" is not defined.
array(
'{
"foo":"bar"
}',
'{
"type": "object",
"properties": {
"foo": {"type": "string"},
"bar": {"type": "string"}
},
"if": {
"properties": {"foo": {"enum": ["bar"]}},
"required": ["foo"]
},
"then": {"required": ["bar"]},
"else": false
}'
),
// If "foo" === "bar", then "bar" must be defined, else Validation Failed.
// But "foo" !== "bar".
array(
'{
"foo":"baz"
}',
'{
"type": "object",
"properties": {
"foo": {"type": "string"},
"bar": {"type": "string"}
},
"if": {
"properties": {"foo": {"enum": ["bar"]}},
"required": ["foo"]
},
"then": {"required": ["bar"]},
"else": false
}'
),
// If "foo" === "bar", then "bar" must === "baz", else Validation Failed.
// But "foo" === "bar" and "bar" !== "baz".
array(
'{
"foo":"bar",
"bar":"potato"
}',
'{
"type": "object",
"properties": {
"foo": {"type": "string"},
"bar": {"type": "string"}
},
"if": {
"properties": {"foo": {"enum": ["bar"]}},
"required": ["foo"]
},
"then": {
"properties": {"bar": {"enum": ["baz"]}},
"required": ["bar"]
},
"else": false
}'
),
// Always go to "else".
// But schema is invalid.
array(
'{
"foo":"bar"
}',
'{
"type": "object",
"properties": {
"foo": {"type": "string"},
"bar": {"type": "string"}
},
"if": false,
"then": true,
"else": {
"properties": {"bar": {"enum": ["baz"]}},
"required": ["bar"]
}
}'
),
// Always go to "then".
// But schema is invalid.
array(
'{
"foo":"bar"
}',
'{
"type": "object",
"properties": {
"foo": {"type": "string"},
"bar": {"type": "string"}
},
"if": true,
"then": {
"properties": {"bar": {"enum": ["baz"]}},
"required": ["bar"]
},
"else": true
}'
)
);
}

public function getValidTests()
{
return array(
// Always validate.
array(
'{
"foo":"bar"
}',
'{
"type": "object",
"properties": {
"foo": {"type": "string"},
"bar": {"type": "string"}
},
"if": true,
"then": true,
"else": false
}'
),
// Always validate schema in then.
array(
'{
"foo":"bar"
}',
'{
"type": "object",
"properties": {
"foo": {"type": "string"},
"bar": {"type": "string"}
},
"if": true,
"then": {
"properties": {"foo": {"enum": ["bar"]}},
"required": ["foo"]
},
"else": false
}'
),
// Always validate schema in else.
array(
'{
"foo":"bar"
}',
'{
"type": "object",
"properties": {
"foo": {"type": "string"},
"bar": {"type": "string"}
},
"if": false,
"then": false,
"else": {
"properties": {"foo": {"enum": ["bar"]}},
"required": ["foo"]
}
}'
),
// "If" is evaluated to true, so "then" is to validate.
array(
'{
"foo":"bar",
"bar":"baz"
}',
'{
"type": "object",
"properties": {
"foo": {"type": "string"},
"bar": {"type": "string"}
},
"if": {
"properties": {"foo": {"enum": ["bar"]}},
"required": ["foo"]
},
"then": {
"properties": {"bar": {"enum": ["baz"]}},
"required": ["bar"]
},
"else": false
}'
),
// "If" is evaluated to false, so "else" is to validate.
array(
'{
"foo":"bar",
"bar":"baz"
}',
'{
"type": "object",
"properties": {
"foo": {"type": "string"},
"bar": {"type": "string"}
},
"if": {
"properties": {"foo": {"enum": ["potato"]}},
"required": ["foo"]
},
"then": false,
"else": {
"properties": {"bar": {"enum": ["baz"]}},
"required": ["bar"]
}
}'
),
);
}
}

0 comments on commit 7fc2e93

Please sign in to comment.