Skip to content

Commit db014d9

Browse files
authored
Merge pull request #5 from Innmind/shape-improvements
Add `Shape::rename()` and `Shape::default()`
2 parents 0afa8ed + d58f00f commit db014d9

File tree

4 files changed

+164
-7
lines changed

4 files changed

+164
-7
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### Added
6+
7+
- `Shape::rename()` to rename a key in the output array
8+
- `Shape::default()` to specify a default value when an optional key is not set
9+
310
## 1.5.0 - 2024-11-10
411

512
### Added

docs/constraints/array-shapes.md

+29
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,32 @@ The optional key will only be present in the output value if it was set in the i
3838

3939
??? tip
4040
If you want to build a shape with a single optional key you can do `#!php Is::shape('key', Is::string())->optional('key')`.
41+
42+
If you don't want to handle the possible absence of the key in the output array you can specify a default value:
43+
44+
```php hl_lines="6"
45+
use Innmind\Validation\Is;
46+
47+
$validate = Is::shape('username', Is::string())
48+
->with('password', Is::string())
49+
->optional('keep-logged-in', Is::string())
50+
->default('keep-logged-in', 'false');
51+
```
52+
53+
## Rename a key
54+
55+
This is useful when the output value no longer matches the input key name.
56+
57+
For example you have a shape containing a list of integers but you want the highest one:
58+
59+
```php
60+
use Innmind\Validation\Is;
61+
62+
$validate = Is::shape(
63+
'versions',
64+
Is::list(Is::int())
65+
->map(\max(...)),
66+
)->rename('versions', 'highest');
67+
```
68+
69+
Now the output type is `array{highest: int}`.

proofs/shape.php

+42
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,28 @@ static function($assert) {
134134
},
135135
);
136136

137+
yield proof(
138+
'Shape with optional key default',
139+
given(Set\Type::any()),
140+
static function($assert, $default) {
141+
$assert->same(
142+
[
143+
'foo' => 42,
144+
'bar' => $default,
145+
],
146+
Shape::of('foo', Is::int())
147+
->optional('bar', Is::bool())
148+
->default('bar', $default)([
149+
'foo' => 42,
150+
])
151+
->match(
152+
static fn($value) => $value,
153+
static fn() => null,
154+
),
155+
);
156+
},
157+
);
158+
137159
yield test(
138160
'Shape with optional key with constraint directly specified',
139161
static function($assert) {
@@ -207,4 +229,24 @@ static function($assert, $value) {
207229
);
208230
},
209231
);
232+
233+
yield test(
234+
'Shape rename key',
235+
static function($assert) {
236+
$assert->same(
237+
[
238+
'bar' => 42,
239+
],
240+
Shape::of('foo', Is::int())
241+
->rename('foo', 'bar')([
242+
'foo' => 42,
243+
'bar' => true,
244+
])
245+
->match(
246+
static fn($value) => $value,
247+
static fn() => null,
248+
),
249+
);
250+
},
251+
);
210252
};

src/Shape.php

+86-7
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,31 @@ final class Shape implements Constraint
1818
private array $constraints;
1919
/** @var list<non-empty-string> */
2020
private array $optional;
21+
/** @var array<non-empty-string, mixed> */
22+
private array $defaults;
23+
/** @var array<non-empty-string, non-empty-string> */
24+
private array $rename;
2125
/** @var ?callable(non-empty-string): non-empty-string */
2226
private $message;
2327

2428
/**
2529
* @param non-empty-array<non-empty-string, Constraint<mixed, mixed>> $constraints
2630
* @param list<non-empty-string> $optional
31+
* @param array<non-empty-string, mixed> $defaults
32+
* @param array<non-empty-string, non-empty-string> $rename
2733
* @param ?callable(non-empty-string): non-empty-string $message
2834
*/
2935
private function __construct(
3036
array $constraints,
3137
array $optional,
32-
?callable $message = null,
38+
array $defaults,
39+
array $rename,
40+
?callable $message,
3341
) {
3442
$this->constraints = $constraints;
3543
$this->optional = $optional;
44+
$this->defaults = $defaults;
45+
$this->rename = $rename;
3646
$this->message = $message;
3747
}
3848

@@ -48,7 +58,13 @@ public function __invoke(mixed $value): Validation
4858
*/
4959
public static function of(string $key, Constraint $constraint): self
5060
{
51-
return new self([$key => $constraint], []);
61+
return new self(
62+
[$key => $constraint],
63+
[],
64+
[],
65+
[],
66+
null,
67+
);
5268
}
5369

5470
/**
@@ -59,7 +75,13 @@ public function with(string $key, Constraint $constraint): self
5975
$constraints = $this->constraints;
6076
$constraints[$key] = $constraint;
6177

62-
return new self($constraints, $this->optional, $this->message);
78+
return new self(
79+
$constraints,
80+
$this->optional,
81+
$this->defaults,
82+
$this->rename,
83+
$this->message,
84+
);
6385
}
6486

6587
/**
@@ -75,15 +97,67 @@ public function optional(string $key, Constraint $constraint = null): self
7597
$constraints[$key] = $constraint;
7698
}
7799

78-
return new self($constraints, $optional, $this->message);
100+
return new self(
101+
$constraints,
102+
$optional,
103+
$this->defaults,
104+
$this->rename,
105+
$this->message,
106+
);
107+
}
108+
109+
/**
110+
* @param non-empty-string $key
111+
*/
112+
public function default(string $key, mixed $value): self
113+
{
114+
if (!\in_array($key, $this->optional, true)) {
115+
throw new \LogicException("No optional key $key defined");
116+
}
117+
118+
$defaults = $this->defaults;
119+
/** @psalm-suppress MixedAssignment */
120+
$defaults[$key] = $value;
121+
122+
return new self(
123+
$this->constraints,
124+
$this->optional,
125+
$defaults,
126+
$this->rename,
127+
$this->message,
128+
);
129+
}
130+
131+
/**
132+
* @param non-empty-string $from
133+
* @param non-empty-string $to
134+
*/
135+
public function rename(string $from, string $to): self
136+
{
137+
$rename = $this->rename;
138+
$rename[$from] = $to;
139+
140+
return new self(
141+
$this->constraints,
142+
$this->optional,
143+
$this->defaults,
144+
$rename,
145+
$this->message,
146+
);
79147
}
80148

81149
/**
82150
* @param callable(non-empty-string): non-empty-string $message
83151
*/
84152
public function withKeyFailure(callable $message): self
85153
{
86-
return new self($this->constraints, $this->optional, $message);
154+
return new self(
155+
$this->constraints,
156+
$this->optional,
157+
$this->defaults,
158+
$this->rename,
159+
$message,
160+
);
87161
}
88162

89163
public function and(Constraint $constraint): Constraint
@@ -140,10 +214,15 @@ private function validate(array $value): Validation
140214

141215
$validation = $validation->and(
142216
$keyValidation->and($ofType)($value),
143-
static function($array, $value) use ($key, $optional) {
217+
function($array, $value) use ($key, $optional) {
218+
$concreteKey = $this->rename[$key] ?? $key;
219+
144220
if ($value !== $optional) {
145221
/** @psalm-suppress MixedAssignment */
146-
$array[$key] = $value;
222+
$array[$concreteKey] = $value;
223+
} else if (\array_key_exists($key, $this->defaults)) {
224+
/** @psalm-suppress MixedAssignment */
225+
$array[$concreteKey] = $this->defaults[$key];
147226
}
148227

149228
return $array;

0 commit comments

Comments
 (0)