Skip to content

Commit

Permalink
Add support for type coercion (#308)
Browse files Browse the repository at this point in the history
* add support for type coercion

* add tests

* move coerce tests out of base

* use flags for mode

* update readme

* fix tests

* remove ws

* use binary literals, explicit cast

* back to hex
  • Loading branch information
shmax authored and bighappyface committed Oct 9, 2016
1 parent fa407eb commit a918d3b
Show file tree
Hide file tree
Showing 8 changed files with 354 additions and 24 deletions.
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;
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 @@ -31,7 +31,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
87 changes: 86 additions & 1 deletion src/JsonSchema/Constraints/ObjectConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,102 @@ 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 (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)
{
$type = isset($definition->type)?$definition->type:null;
if($type){
switch($type){
case "boolean":
$value = $this->toBoolean($value);
break;
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 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
Loading

0 comments on commit a918d3b

Please sign in to comment.