Skip to content

Commit e9451ad

Browse files
committed
Add ArraySchema minContains / maxContains
1 parent d1ccfba commit e9451ad

File tree

6 files changed

+278
-2
lines changed

6 files changed

+278
-2
lines changed

.php-cs-fixer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
// rules is buggy
1616
unset($config['rules']['simplified_null_return']);
1717

18+
$config['rules']['strict_comparison'] = false;
19+
1820
return (new PhpCsFixer\Config)
1921
->setUnsupportedPhpVersionAllowed(true)
2022
->setIndent($config['indent'])

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Heavily inspired by the well-known TypeScript library [zod](https://github.com/c
3535
Through [Composer](http://getcomposer.org) as [chubbyphp/chubbyphp-parsing][1].
3636

3737
```sh
38-
composer require chubbyphp/chubbyphp-parsing "^2.5"
38+
composer require chubbyphp/chubbyphp-parsing "^2.6"
3939
```
4040

4141
## Quick Start

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
},
5151
"extra": {
5252
"branch-alias": {
53-
"dev-master": "2.5-dev"
53+
"dev-master": "2.6-dev"
5454
}
5555
},
5656
"scripts": {

doc/Schema/ArraySchema.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ $schema->maxItems(10); // At most 10 items
2828

2929
```php
3030
$schema->contains(5); // Array must contain value 5
31+
$schema->minContains(5, 2); // Value 5 must appear at least twice
32+
$schema->maxContains(5, 2); // Value 5 may appear at most twice
3133
$schema->uniqueItems(); // Array must contain unique items
3234
```
3335

@@ -152,5 +154,7 @@ $matrixSchema->parse([
152154
| `array.minItems` | Array has fewer items than minimum |
153155
| `array.maxItems` | Array has more items than maximum |
154156
| `array.contains` | Array doesn't contain required value |
157+
| `array.minContains` | Array contains a value less often than required |
158+
| `array.maxContains` | Array contains a value more often than allowed |
155159

156160
Item-level errors will include the array index in the error path (e.g., `items.0`, `items.1`).

src/Schema/ArraySchema.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ final class ArraySchema extends AbstractSchemaInnerParse implements SchemaInterf
4343
public const string ERROR_CONTAINS_CODE = 'array.contains';
4444
public const string ERROR_CONTAINS_TEMPLATE = '{{given}} does not contain {{contains}}';
4545

46+
public const string ERROR_MIN_CONTAINS_CODE = 'array.minContains';
47+
public const string ERROR_MIN_CONTAINS_TEMPLATE = '{{given}} contains {{contains}} {{containsCount}} times, min {{minContains}} required';
48+
49+
public const string ERROR_MAX_CONTAINS_CODE = 'array.maxContains';
50+
public const string ERROR_MAX_CONTAINS_TEMPLATE = '{{given}} contains {{contains}} {{containsCount}} times, max {{maxContains}} allowed';
51+
4652
/** @deprecated: see ERROR_CONTAINS_CODE */
4753
public const string ERROR_INCLUDES_CODE = 'array.includes';
4854

@@ -200,6 +206,66 @@ public function contains(mixed $contains, bool $strict = true): static
200206
});
201207
}
202208

209+
public function minContains(mixed $contains, int $minContains, bool $strict = true): static
210+
{
211+
return $this->postParse(static function (array $array) use ($contains, $minContains, $strict) {
212+
$containsCount = 0;
213+
214+
foreach ($array as $value) {
215+
if (($strict && $value === $contains) || (!$strict && $value == $contains)) {
216+
++$containsCount;
217+
}
218+
}
219+
220+
if ($containsCount < $minContains) {
221+
throw new ErrorsException(
222+
new Error(
223+
self::ERROR_MIN_CONTAINS_CODE,
224+
self::ERROR_MIN_CONTAINS_TEMPLATE,
225+
[
226+
'contains' => $contains,
227+
'containsCount' => $containsCount,
228+
'given' => $array,
229+
'minContains' => $minContains,
230+
]
231+
)
232+
);
233+
}
234+
235+
return $array;
236+
});
237+
}
238+
239+
public function maxContains(mixed $contains, int $maxContains, bool $strict = true): static
240+
{
241+
return $this->postParse(static function (array $array) use ($contains, $maxContains, $strict) {
242+
$containsCount = 0;
243+
244+
foreach ($array as $value) {
245+
if (($strict && $value === $contains) || (!$strict && $value == $contains)) {
246+
++$containsCount;
247+
}
248+
}
249+
250+
if ($containsCount > $maxContains) {
251+
throw new ErrorsException(
252+
new Error(
253+
self::ERROR_MAX_CONTAINS_CODE,
254+
self::ERROR_MAX_CONTAINS_TEMPLATE,
255+
[
256+
'contains' => $contains,
257+
'containsCount' => $containsCount,
258+
'given' => $array,
259+
'maxContains' => $maxContains,
260+
]
261+
)
262+
);
263+
}
264+
265+
return $array;
266+
});
267+
}
268+
203269
/**
204270
* @deprecated use contains($contains, $strict)
205271
*/

tests/Unit/Schema/ArraySchemaTest.php

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public function testImmutability(): void
3232
self::assertNotSame($schema, $schema->minItems(1));
3333
self::assertNotSame($schema, $schema->maxItems(1));
3434
self::assertNotSame($schema, $schema->contains('test'));
35+
self::assertNotSame($schema, $schema->minContains('test', 1));
36+
self::assertNotSame($schema, $schema->maxContains('test', 1));
3537
self::assertNotSame($schema, $schema->uniqueItems());
3638
self::assertNotSame($schema, $schema->filter(static fn (mixed $value) => true));
3739
self::assertNotSame($schema, $schema->map(static fn (mixed $value) => $value));
@@ -550,6 +552,208 @@ public function testParseWithInvalidContains(): void
550552
}
551553
}
552554

