Skip to content

Commit 8a1d02d

Browse files
committed
Merge branch 'develop'
* develop: specify next release add Is::just() add documentation for Shape::rename() and ::default() add Shape::default() add Shape::rename()
2 parents 6550be7 + 129ddec commit 8a1d02d

File tree

8 files changed

+230
-8
lines changed

8 files changed

+230
-8
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 1.6.0 - 2024-11-11
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+
- `Is::just()`
10+
311
## 1.5.0 - 2024-11-10
412

513
### 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}`.

docs/constraints/maybe.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Maybe monad
2+
3+
If a previous contraint outputs a [`Maybe`](https://innmind.org/Immutable/structures/maybe/) and you want to access the inner value you can do:
4+
5+
```php
6+
use Innmind\Validation\Is;
7+
use Innmind\Immutable\Maybe;
8+
9+
$validate = Is::int()
10+
->or(Is::null())
11+
->map(Maybe::of(...))
12+
->and(Is::just());
13+
```
14+
15+
In this example the input can be an `int` or `null` but it will fail the validation in case the value is `null` because `Maybe::of(...)` will move the `null` as a `Nothing` and we say we want a `Just`.

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ nav:
1111
- Array shapes: constraints/array-shapes.md
1212
- Dates: constraints/dates.md
1313
- Objects: constraints/objects.md
14+
- Maybe monad: constraints/maybe.md
1415
- Custom: constraints/custom.md
1516

1617
theme:

proofs/is.php

+29-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
declare(strict_types = 1);
33

44
use Innmind\Validation\Is;
5-
use Innmind\Immutable\Str;
5+
use Innmind\Immutable\{
6+
Str,
7+
Maybe,
8+
};
69
use Innmind\BlackBox\Set;
710

811
return static function() {
@@ -615,4 +618,29 @@ static function($assert, $keys, $values, $integer, $string, $random) {
615618
);
616619
},
617620
);
621+
622+
yield proof(
623+
'Is::just()',
624+
given(Set\Integers::any()),
625+
static function($assert, $value) {
626+
$assert->same(
627+
$value,
628+
Is::int()
629+
->map(Maybe::just(...))
630+
->and(Is::just())($value)->match(
631+
static fn($value) => $value,
632+
static fn() => null,
633+
),
634+
);
635+
636+
$assert->false(
637+
Is::null()
638+
->map(Maybe::of(...))
639+
->and(Is::just())(null)->match(
640+
static fn($value) => $value,
641+
static fn() => false,
642+
),
643+
);
644+
},
645+
);
618646
};

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/Is.php

+20
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use Innmind\Immutable\{
77
Validation,
8+
Maybe,
89
Predicate as PredicateInterface,
910
};
1011

@@ -162,6 +163,25 @@ public static function associativeArray(Constraint $key, Constraint $value): Ass
162163
return AssociativeArray::of($key, $value);
163164
}
164165

166+
/**
167+
* @psalm-pure
168+
* @template V
169+
*
170+
* @param ?non-empty-string $message
171+
*
172+
* @return Constraint<Maybe<V>, V>
173+
*/
174+
public static function just(?string $message = null): Constraint
175+
{
176+
/** @psalm-suppress MixedArgumentTypeCoercion */
177+
return Of::callable(static fn(Maybe $value) => $value->match(
178+
Validation::success(...),
179+
static fn() => Validation::fail(Failure::of(
180+
$message ?? 'No value was provided',
181+
)),
182+
));
183+
}
184+
165185
/**
166186
* @param non-empty-string $message
167187
*

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)