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