Skip to content

Commit 5aca4ba

Browse files
committed
feat: more rules, rules readme and better testing
1 parent 3c8fac5 commit 5aca4ba

11 files changed

+190
-41
lines changed

README.md

+66-22
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ This Laravel package aims to store and manage user settings/preferences in a sim
2121
* [Casting](#casting)
2222
* [Available Casts](#available-casts)
2323
* [Custom Caster](#custom-caster)
24-
* [Custom Rules](#custom-rules)
24+
* [Rules](#rules)
25+
* [Available Rules](#available-rules)
26+
* [Custom Rules](#custom-rules)
2527
* [Policies](#policies)
2628
* [Preference Building](#preference-building)
2729
* [Routing](#routing)
2830
* [Anantomy](#anantomy)
29-
* [Example](#example-)
31+
* [Example](#config-example)
3032
* [Actions](#actions)
3133
* [Middlewares](#middlewares)
3234
* [Security](#security)
@@ -49,8 +51,12 @@ This Laravel package aims to store and manage user settings/preferences in a sim
4951
### Roadmap
5052

5153
- Additional inbuilt Custom Rules -> v2.x
52-
- Allow array of preferenceBuilders in initBuk -> v2.1.1
54+
- Allow array of preferenceBuilders in initBuk -> v2.1.x
55+
- Readme restructuring -> v2.1.x
56+
- QoL Helpers functions (removeAll, quickInit, etc) -> v2.1.x
5357
- Event System -> v2.2
58+
- Api Response customization -> v2.3
59+
- Caching
5460
- Suggestions are welcome
5561

5662
## Installation
@@ -299,8 +305,16 @@ class User extends \Illuminate\Foundation\Auth\User implements PreferenceableMod
299305

300306
set the cast when creating a Preference
301307

308+
> [!NOTE]
309+
> a cast has 3 main jobs
310+
> - Basic validation
311+
> - Casting from and to the database
312+
> - Preparing Api Responses
313+
314+
#### Example:
315+
302316
```php
303-
PreferenceBuilder::init(Preferences::LANGUAGE, Cast::STRING)
317+
PreferenceBuilder::init(Preferences::LANGUAGE, Cast::ENUM)
304318
```
305319

306320
### Available Casts
@@ -361,15 +375,49 @@ enum MyCast: string implements CastableEnum
361375
return match ($this) {
362376
self::TIMEZONE => (string)$value,
363377
};
378+
}
379+
380+
public function castToDto(mixed $value): array
381+
{
382+
return ['value' => $value];
364383
}
365384
}
366385

367-
PreferenceBuilder::init(Preferences::TIMEZONE,MyCast::TIMEZONE)
368-
//->...etc
369-
386+
PreferenceBuilder::init(Preferences::TIMEZONE, MyCast::TIMEZONE)->create();
370387
```
371388

372-
## Custom Rules
389+
## Rules
390+
391+
Additional validation, which can be way more complex than provided by the Cast
392+
393+
### Adding Rules
394+
395+
````php
396+
PreferenceBuilder::init(General::VOLUME, Cast::INT)
397+
->withRule(new LowerThanRule(5))
398+
->updateOrCreate()
399+
400+
401+
PreferenceBuilder::initBulk([
402+
'name' => General::VOLUME,
403+
'cast' => Cast::INT
404+
'rule' => new LowerThanRule(5)
405+
]);
406+
````
407+
408+
### Available Rules
409+
410+
| Rule | Example | Description |
411+
|----------------|----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
412+
| AndRule | `new AndRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5))` | Expects `n` ValidationRule, ensures all pass |
413+
| OrRule | `new OrRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5))` | Expects `n` ValidationRule, ensures at least one passes |
414+
| BetweenRule | `new BetweenRule(2.4, 5.5)` | For INT and FLOAT, check that the value is between min and max |
415+
| InRule | `new InRule("it","en","de")` | Expects the value to be validated to be in that equal to one of the `n` params |
416+
| InstanceOfRule | `new InstanceOfRule(Theme::class)` | For non primitive casts, checks the instance of the value's class to validate. Tip: goes along well with the `OrRule` |
417+
| IsRule | `new IsRule(Type::ITERABLE)` | Expects a `Matteoc99\LaravelPreference\Enums\Type` Enum. Checks e.g. if the value is iterable |
418+
| LowerThanRule | `new LowerThanRule(5)` | For INT and FLOAT, check that the value to be validated is less than the one passed in the constructor |
419+
420+
### Custom Rules
373421

374422
implement `ValidationRule`
375423

@@ -475,7 +523,7 @@ which can all be accessed via the route name: {prefix}.{scope}.{group}.{index/ge
475523
`group`: A mapping of group names to their corresponding Enum classes. See config below
476524
`scope`: A mapping of scope names to their corresponding Eloquent model. See config below
477525

478-
### Example:
526+
### Config Example:
479527

480528
```php
481529
'routes' => [
@@ -511,17 +559,14 @@ will result in the following **route names**:
511559
> [!NOTE]
512560
> Examples are with scope `user` and group `general`
513561
514-
515562
#### INDEX
516563

517-
518564
- Route Name: custom_prefix.user.general.index
519565
- Url params: `scope_id`
520566
- Equivalent to: `$user->getPreferences(General::class)`
521567
- Http method: GET
522568
- Endpoint: 'https://your.domain/custom_prefix/user/{scope_id}/general'
523569

524-
525570
#### GET
526571

527572
- Route Name: custom_prefix.user.general.get
@@ -532,25 +577,23 @@ will result in the following **route names**:
532577

533578
#### UPDATE
534579

535-
- Route Name: custom_prefix.user.general.update
536-
- Url params: `scope_id`,`preference`
537-
- Equivalent to: `$user->setPreference(General::{preference}, >value<)`
580+
- Route Name: custom_prefix.user.general.update
581+
- Url params: `scope_id`,`preference`
582+
- Equivalent to: `$user->setPreference(General::{preference}, >value<)`
538583
- Http method: PATCH/PUT
539584
- Endpoint: https://your.domain/custom_prefix/user/{scope_id}/general/{preference}
540585
- Payload:
541-
`
542-
{
586+
`
587+
{
543588
"value": >value<
544-
}
545-
`
546-
547-
589+
}
590+
`
548591

549592
##### Enum Patching
550593

551594
When creating your enum preference, add `setAllowedClasses` containing the possible enums to reconstruct the value
552595
> [!CAUTION]
553-
> if multiple cases are shared between enums, the first match is taken
596+
> if multiple cases are shared between enums, the first match is taken
554597
555598
then, when sending the value it varies:
556599

@@ -590,6 +633,7 @@ in the config file
590633
'user.general'=> 'verified' // scoped & grouped middleware only for a specific model + enum
591634
],
592635
```
636+
593637
> [!CAUTION]
594638
> **known Issues**: without the web middleware, you won't have access to the user via the Auth facade
595639
> since it's set by the middleware. Looking into an alternative

src/Rules/AndRule.php

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Matteoc99\LaravelPreference\Rules;
4+
5+
6+
use Closure;
7+
use Illuminate\Contracts\Validation\ValidationRule;
8+
use Illuminate\Support\Facades\Validator;
9+
10+
class AndRule implements ValidationRule
11+
{
12+
13+
private array $rules;
14+
15+
public function __construct(...$rules)
16+
{
17+
$this->rules = $rules;
18+
}
19+
20+
21+
public function validate(string $attribute, mixed $value, Closure $fail): void
22+
{
23+
$anyFails = false;
24+
$errorMessage = "";
25+
26+
foreach ($this->rules as $index => $rule) {
27+
$validator = Validator::make([$attribute => $value], [$attribute => $rule]);
28+
29+
if ($validator->fails()) {
30+
$anyFails = true;
31+
32+
$messages = $validator->messages();
33+
$errorMessage = "Rule $index: " . $messages->first($attribute);
34+
35+
break;
36+
}
37+
}
38+
39+
if ($anyFails) {
40+
$fail($errorMessage);
41+
}
42+
}
43+
}

tests/TestSubjects/Models/LowerThanRule.php renamed to src/Rules/LowerThanRule.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
<?php
22

3-
namespace Matteoc99\LaravelPreference\Tests\TestSubjects\Models;
3+
namespace Matteoc99\LaravelPreference\Rules;
44

55
use Closure;
66
use Illuminate\Contracts\Validation\ValidationRule;
77

88
class LowerThanRule implements ValidationRule
99
{
10-
public function __construct(protected int $value) { }
10+
public function __construct(protected float $value) { }
1111

1212
public function message()
1313
{
14-
return sprintf("A value lower than '%d' expected", $this->value);
14+
return sprintf("A value lower than '%d' expected", $this->value);
1515
}
1616

1717
public function validate(string $attribute, mixed $value, Closure $fail): void
1818
{
19-
if (!is_int($value) || $value > $this->value) {
19+
if (!(is_int($value) || is_float($value)) || $value > $this->value) {
2020
$fail($this->message());
2121
}
2222
}

tests/ApiTest/ApiTestCase.php

-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@
33
namespace Matteoc99\LaravelPreference\Tests\ApiTest;
44

55
use Illuminate\Foundation\Application;
6-
use Matteoc99\LaravelPreference\Enums\Cast;
76
use Matteoc99\LaravelPreference\Factory\PreferenceBuilder;
8-
use Matteoc99\LaravelPreference\Models\Preference;
97
use Matteoc99\LaravelPreference\Rules\InRule;
108
use Matteoc99\LaravelPreference\Tests\TestCase;
119
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\General;
1210
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\VideoPreferences;
13-
use Matteoc99\LaravelPreference\Tests\TestSubjects\Models\LowerThanRule;
1411
use Matteoc99\LaravelPreference\Tests\TestSubjects\Models\User;
1512

1613
class ApiTestCase extends TestCase

tests/ApiTest/WorkflowTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
use Matteoc99\LaravelPreference\Enums\Cast;
66
use Matteoc99\LaravelPreference\Factory\PreferenceBuilder;
77
use Matteoc99\LaravelPreference\Rules\BetweenRule;
8+
use Matteoc99\LaravelPreference\Rules\LowerThanRule;
89
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\General;
910
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\OtherPlainEnum;
1011
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\OtherPreferences;
1112
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\PlainEnum;
1213
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\SomePreferences;
1314
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\Theme;
1415
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\VideoPreferences;
15-
use Matteoc99\LaravelPreference\Tests\TestSubjects\Models\LowerThanRule;
1616
use Matteoc99\LaravelPreference\Utils\ConfigHelper;
1717

1818
class WorkflowTest extends ApiTestCase

tests/PreferenceBuilderBulkTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
use Matteoc99\LaravelPreference\Enums\Cast;
77
use Matteoc99\LaravelPreference\Factory\PreferenceBuilder;
88
use Matteoc99\LaravelPreference\Models\Preference;
9+
use Matteoc99\LaravelPreference\Rules\LowerThanRule;
910
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\General;
1011
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\OtherPreferences;
1112
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\VideoPreferences;
12-
use Matteoc99\LaravelPreference\Tests\TestSubjects\Models\LowerThanRule;
1313

1414
class PreferenceBuilderBulkTest extends TestCase
1515
{

tests/PreferenceCastTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
use Illuminate\Validation\ValidationException;
77
use Matteoc99\LaravelPreference\Enums\Cast;
88
use Matteoc99\LaravelPreference\Factory\PreferenceBuilder;
9+
use Matteoc99\LaravelPreference\Rules\LowerThanRule;
910
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\General;
1011
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\OtherPreferences;
1112
use Matteoc99\LaravelPreference\Tests\TestSubjects\Models\CustomCast;
12-
use Matteoc99\LaravelPreference\Tests\TestSubjects\Models\LowerThanRule;
1313

1414
class PreferenceCastTest extends TestCase
1515
{

tests/PreferenceNullableTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
use InvalidArgumentException;
77
use Matteoc99\LaravelPreference\Enums\Cast;
88
use Matteoc99\LaravelPreference\Factory\PreferenceBuilder;
9+
use Matteoc99\LaravelPreference\Rules\LowerThanRule;
910
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\General;
10-
use Matteoc99\LaravelPreference\Tests\TestSubjects\Models\LowerThanRule;
1111

1212
class PreferenceNullableTest extends TestCase
1313
{

tests/RulesTest/CombinedRulesTest.php

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace Matteoc99\LaravelPreference\Tests\RulesTest;
4+
5+
use Matteoc99\LaravelPreference\Enums\Type;
6+
use Matteoc99\LaravelPreference\Rules\AndRule;
7+
use Matteoc99\LaravelPreference\Rules\BetweenRule;
8+
use Matteoc99\LaravelPreference\Rules\InRule;
9+
use Matteoc99\LaravelPreference\Rules\InstanceOfRule;
10+
use Matteoc99\LaravelPreference\Rules\IsRule;
11+
use Matteoc99\LaravelPreference\Rules\LowerThanRule;
12+
use Matteoc99\LaravelPreference\Rules\OrRule;
13+
use Matteoc99\LaravelPreference\Tests\TestCase;
14+
use Matteoc99\LaravelPreference\Tests\TestSubjects\Enums\Theme;
15+
16+
class CombinedRulesTest extends TestCase
17+
{
18+
public static function orRuleProvider()
19+
{
20+
return [
21+
[new OrRule(new BetweenRule(2.4, 5.5), new LowerThanRule(2)), 3, true, 'Expected OrRule to pass when the first rule passes.'],
22+
[new OrRule(new BetweenRule(2.4, 5.5), new LowerThanRule(2)), 1, true, 'Expected OrRule to pass when the second rule passes.'],
23+
[new OrRule(new BetweenRule(2.4, 5.5), new LowerThanRule(2)), 6, false, 'Expected OrRule to fail when neither rule passes.'],
24+
[new OrRule(new InstanceOfRule(Theme::class), new IsRule(Type::ITERABLE)), Theme::LIGHT, true, 'Expected OrRule to pass with an instance of SomeClass.'],
25+
[new OrRule(new InstanceOfRule(Theme::class), new IsRule(Type::ITERABLE)), [], true, 'Expected OrRule to pass with an iterable (array).'],
26+
[new OrRule(new LowerThanRule(5), new BetweenRule(10, 20)), 25, false, 'Expected OrRule to fail when all rules fail.'],
27+
[new OrRule(new AndRule(new BetweenRule(5, 15), new LowerThanRule(20)), new InRule("it", "en", "de")), 10, true, 'Expected OrRule to pass with nested AndRule conditions met.'],
28+
[new OrRule(new AndRule(new BetweenRule(5, 15), new LowerThanRule(20)), new InRule("it", "en", "de")), "en", true, 'Expected OrRule to pass with value in InRule parameters.'],
29+
[new OrRule(new AndRule(new BetweenRule(5, 15), new LowerThanRule(20)), new InRule("it", "en", "de")), "fr", false, 'Expected OrRule to fail when nested conditions are not met.'],
30+
];
31+
}
32+
33+
public static function andRuleProvider(): array
34+
{
35+
return [
36+
[new AndRule(new BetweenRule(2.4, 5.5), new LowerThanRule(6)), 3, true, 'Expected AndRule to pass when all conditions are met.'],
37+
[new AndRule(new BetweenRule(2.4, 5.5), new LowerThanRule(3)), 4, false, 'Expected AndRule to fail when one condition fails.'],
38+
[new AndRule(new BetweenRule(2.4, 5.5), new LowerThanRule(3)), 6, false, 'Expected AndRule to fail when both conditions fail.'],
39+
[new AndRule(new InstanceOfRule(Theme::class), new IsRule(Type::ITERABLE)), Theme::LIGHT, false, 'Expected AndRule to fail since SomeClass is not iterable.'],
40+
[new AndRule(new InstanceOfRule(Theme::class), new IsRule(Type::ITERABLE)), [], false, 'Expected AndRule to fail since array is not an instance of SomeClass.'],
41+
[new AndRule(new OrRule(new BetweenRule(10, 20), new InRule("it", "en", "de")), new LowerThanRule(15)), 12, true, 'Expected AndRule to pass with nested OrRule condition met and value less than 15.'],
42+
[new AndRule(new BetweenRule(2.4, 5.5), new LowerThanRule(5.5)), 5.4, true, 'Expected AndRule to pass at the edge of the range.'],
43+
];
44+
}
45+
46+
/**
47+
* @dataProvider orRuleProvider
48+
* @dataProvider andRuleProvider
49+
*/
50+
public function test_or_rule_validation($rule, $value, $expected, $message)
51+
{
52+
$failed = false;
53+
$failClosure = function ($message) use (&$failed) {
54+
$failed = true;
55+
};
56+
57+
$rule->validate('attribute', $value, $failClosure);
58+
if ($expected) {
59+
$this->assertFalse($failed, $message);
60+
} else {
61+
$this->assertTrue($failed, $message);
62+
}
63+
}
64+
65+
66+
}

0 commit comments

Comments
 (0)