OverwriteRules
This commit is contained in:
parent
3a26a2e0c2
commit
709201547c
8 changed files with 252 additions and 19 deletions
12
src/Attributes/OverwriteRules.php
Normal file
12
src/Attributes/OverwriteRules.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Icefox\DTO\Attributes;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
|
||||||
|
#[Attribute(Attribute::TARGET_METHOD)]
|
||||||
|
class OverwriteRules
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ namespace Icefox\DTO;
|
||||||
use Icefox\DTO\Attributes\FromInput;
|
use Icefox\DTO\Attributes\FromInput;
|
||||||
use Icefox\DTO\Attributes\CastWith;
|
use Icefox\DTO\Attributes\CastWith;
|
||||||
use Icefox\DTO\Attributes\FromRouteParameter;
|
use Icefox\DTO\Attributes\FromRouteParameter;
|
||||||
|
use Icefox\DTO\Attributes\OverwriteRules;
|
||||||
use Icefox\DTO\Support\RuleFactory;
|
use Icefox\DTO\Support\RuleFactory;
|
||||||
use Icefox\DTO\Support\ValueFactory;
|
use Icefox\DTO\Support\ValueFactory;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
@ -66,7 +67,8 @@ trait DataObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$rules = RuleFactory::buildRules($parameters, '');
|
$rules = static::getRules();
|
||||||
|
|
||||||
$validator = static::withValidator($input, $rules);
|
$validator = static::withValidator($input, $rules);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
|
|
@ -101,6 +103,42 @@ trait DataObject
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,array<int, string|Rule>>
|
||||||
|
*/
|
||||||
|
public static function getRules(): array
|
||||||
|
{
|
||||||
|
$parameters = RuleFactory::getParametersMeta(static::class);
|
||||||
|
$customRules = static::rules();
|
||||||
|
$classReflection = new ReflectionClass(static::class);
|
||||||
|
$rulesMethod = $classReflection->getMethod('rules');
|
||||||
|
|
||||||
|
if (!empty($rulesMethod->getAttributes(OverwriteRules::class))) {
|
||||||
|
return $customRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inferredRules = RuleFactory::infer($parameters, '');
|
||||||
|
return self::mergeRules($inferredRules, $customRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,array<int, string|Rule>> $inferredRules
|
||||||
|
* @param array<string,array<int, string|Rule>> $customRules
|
||||||
|
* @return array<string,array<int, string|Rule>>
|
||||||
|
*/
|
||||||
|
protected static function mergeRules(array $inferredRules, array $customRules): array
|
||||||
|
{
|
||||||
|
$merged = $inferredRules;
|
||||||
|
foreach ($customRules as $key => $rules) {
|
||||||
|
if (isset($merged[$key])) {
|
||||||
|
$merged[$key] = array_values(array_unique(array_merge($merged[$key], $rules)));
|
||||||
|
} else {
|
||||||
|
$merged[$key] = $rules;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $merged;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $data
|
* @param array<string,mixed> $data
|
||||||
* @param array<string,array<int, string|Rule>> $rules
|
* @param array<string,array<int, string|Rule>> $rules
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class RuleFactory
|
||||||
$paramsSub = self::getParametersMeta($type->getFqsen()->__toString());
|
$paramsSub = self::getParametersMeta($type->getFqsen()->__toString());
|
||||||
$rules = array_merge(
|
$rules = array_merge(
|
||||||
$rules,
|
$rules,
|
||||||
self::buildRules($paramsSub, $prefix . '.'),
|
self::infer($paramsSub, $prefix . '.'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return $rules;
|
return $rules;
|
||||||
|
|
@ -103,7 +103,7 @@ class RuleFactory
|
||||||
* @param array<ParameterMeta> $parameters
|
* @param array<ParameterMeta> $parameters
|
||||||
* @return array<string,array<int,string|Rule>>
|
* @return array<string,array<int,string|Rule>>
|
||||||
*/
|
*/
|
||||||
public static function buildRules(array $parameters, string $prefix): array
|
public static function infer(array $parameters, string $prefix): array
|
||||||
{
|
{
|
||||||
$rules = [];
|
$rules = [];
|
||||||
foreach ($parameters as $parameter) {
|
foreach ($parameters as $parameter) {
|
||||||
|
|
@ -163,7 +163,7 @@ class RuleFactory
|
||||||
$paramsSub = self::getParametersMeta($type->getName());
|
$paramsSub = self::getParametersMeta($type->getName());
|
||||||
$rules = array_merge(
|
$rules = array_merge(
|
||||||
$rules,
|
$rules,
|
||||||
self::buildRules($paramsSub, $root . '.'),
|
self::infer($paramsSub, $root . '.'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@
|
||||||
|
|
||||||
namespace Tests;
|
namespace Tests;
|
||||||
|
|
||||||
use Icefox\DTO\Support\RuleFactory;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Tests\Classes\ArrayDataObject;
|
use Tests\Classes\ArrayDataObject;
|
||||||
use Tests\Classes\CollectionDataObject;
|
use Tests\Classes\CollectionDataObject;
|
||||||
use Tests\Classes\FromInputObject;
|
use Tests\Classes\FromInputObject;
|
||||||
use Tests\Classes\ObjectWithoutMapper;
|
|
||||||
use Tests\Classes\OptionalData;
|
use Tests\Classes\OptionalData;
|
||||||
use Tests\Classes\OptionalNullableData;
|
use Tests\Classes\OptionalNullableData;
|
||||||
use Tests\Classes\PrimitiveData;
|
use Tests\Classes\PrimitiveData;
|
||||||
|
|
@ -16,7 +14,7 @@ use Tests\Classes\WithMapperObject;
|
||||||
|
|
||||||
describe('primitive data test', function () {
|
describe('primitive data test', function () {
|
||||||
it('creates required rules', function () {
|
it('creates required rules', function () {
|
||||||
$rules = RuleFactory::buildRules(RuleFactory::getParametersMeta(PrimitiveData::class), '');
|
$rules = PrimitiveData::getRules();
|
||||||
expect($rules)->toMatchArray([
|
expect($rules)->toMatchArray([
|
||||||
'string' => ['required'],
|
'string' => ['required'],
|
||||||
'int' => ['required', 'numeric'],
|
'int' => ['required', 'numeric'],
|
||||||
|
|
@ -42,7 +40,7 @@ describe('primitive data test', function () {
|
||||||
|
|
||||||
describe('optional data', function () {
|
describe('optional data', function () {
|
||||||
it('creates optional rules', function () {
|
it('creates optional rules', function () {
|
||||||
$rules = RuleFactory::buildRules(RuleFactory::getParametersMeta(OptionalData::class), '');
|
$rules = OptionalData::getRules();
|
||||||
expect($rules)->toMatchArray([
|
expect($rules)->toMatchArray([
|
||||||
'string' => ['sometimes'],
|
'string' => ['sometimes'],
|
||||||
'int' => ['sometimes', 'numeric'],
|
'int' => ['sometimes', 'numeric'],
|
||||||
|
|
@ -63,7 +61,7 @@ describe('optional data', function () {
|
||||||
|
|
||||||
describe('nullable data', function () {
|
describe('nullable data', function () {
|
||||||
it('creates nullable rules', function () {
|
it('creates nullable rules', function () {
|
||||||
$rules = RuleFactory::buildRules(RuleFactory::getParametersMeta(OptionalNullableData::class), '');
|
$rules = OptionalNullableData::getRules();
|
||||||
expect($rules)->toMatchArray([
|
expect($rules)->toMatchArray([
|
||||||
'string' => ['required'],
|
'string' => ['required'],
|
||||||
'int' => ['nullable', 'numeric'],
|
'int' => ['nullable', 'numeric'],
|
||||||
|
|
@ -93,10 +91,7 @@ describe('nullable data', function () {
|
||||||
describe('reference other DataObject', function () {
|
describe('reference other DataObject', function () {
|
||||||
|
|
||||||
it('creates recursive rules', function () {
|
it('creates recursive rules', function () {
|
||||||
$rules = RuleFactory::buildRules(
|
$rules = RecursiveDataObject::getRules();
|
||||||
RuleFactory::getParametersMeta(RecursiveDataObject::class),
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
expect($rules)->toMatchArray([
|
expect($rules)->toMatchArray([
|
||||||
'string' => ['required'],
|
'string' => ['required'],
|
||||||
'extra.string' => ['required'],
|
'extra.string' => ['required'],
|
||||||
|
|
@ -109,7 +104,7 @@ describe('reference other DataObject', function () {
|
||||||
|
|
||||||
describe('primitive array', function () {
|
describe('primitive array', function () {
|
||||||
it('creates array rules', function () {
|
it('creates array rules', function () {
|
||||||
$rules = RuleFactory::buildRules(RuleFactory::getParametersMeta(ArrayDataObject::class), '');
|
$rules = ArrayDataObject::getRules();
|
||||||
expect($rules)->toMatchArray([
|
expect($rules)->toMatchArray([
|
||||||
'values' => ['required', 'array'],
|
'values' => ['required', 'array'],
|
||||||
'values.*' => ['required', 'numeric'],
|
'values.*' => ['required', 'numeric'],
|
||||||
|
|
@ -120,10 +115,7 @@ describe('primitive array', function () {
|
||||||
|
|
||||||
describe('object array', function () {
|
describe('object array', function () {
|
||||||
it('creates array rules', function () {
|
it('creates array rules', function () {
|
||||||
$rules = RuleFactory::buildRules(
|
$rules = CollectionDataObject::getRules();
|
||||||
RuleFactory::getParametersMeta(CollectionDataObject::class),
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
expect($rules)->toMatchArray([
|
expect($rules)->toMatchArray([
|
||||||
'values' => ['required', 'array'],
|
'values' => ['required', 'array'],
|
||||||
'values.*' => ['required'],
|
'values.*' => ['required'],
|
||||||
|
|
@ -139,7 +131,7 @@ describe('can map input names', function () {
|
||||||
|
|
||||||
it('creates rules with property names', function () {
|
it('creates rules with property names', function () {
|
||||||
|
|
||||||
$rules = RuleFactory::buildRules(RuleFactory::getParametersMeta(FromInputObject::class), '');
|
$rules = FromInputObject::getRules();
|
||||||
expect($rules)->toMatchArray([
|
expect($rules)->toMatchArray([
|
||||||
'text' => ['required' ],
|
'text' => ['required' ],
|
||||||
'standard' => ['required', 'numeric'],
|
'standard' => ['required', 'numeric'],
|
||||||
|
|
|
||||||
120
tests/Rules/RulesTest.php
Normal file
120
tests/Rules/RulesTest.php
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Rules;
|
||||||
|
|
||||||
|
use Icefox\DTO\Support\RuleFactory;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Tests\Rules\WithEmptyOverwriteRules;
|
||||||
|
use Tests\Rules\WithMergedRules;
|
||||||
|
use Tests\Rules\WithOverwriteRules;
|
||||||
|
|
||||||
|
describe('rules array shape', function () {
|
||||||
|
it('returns inferred rules shape from RuleFactory::infer (inferred only)', function () {
|
||||||
|
$parameters = RuleFactory::getParametersMeta(WithMergedRules::class);
|
||||||
|
$rules = RuleFactory::infer($parameters, '');
|
||||||
|
|
||||||
|
expect($rules)->toBe([
|
||||||
|
'value' => ['required', 'numeric'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns inferred rules shape regardless of OverwriteRules attribute', function () {
|
||||||
|
$parameters = RuleFactory::getParametersMeta(WithOverwriteRules::class);
|
||||||
|
$rules = RuleFactory::infer($parameters, '');
|
||||||
|
|
||||||
|
expect($rules)->toBe([
|
||||||
|
'value' => ['required', 'numeric'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRules method', function () {
|
||||||
|
it('returns merged rules from DataObject::getRules()', function () {
|
||||||
|
$rules = WithMergedRules::getRules();
|
||||||
|
|
||||||
|
expect($rules)->toBe([
|
||||||
|
'value' => ['required', 'numeric', 'max:20'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns only custom rules from DataObject::getRules() with OverwriteRules', function () {
|
||||||
|
$rules = WithOverwriteRules::getRules();
|
||||||
|
|
||||||
|
expect($rules)->toBe([
|
||||||
|
'value' => ['numeric', 'max:20'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty rules from DataObject::getRules() with OverwriteRules and no custom rules', function () {
|
||||||
|
$rules = WithEmptyOverwriteRules::getRules();
|
||||||
|
|
||||||
|
expect($rules)->toBe([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rules merging', function () {
|
||||||
|
it('merges custom rules with inferred rules by default', function () {
|
||||||
|
$object = WithMergedRules::fromArray([
|
||||||
|
'value' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($object->value)->toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails validation when merged rule is violated', function () {
|
||||||
|
expect(fn() => WithMergedRules::fromArray([
|
||||||
|
'value' => 25,
|
||||||
|
]))->toThrow(ValidationException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails validation when required rule is violated (inferred)', function () {
|
||||||
|
expect(fn() => WithMergedRules::fromArray([
|
||||||
|
]))->toThrow(ValidationException::class);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rules overwrite', function () {
|
||||||
|
it('uses only custom rules when OverwriteRules attribute is present', function () {
|
||||||
|
$object = WithOverwriteRules::fromArray([
|
||||||
|
'value' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($object->value)->toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails validation when custom rule is violated', function () {
|
||||||
|
expect(fn() => WithOverwriteRules::fromArray([
|
||||||
|
'value' => 25,
|
||||||
|
]))->toThrow(ValidationException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not enforce inferred required rule when overwritten', function () {
|
||||||
|
$object = WithOverwriteRules::fromArray([]);
|
||||||
|
|
||||||
|
expect($object)->toBeInstanceOf(WithOverwriteRules::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not enforce inferred numeric rule when overwritten', function () {
|
||||||
|
$rules = WithOverwriteRules::rules();
|
||||||
|
expect($rules)->toHaveKey('value');
|
||||||
|
expect($rules['value'])->toBe(['numeric', 'max:20']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('empty rules overwrite', function () {
|
||||||
|
it('allows any value when rules are empty with OverwriteRules', function () {
|
||||||
|
$object = WithEmptyOverwriteRules::fromArray([
|
||||||
|
'value' => 999,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($object->value)->toBe(999);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows missing value when rules are empty with OverwriteRules', function () {
|
||||||
|
$object = WithEmptyOverwriteRules::fromArray([]);
|
||||||
|
|
||||||
|
expect($object)->toBeInstanceOf(WithEmptyOverwriteRules::class);
|
||||||
|
});
|
||||||
|
});
|
||||||
23
tests/Rules/WithEmptyOverwriteRules.php
Normal file
23
tests/Rules/WithEmptyOverwriteRules.php
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Rules;
|
||||||
|
|
||||||
|
use Icefox\DTO\Attributes\OverwriteRules;
|
||||||
|
use Icefox\DTO\DataObject;
|
||||||
|
|
||||||
|
readonly class WithEmptyOverwriteRules
|
||||||
|
{
|
||||||
|
use DataObject;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $value,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[OverwriteRules]
|
||||||
|
public static function rules(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
23
tests/Rules/WithMergedRules.php
Normal file
23
tests/Rules/WithMergedRules.php
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Rules;
|
||||||
|
|
||||||
|
use Icefox\DTO\DataObject;
|
||||||
|
|
||||||
|
readonly class WithMergedRules
|
||||||
|
{
|
||||||
|
use DataObject;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $value,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'value' => ['max:20'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
25
tests/Rules/WithOverwriteRules.php
Normal file
25
tests/Rules/WithOverwriteRules.php
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Rules;
|
||||||
|
|
||||||
|
use Icefox\DTO\Attributes\OverwriteRules;
|
||||||
|
use Icefox\DTO\DataObject;
|
||||||
|
|
||||||
|
readonly class WithOverwriteRules
|
||||||
|
{
|
||||||
|
use DataObject;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public int $value,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[OverwriteRules]
|
||||||
|
public static function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'value' => ['numeric', 'max:20'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue