From 8c32e3e54dc50f5225cb5b927f5226ba52fdfeb7 Mon Sep 17 00:00:00 2001 From: steffkes Date: Fri, 3 Jun 2016 17:05:54 +0200 Subject: [PATCH 1/5] use JsonPointer for path handling --- .../Constraints/CollectionConstraint.php | 14 +- src/JsonSchema/Constraints/Constraint.php | 130 ++++++++++-------- .../Constraints/ConstraintInterface.php | 22 +-- src/JsonSchema/Constraints/EnumConstraint.php | 3 +- .../Constraints/FormatConstraint.php | 4 +- .../Constraints/NumberConstraint.php | 4 +- .../Constraints/ObjectConstraint.php | 34 ++--- .../Constraints/SchemaConstraint.php | 8 +- .../Constraints/StringConstraint.php | 4 +- src/JsonSchema/Constraints/TypeConstraint.php | 3 +- .../Constraints/UndefinedConstraint.php | 65 ++++----- src/JsonSchema/Entity/JsonPointer.php | 11 ++ src/JsonSchema/Validator.php | 3 +- .../Constraints/AdditionalPropertiesTest.php | 1 + tests/Constraints/FactoryTest.php | 3 +- tests/Constraints/OfPropertiesTest.php | 3 + tests/Constraints/PointerTest.php | 114 +++++++++++++++ tests/Entity/JsonPointerTest.php | 18 +++ 18 files changed, 311 insertions(+), 133 deletions(-) create mode 100644 tests/Constraints/PointerTest.php diff --git a/src/JsonSchema/Constraints/CollectionConstraint.php b/src/JsonSchema/Constraints/CollectionConstraint.php index 93e0ab7b..1b1554aa 100644 --- a/src/JsonSchema/Constraints/CollectionConstraint.php +++ b/src/JsonSchema/Constraints/CollectionConstraint.php @@ -9,6 +9,8 @@ namespace JsonSchema\Constraints; +use JsonSchema\Entity\JsonPointer; + /** * The CollectionConstraint Constraints, validates an array against a given schema * @@ -20,7 +22,7 @@ class CollectionConstraint extends Constraint /** * {@inheritDoc} */ - public function check($value, $schema = null, $path = null, $i = null) + public function check($value, $schema = null, JsonPointer $path = null, $i = null) { // Verify minItems if (isset($schema->minItems) && count($value) < $schema->minItems) { @@ -52,12 +54,12 @@ public function check($value, $schema = null, $path = null, $i = null) /** * Validates the items * - * @param array $value - * @param \stdClass $schema - * @param string $path - * @param string $i + * @param array $value + * @param \stdClass $schema + * @param JsonPointer|null $path + * @param string $i */ - protected function validateItems($value, $schema = null, $path = null, $i = null) + protected function validateItems($value, $schema = null, JsonPointer $path = null, $i = null) { if (is_object($schema->items)) { // just one type definition for the whole array diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index fbf7f6ae..43ab5a4e 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -11,6 +11,7 @@ use JsonSchema\Uri\UriRetriever; use JsonSchema\Validator; +use JsonSchema\Entity\JsonPointer; /** * The Base Constraints, all Validators should extend this class @@ -80,10 +81,11 @@ public function setUriRetriever(UriRetriever $uriRetriever) /** * {@inheritDoc} */ - public function addError($path, $message, $constraint='', array $more=null) + public function addError(JsonPointer $path = null, $message, $constraint='', array $more=null) { $error = array( - 'property' => $path, + 'property' => $this->convertJsonPointerIntoPropertyPath($path ?: new JsonPointer('')), + 'pointer' => ltrim(strval($path ?: new JsonPointer('')), '#'), 'message' => $message, 'constraint' => $constraint, ); @@ -132,37 +134,32 @@ public function reset() /** * Bubble down the path * - * @param string $path Current path - * @param mixed $i What to append to the path + * @param JsonPointer|null $path Current path + * @param mixed $i What to append to the path * - * @return string + * @return JsonPointer; */ - protected function incrementPath($path, $i) + protected function incrementPath(JsonPointer $path = null, $i) { - if ($path !== '') { - if (is_int($i)) { - $path .= '[' . $i . ']'; - } elseif ($i == '') { - $path .= ''; - } else { - $path .= '.' . $i; - } - } else { - $path = $i; - } - + $path = $path ?: new JsonPointer(''); + $path = $path->withPropertyPaths( + array_merge( + $path->getPropertyPaths(), + array_filter(array($i), 'strlen') + ) + ); return $path; } /** * Validates an array * - * @param mixed $value - * @param mixed $schema - * @param mixed $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param JsonPointer|null $path + * @param mixed $i */ - protected function checkArray($value, $schema = null, $path = null, $i = null) + protected function checkArray($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->getFactory()->createInstanceFor('collection'); $validator->check($value, $schema, $path, $i); @@ -173,13 +170,13 @@ protected function checkArray($value, $schema = null, $path = null, $i = null) /** * Validates an object * - * @param mixed $value - * @param mixed $schema - * @param mixed $path - * @param mixed $i - * @param mixed $patternProperties + * @param mixed $value + * @param mixed $schema + * @param JsonPointer|null $path + * @param mixed $i + * @param mixed $patternProperties */ - protected function checkObject($value, $schema = null, $path = null, $i = null, $patternProperties = null) + protected function checkObject($value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null) { $validator = $this->getFactory()->createInstanceFor('object'); $validator->check($value, $schema, $path, $i, $patternProperties); @@ -190,12 +187,12 @@ protected function checkObject($value, $schema = null, $path = null, $i = null, /** * Validates the type of a property * - * @param mixed $value - * @param mixed $schema - * @param mixed $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param JsonPointer|null $path + * @param mixed $i */ - protected function checkType($value, $schema = null, $path = null, $i = null) + protected function checkType($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->getFactory()->createInstanceFor('type'); $validator->check($value, $schema, $path, $i); @@ -206,12 +203,12 @@ protected function checkType($value, $schema = null, $path = null, $i = null) /** * Checks a undefined element * - * @param mixed $value - * @param mixed $schema - * @param mixed $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param JsonPointer|null $path + * @param mixed $i */ - protected function checkUndefined($value, $schema = null, $path = null, $i = null) + protected function checkUndefined($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->getFactory()->createInstanceFor('undefined'); $validator->check($value, $schema, $path, $i); @@ -222,12 +219,12 @@ protected function checkUndefined($value, $schema = null, $path = null, $i = nul /** * Checks a string element * - * @param mixed $value - * @param mixed $schema - * @param mixed $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param JsonPointer|null $path + * @param mixed $i */ - protected function checkString($value, $schema = null, $path = null, $i = null) + protected function checkString($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->getFactory()->createInstanceFor('string'); $validator->check($value, $schema, $path, $i); @@ -238,12 +235,12 @@ protected function checkString($value, $schema = null, $path = null, $i = null) /** * Checks a number element * - * @param mixed $value - * @param mixed $schema - * @param mixed $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path + * @param mixed $i */ - protected function checkNumber($value, $schema = null, $path = null, $i = null) + protected function checkNumber($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->getFactory()->createInstanceFor('number'); $validator->check($value, $schema, $path, $i); @@ -254,12 +251,12 @@ protected function checkNumber($value, $schema = null, $path = null, $i = null) /** * Checks a enum element * - * @param mixed $value - * @param mixed $schema - * @param mixed $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param JsonPointer|null $path + * @param mixed $i */ - protected function checkEnum($value, $schema = null, $path = null, $i = null) + protected function checkEnum($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->getFactory()->createInstanceFor('enum'); $validator->check($value, $schema, $path, $i); @@ -267,7 +264,15 @@ protected function checkEnum($value, $schema = null, $path = null, $i = null) $this->addErrors($validator->getErrors()); } - protected function checkFormat($value, $schema = null, $path = null, $i = null) + /** + * Checks format of a element + * + * @param mixed $value + * @param mixed $schema + * @param JsonPointer|null $path + * @param mixed $i + */ + protected function checkFormat($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->getFactory()->createInstanceFor('format'); $validator->check($value, $schema, $path, $i); @@ -298,4 +303,19 @@ protected function getTypeCheck() { return $this->getFactory()->getTypeCheck(); } + + /** + * @param JsonPointer $pointer + * @return string property path + */ + protected function convertJsonPointerIntoPropertyPath(JsonPointer $pointer) + { + $result = array_map( + function($path) { + return sprintf(is_numeric($path) ? '[%d]' : '.%s', $path); + }, + $pointer->getPropertyPaths() + ); + return trim(implode('', $result), '.'); + } } diff --git a/src/JsonSchema/Constraints/ConstraintInterface.php b/src/JsonSchema/Constraints/ConstraintInterface.php index d89a46e5..ca123639 100644 --- a/src/JsonSchema/Constraints/ConstraintInterface.php +++ b/src/JsonSchema/Constraints/ConstraintInterface.php @@ -9,6 +9,8 @@ namespace JsonSchema\Constraints; +use JsonSchema\Entity\JsonPointer; + /** * The Constraints Interface * @@ -33,12 +35,12 @@ public function addErrors(array $errors); /** * adds an error * - * @param string $path - * @param string $message - * @param string $constraint the constraint/rule that is broken, e.g.: 'minLength' - * @param array $more more array elements to add to the error + * @param JsonPointer|null $path + * @param string $message + * @param string $constraint the constraint/rule that is broken, e.g.: 'minLength' + * @param array $more more array elements to add to the error */ - public function addError($path, $message, $constraint='', array $more=null); + public function addError(JsonPointer $path = null, $message, $constraint='', array $more=null); /** * checks if the validator has not raised errors @@ -51,11 +53,11 @@ public function isValid(); * invokes the validation of an element * * @abstract - * @param mixed $value - * @param mixed $schema - * @param mixed $path - * @param mixed $i + * @param mixed $value + * @param mixed $schema + * @param JsonPointer|null $path + * @param mixed $i * @throws \JsonSchema\Exception\ExceptionInterface */ - public function check($value, $schema = null, $path = null, $i = null); + public function check($value, $schema = null, JsonPointer $path = null, $i = null); } \ No newline at end of file diff --git a/src/JsonSchema/Constraints/EnumConstraint.php b/src/JsonSchema/Constraints/EnumConstraint.php index 79ce3db3..69fd0308 100644 --- a/src/JsonSchema/Constraints/EnumConstraint.php +++ b/src/JsonSchema/Constraints/EnumConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; use JsonSchema\Validator; +use JsonSchema\Entity\JsonPointer; /** * The EnumConstraint Constraints, validates an element against a given set of possibilities @@ -21,7 +22,7 @@ class EnumConstraint extends Constraint /** * {@inheritDoc} */ - public function check($element, $schema = null, $path = null, $i = null) + public function check($element, $schema = null, JsonPointer $path = null, $i = null) { // Only validate enum if the attribute exists if ($element instanceof UndefinedConstraint && (!isset($schema->required) || !$schema->required)) { diff --git a/src/JsonSchema/Constraints/FormatConstraint.php b/src/JsonSchema/Constraints/FormatConstraint.php index 8843dd61..05d6de99 100644 --- a/src/JsonSchema/Constraints/FormatConstraint.php +++ b/src/JsonSchema/Constraints/FormatConstraint.php @@ -8,7 +8,9 @@ */ namespace JsonSchema\Constraints; + use JsonSchema\Rfc3339; +use JsonSchema\Entity\JsonPointer; /** * Validates against the "format" property @@ -21,7 +23,7 @@ class FormatConstraint extends Constraint /** * {@inheritDoc} */ - public function check($element, $schema = null, $path = null, $i = null) + public function check($element, $schema = null, JsonPointer $path = null, $i = null) { if (!isset($schema->format)) { return; diff --git a/src/JsonSchema/Constraints/NumberConstraint.php b/src/JsonSchema/Constraints/NumberConstraint.php index 7b359921..601b86a0 100644 --- a/src/JsonSchema/Constraints/NumberConstraint.php +++ b/src/JsonSchema/Constraints/NumberConstraint.php @@ -9,6 +9,8 @@ namespace JsonSchema\Constraints; +use JsonSchema\Entity\JsonPointer; + /** * The NumberConstraint Constraints, validates an number against a given schema * @@ -20,7 +22,7 @@ class NumberConstraint extends Constraint /** * {@inheritDoc} */ - public function check($element, $schema = null, $path = null, $i = null) + public function check($element, $schema = null, JsonPointer $path = null, $i = null) { // Verify minimum if (isset($schema->exclusiveMinimum)) { diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 73af5177..f9733971 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -9,6 +9,8 @@ namespace JsonSchema\Constraints; +use JsonSchema\Entity\JsonPointer; + /** * The ObjectConstraint Constraints, validates an object against a given schema * @@ -20,7 +22,7 @@ class ObjectConstraint extends Constraint /** * {@inheritDoc} */ - public function check($element, $definition = null, $path = null, $additionalProp = null, $patternProperties = null) + public function check($element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null) { if ($element instanceof UndefinedConstraint) { return; @@ -40,7 +42,7 @@ public function check($element, $definition = null, $path = null, $additionalPro $this->validateElement($element, $matches, $definition, $path, $additionalProp); } - public function validatePatternProperties($element, $path, $patternProperties) + public function validatePatternProperties($element, JsonPointer $path = null, $patternProperties) { $try = array('/','#','+','~','%'); $matches = array(); @@ -71,13 +73,13 @@ public function validatePatternProperties($element, $path, $patternProperties) /** * Validates the element properties * - * @param \stdClass $element Element to validate - * @param array $matches Matches from patternProperties (if any) - * @param \stdClass $objectDefinition ObjectConstraint definition - * @param string $path Path to test? - * @param mixed $additionalProp Additional properties + * @param \stdClass $element Element to validate + * @param array $matches Matches from patternProperties (if any) + * @param \stdClass $objectDefinition ObjectConstraint definition + * @param JsonPointer|null $path Path to test? + * @param mixed $additionalProp Additional properties */ - public function validateElement($element, $matches, $objectDefinition = null, $path = null, $additionalProp = null) + public function validateElement($element, $matches, $objectDefinition = null, JsonPointer $path = null, $additionalProp = null) { $this->validateMinMaxConstraint($element, $objectDefinition, $path); foreach ($element as $i => $value) { @@ -118,11 +120,11 @@ public function validateElement($element, $matches, $objectDefinition = null, $p /** * Validates the definition properties * - * @param \stdClass $element Element to validate - * @param \stdClass $objectDefinition ObjectConstraint definition - * @param string $path Path? + * @param \stdClass $element Element to validate + * @param \stdClass $objectDefinition ObjectConstraint definition + * @param JsoinPointer|null $path Path? */ - public function validateDefinition($element, $objectDefinition = null, $path = null) + public function validateDefinition($element, $objectDefinition = null, JsonPointer $path = null) { foreach ($objectDefinition as $i => $value) { $property = $this->getProperty($element, $i, new UndefinedConstraint()); @@ -154,11 +156,11 @@ protected function getProperty($element, $property, $fallback = null) /** * validating minimum and maximum property constraints (if present) against an element * - * @param \stdClass $element Element to validate - * @param \stdClass $objectDefinition ObjectConstraint definition - * @param string $path Path to test? + * @param \stdClass $element Element to validate + * @param \stdClass $objectDefinition ObjectConstraint definition + * @param JsonPointer|null $path Path to test? */ - protected function validateMinMaxConstraint($element, $objectDefinition, $path) { + protected function validateMinMaxConstraint($element, $objectDefinition, JsonPointer $path = null) { // Verify minimum number of properties if (isset($objectDefinition->minProperties) && !is_object($objectDefinition->minProperties)) { if ($this->getTypeCheck()->propertyCount($element) < $objectDefinition->minProperties) { diff --git a/src/JsonSchema/Constraints/SchemaConstraint.php b/src/JsonSchema/Constraints/SchemaConstraint.php index ffd4757b..071cd123 100644 --- a/src/JsonSchema/Constraints/SchemaConstraint.php +++ b/src/JsonSchema/Constraints/SchemaConstraint.php @@ -10,6 +10,7 @@ namespace JsonSchema\Constraints; use JsonSchema\Exception\InvalidArgumentException; +use JsonSchema\Entity\JsonPointer; /** * The SchemaConstraint Constraints, validates an element against a given schema @@ -22,19 +23,18 @@ class SchemaConstraint extends Constraint /** * {@inheritDoc} */ - public function check($element, $schema = null, $path = null, $i = null) + public function check($element, $schema = null, JsonPointer $path = null, $i = null) { if ($schema !== null) { // passed schema - $this->checkUndefined($element, $schema, '', ''); + $this->checkUndefined($element, $schema, $path, $i); } elseif ($this->getTypeCheck()->propertyExists($element, $this->inlineSchemaProperty)) { $inlineSchema = $this->getTypeCheck()->propertyGet($element, $this->inlineSchemaProperty); if (is_array($inlineSchema)) { $inlineSchema = json_decode(json_encode($inlineSchema)); } - // inline schema - $this->checkUndefined($element, $inlineSchema, '', ''); + $this->checkUndefined($element, $inlineSchema, $path, $i); } else { throw new InvalidArgumentException('no schema found to verify against'); } diff --git a/src/JsonSchema/Constraints/StringConstraint.php b/src/JsonSchema/Constraints/StringConstraint.php index f57f64c9..44313cb2 100644 --- a/src/JsonSchema/Constraints/StringConstraint.php +++ b/src/JsonSchema/Constraints/StringConstraint.php @@ -9,6 +9,8 @@ namespace JsonSchema\Constraints; +use JsonSchema\Entity\JsonPointer; + /** * The StringConstraint Constraints, validates an string against a given schema * @@ -20,7 +22,7 @@ class StringConstraint extends Constraint /** * {@inheritDoc} */ - public function check($element, $schema = null, $path = null, $i = null) + public function check($element, $schema = null, JsonPointer $path = null, $i = null) { // Verify maxLength if (isset($schema->maxLength) && $this->strlen($element) > $schema->maxLength) { diff --git a/src/JsonSchema/Constraints/TypeConstraint.php b/src/JsonSchema/Constraints/TypeConstraint.php index a7af5748..3e0ac4e1 100644 --- a/src/JsonSchema/Constraints/TypeConstraint.php +++ b/src/JsonSchema/Constraints/TypeConstraint.php @@ -10,6 +10,7 @@ namespace JsonSchema\Constraints; use JsonSchema\Exception\InvalidArgumentException; +use JsonSchema\Entity\JsonPointer; use UnexpectedValueException as StandardUnexpectedValueException; /** @@ -38,7 +39,7 @@ class TypeConstraint extends Constraint /** * {@inheritDoc} */ - public function check($value = null, $schema = null, $path = null, $i = null) + public function check($value = null, $schema = null, JsonPointer $path = null, $i = null) { $type = isset($schema->type) ? $schema->type : null; $isValid = true; diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 63947199..7815e241 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -10,6 +10,7 @@ namespace JsonSchema\Constraints; use JsonSchema\Uri\UriResolver; +use JsonSchema\Entity\JsonPointer; /** * The UndefinedConstraint Constraints @@ -22,14 +23,13 @@ class UndefinedConstraint extends Constraint /** * {@inheritDoc} */ - public function check($value, $schema = null, $path = null, $i = null) + public function check($value, $schema = null, JsonPointer $path = null, $i = null) { if (is_null($schema) || !is_object($schema)) { return; } - $i = is_null($i) ? "" : $i; - $path = $this->incrementPath($path, $i); + $path = $this->incrementPath($path ?: new JsonPointer(''), $i); // check special properties $this->validateCommonProperties($value, $schema, $path); @@ -44,12 +44,12 @@ public function check($value, $schema = null, $path = null, $i = null) /** * Validates the value against the types * - * @param mixed $value - * @param mixed $schema - * @param string $path - * @param string $i + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path + * @param string $i */ - public function validateTypes($value, $schema = null, $path = null, $i = null) + public function validateTypes($value, $schema = null, JsonPointer $path, $i = null) { // check array if ($this->getTypeCheck()->isArray($value)) { @@ -86,12 +86,12 @@ public function validateTypes($value, $schema = null, $path = null, $i = null) /** * Validates common properties * - * @param mixed $value - * @param mixed $schema - * @param string $path - * @param string $i + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path + * @param string $i */ - protected function validateCommonProperties($value, $schema = null, $path = null, $i = "") + protected function validateCommonProperties($value, $schema = null, JsonPointer $path, $i = "") { // if it extends another schema, it must pass that schema as well if (isset($schema->extends)) { @@ -113,7 +113,11 @@ protected function validateCommonProperties($value, $schema = null, $path = null // Draft 4 - Required is an array of strings - e.g. "required": ["foo", ...] foreach ($schema->required as $required) { if (!$this->getTypeCheck()->propertyExists($value, $required)) { - $this->addError((!$path) ? $required : "$path.$required", "The property " . $required . " is required", 'required'); + $this->addError( + $this->incrementPath($path ?: new JsonPointer(''), $required), + "The property " . $required . " is required", + 'required' + ); } } } elseif (isset($schema->required) && !is_array($schema->required)) { @@ -166,12 +170,12 @@ protected function validateCommonProperties($value, $schema = null, $path = null /** * Validate allOf, anyOf, and oneOf properties * - * @param mixed $value - * @param mixed $schema - * @param string $path - * @param string $i + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path + * @param string $i */ - protected function validateOfProperties($value, $schema, $path, $i = "") + protected function validateOfProperties($value, $schema, JsonPointer $path, $i = "") { // Verify type if ($value instanceof UndefinedConstraint) { @@ -220,17 +224,8 @@ protected function validateOfProperties($value, $schema, $path, $i = "") $allErrors = array_merge($allErrors, array_values($this->getErrors())); } if ($matchedSchemas !== 1) { - $this->addErrors( - array_merge( - $allErrors, - array(array( - 'property' => $path, - 'message' => "Failed to match exactly one schema", - 'constraint' => 'oneOf', - ),), - $startErrors - ) - ); + $this->addErrors(array_merge($allErrors, $startErrors)); + $this->addError($path, "Failed to match exactly one schema", 'oneOf'); } else { $this->errors = $startErrors; } @@ -240,12 +235,12 @@ protected function validateOfProperties($value, $schema, $path, $i = "") /** * Validate dependencies * - * @param mixed $value - * @param mixed $dependencies - * @param string $path - * @param string $i + * @param mixed $value + * @param mixed $dependencies + * @param JsonPointer $path + * @param string $i */ - protected function validateDependencies($value, $dependencies, $path, $i = "") + protected function validateDependencies($value, $dependencies, JsonPointer $path, $i = "") { foreach ($dependencies as $key => $dependency) { if ($this->getTypeCheck()->propertyExists($value, $key)) { diff --git a/src/JsonSchema/Entity/JsonPointer.php b/src/JsonSchema/Entity/JsonPointer.php index ff841de0..00e14e09 100644 --- a/src/JsonSchema/Entity/JsonPointer.php +++ b/src/JsonSchema/Entity/JsonPointer.php @@ -100,6 +100,17 @@ public function getPropertyPaths() return $this->propertyPaths; } + /** + * @param array $propertyPaths + * @return JsonPointer + */ + public function withPropertyPaths(array $propertyPaths) + { + $new = clone $this; + $new->propertyPaths = $propertyPaths; + return $new; + } + /** * @return string */ diff --git a/src/JsonSchema/Validator.php b/src/JsonSchema/Validator.php index 14dbb609..b627d785 100644 --- a/src/JsonSchema/Validator.php +++ b/src/JsonSchema/Validator.php @@ -11,6 +11,7 @@ use JsonSchema\Constraints\SchemaConstraint; use JsonSchema\Constraints\Constraint; +use JsonSchema\Entity\JsonPointer; /** * A JsonSchema Constraint @@ -30,7 +31,7 @@ class Validator extends Constraint * * {@inheritDoc} */ - public function check($value, $schema = null, $path = null, $i = null) + public function check($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->getFactory()->createInstanceFor('schema'); $validator->check($value, $schema); diff --git a/tests/Constraints/AdditionalPropertiesTest.php b/tests/Constraints/AdditionalPropertiesTest.php index 24254d0b..4d68654b 100644 --- a/tests/Constraints/AdditionalPropertiesTest.php +++ b/tests/Constraints/AdditionalPropertiesTest.php @@ -34,6 +34,7 @@ public function getInvalidTests() array( array( 'property' => '', + 'pointer' => '', 'message' => 'The property additionalProp is not defined and the definition does not allow additional properties', 'constraint' => 'additionalProp', ) diff --git a/tests/Constraints/FactoryTest.php b/tests/Constraints/FactoryTest.php index e3a2acab..50a2d69a 100644 --- a/tests/Constraints/FactoryTest.php +++ b/tests/Constraints/FactoryTest.php @@ -11,6 +11,7 @@ use JsonSchema\Constraints\Constraint; use JsonSchema\Constraints\Factory; +use JsonSchema\Entity\JsonPointer; use PHPUnit_Framework_TestCase as TestCase; @@ -25,7 +26,7 @@ class MyBadConstraint {} * @package JsonSchema\Tests\Constraints */ class MyStringConstraint extends Constraint { - public function check($value, $schema = null, $path = null, $i = null){} + public function check($value, $schema = null, JsonPointer $path = null, $i = null){} } class FactoryTest extends TestCase diff --git a/tests/Constraints/OfPropertiesTest.php b/tests/Constraints/OfPropertiesTest.php index 34c8cbff..5255fc6e 100644 --- a/tests/Constraints/OfPropertiesTest.php +++ b/tests/Constraints/OfPropertiesTest.php @@ -73,16 +73,19 @@ public function getInvalidTests() array( array( "property" => "prop2", + "pointer" => "/prop2", "message" => "Array value found, but a string is required", "constraint" => "type", ), array( "property" => "prop2", + "pointer" => "/prop2", "message" => "Array value found, but a number is required", "constraint" => "type", ), array( "property" => "prop2", + "pointer" => "/prop2", "message" => "Failed to match exactly one schema", "constraint" => "oneOf", ), diff --git a/tests/Constraints/PointerTest.php b/tests/Constraints/PointerTest.php new file mode 100644 index 00000000..a48eae0d --- /dev/null +++ b/tests/Constraints/PointerTest.php @@ -0,0 +1,114 @@ + 'object', + 'required' => array('prop1', 'prop2', 'prop3', 'prop4'), + 'properties' => array( + 'prop1' => array( + 'type' => 'string' + ), + 'prop2' => array( + 'type' => 'object', + 'required' => array('prop2.1'), + 'properties' => array( + 'prop2.1' => array( + 'type' => 'string' + ) + ) + ), + 'prop3' => array( + 'type' => 'object', + 'required' => array('prop3/1'), + 'properties' => array( + 'prop3/1' => array( + 'type' => 'object', + 'required' => array('prop3/1.1'), + 'properties' => array( + 'prop3/1.1' => array( + 'type' => 'string' + ) + ) + ) + ) + ), + 'prop4' => array( + 'type' => 'array', + 'minItems' => 1, + 'items' => array( + 'type' => 'object', + 'required' => array('prop4-child'), + 'properties' => array( + 'prop4-child' => array( + 'type' => 'string' + ) + ) + ) + ) + ) + ); + + $value = array( + 'prop2' => array( + 'foo' => 'bar' + ), + 'prop3' => array( + 'prop3/1' => array( + 'foo' => 'bar' + ) + ), + 'prop4' => array( + array( + 'foo' => 'bar' + ) + ) + ); + + $validator = new Validator(); + $validator->check(json_decode(json_encode($value)), json_decode(json_encode($schema))); + + $this->assertEquals( + array( + array( + 'property' => 'prop1', + 'pointer' => '/prop1', + 'message' => 'The property prop1 is required', + 'constraint' => 'required' + ), + array( + 'property' => 'prop2.prop2.1', + 'pointer' => '/prop2/prop2.1', + 'message' => 'The property prop2.1 is required', + 'constraint' => 'required' + ), + array( + 'property' => 'prop3.prop3/1.prop3/1.1', + 'pointer' => '/prop3/prop3~11/prop3~11.1', + 'message' => 'The property prop3/1.1 is required', + 'constraint' => 'required' + ), + array( + 'property' => 'prop4[0].prop4-child', + 'pointer' => '/prop4/0/prop4-child', + 'message' => 'The property prop4-child is required', + 'constraint' => 'required' + ) + ), + $validator->getErrors() + ); + } +} diff --git a/tests/Entity/JsonPointerTest.php b/tests/Entity/JsonPointerTest.php index c7a49d22..1cc8c0bf 100644 --- a/tests/Entity/JsonPointerTest.php +++ b/tests/Entity/JsonPointerTest.php @@ -92,4 +92,22 @@ public function getTestData() ); } + + public function testJsonPointerWithPropertyPaths() + { + $initial = new JsonPointer('#/definitions/date'); + + $this->assertEquals(array('definitions', 'date'), $initial->getPropertyPaths()); + $this->assertEquals('#/definitions/date', $initial->getPropertyPathAsString()); + + $modified = $initial->withPropertyPaths(array('~definitions/general', '%custom%')); + + $this->assertNotSame($initial, $modified); + + $this->assertEquals(array('definitions', 'date'), $initial->getPropertyPaths()); + $this->assertEquals('#/definitions/date', $initial->getPropertyPathAsString()); + + $this->assertEquals(array('~definitions/general', '%custom%'), $modified->getPropertyPaths()); + $this->assertEquals('#/~0definitions~1general/%25custom%25', $modified->getPropertyPathAsString()); + } } From 53a8486e6271fe10ec8517a098d6076a4680c4be Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 25 Jul 2016 19:56:47 +0200 Subject: [PATCH 2/5] Change error reporting for invalid types with multiple valid types Currently, if a property has multiple valid types, an error message when a value doesn't conform to the schema, would mention only the last valid type, without mentioning the other valid types. This commit changes, how the TypeConstraint works internally. It does a little bit of refactoring (removing also the "Refacter this" ToDo) and now mentions any valid types on a failure. Fixes #293 --- src/JsonSchema/Constraints/TypeConstraint.php | 117 +++++++++++++----- tests/Constraints/TypeTest.php | 19 +-- 2 files changed, 95 insertions(+), 41 deletions(-) diff --git a/src/JsonSchema/Constraints/TypeConstraint.php b/src/JsonSchema/Constraints/TypeConstraint.php index a7af5748..005b7438 100644 --- a/src/JsonSchema/Constraints/TypeConstraint.php +++ b/src/JsonSchema/Constraints/TypeConstraint.php @@ -41,48 +41,99 @@ class TypeConstraint extends Constraint public function check($value = null, $schema = null, $path = null, $i = null) { $type = isset($schema->type) ? $schema->type : null; - $isValid = true; + $isValid = false; + $wording = array(); if (is_array($type)) { - // @TODO refactor - $validatedOneType = false; - $errors = array(); - foreach ($type as $tp) { - $validator = new static($this->checkMode); - $subSchema = new \stdClass(); - $subSchema->type = $tp; - $validator->check($value, $subSchema, $path, null); - $error = $validator->getErrors(); - - if (!count($error)) { - $validatedOneType = true; - break; - } - - $errors = $error; - } - - if (!$validatedOneType) { - $this->addErrors($errors); - - return; - } + $this->validateTypesArray($value, $type, $wording, $isValid, $path); } elseif (is_object($type)) { $this->checkUndefined($value, $type, $path); + return; } else { $isValid = $this->validateType($value, $type); } if ($isValid === false) { - if (!isset(self::$wording[$type])) { - throw new StandardUnexpectedValueException( - sprintf( - "No wording for %s available, expected wordings are: [%s]", - var_export($type, true), - implode(', ', array_filter(self::$wording))) - ); + if (!is_array($type)) { + $this->validateTypeNameWording($type); + $wording[] = self::$wording[$type]; + } + $this->addError($path, ucwords(gettype($value)) . " value found, but " . + $this->implodeWith($wording, ', ', 'or') . " is required", 'type'); + } + } + + /** + * Validates the given $value against the array of types in $type. Sets the value + * of $isValid to true, if at least one $type mateches the type of $value or the value + * passed as $isValid is already true. + * + * @param mixed $value Value to validate + * @param array $type TypeConstraints to check agains + * @param array $wording An array of wordings of the valid types of the array $type + * @param boolean $isValid The current validation value + */ + protected function validateTypesArray($value, array $type, &$validTypesWording, &$isValid, + $path) { + foreach ($type as $tp) { + // $tp can be an object, if it's a schema instead of a simple type, validate it + // with a new type constraint + if (is_object($tp)) { + if (!$isValid) { + $validator = new static($this->checkMode); + $subSchema = new \stdClass(); + $subSchema->type = $tp; + $validator->check($value, $subSchema, $path, null); + $error = $validator->getErrors(); + $isValid = !(bool)$error; + $validTypesWording[] = self::$wording['object']; + } + } else { + $this->validateTypeNameWording( $tp ); + $validTypesWording[] = self::$wording[$tp]; + if (!$isValid) { + $isValid = $this->validateType( $value, $tp ); + } } - $this->addError($path, ucwords(gettype($value)) . " value found, but " . self::$wording[$type] . " is required", 'type'); + } + } + + /** + * Implodes the given array like implode() with turned around parameters and with the + * difference, that, if $listEnd isn't false, the last element delimiter is $listEnd instead of + * $delimiter. + * + * @param array $elements The elements to implode + * @param string $delimiter The delimiter to use + * @param bool $listEnd The last delimiter to use (defaults to $delimiter) + * @return string + */ + protected function implodeWith(array $elements, $delimiter = ', ', $listEnd = false) { + if ($listEnd === false || !isset($elements[1])) { + return implode(', ', $elements); + } + $lastElement = array_slice($elements, -1); + $firsElements = join(', ', array_slice($elements, 0, -1)); + $implodedElements = array_merge(array($firsElements), $lastElement); + return join(" $listEnd ", $implodedElements); + } + + /** + * Validates the given $type, if there's an associated self::$wording. If not, throws an + * exception. + * + * @param string $type The type to validate + * + * @throws StandardUnexpectedValueException + */ + protected function validateTypeNameWording( $type) { + if (!isset(self::$wording[$type])) { + throw new StandardUnexpectedValueException( + sprintf( + "No wording for %s available, expected wordings are: [%s]", + var_export($type, true), + implode(', ', array_filter(self::$wording))) + ); } } @@ -126,7 +177,7 @@ protected function validateType($value, $type) if ('string' === $type) { return is_string($value); } - + if ('email' === $type) { return is_string($value); } diff --git a/tests/Constraints/TypeTest.php b/tests/Constraints/TypeTest.php index f3f529db..fdc3f907 100644 --- a/tests/Constraints/TypeTest.php +++ b/tests/Constraints/TypeTest.php @@ -26,13 +26,16 @@ class TypeTest extends \PHPUnit_Framework_TestCase public function provideIndefiniteArticlesForTypes() { return array( - array('integer', 'an',), - array('number', 'a',), - array('boolean', 'a',), - array('object', 'an',), - array('array', 'an',), - array('string', 'a',), - array('null', 'a', array(), 'array',), + array('integer', 'an integer',), + array('number', 'a number',), + array('boolean', 'a boolean',), + array('object', 'an object',), + array('array', 'an array',), + array('string', 'a string',), + array('null', 'a null', array(), 'array',), + array(array('string', 'boolean', 'integer'), 'a string, a boolean or an integer',), + array(array('string', 'boolean'), 'a string or a boolean',), + array(array('string'), 'a string',), ); } @@ -43,7 +46,7 @@ public function testIndefiniteArticleForTypeInTypeCheckErrorMessage($type, $word { $constraint = new TypeConstraint(); $constraint->check($value, (object)array('type' => $type)); - $this->assertTypeConstraintError(ucwords($label)." value found, but $wording $type is required", $constraint); + $this->assertTypeConstraintError(ucwords($label)." value found, but $wording is required", $constraint); } /** From 7851a445916abbf36ccf27bd89568a51a354f3ac Mon Sep 17 00:00:00 2001 From: steffkes Date: Tue, 2 Aug 2016 22:36:40 +0200 Subject: [PATCH 3/5] proper typo - checks format of _an_ element --- src/JsonSchema/Constraints/Constraint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 43ab5a4e..a56acbb4 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -265,7 +265,7 @@ protected function checkEnum($value, $schema = null, JsonPointer $path = null, $ } /** - * Checks format of a element + * Checks format of an element * * @param mixed $value * @param mixed $schema From ef13b9b94ce3add40447cb454e511488fefaabe7 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Thu, 11 Aug 2016 13:35:14 +0100 Subject: [PATCH 4/5] Cache the results of RefResolver::resolve() (#290) * Cache the results of RefResolver::resolve() * Add test --- src/JsonSchema/RefResolver.php | 34 ++++++++++++---------------------- tests/RefResolverTest.php | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/JsonSchema/RefResolver.php b/src/JsonSchema/RefResolver.php index 46acc746..00c7a671 100644 --- a/src/JsonSchema/RefResolver.php +++ b/src/JsonSchema/RefResolver.php @@ -30,6 +30,8 @@ class RefResolver /** @var UriResolverInterface */ private $uriResolver; + private $paths = array(); + /** * @param UriRetrieverInterface $retriever * @param UriResolverInterface $uriResolver @@ -48,26 +50,16 @@ public function __construct(UriRetrieverInterface $retriever, UriResolverInterfa * @return object */ public function resolve($sourceUri) - { - return $this->resolveCached($sourceUri, array()); - } - - /** - * @param string $sourceUri URI where this schema was located - * @param array $paths - * @return object - */ - private function resolveCached($sourceUri, array $paths) { $jsonPointer = new JsonPointer($sourceUri); $fileName = $jsonPointer->getFilename(); - if (!array_key_exists($fileName, $paths)) { + if (!array_key_exists($fileName, $this->paths)) { $schema = $this->uriRetriever->retrieve($jsonPointer->getFilename()); - $paths[$jsonPointer->getFilename()] = $schema; - $this->resolveSchemas($schema, $jsonPointer->getFilename(), $paths); + $this->paths[$jsonPointer->getFilename()] = $schema; + $this->resolveSchemas($schema, $jsonPointer->getFilename()); } - $schema = $paths[$fileName]; + $schema = $this->paths[$fileName]; return $this->getRefSchema($jsonPointer, $schema); } @@ -77,16 +69,15 @@ private function resolveCached($sourceUri, array $paths) * * @param object $unresolvedSchema * @param string $fileName - * @param array $paths */ - private function resolveSchemas($unresolvedSchema, $fileName, array $paths) + private function resolveSchemas($unresolvedSchema, $fileName) { $objectIterator = new ObjectIterator($unresolvedSchema); foreach ($objectIterator as $toResolveSchema) { if (property_exists($toResolveSchema, '$ref') && is_string($toResolveSchema->{'$ref'})) { $jsonPointer = new JsonPointer($this->uriResolver->resolve($toResolveSchema->{'$ref'}, $fileName)); - $refSchema = $this->resolveCached((string) $jsonPointer, $paths); - $this->unionSchemas($refSchema, $toResolveSchema, $fileName, $paths); + $refSchema = $this->resolve((string) $jsonPointer); + $this->unionSchemas($refSchema, $toResolveSchema, $fileName); } } } @@ -120,14 +111,13 @@ private function getRefSchema(JsonPointer $jsonPointer, $refSchema) * @param object $refSchema * @param object $schema * @param string $fileName - * @param array $paths */ - private function unionSchemas($refSchema, $schema, $fileName, array $paths) + private function unionSchemas($refSchema, $schema, $fileName) { if (property_exists($refSchema, '$ref')) { $jsonPointer = new JsonPointer($this->uriResolver->resolve($refSchema->{'$ref'}, $fileName)); - $newSchema = $this->resolveCached((string) $jsonPointer, $paths); - $this->unionSchemas($newSchema, $refSchema, $fileName, $paths); + $newSchema = $this->resolve((string) $jsonPointer); + $this->unionSchemas($newSchema, $refSchema, $fileName); } unset($schema->{'$ref'}); diff --git a/tests/RefResolverTest.php b/tests/RefResolverTest.php index 0e3d2cc7..408f56c8 100644 --- a/tests/RefResolverTest.php +++ b/tests/RefResolverTest.php @@ -112,6 +112,29 @@ function testUnresolvableJsonPointExceptionShouldBeThrown() $refResolver->resolve('http://www.example.com/schema.json'); } + public function testExternalReferencesLoadedOnlyOnce() + { + $mainSchema = $this->getMainSchema(); + $schema2 = $this->getSchema2(); + $schema3 = $this->getSchema3(); + + /** @var UriRetriever $uriRetriever */ + $uriRetriever = $this->prophesize('JsonSchema\UriRetrieverInterface'); + $uriRetriever->retrieve('http://www.example.com/schema.json') + ->willReturn($mainSchema) + ->shouldBeCalledTimes(1); + $uriRetriever->retrieve('http://www.my-domain.com/schema2.json') + ->willReturn($schema2) + ->shouldBeCalledTimes(1); + $uriRetriever->retrieve('http://www.my-domain.com/schema3.json') + ->willReturn($schema3) + ->shouldBeCalledTimes(1); + + $refResolver = new RefResolver($uriRetriever->reveal(), new UriResolver()); + $refResolver->resolve('http://www.example.com/schema.json'); + $refResolver->resolve('http://www.example.com/schema.json'); + } + /** * @return object */ From 12965831120bb18852e9865804a6089906323b8e Mon Sep 17 00:00:00 2001 From: Patrick Safarov Date: Mon, 15 Aug 2016 09:39:11 -0700 Subject: [PATCH 5/5] New way of handling references (#277) --- README.md | 10 +- src/JsonSchema/Constraints/Constraint.php | 62 ++++--- src/JsonSchema/Constraints/Factory.php | 47 +++-- .../Constraints/ObjectConstraint.php | 2 +- src/JsonSchema/Constraints/TypeConstraint.php | 2 +- src/JsonSchema/RefResolver.php | 175 ------------------ src/JsonSchema/SchemaStorage.php | 109 +++++++++++ tests/Constraints/BaseTestCase.php | 54 ++---- ...ResolverTest.php => SchemaStorageTest.php} | 139 ++++++-------- 9 files changed, 252 insertions(+), 348 deletions(-) delete mode 100644 src/JsonSchema/RefResolver.php create mode 100644 src/JsonSchema/SchemaStorage.php rename tests/{RefResolverTest.php => SchemaStorageTest.php} (57%) diff --git a/README.md b/README.md index ed88c0e7..c0c9feb2 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,11 @@ See [json-schema](http://json-schema.org/) for more details. ```php resolve('file://' . realpath('schema.json')); - $data = json_decode(file_get_contents('data.json')); // Validate -$validator = new JsonSchema\Validator(); -$validator->check($data, $schema); +$validator = new JsonSchema\Validator; +$validator->check($data, (object)['$ref' => 'file://' . realpath('schema.json')]); if ($validator->isValid()) { echo "The supplied JSON validates against the schema.\n"; diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index a56acbb4..521a0e06 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -9,8 +9,9 @@ namespace JsonSchema\Constraints; +use JsonSchema\SchemaStorage; use JsonSchema\Uri\UriRetriever; -use JsonSchema\Validator; +use JsonSchema\UriRetrieverInterface; use JsonSchema\Entity\JsonPointer; /** @@ -21,6 +22,7 @@ */ abstract class Constraint implements ConstraintInterface { + protected $schemaStorage; protected $checkMode = self::CHECK_MODE_NORMAL; protected $uriRetriever; protected $errors = array(); @@ -35,19 +37,25 @@ abstract class Constraint implements ConstraintInterface private $factory; /** - * @param int $checkMode - * @param UriRetriever $uriRetriever - * @param Factory $factory + * @param int $checkMode + * @param SchemaStorage $schemaStorage + * @param UriRetrieverInterface $uriRetriever + * @param Factory $factory */ - public function __construct($checkMode = self::CHECK_MODE_NORMAL, UriRetriever $uriRetriever = null, Factory $factory = null) - { - $this->checkMode = $checkMode; - $this->uriRetriever = $uriRetriever; - $this->factory = $factory; + public function __construct( + $checkMode = self::CHECK_MODE_NORMAL, + SchemaStorage $schemaStorage = null, + UriRetrieverInterface $uriRetriever = null, + Factory $factory = null + ) { + $this->checkMode = $checkMode; + $this->uriRetriever = $uriRetriever; + $this->factory = $factory; + $this->schemaStorage = $schemaStorage; } /** - * @return UriRetriever $uriRetriever + * @return UriRetrieverInterface $uriRetriever */ public function getUriRetriever() { @@ -64,16 +72,28 @@ public function getUriRetriever() public function getFactory() { if (!$this->factory) { - $this->factory = new Factory($this->getUriRetriever(), $this->checkMode); + $this->factory = new Factory($this->getSchemaStorage(), $this->getUriRetriever(), $this->checkMode); } return $this->factory; } /** - * @param UriRetriever $uriRetriever + * @return SchemaStorage + */ + public function getSchemaStorage() + { + if (is_null($this->schemaStorage)) { + $this->schemaStorage = new SchemaStorage($this->getUriRetriever()); + } + + return $this->schemaStorage; + } + + /** + * @param UriRetrieverInterface $uriRetriever */ - public function setUriRetriever(UriRetriever $uriRetriever) + public function setUriRetriever(UriRetrieverInterface $uriRetriever) { $this->uriRetriever = $uriRetriever; } @@ -211,7 +231,7 @@ protected function checkType($value, $schema = null, JsonPointer $path = null, $ protected function checkUndefined($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->getFactory()->createInstanceFor('undefined'); - $validator->check($value, $schema, $path, $i); + $validator->check($value, $this->schemaStorage->resolveRefSchema($schema), $path, $i); $this->addErrors($validator->getErrors()); } @@ -280,20 +300,6 @@ protected function checkFormat($value, $schema = null, JsonPointer $path = null, $this->addErrors($validator->getErrors()); } - /** - * @param string $uri JSON Schema URI - * @return string JSON Schema contents - */ - protected function retrieveUri($uri) - { - if (null === $this->uriRetriever) { - $this->setUriRetriever(new UriRetriever); - } - $jsonSchema = $this->uriRetriever->retrieve($uri); - // TODO validate using schema - return $jsonSchema; - } - /** * Get the type check based on the set check mode. * diff --git a/src/JsonSchema/Constraints/Factory.php b/src/JsonSchema/Constraints/Factory.php index 7defbd68..bb12ecec 100644 --- a/src/JsonSchema/Constraints/Factory.php +++ b/src/JsonSchema/Constraints/Factory.php @@ -10,13 +10,20 @@ namespace JsonSchema\Constraints; use JsonSchema\Exception\InvalidArgumentException; +use JsonSchema\SchemaStorage; use JsonSchema\Uri\UriRetriever; +use JsonSchema\UriRetrieverInterface; /** * Factory for centralize constraint initialization. */ class Factory { + /** + * @var SchemaStorage + */ + protected $schemaStorage; + /** * @var UriRetriever $uriRetriever */ @@ -50,34 +57,39 @@ class Factory ); /** - * @param UriRetriever $uriRetriever + * @param SchemaStorage $schemaStorage + * @param UriRetrieverInterface $uriRetriever + * @param int $checkMode */ - public function __construct(UriRetriever $uriRetriever = null, $checkMode = Constraint::CHECK_MODE_NORMAL) - { - if (!$uriRetriever) { - $uriRetriever = new UriRetriever(); - } - - $this->uriRetriever = $uriRetriever; + public function __construct( + SchemaStorage $schemaStorage = null, + UriRetrieverInterface $uriRetriever = null, + $checkMode = Constraint::CHECK_MODE_NORMAL + ) { + $this->uriRetriever = $uriRetriever ?: new UriRetriever; + $this->schemaStorage = $schemaStorage ?: new SchemaStorage($this->uriRetriever); $this->checkMode = $checkMode; } /** - * @return UriRetriever + * @return UriRetrieverInterface */ public function getUriRetriever() { return $this->uriRetriever; } + + public function getSchemaStorage() + { + return $this->schemaStorage; + } public function getTypeCheck() { if (!isset($this->typeCheck[$this->checkMode])) { - if ($this->checkMode === Constraint::CHECK_MODE_TYPE_CAST) { - $this->typeCheck[Constraint::CHECK_MODE_TYPE_CAST] = new TypeCheck\LooseTypeCheck(); - } else { - $this->typeCheck[$this->checkMode] = new TypeCheck\StrictTypeCheck(); - } + $this->typeCheck[$this->checkMode] = $this->checkMode === Constraint::CHECK_MODE_TYPE_CAST + ? new TypeCheck\LooseTypeCheck + : new TypeCheck\StrictTypeCheck; } return $this->typeCheck[$this->checkMode]; @@ -112,7 +124,12 @@ public function setConstraintClass($name, $class) public function createInstanceFor($constraintName) { if (array_key_exists($constraintName, $this->constraintMap)) { - return new $this->constraintMap[$constraintName]($this->checkMode, $this->uriRetriever, $this); + return new $this->constraintMap[$constraintName]( + $this->checkMode, + $this->schemaStorage, + $this->uriRetriever, + $this + ); } throw new InvalidArgumentException('Unknown constraint ' . $constraintName); } diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index f9733971..5a12b87f 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -127,7 +127,7 @@ public function validateElement($element, $matches, $objectDefinition = null, Js public function validateDefinition($element, $objectDefinition = null, JsonPointer $path = null) { foreach ($objectDefinition as $i => $value) { - $property = $this->getProperty($element, $i, new UndefinedConstraint()); + $property = $this->getProperty($element, $i, $this->getFactory()->createInstanceFor('undefined')); $definition = $this->getProperty($objectDefinition, $i); $this->checkUndefined($property, $definition, $path, $i); } diff --git a/src/JsonSchema/Constraints/TypeConstraint.php b/src/JsonSchema/Constraints/TypeConstraint.php index 85e695a2..d082f6a4 100644 --- a/src/JsonSchema/Constraints/TypeConstraint.php +++ b/src/JsonSchema/Constraints/TypeConstraint.php @@ -81,7 +81,7 @@ protected function validateTypesArray($value, array $type, &$validTypesWording, // with a new type constraint if (is_object($tp)) { if (!$isValid) { - $validator = new static($this->checkMode); + $validator = $this->getFactory()->createInstanceFor('type'); $subSchema = new \stdClass(); $subSchema->type = $tp; $validator->check($value, $subSchema, $path, null); diff --git a/src/JsonSchema/RefResolver.php b/src/JsonSchema/RefResolver.php deleted file mode 100644 index 00c7a671..00000000 --- a/src/JsonSchema/RefResolver.php +++ /dev/null @@ -1,175 +0,0 @@ - - * @author Rik Jansen - */ -class RefResolver -{ - /** @var UriRetrieverInterface */ - private $uriRetriever; - - /** @var UriResolverInterface */ - private $uriResolver; - - private $paths = array(); - - /** - * @param UriRetrieverInterface $retriever - * @param UriResolverInterface $uriResolver - */ - public function __construct(UriRetrieverInterface $retriever, UriResolverInterface $uriResolver) - { - $this->uriRetriever = $retriever; - $this->uriResolver = $uriResolver; - } - - /** - * Resolves all schema and all $ref references for the give $sourceUri. Recurse through the object to resolve - * references of any child schemas and return the schema. - * - * @param string $sourceUri URI where this schema was located - * @return object - */ - public function resolve($sourceUri) - { - $jsonPointer = new JsonPointer($sourceUri); - - $fileName = $jsonPointer->getFilename(); - if (!array_key_exists($fileName, $this->paths)) { - $schema = $this->uriRetriever->retrieve($jsonPointer->getFilename()); - $this->paths[$jsonPointer->getFilename()] = $schema; - $this->resolveSchemas($schema, $jsonPointer->getFilename()); - } - $schema = $this->paths[$fileName]; - - return $this->getRefSchema($jsonPointer, $schema); - } - - /** - * Recursive resolve schema by traversing through al nodes - * - * @param object $unresolvedSchema - * @param string $fileName - */ - private function resolveSchemas($unresolvedSchema, $fileName) - { - $objectIterator = new ObjectIterator($unresolvedSchema); - foreach ($objectIterator as $toResolveSchema) { - if (property_exists($toResolveSchema, '$ref') && is_string($toResolveSchema->{'$ref'})) { - $jsonPointer = new JsonPointer($this->uriResolver->resolve($toResolveSchema->{'$ref'}, $fileName)); - $refSchema = $this->resolve((string) $jsonPointer); - $this->unionSchemas($refSchema, $toResolveSchema, $fileName); - } - } - } - - /** - * @param JsonPointer $jsonPointer - * @param object $refSchema - * @throws UnresolvableJsonPointerException when json schema file is found but reference can not be resolved - * @return object - */ - private function getRefSchema(JsonPointer $jsonPointer, $refSchema) - { - foreach ($jsonPointer->getPropertyPaths() as $path) { - if (is_object($refSchema) && property_exists($refSchema, $path)) { - $refSchema = $refSchema->{$path}; - } elseif (is_array($refSchema) && array_key_exists($path, $refSchema)) { - $refSchema = $refSchema[$path]; - } else { - throw new UnresolvableJsonPointerException(sprintf( - 'File: %s is found, but could not resolve fragment: %s', - $jsonPointer->getFilename(), - $jsonPointer->getPropertyPathAsString() - )); - } - } - - return $refSchema; - } - - /** - * @param object $refSchema - * @param object $schema - * @param string $fileName - */ - private function unionSchemas($refSchema, $schema, $fileName) - { - if (property_exists($refSchema, '$ref')) { - $jsonPointer = new JsonPointer($this->uriResolver->resolve($refSchema->{'$ref'}, $fileName)); - $newSchema = $this->resolve((string) $jsonPointer); - $this->unionSchemas($newSchema, $refSchema, $fileName); - } - - unset($schema->{'$ref'}); - if (!$this->hasSubSchemas($schema)) { - foreach (get_object_vars($refSchema) as $prop => $value) { - $schema->$prop = $value; - } - } else { - $newSchema = new \stdClass(); - foreach (get_object_vars($schema) as $prop => $value) { - $newSchema->$prop = $value; - unset($schema->$prop); - } - $schema->allOf = array($newSchema, $refSchema); - } - } - - /** - * @param object $schema - * @return bool - */ - private function hasSubSchemas($schema) - { - foreach (array_keys(get_object_vars($schema)) as $propertyName) { - if (in_array($propertyName, $this->getReservedKeysWhichAreInFactSubSchemas())) { - return true; - } - } - - return false; - } - - /** - * @return string[] - */ - private function getReservedKeysWhichAreInFactSubSchemas() - { - return array( - 'additionalItems', - 'additionalProperties', - 'extends', - 'items', - 'disallow', - 'extends', - 'items', - 'type', - 'allOf', - 'anyOf', - 'oneOf', - 'dependencies', - 'patternProperties', - 'properties' - ); - } -} diff --git a/src/JsonSchema/SchemaStorage.php b/src/JsonSchema/SchemaStorage.php new file mode 100644 index 00000000..97f88eb9 --- /dev/null +++ b/src/JsonSchema/SchemaStorage.php @@ -0,0 +1,109 @@ +uriRetriever = $uriRetriever ?: new UriRetriever; + $this->uriResolver = $uriResolver ?: new UriResolver; + } + + /** + * @return UriRetrieverInterface + */ + public function getUriRetriever() + { + return $this->uriRetriever; + } + + /** + * @return UriResolverInterface + */ + public function getUriResolver() + { + return $this->uriResolver; + } + + /** + * @param string $id + * @param object $schema + */ + public function addSchema($id, $schema = null) + { + if (is_null($schema)) { + $schema = $this->uriRetriever->retrieve($id); + } + $objectIterator = new ObjectIterator($schema); + foreach ($objectIterator as $toResolveSchema) { + if (property_exists($toResolveSchema, '$ref') && is_string($toResolveSchema->{'$ref'})) { + $jsonPointer = new JsonPointer($this->uriResolver->resolve($toResolveSchema->{'$ref'}, $id)); + $toResolveSchema->{'$ref'} = (string)$jsonPointer; + } + } + $this->schemas[$id] = $schema; + } + + /** + * @param string $id + * @return object + */ + public function getSchema($id) + { + if (!array_key_exists($id, $this->schemas)) { + $this->addSchema($id); + } + + return $this->schemas[$id]; + } + + public function resolveRef($ref) + { + $jsonPointer = new JsonPointer($ref); + $refSchema = $this->getSchema($jsonPointer->getFilename()); + + foreach ($jsonPointer->getPropertyPaths() as $path) { + if (is_object($refSchema) && property_exists($refSchema, $path)) { + $refSchema = $this->resolveRefSchema($refSchema->{$path}); + } elseif (is_array($refSchema) && array_key_exists($path, $refSchema)) { + $refSchema = $this->resolveRefSchema($refSchema[$path]); + } else { + throw new UnresolvableJsonPointerException(sprintf( + 'File: %s is found, but could not resolve fragment: %s', + $jsonPointer->getFilename(), + $jsonPointer->getPropertyPathAsString() + )); + } + } + + return $refSchema; + } + + /** + * @param $refSchema + * @return object + */ + public function resolveRefSchema($refSchema) + { + if (is_object($refSchema) && property_exists($refSchema, '$ref')) { + $newSchema = $this->resolveRef($refSchema->{'$ref'}); + $refSchema = (object) (get_object_vars($refSchema) + get_object_vars($newSchema)); + unset($refSchema->{'$ref'}); + } + + return $refSchema; + } +} diff --git a/tests/Constraints/BaseTestCase.php b/tests/Constraints/BaseTestCase.php index 52bcd456..fe6c6951 100644 --- a/tests/Constraints/BaseTestCase.php +++ b/tests/Constraints/BaseTestCase.php @@ -10,7 +10,7 @@ namespace JsonSchema\Tests\Constraints; use JsonSchema\Constraints\Constraint; -use JsonSchema\RefResolver; +use JsonSchema\SchemaStorage; use JsonSchema\Uri\UriResolver; use JsonSchema\Validator; use Prophecy\Argument; @@ -29,19 +29,15 @@ abstract class BaseTestCase extends \PHPUnit_Framework_TestCase /** * @dataProvider getInvalidTests */ - public function testInvalidCases($input, $jsonSchema, $checkMode = Constraint::CHECK_MODE_NORMAL, $errors = array()) + public function testInvalidCases($input, $schema, $checkMode = Constraint::CHECK_MODE_NORMAL, $errors = array()) { $checkMode = $checkMode === null ? Constraint::CHECK_MODE_NORMAL : $checkMode; - $schema = json_decode($jsonSchema); - if (is_object($schema)) { - $schema = $this->resolveSchema($schema); - } - - $value = json_decode($input); + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); + $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); - $validator = new Validator($checkMode); - $validator->check($value, $schema); + $validator = new Validator($checkMode, $schemaStorage); + $validator->check(json_decode($input), $schema); if (array() !== $errors) { $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(),true)); @@ -52,22 +48,18 @@ public function testInvalidCases($input, $jsonSchema, $checkMode = Constraint::C /** * @dataProvider getInvalidForAssocTests */ - public function testInvalidCasesUsingAssoc($input, $jsonSchema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST, $errors = array()) + public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST, $errors = array()) { $checkMode = $checkMode === null ? Constraint::CHECK_MODE_TYPE_CAST : $checkMode; if ($checkMode !== Constraint::CHECK_MODE_TYPE_CAST) { $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"'); } - $schema = json_decode($jsonSchema); - if (is_object($schema)) { - $schema = $this->resolveSchema($schema); - } - - $value = json_decode($input, true); + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); + $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); - $validator = new Validator($checkMode); - $validator->check($value, $schema); + $validator = new Validator($checkMode, $schemaStorage); + $validator->check(json_decode($input, true), $schema); if (array() !== $errors) { $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); @@ -80,15 +72,12 @@ public function testInvalidCasesUsingAssoc($input, $jsonSchema, $checkMode = Con */ public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_MODE_NORMAL) { - $schema = json_decode($schema); - if (is_object($schema)) { - $schema = $this->resolveSchema($schema); - } + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); + $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); - $value = json_decode($input); - $validator = new Validator($checkMode); + $validator = new Validator($checkMode, $schemaStorage); + $validator->check(json_decode($input), $schema); - $validator->check($value, $schema); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } @@ -102,12 +91,11 @@ public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constrain } $schema = json_decode($schema); - if (is_object($schema)) { - $schema = $this->resolveSchema($schema); - } + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema), new UriResolver); + $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); $value = json_decode($input, true); - $validator = new Validator($checkMode); + $validator = new Validator($checkMode, $schemaStorage); $validator->check($value, $schema); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); @@ -143,7 +131,7 @@ public function getInvalidForAssocTests() * @param object $schema * @return object */ - private function resolveSchema($schema) + private function getUriRetrieverMock($schema) { $relativeTestsRoot = realpath(__DIR__ . '/../../vendor/json-schema/JSON-Schema-Test-Suite/remotes'); @@ -168,9 +156,7 @@ private function resolveSchema($schema) return json_decode(file_get_contents($relativeTestsRoot . '/folder' . $urlParts['path'])); } }); - $refResolver = new RefResolver($uriRetriever->reveal(), new UriResolver()); - - return $refResolver->resolve('http://www.my-domain.com/schema.json'); + return $uriRetriever->reveal(); } /** diff --git a/tests/RefResolverTest.php b/tests/SchemaStorageTest.php similarity index 57% rename from tests/RefResolverTest.php rename to tests/SchemaStorageTest.php index 408f56c8..ebd96781 100644 --- a/tests/RefResolverTest.php +++ b/tests/SchemaStorageTest.php @@ -9,92 +9,81 @@ namespace JsonSchema\Tests; -use JsonSchema\RefResolver; -use JsonSchema\Uri\UriResolver; +use JsonSchema\SchemaStorage; use JsonSchema\Uri\UriRetriever; use Prophecy\Argument; -/** - * @package JsonSchema\Tests - * @author Joost Nijhuis - * @author Rik Jansen - * @group RefResolver - */ -class RefResolverTest extends \PHPUnit_Framework_TestCase +class SchemaStorageTest extends \PHPUnit_Framework_TestCase { - /** @var RefResolver */ - private $refResolver; - - /** - * {@inheritdoc} - */ - public function setUp() + public function testResolveRef() { - parent::setUp(); + $mainSchema = $this->getMainSchema(); + $mainSchemaPath = 'http://www.example.com/schema.json'; + + $uriRetriever = $this->prophesize('JsonSchema\UriRetrieverInterface'); + $uriRetriever->retrieve($mainSchemaPath)->willReturn($mainSchema)->shouldBeCalled(); + + $schemaStorage = new SchemaStorage($uriRetriever->reveal()); - $this->refResolver = new RefResolver(new UriRetriever(), new UriResolver()); + $this->assertEquals( + (object) array('type' => 'string'), + $schemaStorage->resolveRef("$mainSchemaPath#/definitions/house/properties/door") + ); } + /** + * @depends testResolveRef + */ public function testSchemaWithLocalAndExternalReferencesWithCircularReference() { $mainSchema = $this->getMainSchema(); $schema2 = $this->getSchema2(); $schema3 = $this->getSchema3(); + $mainSchemaPath = 'http://www.example.com/schema.json'; + $schema2Path = 'http://www.my-domain.com/schema2.json'; + $schema3Path = 'http://www.my-domain.com/schema3.json'; + /** @var UriRetriever $uriRetriever */ $uriRetriever = $this->prophesize('JsonSchema\UriRetrieverInterface'); - $uriRetriever->retrieve('http://www.example.com/schema.json') - ->willReturn($mainSchema) - ->shouldBeCalled($mainSchema); - $uriRetriever->retrieve('http://www.my-domain.com/schema2.json') - ->willReturn($schema2) - ->shouldBeCalled(); - $uriRetriever->retrieve('http://www.my-domain.com/schema3.json') - ->willReturn($schema3) - ->shouldBeCalled(); + $uriRetriever->retrieve($mainSchemaPath)->willReturn($mainSchema)->shouldBeCalled(); + $uriRetriever->retrieve($schema2Path)->willReturn($schema2)->shouldBeCalled(); + $uriRetriever->retrieve($schema3Path)->willReturn($schema3)->shouldBeCalled(); - $refResolver = new RefResolver($uriRetriever->reveal(), new UriResolver()); - $refResolver->resolve('http://www.example.com/schema.json'); + $schemaStorage = new SchemaStorage($uriRetriever->reveal()); - // ref schema merged into schema - $this->assertSame($schema2->definitions->car->type, $mainSchema->properties->car->type); - $this->assertSame( - $schema2->definitions->car->additionalProperties, - $mainSchema->properties->car->additionalProperties + // remote ref + $this->assertEquals( + $schemaStorage->resolveRef("$schema2Path#/definitions/car"), + $schemaStorage->resolveRef("$mainSchemaPath#/properties/car") ); - $this->assertSame($schema2->definitions->car->properties, $mainSchema->properties->car->properties); - $this->assertFalse(property_exists($mainSchema->properties->car, '$ref')); - - // ref schema combined with current schema - $this->assertFalse(property_exists($mainSchema->properties->house, '$ref')); - $this->assertSame(true, $mainSchema->properties->house->allOf[0]->additionalProperties); - $this->assertSame($mainSchema->definitions->house, $mainSchema->properties->house->allOf[1]); - - $this->assertNotSame($mainSchema->definitions->house, $mainSchema->definitions->house->properties->house); - $this->assertNotSame( - $mainSchema->definitions->house, - $mainSchema->definitions->house->properties->house->properties->house + $this->assertEquals( + $schemaStorage->resolveRef("$schema3Path#/wheel"), + $schemaStorage->resolveRef("$mainSchemaPath#/properties/car/properties/wheel") ); - $this->assertSame( - $mainSchema->definitions->house->properties->house, - $mainSchema->definitions->house->properties->house->properties->house->properties->house + + // local ref with overriding + $this->assertNotEquals( + $schemaStorage->resolveRef("$mainSchemaPath#/definitions/house/additionalProperties"), + $schemaStorage->resolveRef("$mainSchemaPath#/properties/house/additionalProperties") ); - $this->assertSame( - $mainSchema->definitions->house->properties->house, - $mainSchema->definitions->house->properties->house->properties->house->properties->house->properties->house + $this->assertEquals( + $schemaStorage->resolveRef("$mainSchemaPath#/definitions/house/properties"), + $schemaStorage->resolveRef("$mainSchemaPath#/properties/house/properties") ); - $this->assertNotSame($schema3->wheel, $mainSchema->properties->car->properties->wheel); - $this->assertSame( - $schema3->wheel->properties->spokes, - $mainSchema->properties->car->properties->wheel->properties->spokes + // recursive ref + $this->assertEquals( + $schemaStorage->resolveRef("$mainSchemaPath#/definitions/house"), + $schemaStorage->resolveRef("$mainSchemaPath#/properties/house/properties/house") + ); + $this->assertEquals( + $schemaStorage->resolveRef("$mainSchemaPath#/definitions/house"), + $schemaStorage->resolveRef("$mainSchemaPath#/properties/house/properties/house/properties/house") ); - - $this->assertNotSame($schema3->wheel->properties->car, $mainSchema->properties->car); - $this->assertSame($schema3->wheel->properties->car->properties, $mainSchema->properties->car->properties); } - function testUnresolvableJsonPointExceptionShouldBeThrown() + public function testUnresolvableJsonPointExceptionShouldBeThrown() { $this->setExpectedException( 'JsonSchema\Exception\UnresolvableJsonPointerException', @@ -102,37 +91,15 @@ function testUnresolvableJsonPointExceptionShouldBeThrown() ); $mainSchema = $this->getInvalidSchema(); + $mainSchemaPath = 'http://www.example.com/schema.json'; $uriRetriever = $this->prophesize('JsonSchema\UriRetrieverInterface'); - $uriRetriever->retrieve('http://www.example.com/schema.json') + $uriRetriever->retrieve($mainSchemaPath) ->willReturn($mainSchema) ->shouldBeCalled($mainSchema); - $refResolver = new RefResolver($uriRetriever->reveal(), new UriResolver()); - $refResolver->resolve('http://www.example.com/schema.json'); - } - - public function testExternalReferencesLoadedOnlyOnce() - { - $mainSchema = $this->getMainSchema(); - $schema2 = $this->getSchema2(); - $schema3 = $this->getSchema3(); - - /** @var UriRetriever $uriRetriever */ - $uriRetriever = $this->prophesize('JsonSchema\UriRetrieverInterface'); - $uriRetriever->retrieve('http://www.example.com/schema.json') - ->willReturn($mainSchema) - ->shouldBeCalledTimes(1); - $uriRetriever->retrieve('http://www.my-domain.com/schema2.json') - ->willReturn($schema2) - ->shouldBeCalledTimes(1); - $uriRetriever->retrieve('http://www.my-domain.com/schema3.json') - ->willReturn($schema3) - ->shouldBeCalledTimes(1); - - $refResolver = new RefResolver($uriRetriever->reveal(), new UriResolver()); - $refResolver->resolve('http://www.example.com/schema.json'); - $refResolver->resolve('http://www.example.com/schema.json'); + $schemaStorage = new SchemaStorage($uriRetriever->reveal()); + $schemaStorage->resolveRef("$mainSchemaPath#/definitions/car"); } /**