Skip to content
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

Add If-Then-Else schema validation. #581

Closed
wants to merge 1 commit into from
Closed
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
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
31 changes: 30 additions & 1 deletion src/JsonSchema/Constraints/UndefinedConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Exception\ValidationException;
use JsonSchema\Uri\UriResolver;
use JsonSchema\Validator;

/**
* The UndefinedConstraint Constraints
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
}
}

/**
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"]
}
}'
),
);
}
}