Skip to content
This repository has been archived by the owner on Dec 19, 2022. It is now read-only.

Commit

Permalink
Merge pull request #1 from safe-k/feature/implicit-struct
Browse files Browse the repository at this point in the history
Support implicit struct array validation
  • Loading branch information
seifkamal authored Apr 28, 2020
2 parents e10fc0d + bb60a28 commit 823dec4
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 43 deletions.
43 changes: 36 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ $directory = [
try {
validate($directory, struct('Directory', [
'path' => 'is_dir',
'file' => optional('is_file', __DIR__ . '/directory-validation.php'),
'file' => optional('is_file', __DIR__ . '/README.md'),
'content' => arrayOf(struct('Paragraph', [
'header' => 'is_string',
'line' => not('is_null'),
Expand All @@ -75,11 +75,40 @@ try {
echo "Path: {$directory['path']}" . PHP_EOL;
echo "File: {$directory['file']}" . PHP_EOL;
// Prints:
// Path: /Users/seifkamal/src/struct-array/examples
// File: /Users/seifkamal/src/struct-array/examples/directory-validation.php
// Path: /Users/seifkamal/src/struct-array
// File: /Users/seifkamal/src/struct-array/README.md
```

Here's the same one using static class methods:
You can also just use an array directly, without creating a `Struct`:

```php
<?php

use function SK\StructArray\{
arrayOf, optional, not, validate
};

$directory = [...];

validate($directory, [
'path' => 'is_dir',
'file' => optional('is_file', __DIR__ . '/README.md'),
'content' => arrayOf([
'header' => 'is_string',
'line' => not('is_null'),
]),
]);
```

This is tailored for quick usage, and therefore assumes the defined interface is non-exhaustive
(ie. the array submitted for validation is allowed to have keys that aren't defined here). It also
means error messages will be more generic, ie you'll see:
> Struct failed validation. ...
instead of:
> Directory failed validation. ...
Here's another example directly using the static class methods:

```php
<?php
Expand All @@ -105,7 +134,7 @@ $paragraphStruct = Struct::of('Paragraph', [
]);
$directoryStruct = Struct::of('Directory', [
'path' => 'is_dir',
'file' => Type::optional('is_file', __DIR__ . '/directory-validation.php'),
'file' => Type::optional('is_file', __DIR__ . '/README.md'),
'content' => Type::arrayOf($paragraphStruct),
]);

Expand All @@ -119,8 +148,8 @@ try {
echo "Path: {$directory['path']}" . PHP_EOL;
echo "File: {$directory['file']}" . PHP_EOL;
// Prints:
// Path: /Users/seifkamal/src/struct-array/examples
// File: /Users/seifkamal/src/struct-array/examples/directory-validation.php
// Path: /Users/seifkamal/src/struct-array
// File: /Users/seifkamal/src/struct-array/README.md
```

For more, see the [examples directory](examples).
2 changes: 1 addition & 1 deletion docs/use-case.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ class StatsController {
- Easy to read
- Easy to scale
- Automatic, customisable error messaging; Here's an example:
> Struct 'Event' failed validation: Invalid value for property 'date'
> Event failed validation. Invalid value for property: 'date'
- All possible parameters and their validation rules are documented in code, in the method itself
- Extensible - `Struct`s are essentially arrays of `callable`s (and other `Struct`s), so they can
easily be worked into systems and be extended accordingly
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/InvalidValueException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ class InvalidValueException extends \Exception
{
public function __construct(string $property)
{
parent::__construct("Invalid value for property '{$property}'");
parent::__construct("Invalid value for property: '{$property}'");
}
}
2 changes: 1 addition & 1 deletion src/Exception/MissingPropertyException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ class MissingPropertyException extends \Exception
{
public function __construct(string $property)
{
parent::__construct("missing value for '{$property}'");
parent::__construct("Missing value for property: '{$property}'");
}
}
2 changes: 1 addition & 1 deletion src/Exception/StructValidationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class StructValidationException extends \Exception
public function __construct(Struct $struct, \Throwable $reason)
{
parent::__construct(
"Struct '{$struct->name()}' failed validation: {$reason->getMessage()}",
"{$struct->name()} failed validation. {$reason->getMessage()}",
0,
$reason
);
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/UnexpectedPropertyException.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class UnexpectedPropertyException extends \Exception
public function __construct(string ...$properties)
{
parent::__construct(sprintf(
"Unexpected %s '%s'",
"Unexpected %s: '%s'",
count($properties) === 1 ? 'property' : 'properties',
implode(', ', $properties)
));
Expand Down
19 changes: 15 additions & 4 deletions src/Struct.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ class Struct
{
/** @var string */
private $name;
/** @var callable|Struct[] */
/** @var callable[]|Struct[] */
private $interface;
/** @var bool */
private $exhaustive;

/**
* Struct constructor.
*
* @param string $name
* @param callable|Struct[] $interface
* @param string $name Used in error messaging, useful when nesting or validating multiple Structs.
* @param callable[]|Struct[] $interface A list of expected keys and their associated validator.
* @param bool $exhaustive Used to specify whether the declared Struct properties are exhaustive,
* meaning data arrays submitted for validation must not contain unknown keys. This defaults
* to `true`; Set to `false` if you only want to validate some of the array elements.
Expand All @@ -33,13 +33,24 @@ public static function of(
return $struct;
}

/**
* @internal
*
* @param array $interface
* @return static
*/
public static function default(array $interface): self
{
return static::of('Struct', $interface, false);
}

public function name(): string
{
return $this->name;
}

/**
* @return callable|Struct[]
* @return callable[]|Struct[]
*/
public function interface(): array
{
Expand Down
32 changes: 22 additions & 10 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ class Validator
{
/**
* @param array $data
* @param Struct $struct
* @param array|Struct $struct If an array is supplied, a generic, non-exhaustive Struct is used.
* @return bool
* @throws Exception\StructValidationException
*/
public static function validate(array &$data, Struct $struct): bool
public static function validate(array &$data, $struct): bool
{
try {
if (is_array($struct)) {
$struct = Struct::default($struct);
}

$unexpectedProperties = array_diff_key($data, $struct->interface());
if ($struct->isExhaustive() && !empty($unexpectedProperties)) {
throw new Exception\UnexpectedPropertyException(...array_keys($unexpectedProperties));
Expand All @@ -24,26 +28,34 @@ public static function validate(array &$data, Struct $struct): bool
$value = array_key_exists($field, $data)
? $data[$field]
: Missing::property($field);
$valueIsMissing = is_a($value, Missing::class);
$propertyIsMissing = is_a($value, Missing::class);

if (is_callable($validator)) {
if (!$validator($value)) {
throw new Exception\InvalidValueException($field);
}
// If value has changed, set corresponding data field
if ($valueIsMissing && !is_a($value, Missing::class)) {
if ($propertyIsMissing && !is_a($value, Missing::class)) {
$data[$field] = $value;
}
} elseif (is_a($validator, Struct::class)) {
if ($valueIsMissing) {
throw new Exception\MissingPropertyException($field);
}
return true;
}

if ($propertyIsMissing) {
throw new Exception\MissingPropertyException($field);
}

if (is_array($validator)) {
$validator = Struct::default($validator);
}
if (is_a($validator, Struct::class)) {
if (!validate($value, $validator)) {
throw new Exception\InvalidValueException($field);
}
} else {
throw new Exception\InvalidValidatorException($validator);
return true;
}

throw new Exception\InvalidValidatorException($validator);
}
} catch (\Throwable $t) {
/**
Expand Down
12 changes: 7 additions & 5 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,11 @@ function optional(callable $validator, $default = Missing::class): callable
/**
* @see Struct::of()
*
* @param string $name
* @param callable|Struct[] $interface
* @param bool $exhaustive
* @param string $name Used in error messaging, useful when nesting or validating multiple Structs.
* @param callable[]|Struct[] $interface A list of expected keys and their associated validator.
* @param bool $exhaustive Used to specify whether the declared Struct properties are exhaustive,
* meaning data arrays submitted for validation must not contain unknown keys. This defaults
* to `true`; Set to `false` if you only want to validate some of the array elements.
* @return Struct
*/
function struct(string $name, array $interface, bool $exhaustive = true): Struct
Expand All @@ -89,11 +91,11 @@ function struct(string $name, array $interface, bool $exhaustive = true): Struct
* @see Validator::validate()
*
* @param array $data
* @param Struct $struct
* @param array|Struct $struct If an array is supplied, a generic, non-exhaustive Struct is used.
* @return bool
* @throws Exception\StructValidationException
*/
function validate(array &$data, Struct $struct): bool
function validate(array &$data, $struct): bool
{
return Validator::validate($data, $struct);
}
44 changes: 32 additions & 12 deletions tests/Unit/StructTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace SK\StructArray\Test\Unit;

use PHPUnit\Framework\TestCase;
use SK\StructArray\Exception;
use SK\StructArray\Exception\StructValidationException;
use SK\StructArray\Property\Type;
use SK\StructArray\Struct;
Expand All @@ -15,42 +14,64 @@ class StructTest extends TestCase
/**
* @dataProvider dataProviderForTest
*/
public function test($data, $interface, $exhaustive, $expectedException)
public function test($data, $struct, $expectedException)
{
if ($expectedException) {
$this->expectException(StructValidationException::class);
}

$this->assertTrue(validate($data, Struct::of('Test', $interface, $exhaustive)));
$this->assertTrue(validate($data, $struct));
}

public function dataProviderForTest(): array
{
$name = 'Test';
return [
[
['name' => 'toasty',],
['name' => 'invalid validator'],
true,
Struct::of($name, ['name' => 'invalid validator'], true),
true,
],
[
['name' => 10],
['name' => 'is_string'],
true,
Struct::of($name, ['name' => 'is_string'], true),
true,
],
[
[],
['name' => 'is_string'],
true,
Struct::of($name, ['name' => 'is_string'], true),
true,
],
[
['name' => 'toasty', 'age' => 10],
['name' => 'is_string'],
true,
Struct::of($name, ['name' => 'is_string'], true),
true,
],
[
[
'id' => '123',
'type' => 'theatre',
'date' => new \DateTime(),
'price' => [
'value' => 20.5,
'currency' => 'GBP',
],
'tickets' => ['General', 10],
'onSale' => true,
'artist' => 'some guy',
],
Struct::of($name, [
'id' => Type::allOf('is_string', 'is_numeric'),
'type' => 'is_string',
'date' => Type::anyOf(Type::classOf(\DateTime::class), 'is_null'),
'price' => Struct::of('Price', [
'value' => 'is_float',
'currency' => 'is_string'
]),
'tickets' => Type::arrayOf(Type::not('is_null')),
], false),
null
],
[
[
'id' => '123',
Expand All @@ -74,7 +95,6 @@ public function dataProviderForTest(): array
]),
'tickets' => Type::arrayOf(Type::not('is_null')),
],
false,
null
],
];
Expand Down

0 comments on commit 823dec4

Please sign in to comment.