Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.23% covered (success)
96.23%
51 / 53
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Validators
96.23% covered (success)
96.23%
51 / 53
77.78% covered (warning)
77.78%
7 / 9
12
0.00% covered (danger)
0.00%
0 / 1
 registry
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 messages
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
1
 in
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 escape
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 minItems
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 maxItems
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 color
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 rfc3339
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 timezone
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Validation;
6
7use Celema\Sire\Contract\NamedArguments;
8use Celema\Sire\Contract\Rule;
9use Celema\Sire\Contract\ValidatesEmpty;
10use Celema\Sire\Contract\Validation as ValidationContract;
11use Celema\Sire\Contract\Value;
12use Celema\Sire\RuleRegistry;
13use Celema\Sire\Validation;
14use Cosray\DateTime\Codec;
15use Cosray\Value\Color;
16use Override;
17
18final class Validators
19{
20    public static function registry(): RuleRegistry
21    {
22        return RuleRegistry::withDefaults()->withMany([
23            'color' => self::color(),
24            'minitems' => self::minItems(),
25            'maxitems' => self::maxItems(),
26            'rfc3339' => self::rfc3339(),
27            'timezone' => self::timezone(),
28        ]);
29    }
30
31    /**
32     * Translations of the messages Sire produces itself, by message key.
33     * Cosray's own rules translate their messages.
34     *
35     * @return array<string, string>
36     */
37    public static function messages(): array
38    {
39        $messages = [
40            'extra' => __('validation:extra'),
41            'missing' => __('validation:missing'),
42            'null' => __('validation:null'),
43            'rule.email' => __('validation:email'),
44            'rule.in' => __('validation:in'),
45            'rule.max' => __('validation:max'),
46            'rule.maxlen' => __('validation:maxlen'),
47            'rule.min' => __('validation:min'),
48            'rule.minlen' => __('validation:minlen'),
49            'rule.regex' => __('validation:regex'),
50            'rule.required' => __('validation:required'),
51            'type.bool' => __('validation:type-bool'),
52            'type.float' => __('validation:type-float'),
53            'type.int' => __('validation:type-int'),
54            'type.list' => __('validation:type-list'),
55            'type.number' => __('validation:type-number'),
56            'type.shape' => __('validation:type-shape'),
57            'type.string' => __('validation:type-string'),
58        ];
59
60        // Without a translation, __() returns the id; Sire's English reads better.
61        return array_filter($messages, static fn(string $message): bool => !str_starts_with($message, 'validation:'));
62    }
63
64    /**
65     * An `in:` rule accepting exactly the given values, which may contain
66     * commas, colons, quotes and backslashes. Sire parses the definition
67     * twice (the rule parser splits on `:`, the rule on `,`) and each pass
68     * consumes one level of backslash escapes, so values are escaped twice.
69     *
70     * @param list<scalar> $values
71     */
72    public static function in(array $values): string
73    {
74        $list = implode(',', array_map(
75            static fn(mixed $value): string => self::escape((string) $value, ','),
76            $values,
77        ));
78
79        return 'in:' . self::escape($list, ':');
80    }
81
82    private static function escape(string $value, string $delimiter): string
83    {
84        return addcslashes($value, '\\"\'' . $delimiter);
85    }
86
87    private static function minItems(): Rule
88    {
89        return new class implements Rule, ValidatesEmpty, NamedArguments {
90            #[Override]
91            public string $message {
92                get => __('validation:min-items');
93            }
94
95            #[Override]
96            public array $arguments {
97                get => ['min'];
98            }
99
100            #[Override]
101            public function validate(Value $value, string ...$args): ValidationContract
102            {
103                if (!is_array($value->value)) {
104                    return Validation::invalid();
105                }
106
107                return Validation::from(count($value->value) >= (int) ($args[0] ?? 0));
108            }
109        };
110    }
111
112    private static function maxItems(): Rule
113    {
114        return new class implements Rule, NamedArguments {
115            #[Override]
116            public string $message {
117                get => __('validation:max-items');
118            }
119
120            #[Override]
121            public array $arguments {
122                get => ['max'];
123            }
124
125            #[Override]
126            public function validate(Value $value, string ...$args): ValidationContract
127            {
128                if (!is_array($value->value)) {
129                    return Validation::invalid();
130                }
131
132                return Validation::from(count($value->value) <= (int) ($args[0] ?? 0));
133            }
134        };
135    }
136
137    private static function color(): Rule
138    {
139        return new class implements Rule {
140            #[Override]
141            public string $message {
142                get => __('validation:color');
143            }
144
145            #[Override]
146            public function validate(Value $value, string ...$args): ValidationContract
147            {
148                return Validation::from(Color::normalize($value->value) !== null);
149            }
150        };
151    }
152
153    private static function rfc3339(): Rule
154    {
155        return new class implements Rule {
156            #[Override]
157            public string $message {
158                get => __('validation:rfc3339');
159            }
160
161            #[Override]
162            public function validate(Value $value, string ...$args): ValidationContract
163            {
164                return Validation::from(is_string($value->value) && Codec::parse($value->value) !== null);
165            }
166        };
167    }
168
169    private static function timezone(): Rule
170    {
171        return new class implements Rule {
172            #[Override]
173            public string $message {
174                get => __('validation:timezone');
175            }
176
177            #[Override]
178            public function validate(Value $value, string ...$args): ValidationContract
179            {
180                return Validation::from(Codec::timezone($value->value) !== null);
181            }
182        };
183    }
184}