Skip to content

Commit a918d3b

Browse files
shmaxbighappyface
authored andcommitted
Add support for type coercion (#308)
* 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
1 parent fa407eb commit a918d3b

File tree

8 files changed

+354
-24
lines changed

8 files changed

+354
-24
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,32 @@ if ($validator->isValid()) {
4141
}
4242
}
4343
```
44+
###Type Coercion
45+
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:
46+
```
47+
$request = (object)[
48+
'processRefund'=>"true",
49+
'refundAmount'=>"17"
50+
];
51+
52+
$validator = new \JsonSchema\Validator(\JsonSchema\Constraints\Constraint::CHECK_MODE_TYPE_CAST | \JsonSchema\Constraints\Constraint::CHECK_MODE_COERCE);
53+
$validator->check($request, (object) [
54+
"type"=>"object",
55+
"properties"=>[
56+
"processRefund"=>[
57+
"type"=>"boolean"
58+
],
59+
"refundAmount"=>[
60+
"type"=>"number"
61+
]
62+
]
63+
]); // validates!
64+
65+
is_bool($request->processRefund); // true
66+
is_int($request->refundAmount); // true
67+
```
68+
69+
Note that the ```CHECK_MODE_COERCE``` flag will only take effect when an object is passed into the ```check``` method.
4470

4571
## Running the tests
4672

src/JsonSchema/Constraints/Constraint.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ abstract class Constraint implements ConstraintInterface
2828
protected $errors = array();
2929
protected $inlineSchemaProperty = '$schema';
3030

31-
const CHECK_MODE_NORMAL = 1;
32-
const CHECK_MODE_TYPE_CAST = 2;
31+
const CHECK_MODE_NORMAL = 0x00000001;
32+
const CHECK_MODE_TYPE_CAST = 0x00000002;
33+
const CHECK_MODE_COERCE = 0x00000004;
3334

3435
/**
3536
* @var null|Factory

src/JsonSchema/Constraints/EnumConstraint.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function check($element, $schema = null, JsonPointer $path = null, $i = n
3131

3232
foreach ($schema->enum as $enum) {
3333
$enumType = gettype($enum);
34-
if ($this->checkMode === self::CHECK_MODE_TYPE_CAST && $type == "array" && $enumType == "object") {
34+
if (($this->checkMode & self::CHECK_MODE_TYPE_CAST) && $type == "array" && $enumType == "object") {
3535
if ((object)$element == $enum) {
3636
return;
3737
}

src/JsonSchema/Constraints/Factory.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class Factory
5454
'format' => 'JsonSchema\Constraints\FormatConstraint',
5555
'schema' => 'JsonSchema\Constraints\SchemaConstraint',
5656
'validator' => 'JsonSchema\Validator',
57+
'coercer' => 'JsonSchema\Coerce'
5758
);
5859

5960
/**
@@ -92,7 +93,7 @@ public function getSchemaStorage()
9293
public function getTypeCheck()
9394
{
9495
if (!isset($this->typeCheck[$this->checkMode])) {
95-
$this->typeCheck[$this->checkMode] = $this->checkMode === Constraint::CHECK_MODE_TYPE_CAST
96+
$this->typeCheck[$this->checkMode] = ($this->checkMode & Constraint::CHECK_MODE_TYPE_CAST)
9697
? new TypeCheck\LooseTypeCheck
9798
: new TypeCheck\StrictTypeCheck;
9899
}

src/JsonSchema/Constraints/ObjectConstraint.php

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,102 @@ public function validateElement($element, $matches, $objectDefinition = null, Js
121121
*/
122122
public function validateDefinition($element, $objectDefinition = null, JsonPointer $path = null)
123123
{
124+
$default = $this->getFactory()->createInstanceFor('undefined');
125+
124126
foreach ($objectDefinition as $i => $value) {
125-
$property = $this->getProperty($element, $i, $this->getFactory()->createInstanceFor('undefined'));
127+
$property = $this->getProperty($element, $i, $default);
126128
$definition = $this->getProperty($objectDefinition, $i);
127129

130+
if($this->checkMode & Constraint::CHECK_MODE_TYPE_CAST){
131+
if(!($property instanceof Constraint)) {
132+
$property = $this->coerce($property, $definition);
133+
134+
if($this->checkMode & Constraint::CHECK_MODE_COERCE) {
135+
if (is_object($element)) {
136+
$element->{$i} = $property;
137+
} else {
138+
$element[$i] = $property;
139+
}
140+
}
141+
}
142+
}
143+
128144
if (is_object($definition)) {
129145
// Undefined constraint will check for is_object() and quit if is not - so why pass it?
130146
$this->checkUndefined($property, $definition, $path, $i);
131147
}
132148
}
133149
}
134150

151+
/**
152+
* Converts a value to boolean. For example, "true" becomes true.
153+
* @param $value The value to convert to boolean
154+
* @return bool|mixed
155+
*/
156+
protected function toBoolean($value)
157+
{
158+
if($value === "true"){
159+
return true;
160+
}
161+
162+
if($value === "false"){
163+
return false;
164+
}
165+
166+
return $value;
167+
}
168+
169+
/**
170+
* Converts a numeric string to a number. For example, "4" becomes 4.
171+
*
172+
* @param mixed $value The value to convert to a number.
173+
* @return int|float|mixed
174+
*/
175+
protected function toNumber($value)
176+
{
177+
if(is_numeric($value)) {
178+
return $value + 0; // cast to number
179+
}
180+
181+
return $value;
182+
}
183+
184+
protected function toInteger($value)
185+
{
186+
if(ctype_digit ($value)) {
187+
return (int)$value; // cast to number
188+
}
189+
190+
return $value;
191+
}
192+
193+
/**
194+
* Given a value and a definition, attempts to coerce the value into the
195+
* type specified by the definition's 'type' property.
196+
*
197+
* @param mixed $value Value to coerce.
198+
* @param \stdClass $definition A definition with information about the expected type.
199+
* @return bool|int|string
200+
*/
201+
protected function coerce($value, $definition)
202+
{
203+
$type = isset($definition->type)?$definition->type:null;
204+
if($type){
205+
switch($type){
206+
case "boolean":
207+
$value = $this->toBoolean($value);
208+
break;
209+
case "integer":
210+
$value = $this->toInteger($value);
211+
break;
212+
case "number":
213+
$value = $this->toNumber($value);
214+
break;
215+
}
216+
}
217+
return $value;
218+
}
219+
135220
/**
136221
* retrieves a property from an object or array
137222
*

tests/Constraints/BaseTestCase.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public function getInvalidForAssocTests()
131131
* @param object $schema
132132
* @return object
133133
*/
134-
private function getUriRetrieverMock($schema)
134+
protected function getUriRetrieverMock($schema)
135135
{
136136
$relativeTestsRoot = realpath(__DIR__ . '/../../vendor/json-schema/JSON-Schema-Test-Suite/remotes');
137137

0 commit comments

Comments
 (0)