From aa72e28520e782ae28350b8b1abb0032993fea83 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Wed, 14 Sep 2016 21:22:21 -0700 Subject: [PATCH 1/9] add support for type coercion --- README.md | 24 +++++++ src/JsonSchema/Constraints/Constraint.php | 1 + src/JsonSchema/Constraints/EnumConstraint.php | 2 +- src/JsonSchema/Constraints/Factory.php | 3 +- .../Constraints/ObjectConstraint.php | 69 ++++++++++++++++++- src/JsonSchema/Validator.php | 1 - 6 files changed, 96 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c0c9feb2..b1f6c04a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,30 @@ if ($validator->isValid()) { } } ``` +###Type Coercion +If you're validating data passed to your application via HTTP, you can cast strings and booleans to the expected types defined by your schema: +``` +$request = (object)[ + 'processRefund'=>"true", + 'refundAmount'=>"17" +]; + +$validator = new \JsonSchema\Validator(\JsonSchema\Constraints\Constraint::CHECK_MODE_COERCE); +$validator->check($request, (object) [ + "type"=>"object", + "properties"=>[ + "processRefund"=>[ + "type"=>"boolean" + ], + "refundAmount"=>[ + "type"=>"number" + ] + ] +]); // validates! + +is_bool($request->processRefund); // true +is_int($request->refundAmount); // true +``` ## Running the tests diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 521a0e06..ed1e9d32 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -30,6 +30,7 @@ abstract class Constraint implements ConstraintInterface const CHECK_MODE_NORMAL = 1; const CHECK_MODE_TYPE_CAST = 2; + const CHECK_MODE_COERCE = 3; /** * @var null|Factory diff --git a/src/JsonSchema/Constraints/EnumConstraint.php b/src/JsonSchema/Constraints/EnumConstraint.php index 69fd0308..4bba48f6 100644 --- a/src/JsonSchema/Constraints/EnumConstraint.php +++ b/src/JsonSchema/Constraints/EnumConstraint.php @@ -32,7 +32,7 @@ public function check($element, $schema = null, JsonPointer $path = null, $i = n foreach ($schema->enum as $enum) { $enumType = gettype($enum); - if ($this->checkMode === self::CHECK_MODE_TYPE_CAST && $type == "array" && $enumType == "object") { + if (($this->checkMode === self::CHECK_MODE_TYPE_CAST || $this->checkMode === self::CHECK_MODE_COERCE) && $type == "array" && $enumType == "object") { if ((object)$element == $enum) { return; } diff --git a/src/JsonSchema/Constraints/Factory.php b/src/JsonSchema/Constraints/Factory.php index 83355c5e..64db4abb 100644 --- a/src/JsonSchema/Constraints/Factory.php +++ b/src/JsonSchema/Constraints/Factory.php @@ -54,6 +54,7 @@ class Factory 'format' => 'JsonSchema\Constraints\FormatConstraint', 'schema' => 'JsonSchema\Constraints\SchemaConstraint', 'validator' => 'JsonSchema\Validator', + 'coercer' => 'JsonSchema\Coerce' ); /** @@ -92,7 +93,7 @@ public function getSchemaStorage() public function getTypeCheck() { if (!isset($this->typeCheck[$this->checkMode])) { - $this->typeCheck[$this->checkMode] = $this->checkMode === Constraint::CHECK_MODE_TYPE_CAST + $this->typeCheck[$this->checkMode] = ($this->checkMode === Constraint::CHECK_MODE_TYPE_CAST || $this->checkMode === Constraint::CHECK_MODE_COERCE) ? new TypeCheck\LooseTypeCheck : new TypeCheck\StrictTypeCheck; } diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index df8d8088..0e71da6b 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -121,10 +121,18 @@ public function validateElement($element, $matches, $objectDefinition = null, Js */ public function validateDefinition($element, $objectDefinition = null, JsonPointer $path = null) { + $default = $this->getFactory()->createInstanceFor('undefined'); + foreach ($objectDefinition as $i => $value) { - $property = $this->getProperty($element, $i, $this->getFactory()->createInstanceFor('undefined')); + $property = $this->getProperty($element, $i, $default); $definition = $this->getProperty($objectDefinition, $i); + if($this->checkMode == Constraint::CHECK_MODE_COERCE){ + if(!($property instanceof Constraint)) { + $element->{$i} = $property = $this->coerce($property, $definition); + } + } + 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); @@ -132,6 +140,64 @@ public function validateDefinition($element, $objectDefinition = null, JsonPoint } } + /** + * 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; + } + + /** + * 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) + { + $type = isset($definition->type)?$definition->type:null; + if($type){ + switch($type){ + case "boolean": + $value = $this->toBoolean($value); + break; + + case "number": + $value = $this->toNumber($value); + break; + } + } + return $value; + } + /** * retrieves a property from an object or array * @@ -146,6 +212,7 @@ protected function getProperty($element, $property, $fallback = null) if (is_array($element) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) { return array_key_exists($property, $element) ? $element[$property] : $fallback; } elseif (is_object($element)) { + return property_exists($element, $property) ? $element->$property : $fallback; } diff --git a/src/JsonSchema/Validator.php b/src/JsonSchema/Validator.php index b627d785..cf147d76 100644 --- a/src/JsonSchema/Validator.php +++ b/src/JsonSchema/Validator.php @@ -9,7 +9,6 @@ namespace JsonSchema; -use JsonSchema\Constraints\SchemaConstraint; use JsonSchema\Constraints\Constraint; use JsonSchema\Entity\JsonPointer; From 1e0942e32280fb3ab726468fd14a396d0aa1d3f6 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 19 Sep 2016 19:20:40 -0700 Subject: [PATCH 2/9] add tests --- .idea/workspace.xml | 968 ++++++++++++++++++ README.md | 2 + .../Constraints/ObjectConstraint.php | 18 +- tests/Constraints/BaseTestCase.php | 111 ++ tests/Constraints/CoerciveTest.php | 147 +++ 5 files changed, 1245 insertions(+), 1 deletion(-) create mode 100644 .idea/workspace.xml create mode 100644 tests/Constraints/CoerciveTest.php diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 00000000..181e9eed --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,968 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + DEFINITION_ORDER + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + project + + + + + + + + + + + + + + + + project + + + true + + + + DIRECTORY + + false + + + + + + + + + + + + 1473904299458 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index b1f6c04a..4ffcbc0e 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ is_bool($request->processRefund); // true is_int($request->refundAmount); // true ``` +Note that while coercive checking will work when passing both object and array values as the first argument to check(), only objects will have their values transformed as expected. + ## Running the tests $ vendor/bin/phpunit diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 0e71da6b..4ff51ada 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -129,7 +129,11 @@ public function validateDefinition($element, $objectDefinition = null, JsonPoint if($this->checkMode == Constraint::CHECK_MODE_COERCE){ if(!($property instanceof Constraint)) { - $element->{$i} = $property = $this->coerce($property, $definition); + if(is_object($element)) { + $element->{$i} = $property = $this->coerce($property, $definition); + } else { + $element[$i] = $property = $this->coerce($property, $definition); + } } } @@ -173,6 +177,15 @@ protected function toNumber($value) return $value; } + protected function toInteger($value) + { + if(ctype_digit ($value)) { + return $value + 0; // 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. @@ -190,6 +203,9 @@ protected function coerce($value, $definition) $value = $this->toBoolean($value); break; + case "integer": + $value = $this->toInteger($value); + break; case "number": $value = $this->toNumber($value); break; diff --git a/tests/Constraints/BaseTestCase.php b/tests/Constraints/BaseTestCase.php index fe6c6951..a2485422 100644 --- a/tests/Constraints/BaseTestCase.php +++ b/tests/Constraints/BaseTestCase.php @@ -101,6 +101,85 @@ public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constrain $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } + /** + * @dataProvider getValidCoerceForAssocTests + */ + public function testValidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE) + { + if ($checkMode !== Constraint::CHECK_MODE_COERCE) { + $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); + } + + $schema = json_decode($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, $schemaStorage); + + $validator->check($value, $schema); + $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); + } + + /** + * @dataProvider getValidCoerceTests + */ + public function testValidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE) + { + if ($checkMode !== Constraint::CHECK_MODE_COERCE) { + $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); + } + + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); + $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + + $validator = new Validator($checkMode, $schemaStorage); + $validator->check(json_decode($input), $schema); + + $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); + } + + /** + * @dataProvider getInvalidCoerceTests + */ + public function testInvalidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE, $errors = array()) + { + $checkMode = $checkMode === null ? Constraint::CHECK_MODE_COERCE : $checkMode; + + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); + $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + + $validator = new Validator($checkMode, $schemaStorage); + $validator->check(json_decode($input), $schema); + + if (array() !== $errors) { + $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(),true)); + } + $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); + } + + /** + * @dataProvider getInvalidCoerceForAssocTests + */ + public function testInvalidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE, $errors = array()) + { + $checkMode = $checkMode === null ? Constraint::CHECK_MODE_COERCE : $checkMode; + if ($checkMode !== Constraint::CHECK_MODE_COERCE) { + $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); + } + + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); + $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + + $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)); + } + $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); + } + /** * @return array[] */ @@ -114,6 +193,14 @@ public function getValidForAssocTests() return $this->getValidTests(); } + /** + * @return array[] + */ + public function getValidCoerceForAssocTests() + { + return $this->getValidTests(); + } + /** * @return array[] */ @@ -127,6 +214,30 @@ public function getInvalidForAssocTests() return $this->getInvalidTests(); } + /** + * @return array[] + */ + public function getInvalidCoerceForAssocTests() + { + return $this->getInvalidTests(); + } + + /** + * @return array[] + */ + public function getValidCoerceTests() + { + return $this->getValidTests(); + } + + /** + * @return array[] + */ + public function getInvalidCoerceTests() + { + return $this->getInvalidTests(); + } + /** * @param object $schema * @return object diff --git a/tests/Constraints/CoerciveTest.php b/tests/Constraints/CoerciveTest.php new file mode 100644 index 00000000..3d3bd64c --- /dev/null +++ b/tests/Constraints/CoerciveTest.php @@ -0,0 +1,147 @@ + Date: Mon, 19 Sep 2016 20:19:15 -0700 Subject: [PATCH 3/9] move coerce tests out of base --- .idea/workspace.xml | 320 +++++++++++------------------ tests/Constraints/BaseTestCase.php | 113 +--------- tests/Constraints/CoerciveTest.php | 85 ++++++++ 3 files changed, 202 insertions(+), 316 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 181e9eed..7ba9bf24 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,9 +2,9 @@ - - + + @@ -24,14 +24,14 @@ - + - + - - + + @@ -42,7 +42,7 @@ - + @@ -52,8 +52,8 @@ - - + + @@ -65,24 +65,20 @@ - + - - - - - + - + - - + + @@ -98,6 +94,16 @@ + + + + + + + + + + @@ -105,7 +111,7 @@ - + @@ -115,20 +121,31 @@ - + - + + + + + + + + + + + - - + + + @@ -137,7 +154,7 @@ - + @@ -147,23 +164,13 @@ - + - - - - - - - - - - @@ -177,7 +184,7 @@ - + @@ -194,11 +201,11 @@ - + - - + + @@ -222,12 +229,13 @@ @@ -272,6 +280,7 @@ + @@ -290,52 +299,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -390,24 +331,9 @@ - - - - - - - - - @@ -502,18 +428,18 @@ - - + - + @@ -546,14 +472,6 @@ - - - - - - - - @@ -566,7 +484,6 @@ - @@ -575,11 +492,7 @@ - - - - - + @@ -605,7 +518,9 @@ - + + + @@ -645,7 +560,6 @@ - @@ -715,7 +629,6 @@ - @@ -747,14 +660,6 @@ - - - - - - - - @@ -767,7 +672,6 @@ - @@ -775,7 +679,6 @@ - @@ -783,7 +686,6 @@ - @@ -791,7 +693,6 @@ - @@ -799,7 +700,6 @@ - @@ -807,7 +707,6 @@ - @@ -847,7 +746,7 @@ - + @@ -863,7 +762,7 @@ - + @@ -871,60 +770,26 @@ - + - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - + - - - - - + @@ -932,7 +797,7 @@ - + @@ -940,7 +805,7 @@ - + @@ -948,21 +813,68 @@ - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Constraints/BaseTestCase.php b/tests/Constraints/BaseTestCase.php index a2485422..0aa34476 100644 --- a/tests/Constraints/BaseTestCase.php +++ b/tests/Constraints/BaseTestCase.php @@ -101,85 +101,6 @@ public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constrain $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } - /** - * @dataProvider getValidCoerceForAssocTests - */ - public function testValidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE) - { - if ($checkMode !== Constraint::CHECK_MODE_COERCE) { - $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); - } - - $schema = json_decode($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, $schemaStorage); - - $validator->check($value, $schema); - $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); - } - - /** - * @dataProvider getValidCoerceTests - */ - public function testValidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE) - { - if ($checkMode !== Constraint::CHECK_MODE_COERCE) { - $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); - } - - $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); - - $validator = new Validator($checkMode, $schemaStorage); - $validator->check(json_decode($input), $schema); - - $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); - } - - /** - * @dataProvider getInvalidCoerceTests - */ - public function testInvalidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE, $errors = array()) - { - $checkMode = $checkMode === null ? Constraint::CHECK_MODE_COERCE : $checkMode; - - $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); - - $validator = new Validator($checkMode, $schemaStorage); - $validator->check(json_decode($input), $schema); - - if (array() !== $errors) { - $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(),true)); - } - $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); - } - - /** - * @dataProvider getInvalidCoerceForAssocTests - */ - public function testInvalidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE, $errors = array()) - { - $checkMode = $checkMode === null ? Constraint::CHECK_MODE_COERCE : $checkMode; - if ($checkMode !== Constraint::CHECK_MODE_COERCE) { - $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); - } - - $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); - - $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)); - } - $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); - } - /** * @return array[] */ @@ -193,14 +114,6 @@ public function getValidForAssocTests() return $this->getValidTests(); } - /** - * @return array[] - */ - public function getValidCoerceForAssocTests() - { - return $this->getValidTests(); - } - /** * @return array[] */ @@ -214,35 +127,11 @@ public function getInvalidForAssocTests() return $this->getInvalidTests(); } - /** - * @return array[] - */ - public function getInvalidCoerceForAssocTests() - { - return $this->getInvalidTests(); - } - - /** - * @return array[] - */ - public function getValidCoerceTests() - { - return $this->getValidTests(); - } - - /** - * @return array[] - */ - public function getInvalidCoerceTests() - { - return $this->getInvalidTests(); - } - /** * @param object $schema * @return object */ - private function getUriRetrieverMock($schema) + protected function getUriRetrieverMock($schema) { $relativeTestsRoot = realpath(__DIR__ . '/../../vendor/json-schema/JSON-Schema-Test-Suite/remotes'); diff --git a/tests/Constraints/CoerciveTest.php b/tests/Constraints/CoerciveTest.php index 3d3bd64c..e117b435 100644 --- a/tests/Constraints/CoerciveTest.php +++ b/tests/Constraints/CoerciveTest.php @@ -8,9 +8,92 @@ */ namespace JsonSchema\Tests\Constraints; +use JsonSchema\Constraints\Constraint; +use JsonSchema\SchemaStorage; +use JsonSchema\Uri\UriResolver; +use JsonSchema\Validator; class CoerciveTest extends BasicTypesTest { + /** + * @dataProvider getValidCoerceTests + */ + public function testValidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE) + { + if ($checkMode !== Constraint::CHECK_MODE_COERCE) { + $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); + } + + $schema = json_decode($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, $schemaStorage); + + $validator->check($value, $schema); + $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); + } + + /** + * @dataProvider getValidCoerceTests + */ + public function testValidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE) + { + if ($checkMode !== Constraint::CHECK_MODE_COERCE) { + $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); + } + + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); + $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + + $validator = new Validator($checkMode, $schemaStorage); + $validator->check(json_decode($input), $schema); + + $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); + } + + /** + * @dataProvider getInvalidCoerceTests + */ + public function testInvalidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE, $errors = array()) + { + $checkMode = $checkMode === null ? Constraint::CHECK_MODE_COERCE : $checkMode; + + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); + $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + + $validator = new Validator($checkMode, $schemaStorage); + $validator->check(json_decode($input), $schema); + + if (array() !== $errors) { + $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(),true)); + } + $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); + } + + /** + * @dataProvider getInvalidCoerceTests + */ + public function testInvalidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE, $errors = array()) + { + $checkMode = $checkMode === null ? Constraint::CHECK_MODE_COERCE : $checkMode; + if ($checkMode !== Constraint::CHECK_MODE_COERCE) { + $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); + } + + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); + $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + + $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)); + } + $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); + } + public function getValidCoerceTests() { return array( @@ -55,6 +138,8 @@ public function getValidCoerceTests() ); } + + public function getInvalidCoerceTests() { return array( From bfd0611a64e92a259326c39274e4ac101579175a Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 19 Sep 2016 20:52:43 -0700 Subject: [PATCH 4/9] use flags for mode --- .idea/workspace.xml | 880 ------------------ src/JsonSchema/Constraints/Constraint.php | 6 +- src/JsonSchema/Constraints/EnumConstraint.php | 2 +- src/JsonSchema/Constraints/Factory.php | 2 +- .../Constraints/ObjectConstraint.php | 16 +- tests/Constraints/CoerciveTest.php | 33 +- .../Constraints/NumberAndIntegerTypesTest.php | 18 - 7 files changed, 37 insertions(+), 920 deletions(-) delete mode 100644 .idea/workspace.xml diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 7ba9bf24..00000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,880 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - DEFINITION_ORDER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - project - - - - - - - - - - - - - - - - project - - - true - - - - DIRECTORY - - false - - - - - - - - - - - - 1473904299458 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index ed1e9d32..8b50bfe1 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -28,9 +28,9 @@ abstract class Constraint implements ConstraintInterface protected $errors = array(); protected $inlineSchemaProperty = '$schema'; - const CHECK_MODE_NORMAL = 1; - const CHECK_MODE_TYPE_CAST = 2; - const CHECK_MODE_COERCE = 3; + const CHECK_MODE_NORMAL = 0x00000001; + const CHECK_MODE_TYPE_CAST = 0x00000002; + const CHECK_MODE_COERCE = 0x00000004; /** * @var null|Factory diff --git a/src/JsonSchema/Constraints/EnumConstraint.php b/src/JsonSchema/Constraints/EnumConstraint.php index 4bba48f6..88b78cd7 100644 --- a/src/JsonSchema/Constraints/EnumConstraint.php +++ b/src/JsonSchema/Constraints/EnumConstraint.php @@ -32,7 +32,7 @@ public function check($element, $schema = null, JsonPointer $path = null, $i = n foreach ($schema->enum as $enum) { $enumType = gettype($enum); - if (($this->checkMode === self::CHECK_MODE_TYPE_CAST || $this->checkMode === self::CHECK_MODE_COERCE) && $type == "array" && $enumType == "object") { + if (($this->checkMode & self::CHECK_MODE_TYPE_CAST) && $type == "array" && $enumType == "object") { if ((object)$element == $enum) { return; } diff --git a/src/JsonSchema/Constraints/Factory.php b/src/JsonSchema/Constraints/Factory.php index 64db4abb..8fd72a83 100644 --- a/src/JsonSchema/Constraints/Factory.php +++ b/src/JsonSchema/Constraints/Factory.php @@ -93,7 +93,7 @@ public function getSchemaStorage() public function getTypeCheck() { if (!isset($this->typeCheck[$this->checkMode])) { - $this->typeCheck[$this->checkMode] = ($this->checkMode === Constraint::CHECK_MODE_TYPE_CAST || $this->checkMode === Constraint::CHECK_MODE_COERCE) + $this->typeCheck[$this->checkMode] = ($this->checkMode & Constraint::CHECK_MODE_TYPE_CAST) ? new TypeCheck\LooseTypeCheck : new TypeCheck\StrictTypeCheck; } diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 4ff51ada..851acd0b 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -127,13 +127,17 @@ public function validateDefinition($element, $objectDefinition = null, JsonPoint $property = $this->getProperty($element, $i, $default); $definition = $this->getProperty($objectDefinition, $i); - if($this->checkMode == Constraint::CHECK_MODE_COERCE){ + if($this->checkMode & Constraint::CHECK_MODE_TYPE_CAST){ if(!($property instanceof Constraint)) { - if(is_object($element)) { - $element->{$i} = $property = $this->coerce($property, $definition); - } else { - $element[$i] = $property = $this->coerce($property, $definition); - } + $property = $this->coerce($property, $definition); + + if($this->checkMode & Constraint::CHECK_MODE_COERCE) { + if (is_object($element)) { + $element->{$i} = $property; + } else { + $element[$i] = $property; + } + } } } diff --git a/tests/Constraints/CoerciveTest.php b/tests/Constraints/CoerciveTest.php index e117b435..07afbfd5 100644 --- a/tests/Constraints/CoerciveTest.php +++ b/tests/Constraints/CoerciveTest.php @@ -18,9 +18,9 @@ class CoerciveTest extends BasicTypesTest /** * @dataProvider getValidCoerceTests */ - public function testValidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE) + public function testValidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST) { - if ($checkMode !== Constraint::CHECK_MODE_COERCE) { + if ($checkMode & Constraint::CHECK_MODE_COERCE === 0) { $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); } @@ -38,9 +38,9 @@ public function testValidCoerceCasesUsingAssoc($input, $schema, $checkMode = Con /** * @dataProvider getValidCoerceTests */ - public function testValidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE) + public function testValidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST) { - if ($checkMode !== Constraint::CHECK_MODE_COERCE) { + if ($checkMode & Constraint::CHECK_MODE_COERCE === 0) { $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); } @@ -48,7 +48,17 @@ public function testValidCoerceCases($input, $schema, $checkMode = Constraint::C $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); $validator = new Validator($checkMode, $schemaStorage); - $validator->check(json_decode($input), $schema); + $value = json_decode($input); + + $this->assertTrue(gettype($value->number) == "string"); + $this->assertTrue(gettype($value->integer) == "string"); + $this->assertTrue(gettype($value->boolean) == "string"); + + $validator->check($value, $schema); + + $this->assertTrue(gettype($value->number) == "double"); + $this->assertTrue(gettype($value->integer) == "integer"); + $this->assertTrue(gettype($value->boolean) == "boolean"); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } @@ -56,9 +66,11 @@ public function testValidCoerceCases($input, $schema, $checkMode = Constraint::C /** * @dataProvider getInvalidCoerceTests */ - public function testInvalidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE, $errors = array()) + public function testInvalidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST, $errors = array()) { - $checkMode = $checkMode === null ? Constraint::CHECK_MODE_COERCE : $checkMode; + if ($checkMode & Constraint::CHECK_MODE_COERCE === 0) { + $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); + } $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); @@ -75,10 +87,9 @@ public function testInvalidCoerceCases($input, $schema, $checkMode = Constraint: /** * @dataProvider getInvalidCoerceTests */ - public function testInvalidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE, $errors = array()) + public function testInvalidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST, $errors = array()) { - $checkMode = $checkMode === null ? Constraint::CHECK_MODE_COERCE : $checkMode; - if ($checkMode !== Constraint::CHECK_MODE_COERCE) { + if ($checkMode & Constraint::CHECK_MODE_COERCE === 0) { $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); } @@ -100,7 +111,7 @@ public function getValidCoerceTests() array( '{ "string":"string test", - "number":"1", + "number":"1.5", "integer":"1", "boolean":"true", "object":{}, diff --git a/tests/Constraints/NumberAndIntegerTypesTest.php b/tests/Constraints/NumberAndIntegerTypesTest.php index 932c2f56..c0db510d 100644 --- a/tests/Constraints/NumberAndIntegerTypesTest.php +++ b/tests/Constraints/NumberAndIntegerTypesTest.php @@ -25,24 +25,6 @@ public function getInvalidTests() } }' ), - array( - '{"number": "1.5"}', - '{ - "type": "object", - "properties": { - "number": {"type": "number"} - } - }' - ), - array( - '{"integer": "1"}', - '{ - "type": "object", - "properties": { - "integer": {"type": "integer"} - } - }' - ), array( '{"integer": 1.001}', '{ From aa771b97b354edc988e51675418ef18aac29fc2e Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 19 Sep 2016 20:57:22 -0700 Subject: [PATCH 5/9] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ffcbc0e..3eab3e35 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ $request = (object)[ 'refundAmount'=>"17" ]; -$validator = new \JsonSchema\Validator(\JsonSchema\Constraints\Constraint::CHECK_MODE_COERCE); +$validator = new \JsonSchema\Validator(\JsonSchema\Constraints\Constraint::CHECK_MODE_TYPE_CAST | \JsonSchema\Constraints\Constraint::CHECK_MODE_COERCE); $validator->check($request, (object) [ "type"=>"object", "properties"=>[ @@ -66,7 +66,7 @@ is_bool($request->processRefund); // true is_int($request->refundAmount); // true ``` -Note that while coercive checking will work when passing both object and array values as the first argument to check(), only objects will have their values transformed as expected. +Note that the ```CHECK_MODE_COERCE``` flag will only take effect when an object is passed into the ```check``` method. ## Running the tests From 1ddebe79fd7378618b7e8f6bbba5dcf546b83e10 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Mon, 19 Sep 2016 21:08:40 -0700 Subject: [PATCH 6/9] fix tests --- tests/Constraints/CoerciveTest.php | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/Constraints/CoerciveTest.php b/tests/Constraints/CoerciveTest.php index 07afbfd5..7765a78f 100644 --- a/tests/Constraints/CoerciveTest.php +++ b/tests/Constraints/CoerciveTest.php @@ -18,11 +18,9 @@ class CoerciveTest extends BasicTypesTest /** * @dataProvider getValidCoerceTests */ - public function testValidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST) + public function testValidCoerceCasesUsingAssoc($input, $schema) { - if ($checkMode & Constraint::CHECK_MODE_COERCE === 0) { - $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); - } + $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST; $schema = json_decode($schema); $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema), new UriResolver); @@ -38,11 +36,9 @@ public function testValidCoerceCasesUsingAssoc($input, $schema, $checkMode = Con /** * @dataProvider getValidCoerceTests */ - public function testValidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST) + public function testValidCoerceCases($input, $schema, $errors = array()) { - if ($checkMode & Constraint::CHECK_MODE_COERCE === 0) { - $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); - } + $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST; $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); @@ -66,11 +62,9 @@ public function testValidCoerceCases($input, $schema, $checkMode = Constraint::C /** * @dataProvider getInvalidCoerceTests */ - public function testInvalidCoerceCases($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST, $errors = array()) + public function testInvalidCoerceCases($input, $schema, $errors = array()) { - if ($checkMode & Constraint::CHECK_MODE_COERCE === 0) { - $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); - } + $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST; $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); @@ -87,11 +81,9 @@ public function testInvalidCoerceCases($input, $schema, $checkMode = Constraint: /** * @dataProvider getInvalidCoerceTests */ - public function testInvalidCoerceCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST, $errors = array()) + public function testInvalidCoerceCasesUsingAssoc($input, $schema, $errors = array()) { - if ($checkMode & Constraint::CHECK_MODE_COERCE === 0) { - $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_COERCE"'); - } + $checkMode = Constraint::CHECK_MODE_COERCE | Constraint::CHECK_MODE_TYPE_CAST; $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); From b7afe7a91ead0b41005f9ae5d40c0798fe3ce67c Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 24 Sep 2016 11:53:54 -0700 Subject: [PATCH 7/9] remove ws --- src/JsonSchema/Constraints/ObjectConstraint.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 851acd0b..7e494142 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -206,7 +206,6 @@ protected function coerce($value, $definition) case "boolean": $value = $this->toBoolean($value); break; - case "integer": $value = $this->toInteger($value); break; @@ -232,7 +231,6 @@ protected function getProperty($element, $property, $fallback = null) if (is_array($element) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) { return array_key_exists($property, $element) ? $element[$property] : $fallback; } elseif (is_object($element)) { - return property_exists($element, $property) ? $element->$property : $fallback; } From 75a200b007ef95c2437f37f50f01b5932c3fae9f Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sat, 24 Sep 2016 14:36:07 -0700 Subject: [PATCH 8/9] use binary literals, explicit cast --- src/JsonSchema/Constraints/Constraint.php | 6 +++--- src/JsonSchema/Constraints/ObjectConstraint.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 8b50bfe1..5dd1dcc6 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -28,9 +28,9 @@ abstract class Constraint implements ConstraintInterface protected $errors = array(); protected $inlineSchemaProperty = '$schema'; - const CHECK_MODE_NORMAL = 0x00000001; - const CHECK_MODE_TYPE_CAST = 0x00000002; - const CHECK_MODE_COERCE = 0x00000004; + const CHECK_MODE_NORMAL = 0b00000001; + const CHECK_MODE_TYPE_CAST = 0b00000010; + const CHECK_MODE_COERCE = 0b00000100; /** * @var null|Factory diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 7e494142..8431cd9f 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -184,7 +184,7 @@ protected function toNumber($value) protected function toInteger($value) { if(ctype_digit ($value)) { - return $value + 0; // cast to number + return (int)$value; // cast to number } return $value; From ff07d71eb637c504dcc612e7f34366f4e4a5137b Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Sun, 25 Sep 2016 17:06:01 -0700 Subject: [PATCH 9/9] back to hex --- src/JsonSchema/Constraints/Constraint.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 5dd1dcc6..c5683a64 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -28,9 +28,9 @@ abstract class Constraint implements ConstraintInterface protected $errors = array(); protected $inlineSchemaProperty = '$schema'; - const CHECK_MODE_NORMAL = 0b00000001; - const CHECK_MODE_TYPE_CAST = 0b00000010; - const CHECK_MODE_COERCE = 0b00000100; + const CHECK_MODE_NORMAL = 0x00000001; + const CHECK_MODE_TYPE_CAST = 0x00000002; + const CHECK_MODE_COERCE = 0x00000004; /** * @var null|Factory