diff --git a/README.md b/README.md index 64e11607..b1650988 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,25 @@ -# JSON Schema for PHP [![Build status...](https://secure.travis-ci.org/justinrainbow/json-schema.png)](http://travis-ci.org/justinrainbow/json-schema) +# JSON Schema for PHP [![Build Status](https://secure.travis-ci.org/justinrainbow/json-schema.png)](http://travis-ci.org/justinrainbow/json-schema) -Documentation can be found at http://jsonschema.readthedocs.org/ +A PHP Implementation for validating `JSON` Structures against a given `Schema`. + +See [json-schema](http://json-schema.org/) for more details. + +## Installation + +### Library + + $ git clone https://github.com/justinrainbow/json-schema.git + +### Dependencies + +#### via `submodules` (*will use the Symfony ClassLoader Component*) + + $ git submodule update --init + +#### via [`composer`](https://github.com/composer/composer) (*will use the Composer ClassLoader*) + + $ wget http://getcomposer.org/composer.phar + $ php composer.phar install ## Usage @@ -8,19 +27,18 @@ Documentation can be found at http://jsonschema.readthedocs.org/ validate(json_decode($json), json_decode($schema)); +$validator->check(json_decode($json), json_decode($schema)); -if ($result->valid) { +if ($validator->isValid()) { echo "The supplied JSON validates against the schema.\n"; } else { echo "JSON does not validate. Violations:\n"; - foreach ($result->errors as $error) { - echo "[{$error['property']}] {$error['message']}\n"; + foreach ($validator->getErrors() as $error) { + echo sprintf("[%s] %s\n",$error['property'], $error['message']); } } ``` ## Running the tests - $ git submodule update --init $ phpunit diff --git a/composer.json b/composer.json index 4616d1db..dbf8c508 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "homepage": "https://github.com/justinrainbow/json-schema", "type": "library", "license": "NewBSD", - "version": "1.0.0", + "version": "1.1.0", "authors": [ { "name": "Bruno Prieto Reis", @@ -14,6 +14,14 @@ { "name": "Justin Rainbow", "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" } ], "require": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 19c31d88..af9b0c98 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,9 +12,15 @@ bootstrap="tests/bootstrap.php" verbose="true" > - - - tests - - + + + tests + + + + + + ./src/JsonSchema/ + + diff --git a/src/JsonSchema/Constraints/Collection.php b/src/JsonSchema/Constraints/Collection.php new file mode 100644 index 00000000..a3d99533 --- /dev/null +++ b/src/JsonSchema/Constraints/Collection.php @@ -0,0 +1,95 @@ + + * @author Bruno Prieto Reis + */ +class Collection extends Constraint +{ + /** + * {inheritDoc} + */ + public function check($value, $schema = null, $path = null, $i = null) + { + // verify minItems + if (isset($schema->minItems) && count($value) < $schema->minItems) { + $this->addError($path, "There must be a minimum of " . $schema->minItems . " in the array"); + } + // verify maxItems + if (isset($schema->maxItems) && count($value) > $schema->maxItems) { + $this->addError($path, "There must be a maximum of " . $schema->maxItems . " in the array"); + } + // verify uniqueItems + //TODO array_unique doesnt work with objects + if (isset($schema->uniqueItems) && array_unique($value) != $value) { + $this->addError($path, "There are no duplicates allowed in the array"); + } + + //verify items + if (isset($schema->items)) { + $this->validateItems($value, $schema, $path, $i); + } + } + + /** + * validates the items + * + * @param array $value + * @param \stdClass $schema + * @param string $path + * @param string $i + */ + protected function validateItems($value, $schema = null, $path = null, $i = null) + { + if (!is_array($schema->items)) { + // just one type definition for the whole array + foreach ($value as $k => $v) { + $initErrors = $this->getErrors(); + + //first check if its defined in "items" + if (!isset($schema->additionalItems) || $schema->additionalItems === false) { + $this->checkUndefined($v, $schema->items, $path, $k); + } + + //recheck with "additionalItems" if the first test fails + if (count($initErrors) < count($this->getErrors()) && (isset($schema->additionalItems) && $schema->additionalItems !== false)) { + $secondErrors = $this->getErrors(); + $this->checkUndefined($v, $schema->additionalItems, $path, $k); + } + + //reset errors if needed + if (isset($secondErrors) && count($secondErrors) < $this->getErrors()) { + $this->errors = $secondErrors; + } elseif (isset($secondErrors) && count($secondErrors) == count($this->getErrors())) { + $this->errors = $initErrors; + } + } + } else { + //defined item type definitions + foreach ($value as $k => $v) { + if (array_key_exists($k, $schema->items)) { + $this->checkUndefined($v, $schema->items[$k], $path, $k); + } else { + // additional items + if (array_key_exists('additionalItems', $schema) && $schema->additionalItems !== false) { + $this->checkUndefined($v, $schema->additionalItems, $path, $k); + } else { + $this->addError( + $path, + 'The item ' . $i . '[' . $k . '] is not defined in the objTypeDef and the objTypeDef does not allow additional properties' + ); + } + } + } + + // treat when we have more schema definitions than values + for ($k = count($value); $k < count($schema->items); $k++) { + $this->checkUndefined(new Undefined(), $schema->items[$k], $path, $k); + } + } + } +} \ No newline at end of file diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php new file mode 100644 index 00000000..a6f57aef --- /dev/null +++ b/src/JsonSchema/Constraints/Constraint.php @@ -0,0 +1,198 @@ + + * @author Bruno Prieto Reis + */ +abstract class Constraint implements ConstraintInterface +{ + protected $checkMode = self::CHECK_MODE_NORMAL; + protected $errors = array(); + protected $inlineSchemaProperty = '$schema'; + + const CHECK_MODE_NORMAL = 1; + const CHECK_MODE_TYPE_CAST = 2; + + /** + * @param int $checkMode + */ + public function __construct($checkMode = self::CHECK_MODE_NORMAL) + { + $this->checkMode = $checkMode; + } + + /** + * {inheritDoc} + */ + public function addError($path, $message) + { + $this->errors[] = array( + 'property' => $path, + 'message' => $message + ); + } + + /** + * {inheritDoc} + */ + public function addErrors(array $errors) + { + $this->errors = array_merge($this->errors, $errors); + } + + /** + * {inheritDoc} + */ + public function getErrors() + { + return array_unique($this->errors, SORT_REGULAR); + } + + /** + * bubble down the path + * + * @param string $path + * @param mixed $i + * @return string + */ + protected function incrementPath($path, $i) + { + if ($path !== '') { + if (is_int($i)) { + $path .= '[' . $i . ']'; + } else if ($i == '') { + $path .= ''; + } else { + $path .= '.' . $i; + } + } else { + $path = $i; + } + + return $path; + } + + /** + * validates an array + * + * @param mixed $value + * @param mixed $schema + * @param mixed $path + * @param mixed $i + */ + protected function checkArray($value, $schema = null, $path = null, $i = null) + { + $validator = new Collection($this->checkMode); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * validates an object + * + * @param mixed $value + * @param mixed $schema + * @param mixed $path + * @param mixed $i + */ + protected function checkObject($value, $schema = null, $path = null, $i = null) + { + $validator = new Object($this->checkMode); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * validates the type of a property + * + * @param mixed $value + * @param mixed $schema + * @param mixed $path + * @param mixed $i + */ + protected function checkType($value, $schema = null, $path = null, $i = null) + { + $validator = new Type($this->checkMode); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * checks a undefined element + * + * @param mixed $value + * @param mixed $schema + * @param mixed $path + * @param mixed $i + */ + protected function checkUndefined($value, $schema = null, $path = null, $i = null) + { + $validator = new Undefined($this->checkMode); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * checks a string element + * + * @param mixed $value + * @param mixed $schema + * @param mixed $path + * @param mixed $i + */ + protected function checkString($value, $schema = null, $path = null, $i = null) + { + $validator = new String($this->checkMode); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * checks a number element + * + * @param mixed $value + * @param mixed $schema + * @param mixed $path + * @param mixed $i + */ + protected function checkNumber($value, $schema = null, $path = null, $i = null) + { + $validator = new Number($this->checkMode); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * checks a enum element + * + * @param mixed $value + * @param mixed $schema + * @param mixed $path + * @param mixed $i + */ + protected function checkEnum($value, $schema = null, $path = null, $i = null) + { + $validator = new Enum($this->checkMode); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * {inheritDoc} + */ + public function isValid() + { + return !$this->getErrors(); + } +} \ No newline at end of file diff --git a/src/JsonSchema/Constraints/ConstraintInterface.php b/src/JsonSchema/Constraints/ConstraintInterface.php new file mode 100644 index 00000000..97c533c7 --- /dev/null +++ b/src/JsonSchema/Constraints/ConstraintInterface.php @@ -0,0 +1,51 @@ + + */ +interface ConstraintInterface +{ + /** + * returns all collected errors + * + * @return array + */ + function getErrors(); + + /** + * adds errors to this validator + * + * @param array $errors + */ + function addErrors(array $errors); + + /** + * adds an error + * + * @param $path + * @param $message + */ + function addError($path, $message); + + /** + * checks if the validator has not raised errors + * + * @return boolean + */ + function isValid(); + + /** + * invokes the validation of an element + * + * @abstract + * @param mixed $value + * @param null $schema + * @param null $path + * @param null $i + */ + function check($value, $schema = null, $path = null, $i = null); +} \ No newline at end of file diff --git a/src/JsonSchema/Constraints/Enum.php b/src/JsonSchema/Constraints/Enum.php new file mode 100644 index 00000000..ba0d55c0 --- /dev/null +++ b/src/JsonSchema/Constraints/Enum.php @@ -0,0 +1,29 @@ + + * @author Bruno Prieto Reis + */ +class Enum extends Constraint +{ + /** + * {inheritDoc} + */ + public function check($element, $schema = null, $path = null, $i = null) + { + foreach ($schema->enum as $possibleValue) { + if ($possibleValue == $element) { + $found = true; + break; + } + } + + if (!isset($found)) { + $this->addError($path, "does not have a value in the enumeration " . implode(', ', $schema->enum)); + } + } +} \ No newline at end of file diff --git a/src/JsonSchema/Constraints/Number.php b/src/JsonSchema/Constraints/Number.php new file mode 100644 index 00000000..00331cd7 --- /dev/null +++ b/src/JsonSchema/Constraints/Number.php @@ -0,0 +1,33 @@ + + * @author Bruno Prieto Reis + */ +class Number extends Constraint +{ + /** + * {inheritDoc} + */ + public function check($element, $schema = null, $path = null, $i = null) + { + //verify minimum + if (isset($schema->minimum) && $element < $schema->minimum) { + $this->addError($path, "must have a minimum value of " . $schema->minimum); + } + + //verify maximum + if (isset($schema->maximum) && $element > $schema->maximum) { + $this->addError($path, "must have a maximum value of " . $schema->maximum); + } + + //verify divisibleBy + if (isset($schema->divisibleBy) && $element % $schema->divisibleBy != 0) { + $this->addError($path, "is not divisible by " . $schema->divisibleBy); + } + } +} \ No newline at end of file diff --git a/src/JsonSchema/Constraints/Object.php b/src/JsonSchema/Constraints/Object.php new file mode 100644 index 00000000..12456d2b --- /dev/null +++ b/src/JsonSchema/Constraints/Object.php @@ -0,0 +1,98 @@ + + * @author Bruno Prieto Reis + */ +class Object extends Constraint +{ + /** + * {inheritDoc} + */ + function check($element, $definition = null, $path = null, $additionalProp = null) + { + // validate the definition properties + $this->validateDefinition($element, $definition, $path); + + // additional the element properties + $this->validateElement($element, $definition, $path, $additionalProp); + } + + /** + * validates the element properties + * + * @param \stdClass $element + * @param \stdClass $objectDefinition + * @param string $path + * @param mixed $additionalProp + */ + public function validateElement($element, $objectDefinition = null, $path = null, $additionalProp = null) + { + foreach ($element as $i => $value) { + + $property = $this->getProperty($element, $i, new Undefined()); + $definition = $this->getProperty($objectDefinition, $i); + + //required property + if ($this->getProperty($definition, 'required') && !$property) { + $this->addError($path, "the property " . $i . " is required"); + } + + //no additional properties allowed + if ($additionalProp === false && $this->inlineSchemaProperty !== $i && !$definition) { + $this->addError($path, "The property " . $i . " is not defined and the definition does not allow additional properties"); + } + + // additional properties defined + if ($additionalProp && !$definition) { + $this->checkUndefined($value, $additionalProp, $path, $i); + } + + // property requires presence of another + $require = $this->getProperty($definition, 'requires'); + if ($require && !$this->getProperty($element, $require)) { + $this->addError($path, "the presence of the property " . $i . " requires that " . $require . " also be present"); + } + + //normal property verification + $this->checkUndefined($value, $definition ? : new \stdClass(), $path, $i); + } + } + + /** + * validates the definition properties + * + * @param \stdClass $element + * @param \stdClass $objectDefinition + * @param string $path + */ + public function validateDefinition($element, $objectDefinition = null, $path = null) + { + foreach ($objectDefinition as $i => $value) { + $property = $this->getProperty($element, $i, new Undefined()); + $definition = $this->getProperty($objectDefinition, $i); + $this->checkUndefined($property, $definition, $path, $i); + } + } + + /** + * retrieves a property from an object or array + * + * @param mixed $element + * @param string $property + * @param mixed $fallback + * @return mixed + */ + protected function getProperty($element, $property, $fallback = null) + { + if (is_array($element) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) { + return array_key_exists($property, $element) ? $element[$property] : $fallback; + } else { + return isset($element->$property) ? $element->$property : $fallback; + } + } +} \ No newline at end of file diff --git a/src/JsonSchema/Constraints/Schema.php b/src/JsonSchema/Constraints/Schema.php new file mode 100644 index 00000000..20800fd8 --- /dev/null +++ b/src/JsonSchema/Constraints/Schema.php @@ -0,0 +1,28 @@ + + * @author Bruno Prieto Reis + */ +class Schema extends Constraint +{ + /** + * {inheritDoc} + */ + public function check($element, $schema = null, $path = null, $i = null) + { + if ($schema !== null) { + // passed schema + $this->checkUndefined($element, $schema, '', ''); + } elseif (isset($element->{$this->inlineSchemaProperty})) { + // inline schema + $this->checkUndefined($element, $element->{$this->inlineSchemaProperty}, '', ''); + } else { + throw new \InvalidArgumentException('no schema found to verify against'); + } + } +} \ No newline at end of file diff --git a/src/JsonSchema/Constraints/String.php b/src/JsonSchema/Constraints/String.php new file mode 100644 index 00000000..a9c25dc3 --- /dev/null +++ b/src/JsonSchema/Constraints/String.php @@ -0,0 +1,33 @@ + + * @author Bruno Prieto Reis + */ +class String extends Constraint +{ + /** + * {inheritDoc} + */ + public function check($element, $schema = null, $path = null, $i = null) + { + // verify maxLength + if (isset($schema->maxLength) && strlen($element) > $schema->maxLength) { + $this->addError($path, "must be at most " . $schema->maxLength . " characters long"); + } + + //verify minLength + if (isset($schema->minLength) && strlen($element) < $schema->minLength) { + $this->addError($path, "must be at least " . $schema->minLength . " characters long"); + } + + // verify a regex pattern + if (isset($schema->pattern) && !preg_match('/' . $schema->pattern . '/', $element)) { + $this->addError($path, "does not match the regex pattern " . $schema->pattern); + } + } +} \ No newline at end of file diff --git a/src/JsonSchema/Constraints/Type.php b/src/JsonSchema/Constraints/Type.php new file mode 100644 index 00000000..7e3f70cd --- /dev/null +++ b/src/JsonSchema/Constraints/Type.php @@ -0,0 +1,90 @@ + + * @author Bruno Prieto Reis + */ +class Type extends Constraint +{ + /** + * {inheritDoc} + */ + function check($value = null, $schema = null, $path = null, $i = null) + { + $type = isset($schema->type) ? $schema->type : null; + $isValid = true; + + if (is_array($type)) { + //TODO refactor + $validatedOneType = false; + $errors = array(); + foreach ($type as $tp) { + $validator = new Type($this->checkMode); + $subSchema = new \stdClass(); + $subSchema->type = $tp; + $validator->check($value, $subSchema, $path, null); + $error = $validator->getErrors(); + + if (!count($error)) { + $validatedOneType = true; + break; + } else { + $errors = $error; + } + } + if (!$validatedOneType) { + return $this->addErrors($errors); + } + } elseif (is_object($type)) { + $this->checkUndefined($value, $type, $path); + } else { + $isValid = $this->validateType($value, $type); + } + + if ($isValid === false) { + $this->addError($path, gettype($value) . " value found, but a " . $type . " is required"); + } + } + + /** + * verifies that a given value is of a certain type + * + * @param string $type + * @param mixed $value + * @return boolean + * @throws \InvalidArgumentException + */ + protected function validateType($value, $type) + { + //mostly the case for inline schema + if (!$type) { + return true; + } + + switch ($type) { + case 'integer' : + return (integer)$value == $value ? true : is_int($value); + case 'number' : + return is_numeric($value); + case 'boolean' : + return is_bool($value); + case 'object' : + return is_object($value); + //return ($this::CHECK_MODE_TYPE_CAST == $this->checkMode) ? is_array($value) : is_object($value); + case 'array' : + return is_array($value); + case 'string' : + return is_string($value); + case 'null' : + return is_null($value); + case 'any' : + return true; + default: + throw new \InvalidArgumentException((is_object($value) ? 'object' : $value) . ' is a invalid type for ' . $type); + } + } +} \ No newline at end of file diff --git a/src/JsonSchema/Constraints/Undefined.php b/src/JsonSchema/Constraints/Undefined.php new file mode 100644 index 00000000..524bf830 --- /dev/null +++ b/src/JsonSchema/Constraints/Undefined.php @@ -0,0 +1,107 @@ + + * @author Bruno Prieto Reis + */ +class Undefined extends Constraint +{ + /** + * {inheritDoc} + */ + function check($value, $schema = null, $path = null, $i = null) + { + if (!is_object($schema)) { + return; + } + + $path = $this->incrementPath($path, $i); + + // check special properties + $this->validateCommonProperties($value, $schema, $path); + + // check known types + $this->validateTypes($value, $schema, $path, $i); + + + } + + /** + * validates the value against the types + * + * @param $value + * @param null $schema + * @param null $path + * @param null $i + */ + public function validateTypes($value, $schema = null, $path = null, $i = null) + { + // check array + if (is_array($value)) { + $this->checkArray($value, $schema, $path, $i); + } + + // check object + if (is_object($value) && isset($schema->properties)) { + $this->checkObject($value, $schema->properties, $path, isset($schema->additionalProperties) ? $schema->additionalProperties : null); + } + + // check string + if (is_string($value)) { + $this->checkString($value, $schema, $path, $i); + } + + // check numeric + if (is_numeric($value)) { + $this->checkNumber($value, $schema, $path, $i); + } + + // check enum + if (isset($schema->enum)) { + $this->checkEnum($value, $schema, $path, $i); + } + } + + /** + * validates common properties + * + * @param $value + * @param null $schema + * @param null $path + * @param null $i + */ + protected function validateCommonProperties($value, $schema = null, $path = null, $i = null) + { + // if it extends another schema, it must pass that schema as well + if (isset($schema->extends)) { + $this->checkUndefined($value, $schema->extends, $path, $i); + } + + // verify required values + if (is_object($value) && $value instanceOf Undefined) { + if (isset($schema->required) && $schema->required) { + $this->addError($path, "is missing and it is required"); + } + } else { + $this->checkType($value, $schema, $path); + } + + //verify disallowed items + if (isset($schema->disallow)) { + $initErrors = $this->getErrors(); + + $this->checkUndefined($value, $schema->disallow, $path); + + //if no new errors were raised it must be a disallowed value + if (count($this->getErrors()) == count($initErrors)) { + $this->addError($path, " disallowed value was matched"); + } else { + $this->errors = $initErrors; + } + } + } +} \ No newline at end of file diff --git a/src/JsonSchema/Undefined.php b/src/JsonSchema/Undefined.php deleted file mode 100644 index a78de31c..00000000 --- a/src/JsonSchema/Undefined.php +++ /dev/null @@ -1,16 +0,0 @@ -, Gradua Networks - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace JsonSchema; - -class Undefined -{ -} diff --git a/src/JsonSchema/Validator.php b/src/JsonSchema/Validator.php index 4f87a102..9d49e458 100644 --- a/src/JsonSchema/Validator.php +++ b/src/JsonSchema/Validator.php @@ -1,397 +1,30 @@ , Gradua Networks - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace JsonSchema; -class Validator -{ - public $checkMode = self::CHECK_MODE_NORMAL; - private $errors = array(); - - const CHECK_MODE_NORMAL = 1; - const CHECK_MODE_TYPE_CAST = 2; +use JsonSchema\Constraints\Schema; +use JsonSchema\Constraints\Constraint; +/** + * A JsonSchema Constraint + * + * @see README.md + * @author Robert Schönthal + * @author Bruno Prieto Reis + */ +class Validator extends Constraint +{ /** - * Validates a php object against a schema. Both the php object and the schema - * are supposed to be a result of a json_decode call. - * The validation works as defined by the schema proposal in - * http://json-schema.org + * validates the given data against the schema and returns an object containing the results + * Both the php object and the schema are supposed to be a result of a json_decode call. + * The validation works as defined by the schema proposal in http://json-schema.org * - * @param \stdClass $instance - * @param \stdClass $schema - * @return unknown - */ - public function validate($instance, $schema = null) - { - $this->errors = array(); - - $_changing = false; - - // verify passed schema - if ($schema) { - $this->checkProp($instance, $schema, '', '', $_changing); - } - // verify "inline" schema - $propName = '$schema'; - if (!$_changing && isset($instance->$propName)) { - $this->checkProp($instance, $instance->$propName, '', '', $_changing); - } - // show results - $obj = new \stdClass(); - $obj->valid = ! ((boolean)count($this->errors)); - $obj->errors = $this->errors; - return $obj; - } - - protected function incrementPath($path, $i) - { - if ($path !== '') { - if (is_int($i)) { - $path .= '['.$i.']'; - } else if ($i == '') { - $path .= ''; - } else { - $path .= '.'.$i; - } - } else { - $path = $i; - } - return $path; - } - - protected function checkArray($value, $schema, $path, $i, $_changing) - { - //verify items - if (isset($schema->items)) { - //tuple typing - if (is_array($schema->items)) { - foreach ($value as $k => $v) { - if (array_key_exists($k, $schema->items)) { - $this->checkProp($v, $schema->items[$k], $path, $k, $_changing); - } - else { - // aditional array properties - if (array_key_exists('additionalProperties', $schema)) { - if ($schema->additionalProperties === false) { - $this->adderror( - $path, - 'The item '.$i.'['.$k.'] is not defined in the objTypeDef and the objTypeDef does not allow additional properties' - ); - } - else { - $this->checkProp($v, $schema->additionalProperties, $path, $k, $_changing); - } - } - } - }//foreach ($value as $k => $v) { - // treat when we have more schema definitions than values - for ($k = count($value); $k < count($schema->items); $k++) { - $this->checkProp( - new Undefined(), - $schema->items[$k], $path, $k, $_changing - ); - } - } - // just one type definition for the whole array - else { - foreach ($value as $k => $v) { - $this->checkProp($v, $schema->items, $path, $k, $_changing); - } - } - } - // verify number of array items - if (isset($schema->minItems) && count($value) < $schema->minItems) { - $this->adderror($path,"There must be a minimum of " . $schema->minItems . " in the array"); - } - if (isset($schema->maxItems) && count($value) > $schema->maxItems) { - $this->adderror($path,"There must be a maximum of " . $schema->maxItems . " in the array"); - } - } - - protected function checkProp($value, $schema, $path, $i = '', $_changing = false) - { - if (!is_object($schema)) { - return; - } - $path = $this->incrementPath($path, $i); - // verify readonly - if ($_changing && $schema->readonly) { - $this->adderror($path,'is a readonly field, it can not be changed'); - } - // I think a schema cant be an array, only the items property - /*if (is_array($schema)) { - if (!is_array($value)) { - return array(array('property' => $path,'message' => 'An array tuple is required')); - } - for ($a = 0; $a < count($schema); $a++) { - $this->errors = array_merge( - $this->errors, - $this->checkProp($value->$a, $schema->$a, $path, $i, $_changing) - ); - return $this->errors; - } - }*/ - // if it extends another schema, it must pass that schema as well - if (isset($schema->extends)) { - $this->checkProp($value, $schema->extends, $path, $i, $_changing); - } - // verify required values - if (is_object($value) && $value instanceOf Undefined) { - if (isset($schema->required) && $schema->required) { - $this->adderror($path,"is missing and it is required"); - } - } else { - // normal verifications - $this->errors = array_merge( - $this->errors, - $this->checkType(isset($schema->type) ? $schema->type : null , $value, $path) - ); - } - if (array_key_exists('disallow', $schema)) { - $errorsBeforeDisallowCheck = $this->errors; - $response = $this->checkType($schema->disallow, $value, $path); - if ( - ( count($errorsBeforeDisallowCheck) == count($this->errors) ) && - !count($response) - ) { - $this->adderror($path," disallowed value was matched"); - } - else { - $this->errors = $errorsBeforeDisallowCheck; - } - } - //verify the itens on an array and min and max number of items. - if (is_array($value)) { - if ( - $this->checkMode == $this::CHECK_MODE_TYPE_CAST && - $schema->type == 'object' - ) { - $this->checkObj( - $value, - $schema->properties, - $path, - isset($schema->additionalProperties) ? $schema->additionalProperties : null, - $_changing - ); - } - $this->checkArray($value, $schema, $path, $i, $_changing); - } else if (isset($schema->properties) && is_object($value)) { - ############ verificar! - $this->checkObj( - $value, - $schema->properties, - $path, - isset($schema->additionalProperties) ? $schema->additionalProperties : null, - $_changing - ); - } - // verify a regex pattern - if (isset($schema->pattern) && is_string($value) && !preg_match('/'.$schema->pattern.'/', $value)) { - $this->adderror($path,"does not match the regex pattern " . $schema->pattern); - } - // verify maxLength, minLength, maximum and minimum values - if (isset($schema->maxLength) && is_string($value) && (strlen($value) > $schema->maxLength)) { - $this->adderror($path,"must be at most " . $schema->maxLength . " characters long"); - } - if (isset($schema->minLength) && is_string($value) && strlen($value) < $schema->minLength) { - $this->adderror($path,"must be at least " . $schema->minLength . " characters long"); - } - - if ( - isset($schema->minimum) && - gettype($value) == gettype($schema->minimum) && - $value < $schema->minimum - ) { - $this->adderror($path,"must have a minimum value of " . $schema->minimum); - } - if (isset($schema->maximum) && gettype($value) == gettype($schema->maximum) && $value > $schema->maximum) { - $this->adderror($path,"must have a maximum value of " . $schema->maximum); - } - // verify enum values - if (isset($schema->enum)) { - $found = false; - foreach ($schema->enum as $possibleValue) { - if ($possibleValue == $value) { - $found = true; - break; - } - } - if (!$found) { - $this->adderror($path,"does not have a value in the enumeration " . implode(', ', $schema->enum)); - } - } - if ( - isset($schema->maxDecimal) && - ( ($value * pow(10, $schema->maxDecimal)) != (int)($value * pow(10, $schema->maxDecimal)) ) - ) { - $this->adderror($path,"may only have " . $schema->maxDecimal . " digits of decimal places"); - } - } - - protected function adderror($path, $message) - { - $this->errors[] = array( - 'property' => $path, - 'message' => $message - ); - } - - /** - * Take Care: Value is being passed by ref to continue validation with proper format. - * @return array + * {inheritDoc} */ - protected function checkType($type, &$value, $path) + function check($value, $schema = null, $path = null, $i = null) { - if ($type) { - $wrongType = false; - if (is_string($type) && $type !== 'any') { - if ($type == 'null') { - if (!is_null($value)) { - $wrongType = true; - } - } - else { - if ($type == 'number') { - if ($this->checkMode == $this::CHECK_MODE_TYPE_CAST) { - $wrongType = !$this->checkTypeCast($type, $value); - } - else if (!in_array(gettype($value), array('integer','double'))) { - $wrongType = true; - } - } else{ - if ( - $this->checkMode == $this::CHECK_MODE_TYPE_CAST - && $type == 'integer' - ) { - $wrongType = !$this->checkTypeCast($type, $value); - } else if ( - $this->checkMode == $this::CHECK_MODE_TYPE_CAST - && $type == 'object' && is_array($value) - ) { - $wrongType = false; - } else if ($type !== gettype($value)) { - $wrongType = true; - } - } - } - } - if ($wrongType) { - return array( - array( - 'property' => $path, - 'message' => gettype($value)." value found, but a ".$type." is required" - ) - ); - } - // Union Types :: for now, just return the message for the last expected type!! - if (is_array($type)) { - $validatedOneType = false; - $errors = array(); - foreach ($type as $tp) { - $error = $this->checkType($tp, $value, $path); - if (!count($error)) { - $validatedOneType = true; - break; - } else { - $errors[] = $error; - $errors = $error; - } - } - if (!$validatedOneType) { - return $errors; - } - } else if (is_object($type)) { - $this->checkProp($value, $type, $path); - } - } - return array(); - } - - /** - * Take Care: Value is being passed by ref to continue validation with proper format. - */ - protected function checkTypeCast($type, &$value) - { - switch ($type) { - case 'integer': - $castValue = (integer)$value; - break; - case 'number': - $castValue = (double)$value; - break; - default: - trigger_error('this method should only be called for the above supported types.'); - break; - } - if ((string)$value == (string)$castValue ) { - $res = true; - $value = $castValue; - } else { - $res = false; - } - return $res; - } - - protected function checkObj($instance, $objTypeDef, $path, $additionalProp, $_changing) - { - if ($objTypeDef instanceOf \stdClass) { - if (! (($instance instanceOf \stdClass) || is_array($instance)) ) { - $this->errors[] = array( - 'property' => $path, - 'message' => "an object is required" - ); - } - foreach ($objTypeDef as $i => $value) { - $value = - array_key_exists($i, $instance) ? - (is_array($instance) ? $instance[$i] : $instance->$i) : - new Undefined(); - $propDef = $objTypeDef->$i; - $this->checkProp($value, $propDef, $path, $i, $_changing); - } - } - // additional properties and requires - foreach ($instance as $i => $value) { - // verify additional properties, when its not allowed - if (!isset($objTypeDef->$i) && ($additionalProp === false) && $i !== '$schema' ) { - $this->errors[] = array( - 'property' => $path, - 'message' => "The property " . $i . " is not defined in the objTypeDef and the objTypeDef does not allow additional properties" - ); - } - // verify requires - if ($objTypeDef && isset($objTypeDef->$i) && isset($objTypeDef->$i->requires)) { - $requires = $objTypeDef->$i->requires; - if (!array_key_exists($requires, $instance)) { - $this->errors[] = array( - 'property' => $path, - 'message' => "the presence of the property " . $i . " requires that " . $requires . " also be present" - ); - } - } - $value = is_array($instance) ? $instance[$i] : $instance->$i; - - // To verify additional properties types. - if ($objTypeDef && is_object($objTypeDef) && !isset($objTypeDef->$i)) { - $this->checkProp($value, $additionalProp, $path, $i); - } - // Verify inner schema definitions - $schemaPropName = '$schema'; - if (!$_changing && $value && isset($value->$schemaPropName)) { - $this->errors = array_merge( - $this->errors, - checkProp($value, $value->$schemaPropname, $path, $i) - ); - } - } - return $this->errors; + $validator = new Schema($this->checkMode); + $validator->check($value, $schema); + $this->addErrors($validator->getErrors()); } -} +} \ No newline at end of file diff --git a/tests/JsonSchema/Tests/AdditionalPropertiesTest.php b/tests/JsonSchema/Tests/AdditionalPropertiesTest.php index 3aeb70a2..bc104ea9 100644 --- a/tests/JsonSchema/Tests/AdditionalPropertiesTest.php +++ b/tests/JsonSchema/Tests/AdditionalPropertiesTest.php @@ -25,7 +25,7 @@ public function getInvalidTests() array( array( 'property' => '', - 'message' => 'The property additionalProp is not defined in the objTypeDef and the objTypeDef does not allow additional properties' + 'message' => 'The property additionalProp is not defined and the definition does not allow additional properties' ) ) ), diff --git a/tests/JsonSchema/Tests/ArraysTest.php b/tests/JsonSchema/Tests/ArraysTest.php index fd40b634..83fd82fb 100644 --- a/tests/JsonSchema/Tests/ArraysTest.php +++ b/tests/JsonSchema/Tests/ArraysTest.php @@ -51,7 +51,22 @@ public function getValidTests() "array":{"type":"array"} } }' - ) + ), + array( + '{ + "array":[1,2,"a"] + }', + '{ + "type":"object", + "properties":{ + "array":{ + "type":"array", + "items":{"type":"number"}, + "additionalItems": {"type": "string"} + } + } + }' + ), ); } } diff --git a/tests/JsonSchema/Tests/BaseTestCase.php b/tests/JsonSchema/Tests/BaseTestCase.php index 3f3daac6..b4b784e4 100644 --- a/tests/JsonSchema/Tests/BaseTestCase.php +++ b/tests/JsonSchema/Tests/BaseTestCase.php @@ -9,36 +9,27 @@ abstract class BaseTestCase extends \PHPUnit_Framework_TestCase /** * @dataProvider getInvalidTests */ - public function testInvalidCases($input, $schema, $checkMode = null, $errors = array()) + public function testInvalidCases($input, $schema, $checkMode = Validator::CHECK_MODE_NORMAL, $errors = array()) { - if (null === $checkMode) { - $checkMode = Validator::CHECK_MODE_NORMAL; - } + $validator = new Validator($checkMode); - $validator = new Validator(); - $validator->checkMode = $checkMode; + $validator->check(json_decode($input), json_decode($schema)); - $result = $validator->validate(json_decode($input), json_decode($schema)); if (array() !== $errors) { - $this->assertEquals($errors, $result->errors, var_export($result, true)); + $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(),true)); } - $this->assertFalse($result->valid, var_export($result, true)); + $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); } /** * @dataProvider getValidTests */ - public function testValidCases($input, $schema, $checkMode = null) + public function testValidCases($input, $schema, $checkMode = Validator::CHECK_MODE_NORMAL) { - if (null === $checkMode) { - $checkMode = Validator::CHECK_MODE_NORMAL; - } - - $validator = new Validator(); - $validator->checkMode = $checkMode; + $validator = new Validator($checkMode); - $result = $validator->validate(json_decode($input), json_decode($schema)); - $this->assertTrue($result->valid, var_export($result, true)); + $validator->check(json_decode($input), json_decode($schema)); + $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } abstract public function getValidTests(); diff --git a/tests/JsonSchema/Tests/MaxDecimalTest.php b/tests/JsonSchema/Tests/DivisibleByTest.php similarity index 76% rename from tests/JsonSchema/Tests/MaxDecimalTest.php rename to tests/JsonSchema/Tests/DivisibleByTest.php index d83d48ac..758edce9 100644 --- a/tests/JsonSchema/Tests/MaxDecimalTest.php +++ b/tests/JsonSchema/Tests/DivisibleByTest.php @@ -2,7 +2,7 @@ namespace JsonSchema\Tests; -class MaxDecimalTest extends BaseTestCase +class DivisibleByTest extends BaseTestCase { public function getInvalidTests() { @@ -14,7 +14,7 @@ public function getInvalidTests() '{ "type":"object", "properties":{ - "value":{"type":"number","maxDecimal":3} + "value":{"type":"number","divisibleBy":3} } }' ) @@ -26,12 +26,12 @@ public function getValidTests() return array( array( '{ - "value":5.633 + "value":6 }', '{ "type":"object", "properties":{ - "value":{"type":"number","maxDecimal":3} + "value":{"type":"number","divisibleBy":3} } }' ) diff --git a/tests/JsonSchema/Tests/MinItemsMaxItemsTest.php b/tests/JsonSchema/Tests/MinItemsMaxItemsTest.php index 22df6805..15347492 100644 --- a/tests/JsonSchema/Tests/MinItemsMaxItemsTest.php +++ b/tests/JsonSchema/Tests/MinItemsMaxItemsTest.php @@ -20,7 +20,7 @@ public function getInvalidTests() ), array( '{ - "value":2,2,5,8,5] + "value":[2,2,5,8,5] }', '{ "type":"object", diff --git a/tests/JsonSchema/Tests/PhpTypeCastModeTest.php b/tests/JsonSchema/Tests/PhpTypeCastModeTest.php index 2f83c1f4..f9cdba1b 100644 --- a/tests/JsonSchema/Tests/PhpTypeCastModeTest.php +++ b/tests/JsonSchema/Tests/PhpTypeCastModeTest.php @@ -34,7 +34,7 @@ public function getInvalidTests() '{ "type":"object", "properties":{ - "a":{"type":"integer","maximum":8} + "a":{"type":"integer","maximum":"8"} } }' ) @@ -46,19 +46,31 @@ public function getValidTests() return array( array( '{ - "a":"9" + "a":"7" }', '{ "type":"object", "properties":{ - "a":{"type":"integer","maximum":8.0} + "a":{"type":"integer","maximum":8} } }', Validator::CHECK_MODE_TYPE_CAST ), array( '{ - "a":"9" + "a":1.337 + }', + '{ + "type":"object", + "properties":{ + "a":{"type":"number","maximum":8.0} + } + }', + Validator::CHECK_MODE_TYPE_CAST + ), + array( + '{ + "a":"9e42" }', '{ "type":"object", diff --git a/tests/JsonSchema/Tests/ReadOnlyTest.php b/tests/JsonSchema/Tests/ReadOnlyTest.php new file mode 100644 index 00000000..293e5dd6 --- /dev/null +++ b/tests/JsonSchema/Tests/ReadOnlyTest.php @@ -0,0 +1,39 @@ +add('JsonSchema\Tests', __DIR__); + $loader->register(); -$loader = new Symfony\Component\ClassLoader\UniversalClassLoader(); -$loader->registerNamespace('JsonSchema', __DIR__.'/../src'); -$loader->registerNamespace('JsonSchema\Tests', __DIR__); -$loader->register(); +} elseif(is_readable(__DIR__.'/../vendor/symfony/Component/ClassLoader/UniversalClassLoader.php')) { + //submodule + require_once __DIR__.'/../vendor/symfony/Component/ClassLoader/UniversalClassLoader.php'; + + $loader = new Symfony\Component\ClassLoader\UniversalClassLoader(); + $loader->registerNamespace('JsonSchema', __DIR__.'/../src'); + $loader->registerNamespace('JsonSchema\Tests', __DIR__); + $loader->register(); +} \ No newline at end of file