Skip to content

Commit

Permalink
Merge pull request #8 from schematicon/auto_casting
Browse files Browse the repository at this point in the history
validator: added support for autocasting from a string
  • Loading branch information
hrach authored Apr 18, 2017
2 parents 6fe0085 + 0b5ada2 commit 8179c35
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 20 deletions.
12 changes: 11 additions & 1 deletion src/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ class Result
/** @var array */
private $errors;

/** @var mixed */
private $data;

public function __construct(array $errors)

public function __construct(array $errors, $data)
{
$this->errors = $errors;
$this->data = $data;
}


Expand All @@ -34,4 +38,10 @@ public function isValid(): bool
{
return count($this->errors) === 0;
}


public function getData()
{
return $this->data;
}
}
71 changes: 52 additions & 19 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ public function __construct(array $schema, bool $failFast = false, callable $ref
}


public function validate($data): Result
public function validate($data, bool $autoCoercion = false): Result
{
$outData = $data;
$errors = [];
$stack = [[$this->schema, $data, '/']];
while (list ($schema, $node, $path) = array_pop($stack)) {
$stack = [[$this->schema, & $outData, '/']];
while ($row = array_pop($stack)) {
$schema = $row[0];
$node = & $row[1];
$path = $row[2];

if (isset($schema['reference']) && is_string($schema['reference'])) {
$isValid = $this->validateReference($node, $schema['reference'], $path, $stack);

Expand Down Expand Up @@ -76,16 +81,28 @@ public function validate($data): Result
if (is_bool($node)) {
$isValid = true;
break;
} elseif ($autoCoercion && ($node === '1' || $node === '0')) {
$node = (bool) $node;
$isValid = true;
break;
}
} elseif ($type === 'int') {
if (is_int($node)) {
$isValid = $this->validateNumber($node, $schema, $path, $errors);
break;
} elseif ($autoCoercion && is_string($node) && ($filteredValue = filter_var($node, FILTER_VALIDATE_INT)) !== false) {
$node = $filteredValue;
$isValid = $this->validateNumber($node, $schema, $path, $errors);
break;
}
} elseif ($type === 'float') {
if (is_float($node)) {
$isValid = $this->validateNumber($node, $schema, $path, $errors);
break;
} elseif ($autoCoercion && is_string($node) && ($filteredValue = filter_var($node, FILTER_VALIDATE_FLOAT)) !== false) {
$node = $filteredValue;
$isValid = $this->validateNumber($node, $schema, $path, $errors);
break;
}
} elseif ($type === 'array') {
if (is_array($node) && Helpers::isArray($node)) {
Expand Down Expand Up @@ -117,42 +134,58 @@ public function validate($data): Result
}
}

return new Result($errors);
return new Result($errors, $outData);
}


private function validateInnerProperties($node, array $schema, string $path, array & $stack, array & $errors): bool
private function validateInnerProperties(& $node, array $schema, string $path, array & $stack, array & $errors): bool
{
$isValid = true;
$node = (array) $node; // may be a stdClass
foreach ($schema['properties'] ?? [] as $propName => $propSchema) {
if (isset($node[$propName]) || array_key_exists($propName, $node)) {
$stack[] = [$propSchema, $node[$propName], "$path$propName/"];
if (is_array($node)) {
foreach ($schema['properties'] ?? [] as $propName => $propSchema) {
if (isset($node[$propName]) || array_key_exists($propName, $node)) {
$stack[] = [$propSchema, & $node[$propName], "$path$propName/"];

} elseif (!isset($propSchema['optional']) || !$propSchema['optional']) {
$errors[] = "Missing '$propName' key in '$path' path";
$isValid = false;
if ($this->failFast) {
break;
} elseif (!isset($propSchema['optional']) || !$propSchema['optional']) {
$errors[] = "Missing '$propName' key in '$path' path";
$isValid = false;
if ($this->failFast) {
break;
}
}
}
} else {
// stdClass
foreach ($schema['properties'] ?? [] as $propName => $propSchema) {
if (isset($node->$propName) || property_exists($node, $propName)) {
$stack[] = [$propSchema, & $node->$propName, "$path$propName/"];

} elseif (!isset($propSchema['optional']) || !$propSchema['optional']) {
$errors[] = "Missing '$propName' key in '$path' path";
$isValid = false;
if ($this->failFast) {
break;
}
}
}
}

foreach ($schema['regexpProperties'] ?? [] as $propName => $propSchema) {
$expression = "~$propName~";
foreach ($node as $nodeKey => $nodeValue) {
foreach ($node as $nodeKey => & $nodeValue) {
if (preg_match($expression, $nodeKey) !== 1) {
continue;
}

$stack[] = [$propSchema, $nodeValue, "$path$propName/"];
$stack[] = [$propSchema, & $nodeValue, "$path$propName/"];
}
}

return $isValid;
}


private function validateItems($node, array $schema, string $path, array & $stack, array & $errors): bool
private function validateItems(& $node, array $schema, string $path, array & $stack, array & $errors): bool
{
$isValid = true;

Expand Down Expand Up @@ -180,8 +213,8 @@ private function validateItems($node, array $schema, string $path, array & $stac
}
}

foreach ($node as $index => $value) {
$stack[] = [$schema['item'], $value, "$path$index/"];
foreach ($node as $index => & $value) {
$stack[] = [$schema['item'], & $value, "$path$index/"];
}

return $isValid;
Expand Down
14 changes: 14 additions & 0 deletions tests/cases/validate.array.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ advanced:
properties:
name:
type: string|null
coercion:
type: array
item:
type: bool
NEON
);

Expand Down Expand Up @@ -92,3 +96,13 @@ Assert::same(
["Wrong minimum items count in '/'; expected '1'; got '0'"],
$validator->validate([])->getErrors()
);


// =====================================================================================================================


$validator = new Validator(prepareSchema($config['coercion']));

$result = $validator->validate(['1', '0', true, false, '0', '1'], true);
Assert::true($result->isValid());
Assert::same([true, false, true, false, false, true], $result->getData());
19 changes: 19 additions & 0 deletions tests/cases/validate.bool.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,22 @@ Assert::same(
["Wrong data type in '/'; expected 'bool|null'; got 'array'"],
$validator->validate([])->getErrors()
);


// =====================================================================================================================


$validator = new Validator(prepareSchema($config['nullable']));

$result = $validator->validate('1', true);
Assert::true($result->isValid());
Assert::same(true, $result->getData());

$result = $validator->validate('0', true);
Assert::true($result->isValid());
Assert::same(false, $result->getData());

Assert::same(
["Wrong data type in '/'; expected 'bool|null'; got 'string'"],
$validator->validate('2', true)->getErrors()
);
23 changes: 23 additions & 0 deletions tests/cases/validate.float.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,26 @@ Assert::same(
["Wrong value in '/'; expected number of minimal value '0'; got value '-0.1'"],
$validator->validate(-0.1)->getErrors()
);


// =====================================================================================================================


$validator = new Validator(prepareSchema($config['basic']));

$result = $validator->validate('1.0', true);
Assert::true($result->isValid());
Assert::same(1.0, $result->getData());

$result = $validator->validate('-2', true);
Assert::true($result->isValid());
Assert::same(-2.0, $result->getData());

$result = $validator->validate('0.0', true);
Assert::true($result->isValid());
Assert::same(0.0, $result->getData());

Assert::same(
["Wrong data type in '/'; expected 'float'; got 'string'"],
$validator->validate('2.0.0', true)->getErrors()
);
23 changes: 23 additions & 0 deletions tests/cases/validate.int.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,26 @@ Assert::same(
["Wrong value in '/'; expected number of minimal value '0'; got value '-1'"],
$validator->validate(-1)->getErrors()
);


// =====================================================================================================================


$validator = new Validator(prepareSchema($config['basic']));

$result = $validator->validate('1', true);
Assert::true($result->isValid());
Assert::same(1, $result->getData());

$result = $validator->validate('-2', true);
Assert::true($result->isValid());
Assert::same(-2, $result->getData());

$result = $validator->validate('0', true);
Assert::true($result->isValid());
Assert::same(0, $result->getData());

Assert::same(
["Wrong data type in '/'; expected 'int'; got 'string'"],
$validator->validate('2.0', true)->getErrors()
);
19 changes: 19 additions & 0 deletions tests/cases/validate.map.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ regexp:
regexpProperties:
'.+':
type: int|string
coercion:
type: map
properties:
live: bool
NEON
);

Expand Down Expand Up @@ -155,3 +159,18 @@ Assert::same(
'prop2' => false,
])->getErrors()
);


// =====================================================================================================================


$validator = new Validator(prepareSchema($config['coercion']));

$result = $validator->validate(['live' => '1'], true);
Assert::true($result->isValid());
Assert::same(true, $result->getData()['live']);


$result = $validator->validate((object) ['live' => '1'], true);
Assert::true($result->isValid());
Assert::same(true, $result->getData()->live);

0 comments on commit 8179c35

Please sign in to comment.