From 1224341ed45c91eb7d9b35023c627f175c5932a8 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 26 Nov 2016 13:48:19 -0800 Subject: [PATCH 1/2] Better coercion --- README.md | 9 +- .../Constraints/CollectionConstraint.php | 48 +++++--- src/JsonSchema/Constraints/Constraint.php | 33 +++-- .../Constraints/ObjectConstraint.php | 116 ++++-------------- .../Constraints/SchemaConstraint.php | 11 +- src/JsonSchema/Constraints/TypeConstraint.php | 96 ++++++++++++--- .../Constraints/UndefinedConstraint.php | 49 +++++--- src/JsonSchema/Validator.php | 15 +++ tests/Constraints/CoerciveTest.php | 39 ++++-- 9 files changed, 244 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index 906d4f8d..cb390f2f 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,7 @@ $request = (object)[ 'refundAmount'=>"17" ]; -$factory = new Factory( null, null, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_COERCE ); - -$validator = new Validator($factory); -$validator->check($request, (object) [ +$validator->coerce($request, (object) [ "type"=>"object", "properties"=>(object)[ "processRefund"=>(object)[ @@ -82,8 +79,6 @@ is_bool($request->processRefund); // true is_int($request->refundAmount); // true ``` -Note that the `CHECK_MODE_COERCE` flag will only take effect when an object is passed into the `check()` method. - ### With inline references ```php @@ -144,4 +139,4 @@ $jsonValidator->check($jsonToValidateObject, $jsonSchemaObject); composer test composer testOnly TestClass composer testOnly TestClass::testMethod -``` \ No newline at end of file +``` diff --git a/src/JsonSchema/Constraints/CollectionConstraint.php b/src/JsonSchema/Constraints/CollectionConstraint.php index 6373312c..3dcd611c 100644 --- a/src/JsonSchema/Constraints/CollectionConstraint.php +++ b/src/JsonSchema/Constraints/CollectionConstraint.php @@ -19,10 +19,24 @@ */ class CollectionConstraint extends Constraint { + /** * {@inheritDoc} */ public function check($value, $schema = null, JsonPointer $path = null, $i = null) + { + $this->_check($value, $schema, $path, $i); + } + + /** + * {@inheritDoc} + */ + public function coerce(&$value, $schema = null, JsonPointer $path = null, $i = null) + { + $this->_check($value, $schema, $path, $i, true); + } + + protected function _check(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { // Verify minItems if (isset($schema->minItems) && count($value) < $schema->minItems) { @@ -47,7 +61,7 @@ public function check($value, $schema = null, JsonPointer $path = null, $i = nul // Verify items if (isset($schema->items)) { - $this->validateItems($value, $schema, $path, $i); + $this->validateItems($value, $schema, $path, $i, $coerce); } } @@ -59,7 +73,7 @@ public function check($value, $schema = null, JsonPointer $path = null, $i = nul * @param JsonPointer|null $path * @param string $i */ - protected function validateItems($value, $schema = null, JsonPointer $path = null, $i = null) + protected function validateItems(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { if (is_object($schema->items)) { // just one type definition for the whole array @@ -74,27 +88,27 @@ protected function validateItems($value, $schema = null, JsonPointer $path = nul ) { // performance optimization $type = $schema->items->type; - $validator = $this->factory->createInstanceFor($type === 'integer' ? 'number' : $type); + $typeValidator = $this->factory->createInstanceFor('type'); + $validator = $this->factory->createInstanceFor($type === 'integer' ? 'number' : $type); foreach ($value as $k => $v) { - $k_path = $this->incrementPath($path, $k); + $k_path = $this->incrementPath($path, $k); + if($coerce) { + $typeValidator->coerce($v, $schema->items, $k_path, $i); + } else { + $typeValidator->check($v, $schema->items, $k_path, $i); + } - if (($type === 'string' && !is_string($v)) - || ($type === 'number' && !(is_numeric($v) && !is_string($v))) - || ($type === 'integer' && !is_int($v)) - ){ - $this->addError($k_path, ucwords(gettype($v)) . " value found, but $type is required", 'type'); - } else { - $validator->check($v, $schema, $k_path, $i); - } + $validator->check($v, $schema->items, $k_path, $i); } + $this->addErrors($typeValidator->getErrors()); $this->addErrors($validator->getErrors()); } else { foreach ($value as $k => $v) { $initErrors = $this->getErrors(); // First check if its defined in "items" - $this->checkUndefined($v, $schema->items, $path, $k); + $this->checkUndefined($v, $schema->items, $path, $k, $coerce); // Recheck with "additionalItems" if the first test fails if (count($initErrors) < count($this->getErrors()) && (isset($schema->additionalItems) && $schema->additionalItems !== false)) { @@ -114,19 +128,19 @@ protected function validateItems($value, $schema = null, JsonPointer $path = nul // Defined item type definitions foreach ($value as $k => $v) { if (array_key_exists($k, $schema->items)) { - $this->checkUndefined($v, $schema->items[$k], $path, $k); + $this->checkUndefined($v, $schema->items[$k], $path, $k, $coerce); } else { // Additional items if (property_exists($schema, 'additionalItems')) { if ($schema->additionalItems !== false) { - $this->checkUndefined($v, $schema->additionalItems, $path, $k); + $this->checkUndefined($v, $schema->additionalItems, $path, $k, $coerce); } else { $this->addError( $path, 'The item ' . $i . '[' . $k . '] is not defined and the definition does not allow additional items', 'additionalItems', array('additionalItems' => $schema->additionalItems,)); } } else { // Should be valid against an empty schema - $this->checkUndefined($v, new \stdClass(), $path, $k); + $this->checkUndefined($v, new \stdClass(), $path, $k, $coerce); } } } @@ -134,7 +148,7 @@ protected function validateItems($value, $schema = null, JsonPointer $path = nul // Treat when we have more schema definitions than values, not for empty arrays if (count($value) > 0) { for ($k = count($value); $k < count($schema->items); $k++) { - $this->checkUndefined($this->factory->createInstanceFor('undefined'), $schema->items[$k], $path, $k); + $this->checkUndefined($this->factory->createInstanceFor('undefined'), $schema->items[$k], $path, $k, $coerce); } } } diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 95a63971..26e0344b 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -27,7 +27,6 @@ abstract class Constraint implements ConstraintInterface const CHECK_MODE_NORMAL = 0x00000001; const CHECK_MODE_TYPE_CAST = 0x00000002; - const CHECK_MODE_COERCE = 0x00000004; /** * @var Factory @@ -125,10 +124,14 @@ protected function incrementPath(JsonPointer $path = null, $i) * @param JsonPointer|null $path * @param mixed $i */ - protected function checkArray($value, $schema = null, JsonPointer $path = null, $i = null) + protected function checkArray(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { $validator = $this->factory->createInstanceFor('collection'); - $validator->check($value, $schema, $path, $i); + if($coerce) { + $validator->coerce($value, $schema, $path, $i); + } else { + $validator->check($value, $schema, $path, $i); + } $this->addErrors($validator->getErrors()); } @@ -142,10 +145,14 @@ protected function checkArray($value, $schema = null, JsonPointer $path = null, * @param mixed $i * @param mixed $patternProperties */ - protected function checkObject($value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null) + protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null, $coerce = false) { $validator = $this->factory->createInstanceFor('object'); - $validator->check($value, $schema, $path, $i, $patternProperties); + if($coerce){ + $validator->coerce($value, $schema, $path, $i, $patternProperties); + } else { + $validator->check($value, $schema, $path, $i, $patternProperties); + } $this->addErrors($validator->getErrors()); } @@ -158,10 +165,14 @@ protected function checkObject($value, $schema = null, JsonPointer $path = null, * @param JsonPointer|null $path * @param mixed $i */ - protected function checkType($value, $schema = null, JsonPointer $path = null, $i = null) + protected function checkType(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { $validator = $this->factory->createInstanceFor('type'); - $validator->check($value, $schema, $path, $i); + if($coerce) { + $validator->coerce($value, $schema, $path, $i); + } else { + $validator->check($value, $schema, $path, $i); + } $this->addErrors($validator->getErrors()); } @@ -174,11 +185,15 @@ protected function checkType($value, $schema = null, JsonPointer $path = null, $ * @param JsonPointer|null $path * @param mixed $i */ - protected function checkUndefined($value, $schema = null, JsonPointer $path = null, $i = null) + protected function checkUndefined(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { $validator = $this->factory->createInstanceFor('undefined'); - $validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i); + if($coerce){ + $validator->coerce($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i); + } else { + $validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i); + } $this->addErrors($validator->getErrors()); } diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 3e21a16b..40d843da 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -19,10 +19,23 @@ */ class ObjectConstraint extends Constraint { - /** - * {@inheritDoc} - */ - public function check($element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null) + /** + * {@inheritDoc} + */ + public function check($element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null) + { + $this->_check($element, $definition, $path, $additionalProp, $patternProperties); + } + + /** + * {@inheritDoc} + */ + public function coerce(&$element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null) + { + $this->_check($element, $definition, $path, $additionalProp, $patternProperties, true); + } + + protected function _check(&$element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null, $coerce = false) { if ($element instanceof UndefinedConstraint) { return; @@ -35,7 +48,7 @@ public function check($element, $definition = null, JsonPointer $path = null, $a if ($definition) { // validate the definition properties - $this->validateDefinition($element, $definition, $path); + $this->validateDefinition($element, $definition, $path, $coerce); } // additional the element properties @@ -120,104 +133,19 @@ public function validateElement($element, $matches, $objectDefinition = null, Js * @param \stdClass $objectDefinition ObjectConstraint definition * @param JsonPointer|null $path Path? */ - public function validateDefinition($element, $objectDefinition = null, JsonPointer $path = null) + public function validateDefinition(&$element, $objectDefinition = null, JsonPointer $path = null, $coerce = false) { $undefinedConstraint = $this->factory->createInstanceFor('undefined'); foreach ($objectDefinition as $i => $value) { - $property = $this->getProperty($element, $i, $undefinedConstraint); + $property = &$this->getProperty($element, $i, $undefinedConstraint); $definition = $this->getProperty($objectDefinition, $i); - if($this->factory->getCheckMode() & Constraint::CHECK_MODE_TYPE_CAST){ - if(!($property instanceof Constraint)) { - $property = $this->coerce($property, $definition); - - if($this->factory->getCheckMode() & Constraint::CHECK_MODE_COERCE) { - if (is_object($element)) { - $element->{$i} = $property; - } else { - $element[$i] = $property; - } - } - } - } - if (is_object($definition)) { // Undefined constraint will check for is_object() and quit if is not - so why pass it? - $this->checkUndefined($property, $definition, $path, $i); - } - } - } - - /** - * Converts a value to boolean. For example, "true" becomes true. - * @param $value The value to convert to boolean - * @return bool|mixed - */ - protected function toBoolean($value) - { - if($value === "true"){ - return true; - } - - if($value === "false"){ - return false; - } - - return $value; - } - - /** - * Converts a numeric string to a number. For example, "4" becomes 4. - * - * @param mixed $value The value to convert to a number. - * @return int|float|mixed - */ - protected function toNumber($value) - { - if(is_numeric($value)) { - return $value + 0; // cast to number - } - - return $value; - } - - protected function toInteger($value) - { - if(is_numeric($value) && (int)$value == $value) { - return (int)$value; // cast to number - } - - return $value; - } - - /** - * Given a value and a definition, attempts to coerce the value into the - * type specified by the definition's 'type' property. - * - * @param mixed $value Value to coerce. - * @param \stdClass $definition A definition with information about the expected type. - * @return bool|int|string - */ - protected function coerce($value, $definition) - { - $types = isset($definition->type)?$definition->type:null; - if($types){ - foreach((array)$types as $type) { - switch ($type) { - case "boolean": - $value = $this->toBoolean($value); - break; - case "integer": - $value = $this->toInteger($value); - break; - case "number": - $value = $this->toNumber($value); - break; - } + $this->checkUndefined($property, $definition, $path, $i, $coerce); } } - return $value; } /** @@ -229,7 +157,7 @@ protected function coerce($value, $definition) * * @return mixed */ - protected function getProperty($element, $property, $fallback = null) + protected function &getProperty(&$element, $property, $fallback = null) { if (is_array($element) && (isset($element[$property]) || array_key_exists($property, $element)) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) { return $element[$property]; diff --git a/src/JsonSchema/Constraints/SchemaConstraint.php b/src/JsonSchema/Constraints/SchemaConstraint.php index 071cd123..d8750841 100644 --- a/src/JsonSchema/Constraints/SchemaConstraint.php +++ b/src/JsonSchema/Constraints/SchemaConstraint.php @@ -25,9 +25,13 @@ class SchemaConstraint extends Constraint */ public function check($element, $schema = null, JsonPointer $path = null, $i = null) { + $this->_check($element, $schema, $path, $i); + } + + protected function _check(&$element, $schema = null, JsonPointer $path = null, $i = null, $coerce = false){ if ($schema !== null) { // passed schema - $this->checkUndefined($element, $schema, $path, $i); + $this->checkUndefined($element, $schema, $path, $i, $coerce); } elseif ($this->getTypeCheck()->propertyExists($element, $this->inlineSchemaProperty)) { $inlineSchema = $this->getTypeCheck()->propertyGet($element, $this->inlineSchemaProperty); if (is_array($inlineSchema)) { @@ -39,4 +43,9 @@ public function check($element, $schema = null, JsonPointer $path = null, $i = n throw new InvalidArgumentException('no schema found to verify against'); } } + + public function coerce(&$element, $schema = null, JsonPointer $path = null, $i = null) + { + $this->_check($element, $schema, $path, $i, true); + } } diff --git a/src/JsonSchema/Constraints/TypeConstraint.php b/src/JsonSchema/Constraints/TypeConstraint.php index 1b2125e0..94f57094 100644 --- a/src/JsonSchema/Constraints/TypeConstraint.php +++ b/src/JsonSchema/Constraints/TypeConstraint.php @@ -40,18 +40,31 @@ class TypeConstraint extends Constraint * {@inheritDoc} */ public function check($value = null, $schema = null, JsonPointer $path = null, $i = null) + { + $this->_check($value, $schema, $path, $i); + } + + /** + * {@inheritDoc} + */ + public function coerce(&$value = null, $schema = null, JsonPointer $path = null, $i = null) + { + $this->_check($value, $schema, $path, $i, true); + } + + protected function _check(&$value = null, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { $type = isset($schema->type) ? $schema->type : null; $isValid = false; $wording = array(); if (is_array($type)) { - $this->validateTypesArray($value, $type, $wording, $isValid, $path); + $this->validateTypesArray($value, $type, $wording, $isValid, $path, $coerce); } elseif (is_object($type)) { $this->checkUndefined($value, $type, $path); return; } else { - $isValid = $this->validateType($value, $type); + $isValid = $this->validateType($value, $type, $coerce); } if ($isValid === false) { @@ -75,8 +88,8 @@ public function check($value = null, $schema = null, JsonPointer $path = null, $ * @param boolean $isValid The current validation value * @param $path */ - protected function validateTypesArray($value, array $type, &$validTypesWording, &$isValid, - $path) { + protected function validateTypesArray(&$value, array $type, &$validTypesWording, &$isValid, + $path, $coerce = false) { 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 @@ -94,7 +107,7 @@ protected function validateTypesArray($value, array $type, &$validTypesWording, $this->validateTypeNameWording( $tp ); $validTypesWording[] = self::$wording[$tp]; if (!$isValid) { - $isValid = $this->validateType( $value, $tp ); + $isValid = $this->validateType( $value, $tp, $coerce ); } } } @@ -149,33 +162,46 @@ protected function validateTypeNameWording( $type) { * * @throws InvalidArgumentException */ - protected function validateType($value, $type) + protected function validateType(&$value, $type, $coerce = false) { //mostly the case for inline schema if (!$type) { return true; } + if ('any' === $type) { + return true; + } + + if ('object' === $type) { + return $this->getTypeCheck()->isObject($value); + } + + if ('array' === $type) { + return $this->getTypeCheck()->isArray($value); + } + if ('integer' === $type) { + if ($coerce) { + $value = $this->toInteger($value); + } return is_int($value); } if ('number' === $type) { + if ($coerce) { + $value = $this->toNumber($value); + } return is_numeric($value) && !is_string($value); } if ('boolean' === $type) { + if ($coerce) { + $value = $this->toBoolean($value); + } return is_bool($value); } - if ('object' === $type) { - return $this->getTypeCheck()->isObject($value); - } - - if ('array' === $type) { - return $this->getTypeCheck()->isArray($value); - } - if ('string' === $type) { return is_string($value); } @@ -188,10 +214,48 @@ protected function validateType($value, $type) return is_null($value); } - if ('any' === $type) { + throw new InvalidArgumentException((is_object($value) ? 'object' : $value) . ' is an invalid type for ' . $type); + } + + /** + * Converts a value to boolean. For example, "true" becomes true. + * @param $value The value to convert to boolean + * @return bool|mixed + */ + protected function toBoolean($value) + { + if($value === "true"){ return true; } - throw new InvalidArgumentException((is_object($value) ? 'object' : $value) . ' is an invalid type for ' . $type); + if($value === "false"){ + return false; + } + + return $value; + } + + /** + * Converts a numeric string to a number. For example, "4" becomes 4. + * + * @param mixed $value The value to convert to a number. + * @return int|float|mixed + */ + protected function toNumber($value) + { + if(is_numeric($value)) { + return $value + 0; // cast to number + } + + return $value; + } + + protected function toInteger($value) + { + if(is_numeric($value) && (int)$value == $value) { + return (int)$value; // cast to number + } + + return $value; } } diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 59a8c843..4538f5b4 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -20,10 +20,18 @@ */ class UndefinedConstraint extends Constraint { - /** - * {@inheritDoc} - */ - public function check($value, $schema = null, JsonPointer $path = null, $i = null) + /** + * {@inheritDoc} + */ + public function check($value, $schema = null, JsonPointer $path = null, $i = null){ + $this->_check($value, $schema, $path, $i); + } + + public function coerce(&$value, $schema = null, JsonPointer $path = null, $i = null){ + $this->_check($value, $schema, $path, $i, true); + } + + protected function _check(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { if (is_null($schema) || !is_object($schema)) { return; @@ -32,13 +40,13 @@ public function check($value, $schema = null, JsonPointer $path = null, $i = nul $path = $this->incrementPath($path ?: new JsonPointer(''), $i); // check special properties - $this->validateCommonProperties($value, $schema, $path); + $this->validateCommonProperties($value, $schema, $path, $i, $coerce); // check allOf, anyOf, and oneOf properties - $this->validateOfProperties($value, $schema, $path); + $this->validateOfProperties($value, $schema, $path, '', $coerce); // check known types - $this->validateTypes($value, $schema, $path, $i); + $this->validateTypes($value, $schema, $path, $i, $coerce); } /** @@ -49,11 +57,11 @@ public function check($value, $schema = null, JsonPointer $path = null, $i = nul * @param JsonPointer $path * @param string $i */ - public function validateTypes($value, $schema = null, JsonPointer $path, $i = null) + public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = null, $coerce = false) { // check array if ($this->getTypeCheck()->isArray($value)) { - $this->checkArray($value, $schema, $path, $i); + $this->checkArray($value, $schema, $path, $i, $coerce); } // check object @@ -63,13 +71,14 @@ public function validateTypes($value, $schema = null, JsonPointer $path, $i = nu isset($schema->properties) ? $this->factory->getSchemaStorage()->resolveRefSchema($schema->properties) : $schema, $path, isset($schema->additionalProperties) ? $schema->additionalProperties : null, - isset($schema->patternProperties) ? $schema->patternProperties : null + isset($schema->patternProperties) ? $schema->patternProperties : null, + $coerce ); } // check string if (is_string($value)) { - $this->checkString($value, $schema, $path, $i); + $this->checkString($value, $schema, $path, $i, $coerce); } // check numeric @@ -91,7 +100,7 @@ public function validateTypes($value, $schema = null, JsonPointer $path, $i = nu * @param JsonPointer $path * @param string $i */ - protected function validateCommonProperties($value, $schema = null, JsonPointer $path, $i = "") + protected function validateCommonProperties(&$value, $schema = null, JsonPointer $path, $i = "", $coerce=false) { // if it extends another schema, it must pass that schema as well if (isset($schema->extends)) { @@ -100,10 +109,10 @@ protected function validateCommonProperties($value, $schema = null, JsonPointer } if (is_array($schema->extends)) { foreach ($schema->extends as $extends) { - $this->checkUndefined($value, $extends, $path, $i); + $this->checkUndefined($value, $extends, $path, $i, $coerce); } } else { - $this->checkUndefined($value, $schema->extends, $path, $i); + $this->checkUndefined($value, $schema->extends, $path, $i, $coerce); } } @@ -130,7 +139,7 @@ protected function validateCommonProperties($value, $schema = null, JsonPointer // Verify type if (!($value instanceof UndefinedConstraint)) { - $this->checkType($value, $schema, $path); + $this->checkType($value, $schema, $path, $i, $coerce); } // Verify disallowed items @@ -151,7 +160,7 @@ protected function validateCommonProperties($value, $schema = null, JsonPointer if (isset($schema->not)) { $initErrors = $this->getErrors(); - $this->checkUndefined($value, $schema->not, $path, $i); + $this->checkUndefined($value, $schema->not, $path, $i, $coerce); // if no new errors were raised then the instance validated against the "not" schema if (count($this->getErrors()) == count($initErrors)) { @@ -175,7 +184,7 @@ protected function validateCommonProperties($value, $schema = null, JsonPointer * @param JsonPointer $path * @param string $i */ - protected function validateOfProperties($value, $schema, JsonPointer $path, $i = "") + protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i = "", $coerce = false) { // Verify type if ($value instanceof UndefinedConstraint) { @@ -186,7 +195,7 @@ protected function validateOfProperties($value, $schema, JsonPointer $path, $i = $isValid = true; foreach ($schema->allOf as $allOf) { $initErrors = $this->getErrors(); - $this->checkUndefined($value, $allOf, $path, $i); + $this->checkUndefined($value, $allOf, $path, $i, $coerce); $isValid = $isValid && (count($this->getErrors()) == count($initErrors)); } if (!$isValid) { @@ -199,7 +208,7 @@ protected function validateOfProperties($value, $schema, JsonPointer $path, $i = $startErrors = $this->getErrors(); foreach ($schema->anyOf as $anyOf) { $initErrors = $this->getErrors(); - $this->checkUndefined($value, $anyOf, $path, $i); + $this->checkUndefined($value, $anyOf, $path, $i, $coerce); if ($isValid = (count($this->getErrors()) == count($initErrors))) { break; } @@ -217,7 +226,7 @@ protected function validateOfProperties($value, $schema, JsonPointer $path, $i = $startErrors = $this->getErrors(); foreach ($schema->oneOf as $oneOf) { $this->errors = array(); - $this->checkUndefined($value, $oneOf, $path, $i); + $this->checkUndefined($value, $oneOf, $path, $i, $coerce); if (count($this->getErrors()) == 0) { $matchedSchemas++; } diff --git a/src/JsonSchema/Validator.php b/src/JsonSchema/Validator.php index 39dca096..5f2ba09b 100644 --- a/src/JsonSchema/Validator.php +++ b/src/JsonSchema/Validator.php @@ -37,4 +37,19 @@ public function check($value, $schema = null, JsonPointer $path = null, $i = nul $this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR)); } + + /** + * Does everything that check does, but will also coerce string values in the input to their expected + * types defined in the schema whenever possible. Note that the first argument is passed by reference, + * so you must pass in a variable. + * + * {@inheritDoc} + */ + public function coerce(&$value, $schema = null, JsonPointer $path = null, $i = null) + { + $validator = $this->factory->createInstanceFor('schema'); + $validator->coerce($value, $schema); + + $this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR)); + } } diff --git a/tests/Constraints/CoerciveTest.php b/tests/Constraints/CoerciveTest.php index 5115988a..90b54a05 100644 --- a/tests/Constraints/CoerciveTest.php +++ b/tests/Constraints/CoerciveTest.php @@ -22,7 +22,7 @@ class CoerciveTest extends BasicTypesTest */ public function testValidCoerceCasesUsingAssoc($input, $schema) { - $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST; + $checkMode = Constraint::CHECK_MODE_TYPE_CAST; $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); @@ -31,7 +31,7 @@ public function testValidCoerceCasesUsingAssoc($input, $schema) $value = json_decode($input, true); - $validator->check($value, $schema); + $validator->coerce($value, $schema); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } @@ -40,7 +40,7 @@ public function testValidCoerceCasesUsingAssoc($input, $schema) */ public function testValidCoerceCases($input, $schema, $errors = array()) { - $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST; + $checkMode = Constraint::CHECK_MODE_TYPE_CAST; $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); @@ -52,7 +52,7 @@ public function testValidCoerceCases($input, $schema, $errors = array()) $this->assertTrue(gettype($value->integer) == "string"); $this->assertTrue(gettype($value->boolean) == "string"); - $validator->check($value, $schema); + $validator->coerce($value, $schema); $this->assertTrue(gettype($value->number) == "double"); $this->assertTrue(gettype($value->integer) == "integer"); @@ -81,13 +81,14 @@ public function testValidCoerceCases($input, $schema, $errors = array()) */ public function testInvalidCoerceCases($input, $schema, $errors = array()) { - $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST; + $checkMode = Constraint::CHECK_MODE_TYPE_CAST; $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); - $validator->check(json_decode($input), $schema); + $value = json_decode($input); + $validator->coerce($value, $schema); if (array() !== $errors) { $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); @@ -100,13 +101,14 @@ public function testInvalidCoerceCases($input, $schema, $errors = array()) */ public function testInvalidCoerceCasesUsingAssoc($input, $schema, $errors = array()) { - $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST; + $checkMode = Constraint::CHECK_MODE_TYPE_CAST; $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); - $validator->check(json_decode($input, true), $schema); + $value = json_decode($input, true); + $validator->coerce($value, $schema); if (array() !== $errors) { $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); @@ -128,9 +130,12 @@ public function getValidCoerceTests() "array":[], "null":null, "any": "string", + "allOf": "1", "multitype1": "false", "multitype2": "1.2", "multitype3": "7", + "arrayOfIntegers":["-1","0","1"], + "tupleTyping":["1","2.2","true"], "any1": 2.6, "any2": 4, "any3": false, @@ -150,9 +155,27 @@ public function getValidCoerceTests() "array":{"type":"array"}, "null":{"type":"null"}, "any": {"type":"any"}, + "allOf" : {"allOf":[{ + "type" : "string" + },{ + "type" : "integer" + }]}, "multitype1": {"type":["boolean","integer","number"]}, "multitype2": {"type":["boolean","integer","number"]}, "multitype3": {"type":["boolean","integer","number"]}, + "arrayOfIntegers":{ + "items":{ + "type":"integer" + } + }, + "tupleTyping":{ + "type":"array", + "items":[ + {"type":"integer"}, + {"type":"number"} + ], + "additionalItems":{"type":"boolean"} + }, "any1": {"type":"any"}, "any2": {"type":"any"}, "any3": {"type":"any"}, From 8885faff1c21e4cd5afc9fab3a91841e07ba9365 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 12 Dec 2016 09:20:12 -0800 Subject: [PATCH 2/2] add phpdocs --- src/JsonSchema/Constraints/CollectionConstraint.php | 1 + src/JsonSchema/Constraints/Constraint.php | 4 ++++ src/JsonSchema/Constraints/ObjectConstraint.php | 1 + src/JsonSchema/Constraints/TypeConstraint.php | 3 ++- src/JsonSchema/Constraints/UndefinedConstraint.php | 3 +++ 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/JsonSchema/Constraints/CollectionConstraint.php b/src/JsonSchema/Constraints/CollectionConstraint.php index 3dcd611c..b71ec423 100644 --- a/src/JsonSchema/Constraints/CollectionConstraint.php +++ b/src/JsonSchema/Constraints/CollectionConstraint.php @@ -72,6 +72,7 @@ protected function _check(&$value, $schema = null, JsonPointer $path = null, $i * @param \stdClass $schema * @param JsonPointer|null $path * @param string $i + * @param boolean $coerce */ protected function validateItems(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 26e0344b..5d27e431 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -123,6 +123,7 @@ protected function incrementPath(JsonPointer $path = null, $i) * @param mixed $schema * @param JsonPointer|null $path * @param mixed $i + * @param boolean $coerce */ protected function checkArray(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { @@ -144,6 +145,7 @@ protected function checkArray(&$value, $schema = null, JsonPointer $path = null, * @param JsonPointer|null $path * @param mixed $i * @param mixed $patternProperties + * @param boolean $coerce */ protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null, $coerce = false) { @@ -164,6 +166,7 @@ protected function checkObject(&$value, $schema = null, JsonPointer $path = null * @param mixed $schema * @param JsonPointer|null $path * @param mixed $i + * @param boolean $coerce */ protected function checkType(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { @@ -184,6 +187,7 @@ protected function checkType(&$value, $schema = null, JsonPointer $path = null, * @param mixed $schema * @param JsonPointer|null $path * @param mixed $i + * @param boolean $coerce */ protected function checkUndefined(&$value, $schema = null, JsonPointer $path = null, $i = null, $coerce = false) { diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 40d843da..5e84ac4b 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -132,6 +132,7 @@ public function validateElement($element, $matches, $objectDefinition = null, Js * @param \stdClass $element Element to validate * @param \stdClass $objectDefinition ObjectConstraint definition * @param JsonPointer|null $path Path? + * @param boolean $coerce Whether to coerce strings to expected types or not */ public function validateDefinition(&$element, $objectDefinition = null, JsonPointer $path = null, $coerce = false) { diff --git a/src/JsonSchema/Constraints/TypeConstraint.php b/src/JsonSchema/Constraints/TypeConstraint.php index 94f57094..37c3d791 100644 --- a/src/JsonSchema/Constraints/TypeConstraint.php +++ b/src/JsonSchema/Constraints/TypeConstraint.php @@ -157,7 +157,8 @@ protected function validateTypeNameWording( $type) { * * @param mixed $value Value to validate * @param string $type TypeConstraint to check against - * + * @param boolean $coerce Whether to coerce strings to expected types or not + * * @return boolean * * @throws InvalidArgumentException diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 4538f5b4..73e85a1c 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -56,6 +56,7 @@ protected function _check(&$value, $schema = null, JsonPointer $path = null, $i * @param mixed $schema * @param JsonPointer $path * @param string $i + * @param boolean $coerce */ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = null, $coerce = false) { @@ -99,6 +100,7 @@ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = n * @param mixed $schema * @param JsonPointer $path * @param string $i + * @param boolean $coerce */ protected function validateCommonProperties(&$value, $schema = null, JsonPointer $path, $i = "", $coerce=false) { @@ -183,6 +185,7 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer * @param mixed $schema * @param JsonPointer $path * @param string $i + * @param boolean $coerce */ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i = "", $coerce = false) {