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 [](http://travis-ci.org/justinrainbow/json-schema)
+# JSON Schema for PHP [](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