Skip to content

Commit e48e7ac

Browse files
committed
add AssertListLength and support for non-empty-list type
1 parent 6f20756 commit e48e7ac

14 files changed

+788
-68
lines changed

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ composer require shipmonk/input-mapper
1717
Input Mapper comes with built-in mappers for the following types:
1818

1919
* `array`, `bool`, `float`, `int`, `mixed`, `string`, `list`
20-
* `positive-int`, `negative-int`, `int<TMin, TMax>`
21-
* `array<V>`, `array<K, V>`, `list<V>`
20+
* `positive-int`, `negative-int`, `int<TMin, TMax>`, `non-empty-list`
21+
* `array<V>`, `array<K, V>`, `list<V>`, `non-empty-list<V>`
2222
* `array{K1: V1, ...}`
2323
* `?T`, `Optional<T>`
2424
* `DateTimeInterface`, `DateTimeImmutable`
@@ -53,6 +53,7 @@ Input Mapper comes with some built-in validators:
5353
* `AssertUrl`
5454
* list validators:
5555
* `AssertListItem`
56+
* `AssertListLength`
5657
* date time validators:
5758
* `AssertDateTimeRange`
5859

@@ -73,15 +74,15 @@ class Person
7374
{
7475
public function __construct(
7576
public readonly string $name,
76-
77+
7778
public readonly int $age,
78-
79+
7980
/** @var Optional<string> */
8081
public readonly Optional $email,
81-
82+
8283
/** @var list<string> */
8384
public readonly array $hobbies,
84-
85+
8586
/** @var Optional<list<self>> */
8687
public readonly Optional $friends,
8788
) {}
@@ -138,7 +139,7 @@ class Person
138139
{
139140
public function __construct(
140141
public readonly string $name,
141-
142+
142143
#[AssertIntRange(gte: 18, lte: 99)]
143144
public readonly int $age,
144145
) {}

src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapOptional;
5050
use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\ValidatedMapperCompiler;
5151
use ShipMonk\InputMapper\Compiler\Type\PhpDocTypeUtils;
52+
use ShipMonk\InputMapper\Compiler\Validator\Array\AssertListLength;
5253
use ShipMonk\InputMapper\Compiler\Validator\Int\AssertIntRange;
5354
use ShipMonk\InputMapper\Compiler\Validator\Int\AssertNegativeInt;
5455
use ShipMonk\InputMapper\Compiler\Validator\Int\AssertNonNegativeInt;
@@ -120,6 +121,7 @@ public function create(TypeNode $type, array $options = []): MapperCompiler
120121

121122
default => match ($type->name) {
122123
'list' => new MapList(new MapMixed()),
124+
'non-empty-list' => new ValidatedMapperCompiler(new MapList(new MapMixed()), [new AssertListLength(min: 1)]),
123125
'negative-int' => new ValidatedMapperCompiler(new MapInt(), [new AssertNegativeInt()]),
124126
'non-negative-int' => new ValidatedMapperCompiler(new MapInt(), [new AssertNonNegativeInt()]),
125127
'non-positive-int' => new ValidatedMapperCompiler(new MapInt(), [new AssertNonPositiveInt()]),
@@ -154,6 +156,10 @@ public function create(TypeNode $type, array $options = []): MapperCompiler
154156
1 => new MapList($this->createInner($type->genericTypes[0], $options)),
155157
default => throw CannotCreateMapperCompilerException::fromType($type),
156158
},
159+
'non-empty-list' => match (count($type->genericTypes)) {
160+
1 => new ValidatedMapperCompiler(new MapList($this->createInner($type->genericTypes[0], $options)), [new AssertListLength(min: 1)]),
161+
default => throw CannotCreateMapperCompilerException::fromType($type),
162+
},
157163
Optional::class => match (count($type->genericTypes)) {
158164
1 => new MapOptional($this->createInner($type->genericTypes[0], $options)),
159165
default => throw CannotCreateMapperCompilerException::fromType($type),

src/Compiler/Type/NativeTypeUtils.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
use PhpParser\Node\Name;
1010
use PhpParser\Node\NullableType;
1111
use PhpParser\Node\UnionType;
12+
use function array_splice;
1213
use function count;
1314
use function get_debug_type;
15+
use function is_a;
1416
use function sprintf;
17+
use function strcasecmp;
1518

1619
class NativeTypeUtils
1720
{
@@ -83,11 +86,38 @@ public static function createIntersection(ComplexType|Identifier|Name ...$member
8386
throw new LogicException(sprintf('Unexpected intersection member type: %s', get_debug_type($member)));
8487
}
8588

89+
for ($i = 0; $i < count($types); $i++) {
90+
for ($j = $i + 1; $j < count($types); $j++) {
91+
if (self::isSubTypeOf($types[$i], $types[$j])) {
92+
array_splice($types, $j--, 1);
93+
continue;
94+
}
95+
96+
if (self::isSubTypeOf($types[$j], $types[$i])) {
97+
array_splice($types, $i--, 1);
98+
continue 2;
99+
}
100+
}
101+
}
102+
86103
return match (count($types)) {
87104
0 => new Identifier('mixed'),
88105
1 => $types[0],
89106
default => new IntersectionType($types),
90107
};
91108
}
92109

110+
public static function isSubTypeOf(Identifier|Name $a, Identifier|Name $b): bool
111+
{
112+
if ($a instanceof Identifier && $b instanceof Identifier) {
113+
return strcasecmp($a->name, $b->name) === 0;
114+
}
115+
116+
if ($a instanceof Name && $b instanceof Name) {
117+
return is_a($a->toString(), $b->toString(), true);
118+
}
119+
120+
return false;
121+
}
122+
93123
}

0 commit comments

Comments
 (0)