From 6fbd380dc93debb9a8866869add891c6bfdc5db5 Mon Sep 17 00:00:00 2001 From: Erayd Date: Thu, 2 Feb 2017 21:38:14 +1300 Subject: [PATCH 1/4] Add option to apply default values from the schema --- README.md | 38 ++++- src/JsonSchema/Constraints/Factory.php | 1 + .../Constraints/TypeCheck/LooseTypeCheck.php | 10 ++ .../Constraints/TypeCheck/StrictTypeCheck.php | 5 + .../TypeCheck/TypeCheckInterface.php | 2 + .../Constraints/UndefinedConstraint.php | 36 ++++- tests/Constraints/DefaultPropertiesTest.php | 139 ++++++++++++++++++ tests/Constraints/TypeTest.php | 14 ++ 8 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 tests/Constraints/DefaultPropertiesTest.php diff --git a/README.md b/README.md index fee6dfb9..e3ef0ccf 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,40 @@ $validator->coerce($request, $schema); // equivalent to $validator->validate($data, $schema, Constraint::CHECK_MODE_COERCE_TYPES); ``` +### Default values + +If your schema contains default values, you can have these automatically applied during validation: + +```php +17 +]; + +$validator = new Validator(); + +$validator->validate( + $request, + (object)[ + "type"=>"object", + "properties"=>(object)[ + "processRefund"=>(object)[ + "type"=>"boolean", + "default"=>true + ] + ] + ], + Constraint::CHECK_MODE_APPLY_DEFAULTS +); //validates, and sets defaults for missing properties + +is_bool($request->processRefund); // true +$request->processRefund; // true +``` + ### With inline references ```php @@ -152,9 +186,11 @@ third argument to `Validator::validate()`, or can be provided as the third argum | `Constraint::CHECK_MODE_NORMAL` | Validate in 'normal' mode - this is the default | | `Constraint::CHECK_MODE_TYPE_CAST` | Enable fuzzy type checking for associative arrays and objects | | `Constraint::CHECK_MODE_COERCE_TYPES` | Convert data types to match the schema where possible | +| `Constraint::CHECK_MODE_APPLY_DEFAULTS` | Apply default values from the schema if not set | | `Constraint::CHECK_MODE_EXCEPTIONS` | Throw an exception immediately if validation fails | -Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` will modify your original data. +Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` or `Constraint::CHECK_MODE_APPLY_DEFAULTS` +will modify your original data. ## Running the tests diff --git a/src/JsonSchema/Constraints/Factory.php b/src/JsonSchema/Constraints/Factory.php index f6b97457..6d7c51d5 100644 --- a/src/JsonSchema/Constraints/Factory.php +++ b/src/JsonSchema/Constraints/Factory.php @@ -15,6 +15,7 @@ use JsonSchema\SchemaStorageInterface; use JsonSchema\Uri\UriRetriever; use JsonSchema\UriRetrieverInterface; +use JsonSchema\Constraints\Constraint; /** * Factory for centralize constraint initialization. diff --git a/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php b/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php index 4818bf6d..39dd4926 100644 --- a/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php +++ b/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php @@ -27,6 +27,16 @@ public static function propertyGet($value, $property) return $value[$property]; } + public static function propertySet(&$value, $property, $data) + { + if (is_object($value)) { + $value->{$property} = $data; + } else { + $value[$property] = $data; + } + } + + public static function propertyExists($value, $property) { if (is_object($value)) { diff --git a/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php b/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php index 73bbc383..a6303a7a 100644 --- a/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php +++ b/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php @@ -19,6 +19,11 @@ public static function propertyGet($value, $property) return $value->{$property}; } + public static function propertySet(&$value, $property, $data) + { + $value->{$property} = $data; + } + public static function propertyExists($value, $property) { return property_exists($value, $property); diff --git a/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php b/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php index 5fd68acd..10b40ea0 100644 --- a/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php +++ b/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php @@ -10,6 +10,8 @@ public static function isArray($value); public static function propertyGet($value, $property); + public static function propertySet(&$value, $property, $data); + public static function propertyExists($value, $property); public static function propertyCount($value); diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 2b0688ec..c46b46f7 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; use JsonSchema\Entity\JsonPointer; use JsonSchema\Uri\UriResolver; @@ -57,7 +58,9 @@ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = n } // check object - if ($this->getTypeCheck()->isObject($value)) { + if (LooseTypeCheck::isObject($value)) { // object processing should always be run on assoc arrays, + // so use LooseTypeCheck here even if CHECK_MODE_TYPE_CAST + // is not set (i.e. don't use $this->getTypeCheck() here). $this->checkObject( $value, isset($schema->properties) ? $this->factory->getSchemaStorage()->resolveRefSchema($schema->properties) : $schema, @@ -107,6 +110,37 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } } + // Apply default values from schema + if ($this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { + if ($this->getTypeCheck()->isObject($value) && isset($schema->properties)) { + // $value is an object, so apply default properties if defined + foreach ($schema->properties as $i => $propertyDefinition) { + if (!$this->getTypeCheck()->propertyExists($value, $i) && isset($propertyDefinition->default)) { + $this->getTypeCheck()->propertySet($value, $i, $propertyDefinition->default); + } + } + } elseif ($this->getTypeCheck()->isArray($value)) { + if (isset($schema->properties)) { + // $value is an array, but default properties are defined, so treat as assoc + foreach ($schema->properties as $i => $propertyDefinition) { + if (!isset($value[$i]) && isset($propertyDefinition->default)) { + $value[$i] = $propertyDefinition->default; + } + } + } elseif (isset($schema->items)) { + // $value is an array, and default items are defined - treat as plain array + foreach ($schema->items as $i => $itemDefinition) { + if (!isset($value[$i]) && isset($itemDefinition->default)) { + $value[$i] = $itemDefinition->default; + } + } + } + } elseif (($value instanceof UndefinedConstraint || $value === null) && isset($schema->default)) { + // $value is a leaf, not a container - apply the default directly + $value = $schema->default; + } + } + // Verify required values if ($this->getTypeCheck()->isObject($value)) { if (!($value instanceof self) && isset($schema->required) && is_array($schema->required)) { diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php new file mode 100644 index 00000000..9fb7fb29 --- /dev/null +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -0,0 +1,139 @@ +validate($inputDecoded, json_decode($schema)); + + $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); + + if ($expectOutput !== null) { + $this->assertEquals($expectOutput, json_encode($inputDecoded)); + } + } + + /** + * @dataProvider getValidTests + */ + public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null) + { + $input = json_decode($input, true); + + $factory = new Factory(null, null, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_APPLY_DEFAULTS); + self::testValidCases($input, $schema, $expectOutput, new Validator($factory)); + } + + /** + * @dataProvider getValidTests + */ + public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expectOutput = null) + { + $input = json_decode($input, true); + $factory = new Factory(null, null, Constraint::CHECK_MODE_APPLY_DEFAULTS); + self::testValidCases($input, $schema, $expectOutput, new Validator($factory)); + + } +} diff --git a/tests/Constraints/TypeTest.php b/tests/Constraints/TypeTest.php index 10d3ad21..f571ea26 100644 --- a/tests/Constraints/TypeTest.php +++ b/tests/Constraints/TypeTest.php @@ -10,6 +10,7 @@ namespace JsonSchema\Tests\Constraints; use JsonSchema\Constraints\TypeConstraint; +use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; /** * Class TypeTest @@ -51,6 +52,19 @@ public function testIndefiniteArticleForTypeInTypeCheckErrorMessage($type, $word $this->assertTypeConstraintError(ucwords($label) . " value found, but $wording is required", $constraint); } + /** + * Test uncovered areas of the loose type checker + */ + public function testLooseTypeChecking() + { + $v = new \StdClass(); + $v->property = 'dataOne'; + LooseTypeCheck::propertySet($v, 'property', 'dataTwo'); + $this->assertEquals('dataTwo', $v->property); + $this->assertEquals('dataTwo', LooseTypeCheck::propertyGet($v, 'property')); + $this->assertEquals(1, LooseTypeCheck::propertyCount($v)); + } + /** * Helper to assert an error message * From 16b1a04eee1da21674fe953070f9a4184df284e8 Mon Sep 17 00:00:00 2001 From: Erayd Date: Fri, 17 Feb 2017 11:08:06 +1300 Subject: [PATCH 2/4] Clone default objects instead of passing by reference Objects should always be assigned via clone, to prevent modifications to the input object from also modifying the underlying schema. --- .../Constraints/UndefinedConstraint.php | 20 ++++++++++++--- tests/Constraints/DefaultPropertiesTest.php | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index c46b46f7..44e3fba4 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -116,7 +116,11 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer // $value is an object, so apply default properties if defined foreach ($schema->properties as $i => $propertyDefinition) { if (!$this->getTypeCheck()->propertyExists($value, $i) && isset($propertyDefinition->default)) { - $this->getTypeCheck()->propertySet($value, $i, $propertyDefinition->default); + if (is_object($propertyDefinition->default)) { + $this->getTypeCheck()->propertySet($value, $i, clone $propertyDefinition->default); + } else { + $this->getTypeCheck()->propertySet($value, $i, $propertyDefinition->default); + } } } } elseif ($this->getTypeCheck()->isArray($value)) { @@ -124,20 +128,28 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer // $value is an array, but default properties are defined, so treat as assoc foreach ($schema->properties as $i => $propertyDefinition) { if (!isset($value[$i]) && isset($propertyDefinition->default)) { - $value[$i] = $propertyDefinition->default; + if (is_object($propertyDefinition->default)) { + $value[$i] = clone $propertyDefinition->default; + } else { + $value[$i] = $propertyDefinition->default; + } } } } elseif (isset($schema->items)) { // $value is an array, and default items are defined - treat as plain array foreach ($schema->items as $i => $itemDefinition) { if (!isset($value[$i]) && isset($itemDefinition->default)) { - $value[$i] = $itemDefinition->default; + if (is_object($itemDefinition->default)) { + $value[$i] = clone $itemDefinition->default; + } else { + $value[$i] = $itemDefinition->default; + } } } } } elseif (($value instanceof UndefinedConstraint || $value === null) && isset($schema->default)) { // $value is a leaf, not a container - apply the default directly - $value = $schema->default; + $value = is_object($schema->default) ? clone $schema->default : $schema->default; } } diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index 9fb7fb29..af9ebfd8 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -87,6 +87,16 @@ public function getValidTests() '{"propertyOne":"alreadySetValueOne"}', '{"properties":{"propertyOne":{"type":"string"}}}', '{"propertyOne":"alreadySetValueOne"}' + ), + array(// default property value is an object + '{"propertyOne":"valueOne"}', + '{"properties":{"propertyTwo":{"default":{}}}}', + '{"propertyOne":"valueOne","propertyTwo":{}}' + ), + array(// default item value is an object + '[]', + '{"type":"array","items":[{"default":{}}]}', + '[{}]' ) ); } @@ -136,4 +146,19 @@ public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expect self::testValidCases($input, $schema, $expectOutput, new Validator($factory)); } + + public function testNoModificationViaReferences() + { + $input = json_decode(''); + $schema = jsoN_decode('{"default":{"propertyOne":"valueOne"}}'); + + $validator = new Validator(); + $validator->validate($input, $schema, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_APPLY_DEFAULTS); + + $this->assertEquals('{"propertyOne":"valueOne"}', json_encode($input)); + + $input->propertyOne = "valueTwo"; + $this->assertEquals("valueOne", $schema->default->propertyOne); + + } } From 6c231a61f376721d145cabf6accdf426bd3e7213 Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 22 Feb 2017 10:05:28 +1300 Subject: [PATCH 3/4] Run php-cs-fixer --- src/JsonSchema/Constraints/Factory.php | 2 +- .../Constraints/TypeCheck/LooseTypeCheck.php | 1 - src/JsonSchema/Constraints/UndefinedConstraint.php | 2 +- tests/Constraints/DefaultPropertiesTest.php | 13 ++++++------- tests/Constraints/TypeTest.php | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/JsonSchema/Constraints/Factory.php b/src/JsonSchema/Constraints/Factory.php index 6d7c51d5..8c24873f 100644 --- a/src/JsonSchema/Constraints/Factory.php +++ b/src/JsonSchema/Constraints/Factory.php @@ -9,13 +9,13 @@ namespace JsonSchema\Constraints; +use JsonSchema\Constraints\Constraint; use JsonSchema\Exception\InvalidArgumentException; use JsonSchema\Exception\InvalidConfigException; use JsonSchema\SchemaStorage; use JsonSchema\SchemaStorageInterface; use JsonSchema\Uri\UriRetriever; use JsonSchema\UriRetrieverInterface; -use JsonSchema\Constraints\Constraint; /** * Factory for centralize constraint initialization. diff --git a/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php b/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php index 39dd4926..98428853 100644 --- a/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php +++ b/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php @@ -36,7 +36,6 @@ public static function propertySet(&$value, $property, $data) } } - public static function propertyExists($value, $property) { if (is_object($value)) { diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 44e3fba4..88169306 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -147,7 +147,7 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } } } - } elseif (($value instanceof UndefinedConstraint || $value === null) && isset($schema->default)) { + } elseif (($value instanceof self || $value === null) && isset($schema->default)) { // $value is a leaf, not a container - apply the default directly $value = is_object($schema->default) ? clone $schema->default : $schema->default; } diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index af9ebfd8..6b3c7839 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -8,10 +8,11 @@ */ namespace JsonSchema\Tests\Constraints; -use JsonSchema\SchemaStorage; -use JsonSchema\Validator; + use JsonSchema\Constraints\Constraint; use JsonSchema\Constraints\Factory; +use JsonSchema\SchemaStorage; +use JsonSchema\Validator; class DefaultPropertiesTest extends VeryBaseTestCase { @@ -144,21 +145,19 @@ public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expect $input = json_decode($input, true); $factory = new Factory(null, null, Constraint::CHECK_MODE_APPLY_DEFAULTS); self::testValidCases($input, $schema, $expectOutput, new Validator($factory)); - } public function testNoModificationViaReferences() { $input = json_decode(''); - $schema = jsoN_decode('{"default":{"propertyOne":"valueOne"}}'); + $schema = json_decode('{"default":{"propertyOne":"valueOne"}}'); $validator = new Validator(); $validator->validate($input, $schema, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_APPLY_DEFAULTS); $this->assertEquals('{"propertyOne":"valueOne"}', json_encode($input)); - $input->propertyOne = "valueTwo"; - $this->assertEquals("valueOne", $schema->default->propertyOne); - + $input->propertyOne = 'valueTwo'; + $this->assertEquals('valueOne', $schema->default->propertyOne); } } diff --git a/tests/Constraints/TypeTest.php b/tests/Constraints/TypeTest.php index f571ea26..df8d6dd1 100644 --- a/tests/Constraints/TypeTest.php +++ b/tests/Constraints/TypeTest.php @@ -9,8 +9,8 @@ namespace JsonSchema\Tests\Constraints; -use JsonSchema\Constraints\TypeConstraint; use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; +use JsonSchema\Constraints\TypeConstraint; /** * Class TypeTest From 6ba3c22a9ffd9f1a9d38ae9665095a5fcc72fc6b Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 22 Feb 2017 13:14:48 +1300 Subject: [PATCH 4/4] Remove two duplicate test cases --- tests/Constraints/DefaultPropertiesTest.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index 6b3c7839..6687e7c2 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -34,11 +34,6 @@ public function getValidTests() '{"properties":{"propertyTwo":{"default":"valueTwo"}}}', '{"propertyOne":"valueOne","propertyTwo":"valueTwo"}' ), - array(// fulfil required property with a default value - '{"propertyOne":"valueOne"}', - '{"properties":{"propertyTwo":{"required":true,"default":"valueTwo"}}}', - '{"propertyOne":"valueOne","propertyTwo":"valueTwo"}' - ), array(// default value for sub-property '{"propertyOne":{}}', '{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}', @@ -69,11 +64,6 @@ public function getValidTests() '{"properties":{"propertyOne":{"default":"valueOne"}}}', '{"propertyOne":"alreadySetValueOne"}' ), - array(//default value is required - '{"propertyOne":"valueOne"}', - '{"properties":{"propertyTwo":{"default":"valueTwo","required":true}}}', - '{"propertyOne":"valueOne","propertyTwo":"valueTwo"}' - ), array(//default item value for an array '["valueOne"]', '{"type":"array","items":[{},{"type":"string","default":"valueTwo"}]}',