Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for type coercion #308

Merged
merged 9 commits into from
Oct 9, 2016
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,32 @@ 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_TYPE_CAST | \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
```

Note that the ```CHECK_MODE_COERCE``` flag will only take effect when an object is passed into the ```check``` method.

## Running the tests

Expand Down
5 changes: 3 additions & 2 deletions src/JsonSchema/Constraints/Constraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +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_NORMAL = 0x00000001;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why hex literals?

Copy link
Collaborator Author

@shmax shmax Sep 24, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a style thing I learned decades ago. I guess the idea is that this way it's a little more clear to the reader that we're creating a sequence for the purpose of bit-masking (and you don't have to get out a calculator to figure out what the higher numbers are, though it's not an issue in this simple case).

0x00000001
0x00000002
0x00000004
0x00000008
0x00000010
0x00000020
0x00000040

vs

1
2
4
8
16
32
64

But I could go either way if you feel strongly about it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with it but I would caution that web developers, in my experience, are not usually accustomed to bitwise operations or thinking.

would go with a boolean literal if that is the clarity you intend to convey:

0b0000001 // 1
0b0000010 // 2
0b0000100 // 4

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with binary literals.

const CHECK_MODE_TYPE_CAST = 0x00000002;
const CHECK_MODE_COERCE = 0x00000004;

/**
* @var null|Factory
Expand Down
2 changes: 1 addition & 1 deletion src/JsonSchema/Constraints/EnumConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) && $type == "array" && $enumType == "object") {
if ((object)$element == $enum) {
return;
}
Expand Down
3 changes: 2 additions & 1 deletion src/JsonSchema/Constraints/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class Factory
'format' => 'JsonSchema\Constraints\FormatConstraint',
'schema' => 'JsonSchema\Constraints\SchemaConstraint',
'validator' => 'JsonSchema\Validator',
'coercer' => 'JsonSchema\Coerce'
);

/**
Expand Down Expand Up @@ -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)
? new TypeCheck\LooseTypeCheck
: new TypeCheck\StrictTypeCheck;
}
Expand Down
89 changes: 88 additions & 1 deletion src/JsonSchema/Constraints/ObjectConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,103 @@ 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_TYPE_CAST){
if(!($property instanceof Constraint)) {
$property = $this->coerce($property, $definition);

if($this->checkMode & 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(ctype_digit ($value)) {
return $value + 0; // cast to number
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not cast instead of coerce here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain what it is you have in mind in a bit more detail, please? This is a cast (in the case that a string is input). The actual coercing happens (optionally) in lines 136 and 138 of this file.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am referring to an explicit cast instead of an operation that results in an implicit cast:

return (int) $value;

For the toNumber method it makes sense to use the operation to produce an integer or a float, but for a whole number simply casting to the desired type is more expressive, in my opinion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no problem with that. Changed.

}

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;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead space

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

case "integer":
$value = $this->toInteger($value);
break;
case "number":
$value = $this->toNumber($value);
break;
}
}
return $value;
}

/**
* retrieves a property from an object or array
*
Expand All @@ -146,6 +232,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)) {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead space

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

return property_exists($element, $property) ? $element->$property : $fallback;
}

Expand Down
1 change: 0 additions & 1 deletion src/JsonSchema/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

namespace JsonSchema;

use JsonSchema\Constraints\SchemaConstraint;
use JsonSchema\Constraints\Constraint;
use JsonSchema\Entity\JsonPointer;

Expand Down
2 changes: 1 addition & 1 deletion tests/Constraints/BaseTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public function getInvalidForAssocTests()
* @param object $schema
* @return object
*/
private function getUriRetrieverMock($schema)
protected function getUriRetrieverMock($schema)
{
$relativeTestsRoot = realpath(__DIR__ . '/../../vendor/json-schema/JSON-Schema-Test-Suite/remotes');

Expand Down