From 210e90bd4956a1d031b0963fe3c0095f883ca129 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud <nicolas.giraud@aareon.fr> Date: Wed, 7 Aug 2019 19:17:54 +0200 Subject: [PATCH] Add If-Then-Else schema validation. --- src/JsonSchema/ConstraintError.php | 8 + .../Constraints/UndefinedConstraint.php | 31 ++- tests/Constraints/IfThenElseTest.php | 231 ++++++++++++++++++ 3 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 tests/Constraints/IfThenElseTest.php diff --git a/src/JsonSchema/ConstraintError.php b/src/JsonSchema/ConstraintError.php index fd5ba8e6..de49cdd0 100644 --- a/src/JsonSchema/ConstraintError.php +++ b/src/JsonSchema/ConstraintError.php @@ -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'; @@ -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', diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 5deda0a5..172a2ccf 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -14,6 +14,7 @@ use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\ValidationException; use JsonSchema\Uri\UriResolver; +use JsonSchema\Validator; /** * The UndefinedConstraint Constraints @@ -45,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 @@ -372,6 +373,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(); + if ($schema->if !== false && Validator::ERROR_NONE === $validator->validate($value, $schema->if)) { + 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); + } + } + } } /** diff --git a/tests/Constraints/IfThenElseTest.php b/tests/Constraints/IfThenElseTest.php new file mode 100644 index 00000000..afb93ed1 --- /dev/null +++ b/tests/Constraints/IfThenElseTest.php @@ -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"] + } + }' + ), + ); + } +}