From 15f6d4ae65321326455eb184c596a6fe821d5030 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 23 Feb 2020 19:18:54 +0100 Subject: [PATCH 01/18] travis: uses PHP 7.4 for coding checks --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9599553..ee3bbaf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,7 @@ jobs: - name: Nette Code Checker + php: 7.4 install: - travis_retry composer create-project nette/code-checker temp/code-checker ^3 --no-progress script: @@ -34,6 +35,7 @@ jobs: - name: Nette Coding Standard + php: 7.4 install: - travis_retry composer create-project nette/coding-standard temp/coding-standard ^2 --no-progress script: @@ -47,7 +49,7 @@ jobs: - stage: Code Coverage - php: 7.3 + php: 7.4 script: - vendor/bin/tester -p phpdbg tests -s --coverage ./coverage.xml --coverage-src ./src after_script: From 0e4db5a5ba1dc5b28ec1c895d6b76570d723d881 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 12 May 2020 00:57:11 +0200 Subject: [PATCH 02/18] added funding.yml --- .github/funding.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/funding.yml diff --git a/.github/funding.yml b/.github/funding.yml new file mode 100644 index 0000000..25adc95 --- /dev/null +++ b/.github/funding.yml @@ -0,0 +1,2 @@ +github: dg +custom: "https://nette.org/donate" From 64a33e6434dfd334d4cc96088c20388024db2b0c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 31 Jul 2020 16:49:45 +0200 Subject: [PATCH 03/18] readme.md: updated --- readme.md | 335 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 213 insertions(+), 122 deletions(-) diff --git a/readme.md b/readme.md index e291596..6417278 100644 --- a/readme.md +++ b/readme.md @@ -11,27 +11,33 @@ Nette Schema Introduction ============ -Handy library for validating data structures against a given Schema. +A practical library for validation and normalization of data structures against a given schema with a smart & easy-to-understand API. -Documentation can be found on the [website](https://doc.nette.org/schema). +Documentation can be found on the [website](https://doc.nette.org/schema). If you like it, **[please make a donation now](https://github.com/sponsors/dg)**. Thank you! -If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you! +Installation: - -Installation -============ - -The recommended way to install is via Composer: - -``` +```shell composer require nette/schema ``` It requires PHP version 7.1 and supports PHP up to 7.4. -Usage -===== +Support Project +--------------- + +Do you like Schema? Are you looking forward to the new features? + +[![Donate](https://files.nette.org/icons/donation-1.svg?)](https://nette.org/make-donation?to=schema) + + +Basic Usage +----------- + +In variable `$schema` we have a validation schema (what exactly this means and how to create it we will say later) and in variable `$data` we have a data structure that we want to validate and normalize. This can be, for example, data sent by the user through an API, configuration file, etc. + +The task is handled by the [Nette\Schema\Processor](https://api.nette.org/3.0/Nette/Schema/Processor.html) class, which processes the input and either returns normalized data or throws an [Nette\Schema\ValidationException](https://api.nette.org/3.0/Nette/Schema/ValidationException.html) exception on error. ```php $processor = new Nette\Schema\Processor; @@ -39,15 +45,15 @@ $processor = new Nette\Schema\Processor; try { $normalized = $processor->process($schema, $data); } catch (Nette\Schema\ValidationException $e) { - echo 'Data are not valid: ' . $e->getMessage(); + echo 'Data is invalid: ' . $e->getMessage(); } - -// in case of error it throws Nette\Schema\ValidationException ``` -Defining schema +Defining Schema --------------- +And now let's create a schema. The class [Nette\Schema\Expect](https://api.nette.org/3.0/Nette/Schema/Expect.html) is used to define it, we actually define expectations of what the data should look like. Let's say that the input data must be a structure (e.g. an array) containing elements `processRefund` of type bool and `refundAmount` of type int. + ```php use Nette\Schema\Expect; @@ -55,118 +61,163 @@ $schema = Expect::structure([ 'processRefund' => Expect::bool(), 'refundAmount' => Expect::int(), ]); +``` + +We believe that the schema definition looks clear, even if you see it for the very first time. +Lets send the following data for validation: + +```php $data = [ 'processRefund' => true, 'refundAmount' => 17, ]; -$normalized = $processor->process($schema, $data); // it passes +$normalized = $processor->process($schema, $data); // OK, it passes ``` -If you're validating data passed, you can cast strings and booleans to the expected types defined by your schema: +The output, i.e. the value `$normalized`, is the object `stdClass`. If we want the output to be an array, we add a cast to schema `Expect::structure([...])->castTo('array')`. -```php -$schema = Expect::structure([ - 'processRefund' => Expect::scalar()->castTo('bool'), - 'refundAmount' => Expect::scalar()->castTo('int'), -]); +All elements of the structure are optional and have a default value `null`. Example: +```php $data = [ - 'processRefund' => 1, - 'refundAmount' => '17', + 'refundAmount' => 17, ]; -$normalized = $processor->process($schema, $data); // it passes - -is_bool($normalized->processRefund); // true -is_int($normalized->refundAmount); // true +$normalized = $processor->process($schema, $data); // OK, it passes +// $normalized = {'processRefund' => null, 'refundAmount' => 17} ``` -By default, all properties are optional and have default value `null`, or `[]` in the case of arrays. +The fact that the default value is `null` does not mean that it would be accepted in the input data `'processRefund' => null`. No, the input must be boolean, i.e. only `true` or `false`. We would have to explicitly allow `null` via `Expect::bool()->nullable()`. + +An item can be made mandatory using `Expect::bool()->required()`. We change the default value to `false` using `Expect::bool()->default(false)` or shortly using `Expect::bool(false)`. -You can change the default value as follows: +And what if we wanted to accept `1` and `0` besides booleans? Then we list the allowed values, which we will also normalize to boolean: ```php $schema = Expect::structure([ - 'processRefund' => Expect::bool()->default(true), // or Expect::bool(true) + 'processRefund' => Expect::anyOf(true, false, 1, 0)->castTo('bool'), + 'refundAmount' => Expect::int(), ]); -$data = []; - -// validates, and sets defaults for missing properties $normalized = $processor->process($schema, $data); +is_bool($normalized->processRefund); // true +``` + +Now you know the basics of how the schema is defined and how the individual elements of the structure behave. We will now show what all the other elements can be used in defining a schema. -// $normalized->processRefund === true; + + +Data Types: type() +------------------ + +All standard PHP data types can be listed in the schema: + +```php +Expect::string($default = null) +Expect::int($default = null) +Expect::float($default = null) +Expect::bool($default = null) +Expect::null() +Expect::array($default = []) ``` +And then all types [supported by the Validators](https://doc.nette.org/validators#toc-validation-rules) via `Expect::type('scalar')` or abbreviated `Expect::scalar()`. Also class or interface names are accepted, e.g. `Expect::type('AddressEntity')`. -Arrays of items ---------------- +You can also use union notation: + +```php +Expect::type('bool|string|array') +``` + +The default value is always `null` except for `array` and `list`, where it is an empty array. (A list is an array indexed in ascending order of numeric keys from zero, that is, a non-associative array). + + +Array of Values: arrayOf() listOf() +----------------------------------- -Array where only string items are allowed: +The array is too general structure, it is more useful to specify exactly what elements it can contain. For example, an array whose elements can only be strings: ```php $schema = Expect::arrayOf('string'); -$processor->process($schema, ['key1' => 'a', 'key2' => 'b']); // it passes -$processor->process($schema, ['key' => 123]); // error: The option 'key' expects to be string, int 123 given. +$processor->process($schema, ['hello', 'world']); // OK +$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK +$processor->process($schema, ['key' => 123]); // ERROR: 123 is not a string ``` -Indexed array (ie. with numeric keys) where only string items are allowed: +The list is an indexed array: ```php $schema = Expect::listOf('string'); -$processor->process($schema, ['a', 'b']); // it passes -$processor->process($schema, ['key' => 'a']); // error, unexpected 'key' +$processor->process($schema, ['a', 'b']); // OK +$processor->process($schema, ['a', 123]); // ERROR: 123 is not a string +$processor->process($schema, ['key' => 'a']); // ERROR: is not a list +$processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: is not a list ``` -Enumerated values and anyOf() ------------------------------ +The parameter can also be a schema, so we can write: + +```php +Expect::arrayOf(Expect::bool()) +``` -The `anyOf()` is used to restrict a value to a fixed set of variants or subschemes: +The default value is an empty array. + + +Enumeration: anyOf() +-------------------- + +`anyOf()` is a set of values ​​or schemas that a value can be. Here's how to write an array of elements that can be either `'a'`, `true`, or `null`: ```php $schema = Expect::listOf( Expect::anyOf('a', true, null) ); -$processor->process($schema, ['a', true, null, 'a']); // it passes -$processor->process($schema, ['a', false]); // error: The option '1' expects to be 'a'|true|null, false given. +$processor->process($schema, ['a', true, null, 'a']); // OK +$processor->process($schema, ['a', false]); // ERROR: false does not belong there ``` -Elements can be schema: +The enumeration elements can also be schemas: ```php $schema = Expect::listOf( Expect::anyOf(Expect::string(), true, null) ); -$processor->process($schema, ['foo', true, null, 'bar']); // it passes -$processor->process($schema, [123]); // error: The option '0' expects to be string|true|null, 123 given. +$processor->process($schema, ['foo', true, null, 'bar']); // OK +$processor->process($schema, [123]); // ERROR ``` +The default value is `null`. + + Structures ---------- -Structures are objects with defined keys. Each of these key => pairs is conventionally referred to as a “property”. +Structures are objects with defined keys. Each of these key => value pairs is referred to as a "property": -Structures accept arrays and objects and return `stdClass` objects (unless you change it with `castTo('array')` etc). +Structures accept arrays and objects and return objects `stdClass` (unless you change it with `castTo('array')`, etc.). -By default, all properties are optional and have default value `null`. You can define mandatory properties via `required()`: +By default, all properties are optional and have a default value of `null`. You can define mandatory properties using `required()`: ```php $schema = Expect::structure([ 'required' => Expect::string()->required(), - 'optional' => Expect::string(), // default is null + 'optional' => Expect::string(), // the default value is null ]); -$processor->process($schema, ['optional' => '']); // error: option 'required' is missing -$processor->process($schema, ['required' => 'foo']); // it passes, returns (object) ['required' => 'foo', 'optional' => null] +$processor->process($schema, ['optional' => '']); +// ERROR: option 'required' is missing + +$processor->process($schema, ['required' => 'foo']); +// OK, returns {'required' => 'foo', 'optional' => null} ``` -You can define nullable properties via `nullable()`: +Although `null` is the default value of the `optional` property, it is not allowed in the input data (the value must be a string). Properties accepting `null` are defined using `nullable()`: ```php $schema = Expect::structure([ @@ -174,158 +225,198 @@ $schema = Expect::structure([ 'nullable' => Expect::string()->nullable(), ]); -$processor->process($schema, ['optional' => null]); // error: 'optional' expects to be string, null given. -$processor->process($schema, ['nullable' => null]); // it passes, returns (object) ['optional' => null, 'nullable' => null] +$processor->process($schema, ['optional' => null]); +// ERROR: 'optional' expects to be string, null given. + +$processor->process($schema, ['nullable' => null]); +// OK, returns {'optional' => null, 'nullable' => null} ``` -By default, providing additional properties is forbidden: +By default, there can be no extra items in the input data: ```php $schema = Expect::structure([ 'key' => Expect::string(), ]); -$processor->process($schema, ['additional' => 1]); // error: Unexpected option 'additional' +$processor->process($schema, ['additional' => 1]); +// ERROR: Unexpected option 'additional' ``` -The `otherItems()` is used to control the handling of extra stuff, that is, properties whose names are not listed in `Expect::structure()`: +Which we can change with `otherItems()`. As a parameter, we will specify the schema for each extra element: ```php $schema = Expect::structure([ 'key' => Expect::string(), ])->otherItems(Expect::int()); -$processor->process($schema, ['additional' => 1]); // it passes +$processor->process($schema, ['additional' => 1]); // OK +$processor->process($schema, ['additional' => true]); // ERROR ``` -Size and ranges ---------------- +Ranges: min() max() +------------------- -You can limit the number of elements or properties using the `min()` and `max()`: +Use `min()` and `max()` to limit the number of elements for arrays: ```php // array, at least 10 items, maximum 20 items -$schema = Expect::array()->min(10)->max(20); +Expect::array()->min(10)->max(20); ``` -The length of a string can be constrained using the `min()` and `max()`: +For strings, limit their length: ```php // string, at least 10 characters long, maximum 20 characters -$schema = Expect::string()->min(10)->max(20); +Expect::string()->min(10)->max(20); ``` -Ranges of numbers are specified using a combination of `min()` and `max()`: +For numbers, limit their value: ```php -// integer, between 10 and 20 -$schema = Expect::int()->min(10)->max(20); +// integer, between 10 and 20 inclusive +Expect::int()->min(10)->max(20); ``` -Regular expressions -------------------- +Of course, it is possible to mention only `min()`, or only `max()`: -String can be restricted by regular expression using the `pattern()`: +```php +// string, maximum 20 characters +Expect::string()->max(20); +``` + + +Regular Expressions: pattern() +------------------------------ + +Using `pattern()`, you can specify a regular expression which the **whole** input string must match (i.e. as if it were wrapped in characters `^` a `$`): ```php // just 9 digits -$schema = Expect::string()->pattern('\d{9}'); +Expect::string()->pattern('\d{9}'); ``` -Data mapping to objects ------------------------ -Schema can be generated from class: +Custom Assertions: assert() +--------------------------- + +You can add any other restrictions using `assert(callable $fn)`. + +```php +$countIsEven = function ($v) { return count($v) % 2 === 0; }; + +$schema = Expect::arrayOf('string') + ->assert($countIsEven); // the count must be even + +$processor->process($schema, ['a', 'b']); // OK +$processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 is not even +``` + +Or + +```php +Expect::string()->assert('is_file'); // the file must exist +``` + +You can add your own description for each assertions. It will be part of the error message. + +```php +$schema = Expect::arrayOf('string') + ->assert($countIsEven, 'Even items in array'); + +$processor->process($schema, ['a', 'b', 'c']); +// Failed assertion "Even items in array" for option with value array. +``` + +The method can be called repeatedly to add more assertions. + + +Mapping to Objects: from() +-------------------------- + +You can generate structure schema from the class. Example: ```php class Config { /** @var string */ - public $dsn; - - /** @var string|null */ - public $user; - + public $name; /** @var string|null */ public $password; - /** @var bool */ - public $debugger = true; + public $admin = false; } $schema = Expect::from(new Config); $data = [ - 'dsn' => 'sqlite', - 'user' => 'root' + 'name' => 'jeff', ]; $normalized = $processor->process($schema, $data); -// $normalized is Config class -// $normalized->dsn === 'sqlite' -// $normalized->user === 'root' -// $normalized->password === null -// $normalized->debugger === true +// $normalized instanceof Config +// $normalized = {'name' => 'jeff', 'password' => null, 'admin' => false} ``` -You can even use PHP 7.4 notation: - +If you are using PHP 7.4 or higher, you can use native types: ```php class Config { - public string $dsn; - public ?string $user; + public string $name; public ?string $password; - public bool $debugger = true; + public bool $admin = false; } $schema = Expect::from(new Config); ``` -Or use anonymous class: +Anonymous classes are also supported: ```php $schema = Expect::from(new class { - public string $dsn; - public ?string $user; + public string $name; public ?string $password; - public bool $debugger = true; + public bool $admin = false; }); ``` -Custom normalization --------------------- +Because the information obtained from the class definition may not be sufficient, you can add a custom schema for the elements with the second parameter: ```php -$schema = Expect::arrayOf('string') - ->before(function ($v) { return explode(' ', $v); }); - -$normalized = $processor->process($schema, 'a b c'); // it passes and returns ['a', 'b', 'c'] +$schema = Expect::from(new Config, [ + 'name' => Expect::string()->pattern('\w:.*'), +]); ``` -Custom constraints ------------------- -```php -$schema = Expect::arrayOf('string') - ->assert(function ($v) { return count($v) % 2 === 0; }); // count must be even number +Casting: castTo() +----------------- + +Successfully validated data can be cast: -$processor->process($schema, ['a', 'b']); // it passes, 2 is even number -$processor->process($schema, ['a', 'b', 'c']); // error, 3 is not even number +```php +Expect::scalar()->castTo('string'); ``` -Or +In addition to native PHP types, you can also cast to classes: ```php -$schema = Expect::string()->assert('is_file'); // file must exist +Expect::scalar()->castTo('AddressEntity'); ``` -You can add custom description for every assert. This description will be part of error message. + +Normalization: before() +----------------------- + +Prior to the validation itself, the data can be normalized using the method `before()`. As an example, let's have an element that must be an array of strings (eg `['a', 'b', 'c']`), but receives input in the form of a string `a b c`: ```php +$explode = function ($v) { return explode(' ', $v); }; + $schema = Expect::arrayOf('string') - ->assert(function ($v) { return count($v) % 2 === 0; }, 'Even items in array'); + ->before($explode); -$processor->process($schema, ['a', 'b', 'c']); // Failed assertion "Even items in array" for option with value array. +$normalized = $processor->process($schema, 'a b c'); +// OK, returns ['a', 'b', 'c'] ``` From 72e3deabe0c91226ff990519bb20b32f731286c0 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 31 Jul 2020 17:13:05 +0200 Subject: [PATCH 04/18] Type::items() is internal --- src/Schema/Elements/Type.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Schema/Elements/Type.php b/src/Schema/Elements/Type.php index 9874f65..f3baac4 100644 --- a/src/Schema/Elements/Type.php +++ b/src/Schema/Elements/Type.php @@ -72,6 +72,7 @@ public function max(?float $max): self /** * @param string|Schema $type + * @internal use arrayOf() or listOf() */ public function items($type = 'mixed'): self { From f4e5dee56c7962631159724614ec98c213f481a0 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 16 Aug 2020 16:08:36 +0200 Subject: [PATCH 05/18] tests: test() with description --- tests/Schema/Expect.anyOf.phpt | 22 +++++++++++----------- tests/Schema/Expect.array.phpt | 18 +++++++++--------- tests/Schema/Expect.assert.phpt | 6 +++--- tests/Schema/Expect.before.phpt | 8 ++++---- tests/Schema/Expect.castTo.phpt | 6 +++--- tests/Schema/Expect.dynamic.phpt | 2 +- tests/Schema/Expect.list.phpt | 10 +++++----- tests/Schema/Expect.minmax.phpt | 12 ++++++------ tests/Schema/Expect.pattern.phpt | 4 ++-- tests/Schema/Expect.scalars.phpt | 8 ++++---- tests/Schema/Expect.structure.phpt | 28 ++++++++++++++-------------- tests/Schema/Processor.context.phpt | 2 +- tests/Schema/heterogenous.phpt | 4 ++-- tests/bootstrap.php | 2 +- 14 files changed, 66 insertions(+), 66 deletions(-) diff --git a/tests/Schema/Expect.anyOf.phpt b/tests/Schema/Expect.anyOf.phpt index a04b557..379e1d8 100644 --- a/tests/Schema/Expect.anyOf.phpt +++ b/tests/Schema/Expect.anyOf.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -test(function () { // with scalars +test('with scalars', function () { $schema = Expect::anyOf('one', true, Expect::int()); Assert::same('one', (new Processor)->process($schema, 'one')); @@ -37,7 +37,7 @@ test(function () { // with scalars }); -test(function () { // with complex structure +test('with complex structure', function () { $schema = Expect::anyOf(Expect::listOf('string'), true, Expect::int()); checkValidationErrors(function () use ($schema) { @@ -52,7 +52,7 @@ test(function () { // with complex structure }); -test(function () { // with asserts +test('with asserts', function () { $schema = Expect::anyOf(Expect::string()->assert('strlen'), true); checkValidationErrors(function () use ($schema) { @@ -67,7 +67,7 @@ test(function () { // with asserts }); -test(function () { // no default value +test('no default value', function () { $schema = Expect::structure([ 'key1' => Expect::anyOf(Expect::string(), Expect::int()), 'key2' => Expect::anyOf(Expect::string('default'), true, Expect::int()), @@ -81,7 +81,7 @@ test(function () { // no default value }); -test(function () { // required +test('required', function () { $schema = Expect::structure([ 'key1' => Expect::anyOf(Expect::string(), Expect::int())->required(), 'key2' => Expect::anyOf(Expect::string('default'), true, Expect::int())->required(), @@ -102,7 +102,7 @@ test(function () { // required }); -test(function () { // required as argument +test('required as argument', function () { $schema = Expect::structure([ 'key1' => Expect::anyOf(Expect::string(), Expect::int())->required(), 'key1nr' => Expect::anyOf(Expect::string(), Expect::int())->required(false), @@ -128,7 +128,7 @@ test(function () { // required as argument }); -test(function () { // not nullable +test('not nullable', function () { $schema = Expect::structure([ 'key1' => Expect::anyOf(Expect::string(), Expect::int()), 'key2' => Expect::anyOf(Expect::string('default'), true, Expect::int()), @@ -145,7 +145,7 @@ test(function () { // not nullable }); -test(function () { // required & nullable +test('required & nullable', function () { $schema = Expect::structure([ 'key1' => Expect::anyOf(Expect::string()->nullable(), Expect::int())->required(), 'key2' => Expect::anyOf(Expect::string('default'), true, Expect::int(), null)->required(), @@ -159,7 +159,7 @@ test(function () { // required & nullable }); -test(function () { // nullable anyOf +test('nullable anyOf', function () { $schema = Expect::anyOf(Expect::string(), true)->nullable(); Assert::same('one', (new Processor)->process($schema, 'one')); @@ -172,7 +172,7 @@ test(function () { // nullable anyOf }); -test(function () { // processing +test('processing', function () { $schema = Expect::anyOf(Expect::string(), true)->nullable(); $processor = new Processor; @@ -189,7 +189,7 @@ test(function () { // processing }); -test(function () { // Schema as default value +test('Schema as default value', function () { $default = Expect::structure([ 'key2' => Expect::string(), ])->castTo('array'); diff --git a/tests/Schema/Expect.array.phpt b/tests/Schema/Expect.array.phpt index 16bc44a..f0fa5a5 100644 --- a/tests/Schema/Expect.array.phpt +++ b/tests/Schema/Expect.array.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -test(function () { // without default value +test('without default value', function () { $schema = Expect::array(); Assert::same([], (new Processor)->process($schema, [])); @@ -35,7 +35,7 @@ test(function () { // without default value }); -test(function () { // merging +test('merging', function () { $schema = Expect::array([ 'key1' => 'val1', 'key2' => 'val2', @@ -77,7 +77,7 @@ test(function () { // merging }); -test(function () { // merging & other items validation +test('merging & other items validation', function () { $schema = Expect::array([ 'key1' => 'val1', 'key2' => 'val2', @@ -114,7 +114,7 @@ test(function () { // merging & other items validation }); -test(function () { // merging & other items validation +test('merging & other items validation', function () { $schema = Expect::array()->items('string'); Assert::same([ @@ -147,7 +147,7 @@ test(function () { // merging & other items validation }); -test(function () { // items() & scalar +test('items() & scalar', function () { $schema = Expect::array([ 'a' => 'defval', ])->items('string'); @@ -180,7 +180,7 @@ test(function () { // items() & scalar }); -test(function () { // items() & structure +test('items() & structure', function () { $schema = Expect::array([ 'a' => 'defval', ])->items(Expect::structure(['k' => Expect::string()])); @@ -214,7 +214,7 @@ test(function () { // items() & structure }); -test(function () { // arrayOf() & scalar +test('arrayOf() & scalar', function () { $schema = Expect::arrayOf('string|int'); Assert::same([], (new Processor)->process($schema, [])); @@ -231,14 +231,14 @@ test(function () { // arrayOf() & scalar }); -test(function () { // arrayOf() error +test('arrayOf() error', function () { Assert::exception(function () { Expect::arrayOf(['a' => Expect::string()]); }, TypeError::class); }); -test(function () { // type[] +test('type[]', function () { $schema = Expect::type('int[]'); Assert::same([], (new Processor)->process($schema, null)); diff --git a/tests/Schema/Expect.assert.phpt b/tests/Schema/Expect.assert.phpt index 49d23c8..4338182 100644 --- a/tests/Schema/Expect.assert.phpt +++ b/tests/Schema/Expect.assert.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -test(function () { // single assertion +test('single assertion', function () { $schema = Expect::string()->assert('is_file'); checkValidationErrors(function () use ($schema) { @@ -21,7 +21,7 @@ test(function () { // single assertion }); -test(function () { // multiple assertions +test('multiple assertions', function () { $schema = Expect::string()->assert('ctype_digit')->assert(function ($s) { return strlen($s) >= 3; }); checkValidationErrors(function () use ($schema) { @@ -36,7 +36,7 @@ test(function () { // multiple assertions }); -test(function () { // multiple assertions with custom descriptions +test('multiple assertions with custom descriptions', function () { $schema = Expect::string() ->assert('ctype_digit', 'Is number') ->assert(function ($s) { return strlen($s) >= 3; }, 'Minimal lenght'); diff --git a/tests/Schema/Expect.before.phpt b/tests/Schema/Expect.before.phpt index 0a8cd1d..fce72f3 100644 --- a/tests/Schema/Expect.before.phpt +++ b/tests/Schema/Expect.before.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -test(function () { +test('', function () { $schema = Expect::array() ->before(function ($val) { return explode(',', $val); }); @@ -21,7 +21,7 @@ test(function () { }); -test(function () { // structure property +test('structure property', function () { $schema = Expect::structure([ 'key' => Expect::string()->before('strrev'), ]); @@ -33,7 +33,7 @@ test(function () { // structure property }); -test(function () { // order in structure +test('order in structure', function () { $schema = Expect::structure([ 'a' => Expect::string()->before(function ($val) use (&$order) { $order[] = 'a'; return $val; }), 'b' => Expect::string()->before(function ($val) use (&$order) { $order[] = 'b'; return $val; }), @@ -63,7 +63,7 @@ test(function () { // order in structure }); -test(function () { // order in array +test('order in array', function () { $schema = Expect::array() ->items(Expect::string()->before(function ($val) use (&$order) { $order[] = 'item'; return $val; })) ->before(function ($val) use (&$order) { $order[] = 'array'; return $val; }); diff --git a/tests/Schema/Expect.castTo.phpt b/tests/Schema/Expect.castTo.phpt index bf044ee..0d0d5bb 100644 --- a/tests/Schema/Expect.castTo.phpt +++ b/tests/Schema/Expect.castTo.phpt @@ -10,21 +10,21 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -test(function () { +test('', function () { $schema = Expect::int()->castTo('string'); Assert::same('10', (new Processor)->process($schema, 10)); }); -test(function () { +test('', function () { $schema = Expect::string()->castTo('array'); Assert::same(['foo'], (new Processor)->process($schema, 'foo')); }); -test(function () { +test('', function () { $schema = Expect::array()->castTo('stdClass'); Assert::equal((object) ['a' => 1, 'b' => 2], (new Processor)->process($schema, ['a' => 1, 'b' => 2])); diff --git a/tests/Schema/Expect.dynamic.phpt b/tests/Schema/Expect.dynamic.phpt index a0b3b97..83c376c 100644 --- a/tests/Schema/Expect.dynamic.phpt +++ b/tests/Schema/Expect.dynamic.phpt @@ -23,7 +23,7 @@ class DynamicParameter implements Nette\Schema\DynamicParameter } -test(function () { +test('', function () { $schema = Expect::structure([ 'a' => Expect::string()->dynamic(), 'b' => Expect::string('def')->dynamic(), diff --git a/tests/Schema/Expect.list.phpt b/tests/Schema/Expect.list.phpt index e58bac2..02ef113 100644 --- a/tests/Schema/Expect.list.phpt +++ b/tests/Schema/Expect.list.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -test(function () { // without default value +test('without default value', function () { $schema = Expect::list(); Assert::same([], (new Processor)->process($schema, [])); @@ -37,7 +37,7 @@ test(function () { // without default value }); -test(function () { // merging +test('merging', function () { $schema = Expect::list([1, 2, 3]); Assert::same([1, 2, 3], (new Processor)->process($schema, [])); @@ -48,7 +48,7 @@ test(function () { // merging }); -test(function () { // merging & other items validation +test('merging & other items validation', function () { $schema = Expect::list([1, 2, 3])->items('string'); Assert::same([1, 2, 3], (new Processor)->process($schema, [])); @@ -67,7 +67,7 @@ test(function () { // merging & other items validation }); -test(function () { // listOf() & scalar +test('listOf() & scalar', function () { $schema = Expect::listOf('string'); Assert::same([], (new Processor)->process($schema, [])); @@ -88,7 +88,7 @@ test(function () { // listOf() & scalar }); -test(function () { // listOf() & error +test('listOf() & error', function () { Assert::exception(function () { Expect::listOf(['a' => Expect::string()]); }, TypeError::class); diff --git a/tests/Schema/Expect.minmax.phpt b/tests/Schema/Expect.minmax.phpt index b87ec8b..fe74eb9 100644 --- a/tests/Schema/Expect.minmax.phpt +++ b/tests/Schema/Expect.minmax.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -test(function () { // int & min +test('int & min', function () { $schema = Expect::int()->min(10); Assert::same(10, (new Processor)->process($schema, 10)); @@ -21,7 +21,7 @@ test(function () { // int & min }); -test(function () { // int & max +test('int & max', function () { $schema = Expect::int()->max(20); Assert::same(20, (new Processor)->process($schema, 20)); @@ -32,7 +32,7 @@ test(function () { // int & max }); -test(function () { // int & min & max +test('int & min & max', function () { $schema = Expect::int()->min(10)->max(20); Assert::same(10, (new Processor)->process($schema, 10)); @@ -48,7 +48,7 @@ test(function () { // int & min & max }); -test(function () { // string +test('string', function () { $schema = Expect::string()->min(1)->max(5); Assert::same('hello', (new Processor)->process($schema, 'hello')); @@ -64,7 +64,7 @@ test(function () { // string }); -test(function () { // array +test('array', function () { $schema = Expect::array()->min(1)->max(3); Assert::same([1], (new Processor)->process($schema, [1])); @@ -80,7 +80,7 @@ test(function () { // array }); -test(function () { // structure +test('structure', function () { $schema = Expect::structure([])->otherItems('int')->min(1)->max(3); Assert::equal((object) [1], (new Processor)->process($schema, [1])); diff --git a/tests/Schema/Expect.pattern.phpt b/tests/Schema/Expect.pattern.phpt index cd802e6..73aadcd 100644 --- a/tests/Schema/Expect.pattern.phpt +++ b/tests/Schema/Expect.pattern.phpt @@ -10,14 +10,14 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -test(function () { +test('', function () { $schema = Expect::string()->pattern('\d{9}'); Assert::same('123456789', (new Processor)->process($schema, '123456789')); }); -test(function () { +test('', function () { $schema = Expect::string()->pattern('\d{9}'); checkValidationErrors(function () use ($schema) { diff --git a/tests/Schema/Expect.scalars.phpt b/tests/Schema/Expect.scalars.phpt index 066be8b..669e964 100644 --- a/tests/Schema/Expect.scalars.phpt +++ b/tests/Schema/Expect.scalars.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -test(function () { +test('', function () { $schema = Expect::scalar(); Assert::same('hello', (new Processor)->process($schema, 'hello')); @@ -27,7 +27,7 @@ test(function () { }); -test(function () { +test('', function () { $schema = Expect::string(); Assert::same('hello', (new Processor)->process($schema, 'hello')); @@ -50,7 +50,7 @@ test(function () { }); -test(function () { +test('', function () { $schema = Expect::type('string|bool'); Assert::same('one', (new Processor)->process($schema, 'one')); @@ -71,7 +71,7 @@ test(function () { }); -test(function () { +test('', function () { $schema = Expect::type('string')->nullable(); Assert::same('one', (new Processor)->process($schema, 'one')); diff --git a/tests/Schema/Expect.structure.phpt b/tests/Schema/Expect.structure.phpt index 088d248..eaa3ac2 100644 --- a/tests/Schema/Expect.structure.phpt +++ b/tests/Schema/Expect.structure.phpt @@ -10,7 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -test(function () { // without items +test('without items', function () { $schema = Expect::structure([]); Assert::equal((object) [], (new Processor)->process($schema, [])); @@ -39,7 +39,7 @@ test(function () { // without items }); -test(function () { // accepts object +test('accepts object', function () { $schema = Expect::structure(['a' => Expect::string()]); Assert::equal((object) ['a' => null], (new Processor)->process($schema, (object) [])); @@ -61,7 +61,7 @@ test(function () { // accepts object }); -test(function () { // scalar items +test('scalar items', function () { $schema = Expect::structure([ 'a' => Expect::string(), 'b' => Expect::int(), @@ -82,7 +82,7 @@ test(function () { // scalar items }); -test(function () { // array items +test('array items', function () { $schema = Expect::structure([ 'a' => Expect::array(), 'b' => Expect::array([]), @@ -101,14 +101,14 @@ test(function () { // array items }); -test(function () { // default value must be readonly +test('default value must be readonly', function () { Assert::exception(function () { $schema = Expect::structure([])->default([]); }, Nette\InvalidStateException::class); }); -test(function () { // with indexed item +test('with indexed item', function () { $schema = Expect::structure([ 'key1' => Expect::string(), 'key2' => Expect::string(), @@ -161,7 +161,7 @@ test(function () { // with indexed item }); -test(function () { // with indexed item & otherItems +test('with indexed item & otherItems', function () { $schema = Expect::structure([ 'key1' => Expect::string(), 'key2' => Expect::string(), @@ -218,7 +218,7 @@ test(function () { // with indexed item & otherItems }); -test(function () { // item with default value +test('item with default value', function () { $schema = Expect::structure([ 'b' => Expect::string(123), ]); @@ -243,7 +243,7 @@ test(function () { // item with default value }); -test(function () { // item without default value +test('item without default value', function () { $schema = Expect::structure([ 'b' => Expect::string(), ]); @@ -262,7 +262,7 @@ test(function () { // item without default value }); -test(function () { // required item +test('required item', function () { $schema = Expect::structure([ 'b' => Expect::string()->required(), 'c' => Expect::array()->required(), @@ -286,7 +286,7 @@ test(function () { // required item }); -test(function () { // other items +test('other items', function () { $schema = Expect::structure([ 'key' => Expect::string(), ])->otherItems(Expect::string()); @@ -300,7 +300,7 @@ test(function () { // other items }); -test(function () { // structure items +test('structure items', function () { $schema = Expect::structure([ 'a' => Expect::structure([ 'x' => Expect::string('defval'), @@ -369,7 +369,7 @@ test(function () { // structure items }); -test(function () { // processing +test('processing', function () { $schema = Expect::structure([ 'a' => Expect::structure([ 'x' => Expect::string('defval'), @@ -398,7 +398,7 @@ test(function () { // processing }); -test(function () { // processing without default values +test('processing without default values', function () { $schema = Expect::structure([ 'a' => Expect::string(), // implicit default 'b' => Expect::string('hello'), // explicit default diff --git a/tests/Schema/Processor.context.phpt b/tests/Schema/Processor.context.phpt index 193cbf9..8440a50 100644 --- a/tests/Schema/Processor.context.phpt +++ b/tests/Schema/Processor.context.phpt @@ -10,7 +10,7 @@ use Nette\Schema\Processor; require __DIR__ . '/../bootstrap.php'; -test(function () { +test('', function () { $schema = Expect::structure([ 'r' => Expect::string()->required(), ]); diff --git a/tests/Schema/heterogenous.phpt b/tests/Schema/heterogenous.phpt index 89c994b..de2f8df 100644 --- a/tests/Schema/heterogenous.phpt +++ b/tests/Schema/heterogenous.phpt @@ -39,7 +39,7 @@ class MySchema implements Schema } -test(function () { +test('', function () { $schema = Expect::arrayOf(new MySchema); $processor = new Processor; @@ -49,7 +49,7 @@ test(function () { }); -test(function () { +test('', function () { $schema = Expect::arrayOf(new MySchema); $processor = new Processor; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index ca6b68f..25c0960 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -18,7 +18,7 @@ date_default_timezone_set('Europe/Prague'); -function test(\Closure $function): void +function test(string $title, Closure $function): void { $function(); } From aad9f72d0b4c281e6136350353a905d2e9474d11 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 15 Oct 2020 17:40:44 +0200 Subject: [PATCH 06/18] tested on PHP 8.0 --- .travis.yml | 3 ++- readme.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ee3bbaf..4072ec2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ php: - 7.2 - 7.3 - 7.4 + - nightly before_install: # turn off XDebug @@ -62,7 +63,7 @@ jobs: - stage: Code Coverage -sudo: false +dist: xenial cache: directories: diff --git a/readme.md b/readme.md index 6417278..d9e7eb9 100644 --- a/readme.md +++ b/readme.md @@ -21,7 +21,7 @@ Installation: composer require nette/schema ``` -It requires PHP version 7.1 and supports PHP up to 7.4. +It requires PHP version 7.1 and supports PHP up to 8.0. Support Project From 5328fde1fe1f7180b9f247ce6b8a057d03ca4c86 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 15 Oct 2020 17:41:40 +0200 Subject: [PATCH 07/18] added GitHub Actions workflow --- .github/workflows/coding-style.yml | 31 ++++++++++++++ .github/workflows/static-analysis.yml | 21 ++++++++++ .github/workflows/tests.yml | 58 +++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 .github/workflows/coding-style.yml create mode 100644 .github/workflows/static-analysis.yml create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml new file mode 100644 index 0000000..1fe1626 --- /dev/null +++ b/.github/workflows/coding-style.yml @@ -0,0 +1,31 @@ +name: Coding Style + +on: [push, pull_request] + +jobs: + nette_cc: + name: Nette Code Checker + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v1 + with: + php-version: 7.4 + coverage: none + + - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress + - run: php temp/code-checker/code-checker --strict-types --ignore "tests/*/fixtures" + + + nette_cs: + name: Nette Coding Standard + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v1 + with: + php-version: 7.4 + coverage: none + + - run: composer create-project nette/coding-standard temp/coding-standard ^2 --no-progress + - run: php temp/coding-standard/ecs check src tests --config temp/coding-standard/coding-standard-php71.yml diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..e129028 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,21 @@ +name: Static Analysis (only informative) + +on: + push: + branches: + - master + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v1 + with: + php-version: 7.4 + coverage: none + + - run: composer install --no-progress --prefer-dist + - run: composer phpstan + continue-on-error: true # is only informative diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..983b2a2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,58 @@ +name: Tests + +on: [push, pull_request] + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + php: ['7.1', '7.2', '7.3', '7.4', '8.0'] + + fail-fast: false + + name: PHP ${{ matrix.php }} tests + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - run: composer install --no-progress --prefer-dist + - run: vendor/bin/tester tests -s -C + - if: failure() + uses: actions/upload-artifact@v2 + with: + name: output + path: tests/**/output + + + lowest_dependencies: + name: Lowest Dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v1 + with: + php-version: 7.1 + coverage: none + + - run: composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable + - run: vendor/bin/tester tests -s -C + + + code_coverage: + name: Code Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + coverage: none + + - run: composer install --no-progress --prefer-dist + - run: wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar + - run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src + - run: php coveralls.phar --verbose --config tests/.coveralls.yml From 04d8fb23d2086f6aaa91eeb6352d4118adebd280 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 30 May 2020 15:34:58 +0200 Subject: [PATCH 08/18] opened 1.1-dev --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ae24819..02a5ea5 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.1-dev" } } } From 25c91626401e08d20d7eba1b791f2e44b25e234f Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 30 May 2020 14:48:40 +0200 Subject: [PATCH 09/18] addError(): renamed 'hint' to 'expected' --- src/Schema/Context.php | 4 ++-- src/Schema/Elements/AnyOf.php | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Schema/Context.php b/src/Schema/Context.php index e665d29..4abfe8c 100644 --- a/src/Schema/Context.php +++ b/src/Schema/Context.php @@ -32,8 +32,8 @@ final class Context public $dynamics = []; - public function addError($message, $hint = null) + public function addError($message, $expected = null) { - $this->errors[] = (object) ['message' => $message, 'path' => $this->path, 'hint' => $hint]; + $this->errors[] = (object) ['message' => $message, 'path' => $this->path, 'expected' => $expected]; } } diff --git a/src/Schema/Elements/AnyOf.php b/src/Schema/Elements/AnyOf.php index ab75fbc..934d02e 100644 --- a/src/Schema/Elements/AnyOf.php +++ b/src/Schema/Elements/AnyOf.php @@ -68,7 +68,7 @@ public function merge($value, $base) public function complete($value, Nette\Schema\Context $context) { - $hints = $innerErrors = []; + $expecteds = $innerErrors = []; foreach ($this->set as $item) { if ($item instanceof Schema) { $dolly = new Context; @@ -78,25 +78,25 @@ public function complete($value, Nette\Schema\Context $context) return $this->doFinalize($res, $context); } foreach ($dolly->errors as $error) { - if ($error->path !== $context->path || !$error->hint) { + if ($error->path !== $context->path || !$error->expected) { $innerErrors[] = $error; } else { - $hints[] = $error->hint; + $expecteds[] = $error->expected; } } } else { if ($item === $value) { return $this->doFinalize($value, $context); } - $hints[] = static::formatValue($item); + $expecteds[] = static::formatValue($item); } } if ($innerErrors) { $context->errors = array_merge($context->errors, $innerErrors); } else { - $hints = implode('|', array_unique($hints)); - $context->addError("The option %path% expects to be $hints, " . static::formatValue($value) . ' given.'); + $expecteds = implode('|', array_unique($expecteds)); + $context->addError("The option %path% expects to be $expecteds, " . static::formatValue($value) . ' given.'); } } From 7e3f85794a90b9a525ceed230e4e253f0f5f3928 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 14 Oct 2020 00:39:30 +0200 Subject: [PATCH 10/18] added Message --- src/Schema/Context.php | 6 ++--- src/Schema/Elements/AnyOf.php | 4 ++-- src/Schema/Message.php | 42 +++++++++++++++++++++++++++++++++++ src/Schema/Processor.php | 3 +-- 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 src/Schema/Message.php diff --git a/src/Schema/Context.php b/src/Schema/Context.php index 4abfe8c..8a737b3 100644 --- a/src/Schema/Context.php +++ b/src/Schema/Context.php @@ -25,15 +25,15 @@ final class Context /** @var string[] */ public $path = []; - /** @var \stdClass[] */ + /** @var Message[] */ public $errors = []; /** @var array[] */ public $dynamics = []; - public function addError($message, $expected = null) + public function addError($message, $expected = null): Message { - $this->errors[] = (object) ['message' => $message, 'path' => $this->path, 'expected' => $expected]; + return $this->errors[] = new Message($message, $this->path, ['expected' => $expected]); } } diff --git a/src/Schema/Elements/AnyOf.php b/src/Schema/Elements/AnyOf.php index 934d02e..3a8dc7a 100644 --- a/src/Schema/Elements/AnyOf.php +++ b/src/Schema/Elements/AnyOf.php @@ -78,10 +78,10 @@ public function complete($value, Nette\Schema\Context $context) return $this->doFinalize($res, $context); } foreach ($dolly->errors as $error) { - if ($error->path !== $context->path || !$error->expected) { + if ($error->path !== $context->path || !$error->variables['expected']) { $innerErrors[] = $error; } else { - $expecteds[] = $error->expected; + $expecteds[] = $error->variables['expected']; } } } else { diff --git a/src/Schema/Message.php b/src/Schema/Message.php new file mode 100644 index 0000000..81ea6cc --- /dev/null +++ b/src/Schema/Message.php @@ -0,0 +1,42 @@ +message = $message; + $this->path = $path; + $this->variables = $variables; + } + + + public function toString(): string + { + $pathStr = " '" . implode(' › ', $this->path) . "'"; + return str_replace(' %path%', $this->path ? $pathStr : '', $this->message); + } +} diff --git a/src/Schema/Processor.php b/src/Schema/Processor.php index ee77662..f8f7d07 100644 --- a/src/Schema/Processor.php +++ b/src/Schema/Processor.php @@ -74,8 +74,7 @@ private function throwsErrors(Context $context): void { $messages = []; foreach ($context->errors as $error) { - $pathStr = " '" . implode(' › ', $error->path) . "'"; - $messages[] = str_replace(' %path%', $error->path ? $pathStr : '', $error->message); + $messages[] = $error->toString(); } if ($messages) { throw new ValidationException($messages[0], $messages); From 104a93cea56d890c107ea1be09b8082b71887487 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 30 May 2020 14:37:56 +0200 Subject: [PATCH 11/18] ValidationException::getMessageObjects() returns array of Messages --- readme.md | 3 +++ src/Schema/Processor.php | 8 ++------ src/Schema/ValidationException.php | 27 +++++++++++++++++++++++---- tests/Schema/Processor.context.phpt | 14 +++++++++++++- tests/bootstrap.php | 3 ++- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/readme.md b/readme.md index d9e7eb9..096ec9f 100644 --- a/readme.md +++ b/readme.md @@ -49,6 +49,9 @@ try { } ``` +Method `$e->getMessages()` returns array of all message strings and `$e->getMessageObjects()` return all messages as [Nette\Schema\Message](https://api.nette.org/3.0/Nette/Schema/Message.html) objects. + + Defining Schema --------------- diff --git a/src/Schema/Processor.php b/src/Schema/Processor.php index f8f7d07..d942766 100644 --- a/src/Schema/Processor.php +++ b/src/Schema/Processor.php @@ -72,12 +72,8 @@ public function processMultiple(Schema $schema, array $dataset) private function throwsErrors(Context $context): void { - $messages = []; - foreach ($context->errors as $error) { - $messages[] = $error->toString(); - } - if ($messages) { - throw new ValidationException($messages[0], $messages); + if ($context->errors) { + throw new ValidationException(null, $context->errors); } } diff --git a/src/Schema/ValidationException.php b/src/Schema/ValidationException.php index 6ffe52e..4c2af02 100644 --- a/src/Schema/ValidationException.php +++ b/src/Schema/ValidationException.php @@ -17,18 +17,37 @@ */ class ValidationException extends Nette\InvalidStateException { - /** @var array */ + /** @var Message[] */ private $messages; - public function __construct(string $message, array $messages = []) + /** + * @param Message[] $messages + */ + public function __construct(?string $message, array $messages = []) { - parent::__construct($message); - $this->messages = $messages ?: [$message]; + parent::__construct($message ?: $messages[0]->toString()); + $this->messages = $messages; } + /** + * @return string[] + */ public function getMessages(): array + { + $res = []; + foreach ($this->messages as $message) { + $res[] = $message->toString(); + } + return $res; + } + + + /** + * @return Message[] + */ + public function getMessageObjects(): array { return $this->messages; } diff --git a/tests/Schema/Processor.context.phpt b/tests/Schema/Processor.context.phpt index 8440a50..449a738 100644 --- a/tests/Schema/Processor.context.phpt +++ b/tests/Schema/Processor.context.phpt @@ -5,6 +5,7 @@ declare(strict_types=1); use Nette\Schema\Context; use Nette\Schema\Expect; use Nette\Schema\Processor; +use Tester\Assert; require __DIR__ . '/../bootstrap.php'; @@ -20,7 +21,18 @@ test('', function () { $context->path = ['first']; }; - checkValidationErrors(function () use ($schema, $processor) { + $e = checkValidationErrors(function () use ($schema, $processor) { $processor->process($schema, []); }, ["The mandatory option 'first › r' is missing."]); + + Assert::equal( + [ + new Nette\Schema\Message( + 'The mandatory option %path% is missing.', + ['first', 'r'], + ['expected' => null] + ), + ], + $e->getMessageObjects() + ); }); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 25c0960..0d44959 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -24,9 +24,10 @@ function test(string $title, Closure $function): void } -function checkValidationErrors(\Closure $function, array $messages): void +function checkValidationErrors(\Closure $function, array $messages): Nette\Schema\ValidationException { $e = Assert::exception($function, Nette\Schema\ValidationException::class); Assert::same($messages, $e->getMessages()); Assert::same($messages[0], $e->getMessage()); + return $e; } From 8ba04d813b9473eef5140d336d0bd88f792d03d8 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 30 May 2020 13:26:46 +0200 Subject: [PATCH 12/18] added error codes --- src/Schema/Context.php | 4 ++-- src/Schema/Elements/AnyOf.php | 10 ++++++++-- src/Schema/Elements/Base.php | 16 +++++++++++++--- src/Schema/Elements/Structure.php | 5 ++++- src/Schema/Elements/Type.php | 5 ++++- src/Schema/Message.php | 16 +++++++++++++++- tests/Schema/Processor.context.phpt | 1 + 7 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/Schema/Context.php b/src/Schema/Context.php index 8a737b3..ce7d340 100644 --- a/src/Schema/Context.php +++ b/src/Schema/Context.php @@ -32,8 +32,8 @@ final class Context public $dynamics = []; - public function addError($message, $expected = null): Message + public function addError(string $message, string $code, string $expected = null): Message { - return $this->errors[] = new Message($message, $this->path, ['expected' => $expected]); + return $this->errors[] = new Message($message, $code, $this->path, ['expected' => $expected]); } } diff --git a/src/Schema/Elements/AnyOf.php b/src/Schema/Elements/AnyOf.php index 3a8dc7a..8c2bb08 100644 --- a/src/Schema/Elements/AnyOf.php +++ b/src/Schema/Elements/AnyOf.php @@ -96,7 +96,10 @@ public function complete($value, Nette\Schema\Context $context) $context->errors = array_merge($context->errors, $innerErrors); } else { $expecteds = implode('|', array_unique($expecteds)); - $context->addError("The option %path% expects to be $expecteds, " . static::formatValue($value) . ' given.'); + $context->addError( + "The option %path% expects to be $expecteds, " . static::formatValue($value) . ' given.', + Nette\Schema\Message::UNEXPECTED_VALUE + ); } } @@ -104,7 +107,10 @@ public function complete($value, Nette\Schema\Context $context) public function completeDefault(Context $context) { if ($this->required) { - $context->addError('The mandatory option %path% is missing.'); + $context->addError( + 'The mandatory option %path% is missing.', + Nette\Schema\Message::OPTION_MISSING + ); return null; } if ($this->default instanceof Schema) { diff --git a/src/Schema/Elements/Base.php b/src/Schema/Elements/Base.php index ede2aaf..4dcc297 100644 --- a/src/Schema/Elements/Base.php +++ b/src/Schema/Elements/Base.php @@ -72,7 +72,10 @@ public function assert(callable $handler, string $description = null): self public function completeDefault(Context $context) { if ($this->required) { - $context->addError('The mandatory option %path% is missing.'); + $context->addError( + 'The mandatory option %path% is missing.', + Nette\Schema\Message::OPTION_MISSING + ); return null; } return $this->default; @@ -94,7 +97,11 @@ private function doValidate($value, string $expected, Context $context): bool Nette\Utils\Validators::assert($value, $expected, 'option %path%'); return true; } catch (Nette\Utils\AssertionException $e) { - $context->addError($e->getMessage(), $expected); + $context->addError( + $e->getMessage(), + Nette\Schema\Message::UNEXPECTED_VALUE, + $expected + ); return false; } } @@ -113,7 +120,10 @@ private function doFinalize($value, Context $context) foreach ($this->asserts as $i => [$handler, $description]) { if (!$handler($value)) { $expected = $description ? ('"' . $description . '"') : (is_string($handler) ? "$handler()" : "#$i"); - $context->addError("Failed assertion $expected for option %path% with value " . static::formatValue($value) . '.'); + $context->addError( + "Failed assertion $expected for option %path% with value " . static::formatValue($value) . '.', + Nette\Schema\Message::FAILED_ASSERTION + ); return; } } diff --git a/src/Schema/Elements/Structure.php b/src/Schema/Elements/Structure.php index f0d187d..d875e6a 100644 --- a/src/Schema/Elements/Structure.php +++ b/src/Schema/Elements/Structure.php @@ -144,7 +144,10 @@ public function complete($value, Nette\Schema\Context $context) $s = implode("', '", array_map(function ($key) use ($context) { return implode(' › ', array_merge($context->path, [$key])); }, $hint ? [$extraKeys[0]] : $extraKeys)); - $context->addError("Unexpected option '$s'" . ($hint ? ", did you mean '$hint'?" : '.')); + $context->addError( + "Unexpected option '$s'" . ($hint ? ", did you mean '$hint'?" : '.'), + Nette\Schema\Message::UNEXPECTED_KEY + ); } } diff --git a/src/Schema/Elements/Type.php b/src/Schema/Elements/Type.php index f3baac4..b042776 100644 --- a/src/Schema/Elements/Type.php +++ b/src/Schema/Elements/Type.php @@ -141,7 +141,10 @@ public function complete($value, Context $context) return; } if ($this->pattern !== null && !preg_match("\x01^(?:$this->pattern)$\x01Du", $value)) { - $context->addError("The option %path% expects to match pattern '$this->pattern', '$value' given."); + $context->addError( + "The option %path% expects to match pattern '$this->pattern', '$value' given.", + Nette\Schema\Message::PATTERN_MISMATCH + ); return; } diff --git a/src/Schema/Message.php b/src/Schema/Message.php index 81ea6cc..491cf66 100644 --- a/src/Schema/Message.php +++ b/src/Schema/Message.php @@ -16,9 +16,22 @@ final class Message { use Nette\SmartObject; + public const OPTION_MISSING = 'schema.optionMissing'; + + public const PATTERN_MISMATCH = 'schema.patternMismatch'; + + public const UNEXPECTED_VALUE = 'schema.unexpectedValue'; + + public const FAILED_ASSERTION = 'schema.failedAssertion'; + + public const UNEXPECTED_KEY = 'schema.unexpectedKey'; + /** @var string */ public $message; + /** @var string */ + public $code; + /** @var string[] */ public $path; @@ -26,9 +39,10 @@ final class Message public $variables; - public function __construct(string $message, array $path, array $variables = []) + public function __construct(string $message, string $code, array $path, array $variables = []) { $this->message = $message; + $this->code = $code; $this->path = $path; $this->variables = $variables; } diff --git a/tests/Schema/Processor.context.phpt b/tests/Schema/Processor.context.phpt index 449a738..c5fcf47 100644 --- a/tests/Schema/Processor.context.phpt +++ b/tests/Schema/Processor.context.phpt @@ -29,6 +29,7 @@ test('', function () { [ new Nette\Schema\Message( 'The mandatory option %path% is missing.', + Nette\Schema\Message::OPTION_MISSING, ['first', 'r'], ['expected' => null] ), From 6a7939b9e905539f4799895b19ec4b3d2b74dc0d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 30 May 2020 14:48:40 +0200 Subject: [PATCH 13/18] supports new %placeholders%, formatValue moved to Message --- src/Schema/Context.php | 4 ++-- src/Schema/Elements/AnyOf.php | 13 ++++++++----- src/Schema/Elements/Base.php | 21 ++++----------------- src/Schema/Elements/Structure.php | 3 ++- src/Schema/Elements/Type.php | 5 +++-- src/Schema/Message.php | 29 +++++++++++++++++++++++++++-- tests/Schema/Processor.context.phpt | 3 +-- 7 files changed, 47 insertions(+), 31 deletions(-) diff --git a/src/Schema/Context.php b/src/Schema/Context.php index ce7d340..e1a5867 100644 --- a/src/Schema/Context.php +++ b/src/Schema/Context.php @@ -32,8 +32,8 @@ final class Context public $dynamics = []; - public function addError(string $message, string $code, string $expected = null): Message + public function addError(string $message, string $code, array $variables = []): Message { - return $this->errors[] = new Message($message, $code, $this->path, ['expected' => $expected]); + return $this->errors[] = new Message($message, $code, $this->path, $variables); } } diff --git a/src/Schema/Elements/AnyOf.php b/src/Schema/Elements/AnyOf.php index 8c2bb08..777a6fd 100644 --- a/src/Schema/Elements/AnyOf.php +++ b/src/Schema/Elements/AnyOf.php @@ -78,7 +78,7 @@ public function complete($value, Nette\Schema\Context $context) return $this->doFinalize($res, $context); } foreach ($dolly->errors as $error) { - if ($error->path !== $context->path || !$error->variables['expected']) { + if ($error->path !== $context->path || empty($error->variables['expected'])) { $innerErrors[] = $error; } else { $expecteds[] = $error->variables['expected']; @@ -88,17 +88,20 @@ public function complete($value, Nette\Schema\Context $context) if ($item === $value) { return $this->doFinalize($value, $context); } - $expecteds[] = static::formatValue($item); + $expecteds[] = Nette\Schema\Message::formatValue($item); } } if ($innerErrors) { $context->errors = array_merge($context->errors, $innerErrors); } else { - $expecteds = implode('|', array_unique($expecteds)); $context->addError( - "The option %path% expects to be $expecteds, " . static::formatValue($value) . ' given.', - Nette\Schema\Message::UNEXPECTED_VALUE + 'The option %path% expects to be %expected%, %value% given.', + Nette\Schema\Message::UNEXPECTED_VALUE, + [ + 'value' => $value, + 'expected' => implode('|', array_unique($expecteds)), + ] ); } } diff --git a/src/Schema/Elements/Base.php b/src/Schema/Elements/Base.php index 4dcc297..44e4625 100644 --- a/src/Schema/Elements/Base.php +++ b/src/Schema/Elements/Base.php @@ -100,7 +100,7 @@ private function doValidate($value, string $expected, Context $context): bool $context->addError( $e->getMessage(), Nette\Schema\Message::UNEXPECTED_VALUE, - $expected + ['value' => $value, 'expected' => $expected] ); return false; } @@ -121,8 +121,9 @@ private function doFinalize($value, Context $context) if (!$handler($value)) { $expected = $description ? ('"' . $description . '"') : (is_string($handler) ? "$handler()" : "#$i"); $context->addError( - "Failed assertion $expected for option %path% with value " . static::formatValue($value) . '.', - Nette\Schema\Message::FAILED_ASSERTION + 'Failed assertion %assertion% for option %path% with value %value%.', + Nette\Schema\Message::FAILED_ASSERTION, + ['value' => $value, 'assertion' => $expected] ); return; } @@ -130,18 +131,4 @@ private function doFinalize($value, Context $context) return $value; } - - - private static function formatValue($value): string - { - if (is_string($value)) { - return "'$value'"; - } elseif (is_bool($value)) { - return $value ? 'true' : 'false'; - } elseif (is_scalar($value)) { - return (string) $value; - } else { - return strtolower(gettype($value)); - } - } } diff --git a/src/Schema/Elements/Structure.php b/src/Schema/Elements/Structure.php index d875e6a..2cccac8 100644 --- a/src/Schema/Elements/Structure.php +++ b/src/Schema/Elements/Structure.php @@ -146,7 +146,8 @@ public function complete($value, Nette\Schema\Context $context) }, $hint ? [$extraKeys[0]] : $extraKeys)); $context->addError( "Unexpected option '$s'" . ($hint ? ", did you mean '$hint'?" : '.'), - Nette\Schema\Message::UNEXPECTED_KEY + Nette\Schema\Message::UNEXPECTED_KEY, + ['hint' => $hint] ); } } diff --git a/src/Schema/Elements/Type.php b/src/Schema/Elements/Type.php index b042776..637bccb 100644 --- a/src/Schema/Elements/Type.php +++ b/src/Schema/Elements/Type.php @@ -142,8 +142,9 @@ public function complete($value, Context $context) } if ($this->pattern !== null && !preg_match("\x01^(?:$this->pattern)$\x01Du", $value)) { $context->addError( - "The option %path% expects to match pattern '$this->pattern', '$value' given.", - Nette\Schema\Message::PATTERN_MISMATCH + "The option %path% expects to match pattern '%pattern%', %value% given.", + Nette\Schema\Message::PATTERN_MISMATCH, + ['value' => $value, 'pattern' => $this->pattern] ); return; } diff --git a/src/Schema/Message.php b/src/Schema/Message.php index 491cf66..4159a10 100644 --- a/src/Schema/Message.php +++ b/src/Schema/Message.php @@ -16,14 +16,19 @@ final class Message { use Nette\SmartObject; + /** no variables */ public const OPTION_MISSING = 'schema.optionMissing'; + /** variables: {value: string, pattern: string} */ public const PATTERN_MISMATCH = 'schema.patternMismatch'; + /** variables: {value: mixed, expected: string} */ public const UNEXPECTED_VALUE = 'schema.unexpectedValue'; + /** variables: {value: mixed, assertion: string} */ public const FAILED_ASSERTION = 'schema.failedAssertion'; + /** variables: {hint: string} */ public const UNEXPECTED_KEY = 'schema.unexpectedKey'; /** @var string */ @@ -50,7 +55,27 @@ public function __construct(string $message, string $code, array $path, array $v public function toString(): string { - $pathStr = " '" . implode(' › ', $this->path) . "'"; - return str_replace(' %path%', $this->path ? $pathStr : '', $this->message); + $vars = $this->variables; + $vars['path'] = $this->path ? "'" . implode(' › ', $this->path) . "'" : null; + $vars['value'] = self::formatValue($vars['value'] ?? null); + + return preg_replace_callback('~( ?)%(\w+)%~', function ($m) use ($vars) { + [, $space, $key] = $m; + return $vars[$key] === null ? '' : $space . $vars[$key]; + }, $this->message); + } + + + public static function formatValue($value): string + { + if (is_string($value)) { + return "'$value'"; + } elseif (is_bool($value)) { + return $value ? 'true' : 'false'; + } elseif (is_scalar($value)) { + return (string) $value; + } else { + return strtolower(gettype($value)); + } } } diff --git a/tests/Schema/Processor.context.phpt b/tests/Schema/Processor.context.phpt index c5fcf47..982e424 100644 --- a/tests/Schema/Processor.context.phpt +++ b/tests/Schema/Processor.context.phpt @@ -30,8 +30,7 @@ test('', function () { new Nette\Schema\Message( 'The mandatory option %path% is missing.', Nette\Schema\Message::OPTION_MISSING, - ['first', 'r'], - ['expected' => null] + ['first', 'r'] ), ], $e->getMessageObjects() From ea1036e776d4709beecf14d0070d5b39ddcc7d46 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 30 May 2020 16:05:13 +0200 Subject: [PATCH 14/18] Structure: concatenated error message replaced by individual messages --- src/Schema/Elements/Structure.php | 18 +++++++++--------- tests/Schema/Expect.structure.phpt | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Schema/Elements/Structure.php b/src/Schema/Elements/Structure.php index 2cccac8..dac5851 100644 --- a/src/Schema/Elements/Structure.php +++ b/src/Schema/Elements/Structure.php @@ -140,15 +140,15 @@ public function complete($value, Nette\Schema\Context $context) if ($this->otherItems) { $items += array_fill_keys($extraKeys, $this->otherItems); } else { - $hint = Nette\Utils\Helpers::getSuggestion(array_map('strval', array_keys($items)), (string) $extraKeys[0]); - $s = implode("', '", array_map(function ($key) use ($context) { - return implode(' › ', array_merge($context->path, [$key])); - }, $hint ? [$extraKeys[0]] : $extraKeys)); - $context->addError( - "Unexpected option '$s'" . ($hint ? ", did you mean '$hint'?" : '.'), - Nette\Schema\Message::UNEXPECTED_KEY, - ['hint' => $hint] - ); + $keys = array_map('strval', array_keys($items)); + foreach ($extraKeys as $key) { + $hint = Nette\Utils\Helpers::getSuggestion($keys, (string) $key); + $context->addError( + 'Unexpected option %path%' . ($hint ? ", did you mean '%hint%'?" : '.'), + Nette\Schema\Message::UNEXPECTED_KEY, + ['hint' => $hint] + )->path[] = $key; + } } } diff --git a/tests/Schema/Expect.structure.phpt b/tests/Schema/Expect.structure.phpt index eaa3ac2..93a81f5 100644 --- a/tests/Schema/Expect.structure.phpt +++ b/tests/Schema/Expect.structure.phpt @@ -17,7 +17,7 @@ test('without items', function () { checkValidationErrors(function () use ($schema) { (new Processor)->process($schema, [1, 2, 3]); - }, ["Unexpected option '0', '1', '2'."]); + }, ["Unexpected option '0'.", "Unexpected option '1'.", "Unexpected option '2'."]); checkValidationErrors(function () use ($schema) { (new Processor)->process($schema, ['key' => 'val']); @@ -141,7 +141,8 @@ test('with indexed item', function () { checkValidationErrors(function () use ($processor, $schema) { $processor->process($schema, [1, 2, 3]); }, [ - "Unexpected option '1', '2'.", + "Unexpected option '1'.", + "Unexpected option '2'.", "The option '0' expects to be string, int 1 given.", ]); @@ -227,7 +228,11 @@ test('item with default value', function () { checkValidationErrors(function () use ($schema) { (new Processor)->process($schema, [1, 2, 3]); - }, ["Unexpected option '0', did you mean 'b'?"]); + }, [ + "Unexpected option '0', did you mean 'b'?", + "Unexpected option '1', did you mean 'b'?", + "Unexpected option '2', did you mean 'b'?", + ]); Assert::equal((object) ['b' => 123], (new Processor)->process($schema, [])); @@ -318,6 +323,8 @@ test('structure items', function () { (new Processor)->process($schema, [1, 2, 3]); }, [ "Unexpected option '0', did you mean 'a'?", + "Unexpected option '1', did you mean 'a'?", + "Unexpected option '2', did you mean 'a'?", "The mandatory option 'b › y' is missing.", ]); @@ -354,7 +361,8 @@ test('structure items', function () { checkValidationErrors(function () use ($schema) { (new Processor)->process($schema, ['b' => ['x1' => 'val', 'x2' => 'val']]); }, [ - "Unexpected option 'b › x1', 'b › x2'.", + "Unexpected option 'b › x1'.", + "Unexpected option 'b › x2'.", "The mandatory option 'b › y' is missing.", ]); From acb46e98ffcbbf948995fc39fe03730fbfec570d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 14 Oct 2020 01:48:30 +0200 Subject: [PATCH 15/18] Processor: refactoring, added $this->context --- src/Schema/Processor.php | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/Schema/Processor.php b/src/Schema/Processor.php index d942766..a2ef02c 100644 --- a/src/Schema/Processor.php +++ b/src/Schema/Processor.php @@ -22,6 +22,9 @@ final class Processor /** @var array */ public $onNewContext = []; + /** @var Context|null */ + private $context; + /** @var bool */ private $skipDefaults; @@ -39,11 +42,11 @@ public function skipDefaults(bool $value = true) */ public function process(Schema $schema, $data) { - $context = $this->createContext(); - $data = $schema->normalize($data, $context); - $this->throwsErrors($context); - $data = $schema->complete($data, $context); - $this->throwsErrors($context); + $this->createContext(); + $data = $schema->normalize($data, $this->context); + $this->throwsErrors(); + $data = $schema->complete($data, $this->context); + $this->throwsErrors(); return $data; } @@ -55,34 +58,33 @@ public function process(Schema $schema, $data) */ public function processMultiple(Schema $schema, array $dataset) { - $context = $this->createContext(); + $this->createContext(); $flatten = null; $first = true; foreach ($dataset as $data) { - $data = $schema->normalize($data, $context); - $this->throwsErrors($context); + $data = $schema->normalize($data, $this->context); + $this->throwsErrors(); $flatten = $first ? $data : $schema->merge($data, $flatten); $first = false; } - $data = $schema->complete($flatten, $context); - $this->throwsErrors($context); + $data = $schema->complete($flatten, $this->context); + $this->throwsErrors(); return $data; } - private function throwsErrors(Context $context): void + private function throwsErrors(): void { - if ($context->errors) { - throw new ValidationException(null, $context->errors); + if ($this->context->errors) { + throw new ValidationException(null, $this->context->errors); } } - private function createContext(): Context + private function createContext() { - $context = new Context; - $context->skipDefaults = $this->skipDefaults; - $this->onNewContext($context); - return $context; + $this->context = new Context; + $this->context->skipDefaults = $this->skipDefaults; + $this->onNewContext($this->context); } } From 772edc144623122c30a1efe654c5a005075d904e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 13 Oct 2020 23:21:24 +0200 Subject: [PATCH 16/18] added support for warnings --- src/Schema/Context.php | 9 +++++++++ src/Schema/Elements/AnyOf.php | 1 + src/Schema/Processor.php | 13 +++++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/Schema/Context.php b/src/Schema/Context.php index e1a5867..fdbe292 100644 --- a/src/Schema/Context.php +++ b/src/Schema/Context.php @@ -28,6 +28,9 @@ final class Context /** @var Message[] */ public $errors = []; + /** @var Message[] */ + public $warnings = []; + /** @var array[] */ public $dynamics = []; @@ -36,4 +39,10 @@ public function addError(string $message, string $code, array $variables = []): { return $this->errors[] = new Message($message, $code, $this->path, $variables); } + + + public function addWarning(string $message, string $code, array $variables = []): Message + { + return $this->warnings[] = new Message($message, $code, $this->path, $variables); + } } diff --git a/src/Schema/Elements/AnyOf.php b/src/Schema/Elements/AnyOf.php index 777a6fd..2e25f3c 100644 --- a/src/Schema/Elements/AnyOf.php +++ b/src/Schema/Elements/AnyOf.php @@ -75,6 +75,7 @@ public function complete($value, Nette\Schema\Context $context) $dolly->path = $context->path; $res = $item->complete($value, $dolly); if (!$dolly->errors) { + $context->warnings = array_merge($context->warnings, $dolly->warnings); return $this->doFinalize($res, $context); } foreach ($dolly->errors as $error) { diff --git a/src/Schema/Processor.php b/src/Schema/Processor.php index a2ef02c..392a3de 100644 --- a/src/Schema/Processor.php +++ b/src/Schema/Processor.php @@ -73,6 +73,19 @@ public function processMultiple(Schema $schema, array $dataset) } + /** + * @return string[] + */ + public function getWarnings(): array + { + $res = []; + foreach ($this->context->warnings as $message) { + $res[] = $message->toString(); + } + return $res; + } + + private function throwsErrors(): void { if ($this->context->errors) { From 7a5fc30792b1bb8a3538c427f4ea8c352580ad11 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 14 Oct 2020 02:06:56 +0200 Subject: [PATCH 17/18] Allow options to be marked as deprecated [Closes #27] --- readme.md | 14 ++++++++++++++ src/Schema/Elements/Base.php | 20 +++++++++++++++++++- src/Schema/Message.php | 3 +++ tests/Schema/Expect.anyOf.phpt | 15 +++++++++++++++ tests/Schema/Expect.structure.phpt | 28 ++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 096ec9f..822729f 100644 --- a/readme.md +++ b/readme.md @@ -257,6 +257,20 @@ $processor->process($schema, ['additional' => 1]); // OK $processor->process($schema, ['additional' => true]); // ERROR ``` +Deprecations +------------ + +You can deprecate property using the `deprecated([string $message])` method. Deprecation notices are returned by `$processor->getWarnings()`. + +```php +$schema = Expect::structure([ + 'old' => Expect::int()->deprecated('The option %path% is deprecated'), +]); + +$processor->process($schema, ['old' => 1]); // OK +$processor->getWarnings(); // ["The option 'old' is deprecated"] +``` + Ranges: min() max() ------------------- diff --git a/src/Schema/Elements/Base.php b/src/Schema/Elements/Base.php index 44e4625..bdf6e63 100644 --- a/src/Schema/Elements/Base.php +++ b/src/Schema/Elements/Base.php @@ -33,6 +33,9 @@ trait Base /** @var string|null */ private $castTo; + /** @var string|null */ + private $deprecated; + public function default($value): self { @@ -69,6 +72,14 @@ public function assert(callable $handler, string $description = null): self } + /** Marks option as deprecated */ + public function deprecated(string $message = 'Option %path% is deprecated.'): self + { + $this->deprecated = $message; + return $this; + } + + public function completeDefault(Context $context) { if ($this->required) { @@ -95,7 +106,6 @@ private function doValidate($value, string $expected, Context $context): bool { try { Nette\Utils\Validators::assert($value, $expected, 'option %path%'); - return true; } catch (Nette\Utils\AssertionException $e) { $context->addError( $e->getMessage(), @@ -104,6 +114,14 @@ private function doValidate($value, string $expected, Context $context): bool ); return false; } + + if ($this->deprecated !== null) { + $context->addWarning( + $this->deprecated, + Nette\Schema\Message::DEPRECATED + ); + } + return true; } diff --git a/src/Schema/Message.php b/src/Schema/Message.php index 4159a10..4051f36 100644 --- a/src/Schema/Message.php +++ b/src/Schema/Message.php @@ -31,6 +31,9 @@ final class Message /** variables: {hint: string} */ public const UNEXPECTED_KEY = 'schema.unexpectedKey'; + /** no variables */ + public const DEPRECATED = 'schema.deprecated'; + /** @var string */ public $message; diff --git a/tests/Schema/Expect.anyOf.phpt b/tests/Schema/Expect.anyOf.phpt index 379e1d8..f66462e 100644 --- a/tests/Schema/Expect.anyOf.phpt +++ b/tests/Schema/Expect.anyOf.phpt @@ -159,6 +159,21 @@ test('required & nullable', function () { }); +test('deprecated item', function () { + $schema = Expect::anyOf('one', true, Expect::int()->deprecated()); + + $processor = new Processor; + Assert::same('one', $processor->process($schema, 'one')); + Assert::same([], $processor->getWarnings()); + + Assert::same(true, $processor->process($schema, true)); + Assert::same([], $processor->getWarnings()); + + Assert::same(123, $processor->process($schema, 123)); + Assert::same(['Option is deprecated.'], $processor->getWarnings()); +}); + + test('nullable anyOf', function () { $schema = Expect::anyOf(Expect::string(), true)->nullable(); diff --git a/tests/Schema/Expect.structure.phpt b/tests/Schema/Expect.structure.phpt index 93a81f5..2424f52 100644 --- a/tests/Schema/Expect.structure.phpt +++ b/tests/Schema/Expect.structure.phpt @@ -426,3 +426,31 @@ test('processing without default values', function () { $processor->process($schema, ['d' => 'newval']) ); }); + + +test('deprecated item', function () { + $schema = Expect::structure([ + 'b' => Expect::string()->deprecated('depr %path%'), + ]); + + $processor = new Processor; + Assert::equal( + (object) ['b' => 'val'], + $processor->process($schema, ['b' => 'val']) + ); + Assert::same(["depr 'b'"], $processor->getWarnings()); +}); + + +test('deprecated other items', function () { + $schema = Expect::structure([ + 'key' => Expect::string(), + ])->otherItems(Expect::string()->deprecated()); + + $processor = new Processor; + Assert::equal((object) ['key' => null], $processor->process($schema, [])); + Assert::same([], $processor->getWarnings()); + + Assert::equal((object) ['key' => null, 'other' => 'foo'], $processor->process($schema, ['other' => 'foo'])); + Assert::same(["Option 'other' is deprecated."], $processor->getWarnings()); +}); From a05751e52ad8e883e54316f4ea82465f0af594e0 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Fri, 16 Oct 2020 19:53:35 -0400 Subject: [PATCH 18/18] Add option to prevent merging of arrayOf, listOf defaults --- src/Schema/Elements/Type.php | 14 ++++++++++ tests/Schema/Expect.array.phpt | 15 +++++++++++ tests/Schema/Expect.list.phpt | 15 +++++++++++ tests/Schema/Expect.structure.phpt | 43 ++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+) diff --git a/src/Schema/Elements/Type.php b/src/Schema/Elements/Type.php index 637bccb..1998e9f 100644 --- a/src/Schema/Elements/Type.php +++ b/src/Schema/Elements/Type.php @@ -33,6 +33,9 @@ final class Type implements Schema /** @var string|null */ private $pattern; + /** @var bool */ + private $preventMergingDefaults = false; + public function __construct(string $type) { @@ -88,6 +91,13 @@ public function pattern(?string $pattern): self } + public function preventMergingDefaults(bool $prevent = true): self + { + $this->preventMergingDefaults = $prevent; + return $this; + } + + /********************* processing ****************d*g**/ @@ -165,6 +175,10 @@ public function complete($value, Context $context) } } + if ($this->preventMergingDefaults && is_array($value) && count($value) > 0) { + $value[Helpers::PREVENT_MERGING] = true; + } + $value = Helpers::merge($value, $this->default); return $this->doFinalize($value, $context); } diff --git a/tests/Schema/Expect.array.phpt b/tests/Schema/Expect.array.phpt index f0fa5a5..22cd7fa 100644 --- a/tests/Schema/Expect.array.phpt +++ b/tests/Schema/Expect.array.phpt @@ -231,6 +231,21 @@ test('arrayOf() & scalar', function () { }); +test('arrayOf() with defaults', function () { + $schema = Expect::arrayOf('string|int')->default(['foo', 42]); + + Assert::same(['foo', 42], (new Processor)->process($schema, null)); + Assert::same(['foo', 42], (new Processor)->process($schema, [])); + Assert::same(['foo', 42, 'bar'], (new Processor)->process($schema, ['bar'])); + + $schema->preventMergingDefaults(); + + Assert::same(['foo', 42], (new Processor)->process($schema, null)); + Assert::same(['foo', 42], (new Processor)->process($schema, [])); + Assert::same(['bar'], (new Processor)->process($schema, ['bar'])); +}); + + test('arrayOf() error', function () { Assert::exception(function () { Expect::arrayOf(['a' => Expect::string()]); diff --git a/tests/Schema/Expect.list.phpt b/tests/Schema/Expect.list.phpt index 02ef113..201f91f 100644 --- a/tests/Schema/Expect.list.phpt +++ b/tests/Schema/Expect.list.phpt @@ -88,6 +88,21 @@ test('listOf() & scalar', function () { }); +test('listOf() with defaults', function () { + $schema = Expect::listOf('string|int')->default(['foo', 42]); + + Assert::same(['foo', 42], (new Processor)->process($schema, null)); + Assert::same(['foo', 42], (new Processor)->process($schema, [])); + Assert::same(['foo', 42, 'bar'], (new Processor)->process($schema, ['bar'])); + + $schema->preventMergingDefaults(); + + Assert::same(['foo', 42], (new Processor)->process($schema, null)); + Assert::same(['foo', 42], (new Processor)->process($schema, [])); + Assert::same(['bar'], (new Processor)->process($schema, ['bar'])); +}); + + test('listOf() & error', function () { Assert::exception(function () { Expect::listOf(['a' => Expect::string()]); diff --git a/tests/Schema/Expect.structure.phpt b/tests/Schema/Expect.structure.phpt index 2424f52..2d49160 100644 --- a/tests/Schema/Expect.structure.phpt +++ b/tests/Schema/Expect.structure.phpt @@ -247,6 +247,49 @@ test('item with default value', function () { Assert::equal((object) ['b' => 'val'], (new Processor)->process($schema, ['b' => 'val'])); }); +test('list with default value', function () { + $schema = Expect::structure([ + 'b' => Expect::listOf('int')->default([1, 2, 3]), + ]); + + Assert::equal((object) ['b' => [1, 2, 3]], (new Processor)->process($schema, null)); + Assert::equal((object) ['b' => [1, 2, 3]], (new Processor)->process($schema, [])); + Assert::equal((object) ['b' => [1, 2, 3]], (new Processor)->process($schema, ['b' => null])); + Assert::equal((object) ['b' => [1, 2, 3]], (new Processor)->process($schema, ['b' => []])); + Assert::equal((object) ['b' => [1, 2, 3, 4]], (new Processor)->process($schema, ['b' => [4]])); + + $schema = Expect::structure([ + 'b' => Expect::listOf('int')->default([1, 2, 3])->preventMergingDefaults(), + ]); + + Assert::equal((object) ['b' => [1, 2, 3]], (new Processor)->process($schema, null)); + Assert::equal((object) ['b' => [1, 2, 3]], (new Processor)->process($schema, [])); + Assert::equal((object) ['b' => [1, 2, 3]], (new Processor)->process($schema, ['b' => null])); + Assert::equal((object) ['b' => [1, 2, 3]], (new Processor)->process($schema, ['b' => []])); + Assert::equal((object) ['b' => [4]], (new Processor)->process($schema, ['b' => [4]])); +}); + +test('array with default value', function () { + $schema = Expect::structure([ + 'b' => Expect::arrayOf('int')->default(['x' => 1]), + ]); + + Assert::equal((object) ['b' => ['x' => 1]], (new Processor)->process($schema, null)); + Assert::equal((object) ['b' => ['x' => 1]], (new Processor)->process($schema, [])); + Assert::equal((object) ['b' => ['x' => 1]], (new Processor)->process($schema, ['b' => null])); + Assert::equal((object) ['b' => ['x' => 1]], (new Processor)->process($schema, ['b' => []])); + Assert::equal((object) ['b' => ['x' => 1, 'y' => 2]], (new Processor)->process($schema, ['b' => ['y' => 2]])); + + $schema = Expect::structure([ + 'b' => Expect::arrayOf('int')->default(['x' => 1])->preventMergingDefaults(), + ]); + + Assert::equal((object) ['b' => ['x' => 1]], (new Processor)->process($schema, null)); + Assert::equal((object) ['b' => ['x' => 1]], (new Processor)->process($schema, [])); + Assert::equal((object) ['b' => ['x' => 1]], (new Processor)->process($schema, ['b' => null])); + Assert::equal((object) ['b' => ['x' => 1]], (new Processor)->process($schema, ['b' => []])); + Assert::equal((object) ['b' => ['y' => 2]], (new Processor)->process($schema, ['b' => ['y' => 2]])); +}); test('item without default value', function () { $schema = Expect::structure([