From 8565543552971f572834210627f12d591c647077 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 17 Dec 2020 22:49:42 +0100 Subject: [PATCH 1/9] opened 2.0-dev --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 56b8452..41ba39e 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "2.0-dev" } } } From 836909e3d65767df668b76a6f44113e1ec15c993 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 9 Oct 2021 22:47:06 +0200 Subject: [PATCH 2/9] Schema: added return type hints (BC break) --- src/Schema/Schema.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Schema/Schema.php b/src/Schema/Schema.php index 3ded769..b244376 100644 --- a/src/Schema/Schema.php +++ b/src/Schema/Schema.php @@ -14,24 +14,18 @@ interface Schema { /** * Normalization. - * @return mixed */ - function normalize(mixed $value, Context $context); + function normalize(mixed $value, Context $context): mixed; /** * Merging. - * @return mixed */ - function merge(mixed $value, mixed $base); + function merge(mixed $value, mixed $base): mixed; /** * Validation and finalization. - * @return mixed */ - function complete(mixed $value, Context $context); + function complete(mixed $value, Context $context): mixed; - /** - * @return mixed - */ - function completeDefault(Context $context); + function completeDefault(Context $context): mixed; } From 6235ecb26550a50fb033076d846b28d6b5d9c3bf Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 31 Oct 2023 12:20:16 +0100 Subject: [PATCH 3/9] Expect::from() removed support for phpDoc annotations (BC break) --- readme.md | 22 +----- src/Schema/Expect.php | 5 +- src/Schema/Helpers.php | 36 ---------- tests/Schema/Expect.from.php80.phpt | 57 ---------------- tests/Schema/Expect.from.phpt | 76 ++++++++++----------- tests/Schema/Helpers.getPropertyType.phpt | 37 ---------- tests/Schema/Helpers.parseAnnotation().phpt | 60 ---------------- 7 files changed, 39 insertions(+), 254 deletions(-) delete mode 100644 tests/Schema/Expect.from.php80.phpt delete mode 100644 tests/Schema/Helpers.getPropertyType.phpt delete mode 100644 tests/Schema/Helpers.parseAnnotation().phpt diff --git a/readme.md b/readme.md index 5ee1382..e5296c0 100644 --- a/readme.md +++ b/readme.md @@ -486,12 +486,9 @@ You can generate structure schema from the class. Example: ```php class Config { - /** @var string */ - public $name; - /** @var string|null */ - public $password; - /** @var bool */ - public $admin = false; + public string $name; + public ?string $password; + public bool $admin = false; } $schema = Expect::from(new Config); @@ -505,19 +502,6 @@ $normalized = $processor->process($schema, $data); // $normalized = {'name' => 'jeff', 'password' => null, 'admin' => false} ``` -If you are using PHP 7.4 or higher, you can use native types: - -```php -class Config -{ - public string $name; - public ?string $password; - public bool $admin = false; -} - -$schema = Expect::from(new Config); -``` - Anonymous classes are also supported: ```php diff --git a/src/Schema/Expect.php b/src/Schema/Expect.php index eab3c84..a6d5987 100644 --- a/src/Schema/Expect.php +++ b/src/Schema/Expect.php @@ -73,14 +73,11 @@ public static function from(object $object, array $items = []): Structure foreach ($props as $prop) { $item = &$items[$prop->getName()]; if (!$item) { - $type = Helpers::getPropertyType($prop) ?? 'mixed'; - $item = new Type($type); + $item = new Type((string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed')); if ($prop instanceof \ReflectionProperty ? $prop->isInitialized($object) : $prop->isOptional()) { $def = ($prop instanceof \ReflectionProperty ? $prop->getValue($object) : $prop->getDefaultValue()); if (is_object($def)) { $item = static::from($def); - } elseif ($def === null && !Nette\Utils\Validators::is(null, $type)) { - $item->required(); } else { $item->default($def); } diff --git a/src/Schema/Helpers.php b/src/Schema/Helpers.php index 70bf183..8f4da1e 100644 --- a/src/Schema/Helpers.php +++ b/src/Schema/Helpers.php @@ -10,7 +10,6 @@ namespace Nette\Schema; use Nette; -use Nette\Utils\Reflection; /** @@ -55,41 +54,6 @@ public static function merge(mixed $value, mixed $base): mixed } - public static function getPropertyType(\ReflectionProperty|\ReflectionParameter $prop): ?string - { - if ($type = Nette\Utils\Type::fromReflection($prop)) { - return (string) $type; - } elseif ( - ($prop instanceof \ReflectionProperty) - && ($type = preg_replace('#\s.*#', '', (string) self::parseAnnotation($prop, 'var'))) - ) { - $class = Reflection::getPropertyDeclaringClass($prop); - return preg_replace_callback('#[\w\\\\]+#', fn($m) => Reflection::expandClassName($m[0], $class), $type); - } - - return null; - } - - - /** - * Returns an annotation value. - * @param \ReflectionProperty $ref - */ - public static function parseAnnotation(\Reflector $ref, string $name): ?string - { - if (!Reflection::areCommentsAvailable()) { - throw new Nette\InvalidStateException('You have to enable phpDoc comments in opcode cache.'); - } - - $re = '#[\s*]@' . preg_quote($name, '#') . '(?=\s|$)(?:[ \t]+([^@\s]\S*))?#'; - if ($ref->getDocComment() && preg_match($re, trim($ref->getDocComment(), '/*'), $m)) { - return $m[1] ?? ''; - } - - return null; - } - - public static function formatValue(mixed $value): string { if ($value instanceof DynamicParameter) { diff --git a/tests/Schema/Expect.from.php80.phpt b/tests/Schema/Expect.from.php80.phpt deleted file mode 100644 index 1cf34c6..0000000 --- a/tests/Schema/Expect.from.php80.phpt +++ /dev/null @@ -1,57 +0,0 @@ - Expect::string('mysql'), - 'user' => Expect::type('?string')->required(), - 'password' => Expect::type('?string'), - 'options' => Expect::type('array|int')->default([]), - 'debugger' => Expect::bool(true), - 'mixed' => Expect::mixed()->required(), - 'arr' => Expect::type('array')->default([1]), - ], $schema->items); - Assert::type($obj, (new Processor)->process($schema, ['user' => '', 'mixed' => ''])); -}); - - -Assert::with(Structure::class, function () { // constructor injection - $schema = Expect::from($obj = new class ('') { - public function __construct( - public ?string $user, - public ?string $password = null, - ) { - } - }); - - Assert::type(Structure::class, $schema); - Assert::equal([ - 'user' => Expect::type('?string')->required(), - 'password' => Expect::type('?string'), - ], $schema->items); - Assert::equal( - new $obj('foo', 'bar'), - (new Processor)->process($schema, ['user' => 'foo', 'password' => 'bar']), - ); -}); diff --git a/tests/Schema/Expect.from.phpt b/tests/Schema/Expect.from.phpt index 1881202..cb65ea9 100644 --- a/tests/Schema/Expect.from.phpt +++ b/tests/Schema/Expect.from.phpt @@ -22,76 +22,70 @@ Assert::with(Structure::class, function () { Assert::with(Structure::class, function () { $schema = Expect::from($obj = new class { - /** @var string */ - public $dsn = 'mysql'; - - /** @var string|null */ - public $user; - - /** @var ?string */ - public $password; - - /** @var string[] */ - public $options = [1]; - - /** @var bool */ - public $debugger = true; - public $mixed; - - /** @var array|null */ - public $arr; - - /** @var string */ - public $required; + public string $dsn = 'mysql'; + public ?string $user; + public ?string $password = null; + public array|int $options = []; + public bool $debugger = true; + public mixed $mixed; + public array $arr = [1]; }); Assert::type(Structure::class, $schema); Assert::equal([ 'dsn' => Expect::string('mysql'), - 'user' => Expect::type('string|null'), + 'user' => Expect::type('?string')->required(), 'password' => Expect::type('?string'), - 'options' => Expect::type('string[]')->default([1]), + 'options' => Expect::type('array|int')->default([]), 'debugger' => Expect::bool(true), - 'mixed' => Expect::mixed(), - 'arr' => Expect::type('array|null')->default(null), - 'required' => Expect::type('string')->required(), + 'mixed' => Expect::mixed()->required(), + 'arr' => Expect::type('array')->default([1]), ], $schema->items); - Assert::type($obj, (new Processor)->process($schema, ['required' => ''])); + Assert::type($obj, (new Processor)->process($schema, ['user' => '', 'mixed' => ''])); }); -Assert::exception(function () { - Expect::from(new class { - /** @var Unknown */ - public $unknown; +Assert::with(Structure::class, function () { // constructor injection + $schema = Expect::from($obj = new class ('') { + public function __construct( + public ?string $user, + public ?string $password = null, + ) { + } }); -}, Nette\NotImplementedException::class, 'Anonymous classes are not supported.'); + + Assert::type(Structure::class, $schema); + Assert::equal([ + 'user' => Expect::type('?string')->required(), + 'password' => Expect::type('?string'), + ], $schema->items); + Assert::equal( + new $obj('foo', 'bar'), + (new Processor)->process($schema, ['user' => 'foo', 'password' => 'bar']), + ); +}); Assert::with(Structure::class, function () { // overwritten item $schema = Expect::from(new class { - /** @var string */ - public $dsn = 'mysql'; + public string $dsn = 'mysql'; - /** @var string|null */ - public $user; + public ?string $user; }, ['dsn' => Expect::int(123)]); Assert::equal([ 'dsn' => Expect::int(123), - 'user' => Expect::type('string|null'), + 'user' => Expect::type('?string')->required(), ], $schema->items); }); Assert::with(Structure::class, function () { // nested object $obj = new class { - /** @var object */ - public $inner; + public object $inner; }; $obj->inner = new class { - /** @var string */ - public $name; + public string $name; }; $schema = Expect::from($obj); diff --git a/tests/Schema/Helpers.getPropertyType.phpt b/tests/Schema/Helpers.getPropertyType.phpt deleted file mode 100644 index 998bec5..0000000 --- a/tests/Schema/Helpers.getPropertyType.phpt +++ /dev/null @@ -1,37 +0,0 @@ - Date: Sun, 20 Dec 2020 22:52:11 +0100 Subject: [PATCH 4/9] Type: mergeDefaults() are disabled by default (BC break) [Closes #28, Closes #31] --- readme.md | 2 +- src/Schema/Elements/Type.php | 6 +++++- tests/Schema/Expect.array.phpt | 34 +++++++++++++++------------------- tests/Schema/Expect.list.phpt | 12 ++++++------ 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/readme.md b/readme.md index e5296c0..b9febfa 100644 --- a/readme.md +++ b/readme.md @@ -177,7 +177,7 @@ The parameter can also be a schema, so we can write: Expect::arrayOf(Expect::bool()) ``` -The default value is an empty array. If you specify a default value, it will be merged with the passed data. This can be disabled using `mergeDefaults(false)`. +The default value is an empty array. If you specify a default value and call `mergeDefaults()`, it will be merged with the passed data. Enumeration: anyOf() diff --git a/src/Schema/Elements/Type.php b/src/Schema/Elements/Type.php index 69d5299..7f5d68d 100644 --- a/src/Schema/Elements/Type.php +++ b/src/Schema/Elements/Type.php @@ -26,7 +26,7 @@ final class Type implements Schema /** @var array{?float, ?float} */ private array $range = [null, null]; private ?string $pattern = null; - private bool $merge = true; + private bool $merge = false; public function __construct(string $type) @@ -44,8 +44,12 @@ public function nullable(): self } + /** @deprecated mergeDefaults is disabled by default */ public function mergeDefaults(bool $state = true): self { + if ($state === true) { + trigger_error(__METHOD__ . '() is deprecated and will be removed in the next major version.', E_USER_DEPRECATED); + } $this->merge = $state; return $this; } diff --git a/tests/Schema/Expect.array.phpt b/tests/Schema/Expect.array.phpt index 6d6c5f7..9a9808e 100644 --- a/tests/Schema/Expect.array.phpt +++ b/tests/Schema/Expect.array.phpt @@ -36,13 +36,13 @@ test('without default value', function () { }); -test('not merging', function () { +test('not merging default value', function () { $schema = Expect::array([ 'key1' => 'val1', 'key2' => 'val2', 'val3', 'arr' => ['item'], - ])->mergeDefaults(false); + ]); Assert::same([], (new Processor)->process($schema, [])); @@ -53,13 +53,13 @@ test('not merging', function () { }); -test('merging', function () { - $schema = Expect::array([ +test('merging default value', function () { + $schema = @Expect::array([ // mergeDefaults() is deprecated 'key1' => 'val1', 'key2' => 'val2', 'val3', 'arr' => ['item'], - ]); + ])->mergeDefaults(true); Assert::same([ 'key1' => 'val1', @@ -131,12 +131,12 @@ test('merging', function () { }); -test('merging & other items validation', function () { - $schema = Expect::array([ +test('merging default value & other items validation', function () { + $schema = @Expect::array([ // mergeDefaults() is deprecated 'key1' => 'val1', 'key2' => 'val2', 'val3', - ])->items('string'); + ])->mergeDefaults(true)->items('string'); Assert::same([ 'key1' => 'val1', @@ -169,7 +169,7 @@ test('merging & other items validation', function () { }); -test('merging & other items validation', function () { +test('merging default value & other items validation', function () { $schema = Expect::array()->items('string'); Assert::same([ @@ -204,11 +204,9 @@ test('merging & other items validation', function () { test('items() & scalar', function () { - $schema = Expect::array([ - 'a' => 'defval', - ])->items('string'); + $schema = Expect::array()->items('string'); - Assert::same(['a' => 'defval'], (new Processor)->process($schema, [])); + Assert::same([], (new Processor)->process($schema, [])); checkValidationErrors(function () use ($schema) { (new Processor)->process($schema, [1, 2, 3]); @@ -232,16 +230,14 @@ test('items() & scalar', function () { (new Processor)->process($schema, ['b' => null]); }, ["The item 'b' expects to be string, null given."]); - Assert::same(['a' => 'defval', 'b' => 'val'], (new Processor)->process($schema, ['b' => 'val'])); + Assert::same(['b' => 'val'], (new Processor)->process($schema, ['b' => 'val'])); }); test('items() & structure', function () { - $schema = Expect::array([ - 'a' => 'defval', - ])->items(Expect::structure(['k' => Expect::string()])); + $schema = Expect::array([])->items(Expect::structure(['k' => Expect::string()])); - Assert::same(['a' => 'defval'], (new Processor)->process($schema, [])); + Assert::same([], (new Processor)->process($schema, [])); checkValidationErrors(function () use ($schema) { (new Processor)->process($schema, ['a' => 'val']); @@ -264,7 +260,7 @@ test('items() & structure', function () { }, ["Unexpected item 'b\u{a0}›\u{a0}a', did you mean 'k'?"]); Assert::equal( - ['a' => 'defval', 'b' => (object) ['k' => 'val']], + ['b' => (object) ['k' => 'val']], (new Processor)->process($schema, ['b' => ['k' => 'val']]), ); }); diff --git a/tests/Schema/Expect.list.phpt b/tests/Schema/Expect.list.phpt index 52a3e53..f565b53 100644 --- a/tests/Schema/Expect.list.phpt +++ b/tests/Schema/Expect.list.phpt @@ -37,8 +37,8 @@ test('without default value', function () { }); -test('not merging', function () { - $schema = Expect::list([1, 2, 3])->mergeDefaults(false); +test('not merging default value', function () { + $schema = Expect::list([1, 2, 3]); Assert::same([], (new Processor)->process($schema, [])); @@ -48,8 +48,8 @@ test('not merging', function () { }); -test('merging', function () { - $schema = Expect::list([1, 2, 3]); +test('merging default value', function () { + $schema = @Expect::list([1, 2, 3])->mergeDefaults(true); // mergeDefaults() is deprecated Assert::same([1, 2, 3], (new Processor)->process($schema, [])); @@ -59,8 +59,8 @@ test('merging', function () { }); -test('merging & other items validation', function () { - $schema = Expect::list([1, 2, 3])->items('string'); +test('merging default value & other items validation', function () { + $schema = @Expect::list([1, 2, 3])->mergeDefaults(true)->items('string'); // mergeDefaults() is deprecated Assert::same([1, 2, 3], (new Processor)->process($schema, [])); From 368f2fb65ae6afa368f28fc8129d8f4dcb5edf40 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 28 Apr 2024 23:11:29 +0200 Subject: [PATCH 5/9] Expect::from() works with class names --- src/Schema/Expect.php | 41 ++++--- ...ect.from.phpt => Expect.from.dynamic.phpt} | 0 tests/Schema/Expect.from.static.phpt | 109 ++++++++++++++++++ 3 files changed, 137 insertions(+), 13 deletions(-) rename tests/Schema/{Expect.from.phpt => Expect.from.dynamic.phpt} (100%) create mode 100644 tests/Schema/Expect.from.static.phpt diff --git a/src/Schema/Expect.php b/src/Schema/Expect.php index a6d5987..4590610 100644 --- a/src/Schema/Expect.php +++ b/src/Schema/Expect.php @@ -63,27 +63,42 @@ public static function structure(array $shape): Structure } - public static function from(object $object, array $items = []): Structure + public static function from(object|string $object, array $items = []): Structure { - $ro = new \ReflectionObject($object); + $ro = new \ReflectionClass($object); $props = $ro->hasMethod('__construct') ? $ro->getMethod('__construct')->getParameters() : $ro->getProperties(); foreach ($props as $prop) { - $item = &$items[$prop->getName()]; - if (!$item) { - $item = new Type((string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed')); - if ($prop instanceof \ReflectionProperty ? $prop->isInitialized($object) : $prop->isOptional()) { - $def = ($prop instanceof \ReflectionProperty ? $prop->getValue($object) : $prop->getDefaultValue()); - if (is_object($def)) { - $item = static::from($def); - } else { - $item->default($def); - } + \assert($prop instanceof \ReflectionProperty || $prop instanceof \ReflectionParameter); + if ($item = &$items[$prop->getName()]) { + continue; + } + + $item = new Type($propType = (string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed')); + if (class_exists($propType)) { + $item = static::from($propType); + } + + $hasDefault = match (true) { + $prop instanceof \ReflectionParameter => $prop->isOptional(), + is_object($object) => $prop->isInitialized($object), + default => $prop->hasDefaultValue(), + }; + if ($hasDefault) { + $default = match (true) { + $prop instanceof \ReflectionParameter => $prop->getDefaultValue(), + is_object($object) => $prop->getValue($object), + default => $prop->getDefaultValue(), + }; + if (is_object($default)) { + $item = static::from($default); } else { - $item->required(); + $item->default($default); } + } else { + $item->required(); } } diff --git a/tests/Schema/Expect.from.phpt b/tests/Schema/Expect.from.dynamic.phpt similarity index 100% rename from tests/Schema/Expect.from.phpt rename to tests/Schema/Expect.from.dynamic.phpt diff --git a/tests/Schema/Expect.from.static.phpt b/tests/Schema/Expect.from.static.phpt new file mode 100644 index 0000000..581a4f3 --- /dev/null +++ b/tests/Schema/Expect.from.static.phpt @@ -0,0 +1,109 @@ +items); + Assert::type(stdClass::class, (new Processor)->process($schema, [])); +}); + + +Assert::with(Structure::class, function () { + class Data1 + { + public string $dsn = 'mysql'; + public ?string $user; + public ?string $password = null; + public array|int $options = []; + public bool $debugger = true; + public mixed $mixed; + public array $arr = [1]; + } + + $schema = Expect::from(Data1::class); + + Assert::type(Structure::class, $schema); + Assert::equal([ + 'dsn' => Expect::string('mysql'), + 'user' => Expect::type('?string')->required(), + 'password' => Expect::type('?string'), + 'options' => Expect::type('array|int')->default([]), + 'debugger' => Expect::bool(true), + 'mixed' => Expect::mixed()->required(), + 'arr' => Expect::type('array')->default([1]), + ], $schema->items); + Assert::type(Data1::class, (new Processor)->process($schema, ['user' => '', 'mixed' => ''])); +}); + + +Assert::with(Structure::class, function () { // constructor injection + class Data2 + { + public function __construct( + public ?string $user, + public ?string $password = null, + ) { + } + } + + $schema = Expect::from(Data2::class); + + Assert::type(Structure::class, $schema); + Assert::equal([ + 'user' => Expect::type('?string')->required(), + 'password' => Expect::type('?string'), + ], $schema->items); + Assert::equal( + new Data2('foo', 'bar'), + (new Processor)->process($schema, ['user' => 'foo', 'password' => 'bar']), + ); +}); + + +Assert::with(Structure::class, function () { // overwritten item + class Data3 + { + public string $dsn = 'mysql'; + public ?string $user; + } + + $schema = Expect::from(Data3::class, ['dsn' => Expect::int(123)]); + + Assert::equal([ + 'dsn' => Expect::int(123), + 'user' => Expect::type('?string')->required(), + ], $schema->items); +}); + + +Assert::with(Structure::class, function () { // nested object + class Data4 + { + public Data5 $inner; + } + + class Data5 + { + public string $name; + } + + $schema = Expect::from(Data4::class); + + Assert::equal([ + 'inner' => Expect::structure([ + 'name' => Expect::string()->required(), + ])->castTo(Data5::class), + ], $schema->items); +}); From 4de647141dc5ee1d923e18fcbc7fa7592d75d0e1 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 7 Oct 2024 01:04:18 +0200 Subject: [PATCH 6/9] Type: added mergeMode() [WIP] TODO: Type should not support the array at all --- src/Schema/Elements/Type.php | 17 +++++++++++++---- src/Schema/MergeMode.php | 23 +++++++++++++++++++++++ tests/Schema/Expect.array.phpt | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 src/Schema/MergeMode.php diff --git a/src/Schema/Elements/Type.php b/src/Schema/Elements/Type.php index 7f5d68d..c752e0c 100644 --- a/src/Schema/Elements/Type.php +++ b/src/Schema/Elements/Type.php @@ -12,6 +12,7 @@ use Nette\Schema\Context; use Nette\Schema\DynamicParameter; use Nette\Schema\Helpers; +use Nette\Schema\MergeMode; use Nette\Schema\Schema; @@ -27,6 +28,7 @@ final class Type implements Schema private array $range = [null, null]; private ?string $pattern = null; private bool $merge = false; + private ?MergeMode $mergeMode = null; public function __construct(string $type) @@ -55,6 +57,13 @@ public function mergeDefaults(bool $state = true): self } + public function mergeMode(MergeMode $mode): self + { + $this->mergeMode = $mode; + return $this; + } + + public function dynamic(): self { $this->type = DynamicParameter::class . '|' . $this->type; @@ -134,19 +143,19 @@ public function normalize(mixed $value, Context $context): mixed public function merge(mixed $value, mixed $base): mixed { - if (is_array($value) && isset($value[Helpers::PreventMerging])) { + if ($this->mergeMode === MergeMode::Replace || (is_array($value) && isset($value[Helpers::PreventMerging]))) { unset($value[Helpers::PreventMerging]); return $value; } - if (is_array($value) && is_array($base) && $this->itemsValue) { - $index = 0; + if (is_array($value) && is_array($base) && ($this->itemsValue || $this->mergeMode)) { + $index = $this->mergeMode === MergeMode::OverwriteKeys ? null : 0; foreach ($value as $key => $val) { if ($key === $index) { $base[] = $val; $index++; } else { - $base[$key] = array_key_exists($key, $base) + $base[$key] = array_key_exists($key, $base) && $this->itemsValue ? $this->itemsValue->merge($val, $base[$key]) : $val; } diff --git a/src/Schema/MergeMode.php b/src/Schema/MergeMode.php new file mode 100644 index 0000000..633fb6f --- /dev/null +++ b/src/Schema/MergeMode.php @@ -0,0 +1,23 @@ +process($schema, []), ); }); + + +test('merge modes', function () { + $schema = Expect::structure([ + 'foo1' => Expect::array()->mergeMode(MergeMode::Replace), + 'foo2' => Expect::array()->mergeMode(MergeMode::OverwriteKeys), + 'foo3' => Expect::array()->mergeMode(MergeMode::AppendKeys), + ]); + + $processor = new Processor; + + Assert::equal( + (object) [ + 'foo1' => ['key' => 'new'], + 'foo2' => ['new', 'key' => 'new'], + 'foo3' => ['old', 'new', 'key' => 'new'], + ], + $processor->processMultiple($schema, [ + [ + 'foo1' => ['old', 'key' => '1'], + 'foo2' => ['old', 'key' => '1'], + 'foo3' => ['old', 'key' => '1'], + ], + [ + 'foo1' => ['key' => 'new'], + 'foo2' => ['new', 'key' => 'new'], + 'foo3' => ['new', 'key' => 'new'], + ], + ]), + ); +}); From 4682a8021756395020b2cc76aa15fa5cc93c6e9f Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 5 Oct 2024 04:19:27 +0200 Subject: [PATCH 7/9] Type::merge() merges arrays only according to the schema (BC break) --- src/Schema/Elements/Type.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Schema/Elements/Type.php b/src/Schema/Elements/Type.php index c752e0c..7b53385 100644 --- a/src/Schema/Elements/Type.php +++ b/src/Schema/Elements/Type.php @@ -28,7 +28,7 @@ final class Type implements Schema private array $range = [null, null]; private ?string $pattern = null; private bool $merge = false; - private ?MergeMode $mergeMode = null; + private MergeMode $mergeMode = MergeMode::AppendKeys; public function __construct(string $type) @@ -148,7 +148,7 @@ public function merge(mixed $value, mixed $base): mixed return $value; } - if (is_array($value) && is_array($base) && ($this->itemsValue || $this->mergeMode)) { + if (is_array($value) && is_array($base)) { $index = $this->mergeMode === MergeMode::OverwriteKeys ? null : 0; foreach ($value as $key => $val) { if ($key === $index) { @@ -164,7 +164,7 @@ public function merge(mixed $value, mixed $base): mixed return $base; } - return Helpers::merge($value, $base); + return $value === null && is_array($base) ? $base : $value; } From 2073a5a4156aa8f2849f2e81e2b743f338ed3f45 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 5 Oct 2024 05:27:18 +0200 Subject: [PATCH 8/9] removed support for key PreventMerging (BC break) --- src/Schema/Elements/AnyOf.php | 5 ----- src/Schema/Elements/Structure.php | 8 -------- src/Schema/Elements/Type.php | 19 ++--------------- tests/Schema/Expect.array.phpt | 34 ------------------------------- 4 files changed, 2 insertions(+), 64 deletions(-) diff --git a/src/Schema/Elements/AnyOf.php b/src/Schema/Elements/AnyOf.php index 6c9d0ce..71a68f3 100644 --- a/src/Schema/Elements/AnyOf.php +++ b/src/Schema/Elements/AnyOf.php @@ -64,11 +64,6 @@ public function normalize(mixed $value, Context $context): mixed public function merge(mixed $value, mixed $base): mixed { - if (is_array($value) && isset($value[Helpers::PreventMerging])) { - unset($value[Helpers::PreventMerging]); - return $value; - } - return Helpers::merge($value, $base); } diff --git a/src/Schema/Elements/Structure.php b/src/Schema/Elements/Structure.php index 66e501a..8b83b71 100644 --- a/src/Schema/Elements/Structure.php +++ b/src/Schema/Elements/Structure.php @@ -94,10 +94,6 @@ public function getShape(): array public function normalize(mixed $value, Context $context): mixed { - if ($prevent = (is_array($value) && isset($value[Helpers::PreventMerging]))) { - unset($value[Helpers::PreventMerging]); - } - $value = $this->doNormalize($value, $context); if (is_object($value)) { $value = (array) $value; @@ -112,10 +108,6 @@ public function normalize(mixed $value, Context $context): mixed array_pop($context->path); } } - - if ($prevent) { - $value[Helpers::PreventMerging] = true; - } } return $value; diff --git a/src/Schema/Elements/Type.php b/src/Schema/Elements/Type.php index 7b53385..aa657d0 100644 --- a/src/Schema/Elements/Type.php +++ b/src/Schema/Elements/Type.php @@ -112,10 +112,6 @@ public function pattern(?string $pattern): self public function normalize(mixed $value, Context $context): mixed { - if ($prevent = (is_array($value) && isset($value[Helpers::PreventMerging]))) { - unset($value[Helpers::PreventMerging]); - } - $value = $this->doNormalize($value, $context); if (is_array($value) && $this->itemsValue) { $res = []; @@ -133,18 +129,13 @@ public function normalize(mixed $value, Context $context): mixed $value = $res; } - if ($prevent && is_array($value)) { - $value[Helpers::PreventMerging] = true; - } - return $value; } public function merge(mixed $value, mixed $base): mixed { - if ($this->mergeMode === MergeMode::Replace || (is_array($value) && isset($value[Helpers::PreventMerging]))) { - unset($value[Helpers::PreventMerging]); + if ($this->mergeMode === MergeMode::Replace) { return $value; } @@ -170,12 +161,6 @@ public function merge(mixed $value, mixed $base): mixed public function complete(mixed $value, Context $context): mixed { - $merge = $this->merge; - if (is_array($value) && isset($value[Helpers::PreventMerging])) { - unset($value[Helpers::PreventMerging]); - $merge = false; - } - if ($value === null && is_array($this->default)) { $value = []; // is unable to distinguish null from array in NEON } @@ -187,7 +172,7 @@ public function complete(mixed $value, Context $context): mixed $isOk() && Helpers::validateRange($value, $this->range, $context, $this->type); $isOk() && $value !== null && $this->pattern !== null && Helpers::validatePattern($value, $this->pattern, $context); $isOk() && is_array($value) && $this->validateItems($value, $context); - $isOk() && $merge && $value = Helpers::merge($value, $this->default); + $isOk() && $this->merge && $value = Helpers::merge($value, $this->default); $isOk() && $value = $this->doTransform($value, $context); if (!$isOk()) { return null; diff --git a/tests/Schema/Expect.array.phpt b/tests/Schema/Expect.array.phpt index 7bd2679..26cee0a 100644 --- a/tests/Schema/Expect.array.phpt +++ b/tests/Schema/Expect.array.phpt @@ -3,7 +3,6 @@ declare(strict_types=1); use Nette\Schema\Expect; -use Nette\Schema\Helpers; use Nette\Schema\MergeMode; use Nette\Schema\Processor; use Tester\Assert; @@ -96,39 +95,6 @@ test('merging default value', function () { 'arr' => ['newitem'], ]), ); - - Assert::same( - [ - 'key1' => 'newval', - 'key3' => 'newval', - 'newval3', - 'arr' => ['newitem'], - ], - (new Processor)->process($schema, [ - Helpers::PreventMerging => true, - 'key1' => 'newval', - 'key3' => 'newval', - 'newval3', - 'arr' => ['newitem'], - ]), - ); - - Assert::same( - [ - 'key1' => 'newval', - 'key2' => 'val2', - 'val3', - 'arr' => ['newitem'], - 'key3' => 'newval', - 'newval3', - ], - (new Processor)->process($schema, [ - 'key1' => 'newval', - 'key3' => 'newval', - 'newval3', - 'arr' => [Helpers::PreventMerging => true, 'newitem'], - ]), - ); }); From 5e7a6f36de4fb5dea08a1b90709631213f4d52c6 Mon Sep 17 00:00:00 2001 From: Sven Luijten <11269635+svenluijten@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:22:42 +0200 Subject: [PATCH 9/9] FIx links to doc.nette.org in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Or: why I dislike external sites to host essential documentation 💅 --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index b9febfa..a6dbb85 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,7 @@ Introduction 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/en/schema). Installation: @@ -127,7 +127,7 @@ 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')`. +And then all types [supported by the Validators](https://doc.nette.org/validators#toc-expected-types) via `Expect::type('scalar')` or abbreviated `Expect::scalar()`. Also class or interface names are accepted, e.g. `Expect::type('AddressEntity')`. You can also use union notation: