From a8384b825256e074c5d3221a9234592ff12b1b83 Mon Sep 17 00:00:00 2001 From: Seif Kamal Date: Mon, 27 Apr 2020 15:54:32 +0100 Subject: [PATCH 1/5] Amend example file path --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fd86d78..7aec6d8 100644 --- a/README.md +++ b/README.md @@ -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'), @@ -75,8 +75,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 ``` Here's the same one using static class methods: @@ -105,7 +105,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), ]); @@ -119,8 +119,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). From e2f0397337675fc1caa6ee7ad396988ea68b2845 Mon Sep 17 00:00:00 2001 From: Seif Kamal Date: Mon, 27 Apr 2020 19:50:38 +0100 Subject: [PATCH 2/5] Simplify validation logic --- src/Validator.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Validator.php b/src/Validator.php index 54039f4..7cb8e75 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -24,26 +24,31 @@ 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_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) { /** From f7184ba85818e7567dd377fb1a6b12d0bf8a84f3 Mon Sep 17 00:00:00 2001 From: Seif Kamal Date: Tue, 28 Apr 2020 11:21:08 +0100 Subject: [PATCH 3/5] Make error messages more consistent --- docs/use-case.md | 2 +- src/Exception/InvalidValueException.php | 2 +- src/Exception/MissingPropertyException.php | 2 +- src/Exception/StructValidationException.php | 2 +- src/Exception/UnexpectedPropertyException.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/use-case.md b/docs/use-case.md index 3615510..7a8136e 100644 --- a/docs/use-case.md +++ b/docs/use-case.md @@ -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 diff --git a/src/Exception/InvalidValueException.php b/src/Exception/InvalidValueException.php index 74be741..194d1ea 100644 --- a/src/Exception/InvalidValueException.php +++ b/src/Exception/InvalidValueException.php @@ -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}'"); } } diff --git a/src/Exception/MissingPropertyException.php b/src/Exception/MissingPropertyException.php index 707bfa8..a934890 100644 --- a/src/Exception/MissingPropertyException.php +++ b/src/Exception/MissingPropertyException.php @@ -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}'"); } } diff --git a/src/Exception/StructValidationException.php b/src/Exception/StructValidationException.php index 86782a9..9d5d561 100644 --- a/src/Exception/StructValidationException.php +++ b/src/Exception/StructValidationException.php @@ -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 ); diff --git a/src/Exception/UnexpectedPropertyException.php b/src/Exception/UnexpectedPropertyException.php index e7fdaf1..fe17b43 100644 --- a/src/Exception/UnexpectedPropertyException.php +++ b/src/Exception/UnexpectedPropertyException.php @@ -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) )); From 3286e1e17f6b3d6e7e5b39e153402dc22b611296 Mon Sep 17 00:00:00 2001 From: Seif Kamal Date: Tue, 28 Apr 2020 11:44:47 +0100 Subject: [PATCH 4/5] Update method doc --- src/Struct.php | 8 ++++---- src/functions.php | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Struct.php b/src/Struct.php index 038b096..afaf4b0 100644 --- a/src/Struct.php +++ b/src/Struct.php @@ -6,7 +6,7 @@ class Struct { /** @var string */ private $name; - /** @var callable|Struct[] */ + /** @var callable[]|Struct[] */ private $interface; /** @var bool */ private $exhaustive; @@ -14,8 +14,8 @@ class Struct /** * 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. @@ -39,7 +39,7 @@ public function name(): string } /** - * @return callable|Struct[] + * @return callable[]|Struct[] */ public function interface(): array { diff --git a/src/functions.php b/src/functions.php index c296071..65061b7 100644 --- a/src/functions.php +++ b/src/functions.php @@ -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 From bb60a281dab0d6d3c8104c46f332227bf25394b3 Mon Sep 17 00:00:00 2001 From: Seif Kamal Date: Tue, 28 Apr 2020 11:55:39 +0100 Subject: [PATCH 5/5] Support implicit struct array validation --- README.md | 31 ++++++++++++++++++++++++++- src/Struct.php | 11 ++++++++++ src/Validator.php | 11 ++++++++-- src/functions.php | 4 ++-- tests/Unit/StructTest.php | 44 ++++++++++++++++++++++++++++----------- 5 files changed, 84 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7aec6d8..b41a2f0 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,36 @@ echo "File: {$directory['file']}" . PHP_EOL; // 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 + '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 name; diff --git a/src/Validator.php b/src/Validator.php index 7cb8e75..a19379f 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -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)); @@ -41,6 +45,9 @@ public static function validate(array &$data, Struct $struct): bool 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); diff --git a/src/functions.php b/src/functions.php index 65061b7..b2479da 100644 --- a/src/functions.php +++ b/src/functions.php @@ -91,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); } diff --git a/tests/Unit/StructTest.php b/tests/Unit/StructTest.php index fbd8bf9..30fd12e 100644 --- a/tests/Unit/StructTest.php +++ b/tests/Unit/StructTest.php @@ -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; @@ -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', @@ -74,7 +95,6 @@ public function dataProviderForTest(): array ]), 'tickets' => Type::arrayOf(Type::not('is_null')), ], - false, null ], ];