Skip to content

Commit 0b36be7

Browse files
committed
Add uniqueItems
1 parent e39a26b commit 0b36be7

3 files changed

Lines changed: 67 additions & 0 deletions

File tree

doc/Schema/ArraySchema.md

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

2929
```php
3030
$schema->contains(5); // Array must contain value 5
31+
$schema->uniqueItems(); // Array must contain unique items
3132
```
3233

3334
## Transformations

src/Schema/ArraySchema.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ final class ArraySchema extends AbstractSchemaInnerParse implements SchemaInterf
4949
/** @deprecated: see ERROR_CONTAINS_TEMPLATE */
5050
public const string ERROR_INCLUDES_TEMPLATE = '{{given}} does not include {{includes}}';
5151

52+
public const string ERROR_UNIQUE_ITEMS_CODE = 'array.uniqueItems';
53+
public const string ERROR_UNIQUE_ITEMS_TEMPLATE = 'Duplicate keys {{duplicateKeys}}, {{given}} given';
54+
5255
public function __construct(private SchemaInterface $itemSchema) {}
5356

5457
public function exactItems(int $exactItems): static
@@ -219,6 +222,32 @@ public function includes(mixed $includes, bool $strict = true): static
219222
});
220223
}
221224

225+
public function uniqueItems(): static
226+
{
227+
return $this->postParse(static function (array $array) {
228+
$uniqueArray = array_unique($array);
229+
230+
if (\count($uniqueArray) === \count($array)) {
231+
return $array;
232+
}
233+
234+
$duplicateKeys = array_values(
235+
array_diff(
236+
array_keys($array),
237+
array_keys($uniqueArray)
238+
)
239+
);
240+
241+
throw new ErrorsException(
242+
new Error(
243+
self::ERROR_UNIQUE_ITEMS_CODE,
244+
self::ERROR_UNIQUE_ITEMS_TEMPLATE,
245+
['duplicateKeys' => $duplicateKeys, 'given' => $array]
246+
)
247+
);
248+
});
249+
}
250+
222251
/**
223252
* @param \Closure(mixed $value, int $index): bool $filter
224253
*/

tests/Unit/Schema/ArraySchemaTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ 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->uniqueItems());
3536
self::assertNotSame($schema, $schema->filter(static fn (mixed $value) => true));
3637
self::assertNotSame($schema, $schema->map(static fn (mixed $value) => $value));
3738
self::assertNotSame($schema, $schema->sort());
@@ -654,6 +655,42 @@ public function testParseWithInvalidIncludes(): void
654655
}
655656
}
656657

658+
public function testParseWithValidUniqueItems(): void
659+
{
660+
$input = [1, 2, 3];
661+
662+
$schema = (new ArraySchema(new IntSchema()))->uniqueItems();
663+
664+
self::assertSame($input, $schema->parse($input));
665+
}
666+
667+
public function testParseWithInvalidUniqueItemsWithTwoTimesTheSameElementsPlusOne(): void
668+
{
669+
$input = [5, 7, 7, 12, 3, 9, 5, 14, 2, 9, 6, 3, 18, 4, 14, 10, 2, 11, 6, 13, 8, 12, 15, 16, 8, 17, 19, 20, 21, 15];
670+
671+
$schema = (new ArraySchema(new IntSchema()))->uniqueItems();
672+
673+
try {
674+
$schema->parse($input);
675+
676+
throw new \Exception('code should not be reached');
677+
} catch (ErrorsException $errorsException) {
678+
self::assertSame([
679+
[
680+
'path' => '',
681+
'error' => [
682+
'code' => 'array.uniqueItems',
683+
'template' => 'Duplicate keys {{duplicateKeys}}, {{given}} given',
684+
'variables' => [
685+
'duplicateKeys' => [2, 6, 9, 11, 14, 16, 18, 21, 24, 29],
686+
'given' => $input,
687+
],
688+
],
689+
],
690+
], $errorsException->errors->jsonSerialize());
691+
}
692+
}
693+
657694
public function testParseWithFilter(): void
658695
{
659696
$input = [1, 2, 3, 4, 5];

0 commit comments

Comments
 (0)