555+
public function testParseWithValidMinContains(): void
556+
{
557+
$dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00');
558+
$dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
559+
560+
$input = [$dateTime1, $dateTime2, $dateTime2];
561+
562+
$schema = (new ArraySchema(new DateTimeSchema()))->minContains($dateTime2, 2);
563+
564+
self::assertSame($input, $schema->parse($input));
565+
}
566+
567+
public function testParseWithValidMinContainsWithEqualButNotSame(): void
568+
{
569+
$dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00');
570+
$dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
571+
572+
$dateTime2Equal = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
573+
574+
$input = [$dateTime1, $dateTime2, $dateTime2Equal];
575+
576+
$schema = (new ArraySchema(new DateTimeSchema()))->minContains($dateTime2Equal, 2, false);
577+
578+
self::assertSame($input, $schema->parse($input));
579+
}
580+
581+
public function testParseWithInvalidMinContainsWithEqualButNotSameAndStrictFalse(): void
582+
{
583+
$dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00');
584+
$dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
585+
586+
$dateTime2Equal = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
587+
588+
$dateTime2Equal2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
589+
590+
$input = [$dateTime1, $dateTime2, $dateTime2Equal2];
591+
592+
$schema = (new ArraySchema(new DateTimeSchema()))->minContains($dateTime2Equal, 2, false);
593+
594+
try {
595+
(new ArraySchema(new DateTimeSchema()))->minContains($dateTime2Equal, 2)->parse($input);
596+
597+
throw new \Exception('code should not be reached');
598+
} catch (ErrorsException $errorsException) {
599+
self::assertSame(
600+
[
601+
[
602+
'path' => '',
603+
'error' => [
604+
'code' => 'array.minContains',
605+
'template' => '{{given}} contains {{contains}} {{containsCount}} times, min {{minContains}} required',
606+
'variables' => [
607+
'contains' => json_decode(json_encode($dateTime2Equal), true),
608+
'containsCount' => 0,
609+
'given' => json_decode(json_encode($input), true),
610+
'minContains' => 2,
611+
],
612+
],
613+
],
614+
],
615+
$errorsException->errors->jsonSerialize()
616+
);
617+
}
618+
619+
self::assertSame($input, $schema->parse($input));
620+
}
621+
622+
public function testParseWithInvalidMinContains(): void
623+
{
624+
$dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00');
625+
$dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
626+
627+
$input = [$dateTime1, $dateTime2, $dateTime1];
628+
629+
$schema = (new ArraySchema(new DateTimeSchema()))->minContains($dateTime2, 2);
630+
631+
try {
632+
$schema->parse($input);
633+
634+
throw new \Exception('code should not be reached');
635+
} catch (ErrorsException $errorsException) {
636+
self::assertSame(
637+
[
638+
[
639+
'path' => '',
640+
'error' => [
641+
'code' => 'array.minContains',
642+
'template' => '{{given}} contains {{contains}} {{containsCount}} times, min {{minContains}} required',
643+
'variables' => [
644+
'contains' => json_decode(json_encode($dateTime2), true),
645+
'containsCount' => 1,
646+
'given' => json_decode(json_encode($input), true),
647+
'minContains' => 2,
648+
],
649+
],
650+
],
651+
],
652+
$errorsException->errors->jsonSerialize()
653+
);
654+
}
655+
}
656+
657+
public function testParseWithValidMaxContains(): void
658+
{
659+
$dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00');
660+
$dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
661+
662+
$input = [$dateTime1, $dateTime2, $dateTime2];
663+
664+
$schema = (new ArraySchema(new DateTimeSchema()))->maxContains($dateTime2, 2);
665+
666+
self::assertSame($input, $schema->parse($input));
667+
}
668+
669+
public function testParseWithValidMaxContainsWithEqualButNotSame(): void
670+
{
671+
$dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00');
672+
$dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
673+
674+
$dateTime2Equal = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
675+
676+
$input = [$dateTime1, $dateTime2, $dateTime2Equal];
677+
678+
$schema = (new ArraySchema(new DateTimeSchema()))->maxContains($dateTime2Equal, 2, false);
679+
680+
self::assertSame($input, $schema->parse($input));
681+
}
682+
683+
public function testParseWithInvalidMaxContainsWithEqualButNotSameAndStrictFalse(): void
684+
{
685+
$dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00');
686+
$dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
687+
688+
$dateTime2Equal = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
689+
690+
$input = [$dateTime1, $dateTime2, $dateTime2Equal];
691+
692+
$schema = (new ArraySchema(new DateTimeSchema()))->maxContains($dateTime2Equal, 1, false);
693+
694+
self::assertSame($input, (new ArraySchema(new DateTimeSchema()))->maxContains($dateTime2Equal, 1)->parse($input));
695+
696+
try {
697+
$schema->parse($input);
698+
699+
throw new \Exception('code should not be reached');
700+
} catch (ErrorsException $errorsException) {
701+
self::assertSame(
702+
[
703+
[
704+
'path' => '',
705+
'error' => [
706+
'code' => 'array.maxContains',
707+
'template' => '{{given}} contains {{contains}} {{containsCount}} times, max {{maxContains}} allowed',
708+
'variables' => [
709+
'contains' => json_decode(json_encode($dateTime2Equal), true),
710+
'containsCount' => 2,
711+
'given' => json_decode(json_encode($input), true),
712+
'maxContains' => 1,
713+
],
714+
],
715+
],
716+
],
717+
$errorsException->errors->jsonSerialize()
718+
);
719+
}
720+
}
721+
722+
public function testParseWithInvalidMaxContains(): void
723+
{
724+
$dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00');
725+
$dateTime2 = new \DateTimeImmutable('2024-01-21T09:15:00+00:00');
726+
727+
$input = [$dateTime1, $dateTime2, $dateTime2, $dateTime2, $dateTime2];
728+
729+
$schema = (new ArraySchema(new DateTimeSchema()))->maxContains($dateTime2, 2);
730+
731+
try {
732+
$schema->parse($input);
733+
734+
throw new \Exception('code should not be reached');
735+
} catch (ErrorsException $errorsException) {
736+
self::assertSame(
737+
[
738+
[
739+
'path' => '',
740+
'error' => [
741+
'code' => 'array.maxContains',
742+
'template' => '{{given}} contains {{contains}} {{containsCount}} times, max {{maxContains}} allowed',
743+
'variables' => [
744+
'contains' => json_decode(json_encode($dateTime2), true),
745+
'containsCount' => 4,
746+
'given' => json_decode(json_encode($input), true),
747+
'maxContains' => 2,
748+
],
749+
],
750+
],
751+
],
752+
$errorsException->errors->jsonSerialize()
753+
);
754+
}
755+
}
756+
553757
public function testParseWithValidIncludes(): void
554758
{
555759
$dateTime1 = new \DateTimeImmutable('2024-01-20T09:15:00+00:00');

0 commit comments

Comments
 (0)