Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
* develop:
  specify next release
  add Is::just()
  add documentation for Shape::rename() and ::default()
  add Shape::default()
  add Shape::rename()
  • Loading branch information
Baptouuuu committed Nov 11, 2024
2 parents 6550be7 + 129ddec commit 8a1d02d
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 8 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.6.0 - 2024-11-11

### Added

- `Shape::rename()` to rename a key in the output array
- `Shape::default()` to specify a default value when an optional key is not set
- `Is::just()`

## 1.5.0 - 2024-11-10

### Added
Expand Down
29 changes: 29 additions & 0 deletions docs/constraints/array-shapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,32 @@ The optional key will only be present in the output value if it was set in the i

??? tip
If you want to build a shape with a single optional key you can do `#!php Is::shape('key', Is::string())->optional('key')`.

If you don't want to handle the possible absence of the key in the output array you can specify a default value:

```php hl_lines="6"
use Innmind\Validation\Is;

$validate = Is::shape('username', Is::string())
->with('password', Is::string())
->optional('keep-logged-in', Is::string())
->default('keep-logged-in', 'false');
```

## Rename a key

This is useful when the output value no longer matches the input key name.

For example you have a shape containing a list of integers but you want the highest one:

```php
use Innmind\Validation\Is;

$validate = Is::shape(
'versions',
Is::list(Is::int())
->map(\max(...)),
)->rename('versions', 'highest');
```

Now the output type is `array{highest: int}`.
15 changes: 15 additions & 0 deletions docs/constraints/maybe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Maybe monad

If a previous contraint outputs a [`Maybe`](https://innmind.org/Immutable/structures/maybe/) and you want to access the inner value you can do:

```php
use Innmind\Validation\Is;
use Innmind\Immutable\Maybe;

$validate = Is::int()
->or(Is::null())
->map(Maybe::of(...))
->and(Is::just());
```

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`.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ nav:
- Array shapes: constraints/array-shapes.md
- Dates: constraints/dates.md
- Objects: constraints/objects.md
- Maybe monad: constraints/maybe.md
- Custom: constraints/custom.md

theme:
Expand Down
30 changes: 29 additions & 1 deletion proofs/is.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
declare(strict_types = 1);

use Innmind\Validation\Is;
use Innmind\Immutable\Str;
use Innmind\Immutable\{
Str,
Maybe,
};
use Innmind\BlackBox\Set;

return static function() {
Expand Down Expand Up @@ -615,4 +618,29 @@ static function($assert, $keys, $values, $integer, $string, $random) {
);
},
);

yield proof(
'Is::just()',
given(Set\Integers::any()),
static function($assert, $value) {
$assert->same(
$value,
Is::int()
->map(Maybe::just(...))
->and(Is::just())($value)->match(
static fn($value) => $value,
static fn() => null,
),
);

$assert->false(
Is::null()
->map(Maybe::of(...))
->and(Is::just())(null)->match(
static fn($value) => $value,
static fn() => false,
),
);
},
);
};
42 changes: 42 additions & 0 deletions proofs/shape.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,28 @@ static function($assert) {
},
);

yield proof(
'Shape with optional key default',
given(Set\Type::any()),
static function($assert, $default) {
$assert->same(
[
'foo' => 42,
'bar' => $default,
],
Shape::of('foo', Is::int())
->optional('bar', Is::bool())
->default('bar', $default)([
'foo' => 42,
])
->match(
static fn($value) => $value,
static fn() => null,
),
);
},
);

yield test(
'Shape with optional key with constraint directly specified',
static function($assert) {
Expand Down Expand Up @@ -207,4 +229,24 @@ static function($assert, $value) {
);
},
);

yield test(
'Shape rename key',
static function($assert) {
$assert->same(
[
'bar' => 42,
],
Shape::of('foo', Is::int())
->rename('foo', 'bar')([
'foo' => 42,
'bar' => true,
])
->match(
static fn($value) => $value,
static fn() => null,
),
);
},
);
};
20 changes: 20 additions & 0 deletions src/Is.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use Innmind\Immutable\{
Validation,
Maybe,
Predicate as PredicateInterface,
};

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

/**
* @psalm-pure
* @template V
*
* @param ?non-empty-string $message
*
* @return Constraint<Maybe<V>, V>
*/
public static function just(?string $message = null): Constraint
{
/** @psalm-suppress MixedArgumentTypeCoercion */
return Of::callable(static fn(Maybe $value) => $value->match(
Validation::success(...),
static fn() => Validation::fail(Failure::of(
$message ?? 'No value was provided',
)),
));
}

/**
* @param non-empty-string $message
*
Expand Down
93 changes: 86 additions & 7 deletions src/Shape.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,31 @@ final class Shape implements Constraint
private array $constraints;
/** @var list<non-empty-string> */
private array $optional;
/** @var array<non-empty-string, mixed> */
private array $defaults;
/** @var array<non-empty-string, non-empty-string> */
private array $rename;
/** @var ?callable(non-empty-string): non-empty-string */
private $message;

/**
* @param non-empty-array<non-empty-string, Constraint<mixed, mixed>> $constraints
* @param list<non-empty-string> $optional
* @param array<non-empty-string, mixed> $defaults
* @param array<non-empty-string, non-empty-string> $rename
* @param ?callable(non-empty-string): non-empty-string $message
*/
private function __construct(
array $constraints,
array $optional,
?callable $message = null,
array $defaults,
array $rename,
?callable $message,
) {
$this->constraints = $constraints;
$this->optional = $optional;
$this->defaults = $defaults;
$this->rename = $rename;
$this->message = $message;
}

Expand All @@ -48,7 +58,13 @@ public function __invoke(mixed $value): Validation
*/
public static function of(string $key, Constraint $constraint): self
{
return new self([$key => $constraint], []);
return new self(
[$key => $constraint],
[],
[],
[],
null,
);
}

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

return new self($constraints, $this->optional, $this->message);
return new self(
$constraints,
$this->optional,
$this->defaults,
$this->rename,
$this->message,
);
}

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

return new self($constraints, $optional, $this->message);
return new self(
$constraints,
$optional,
$this->defaults,
$this->rename,
$this->message,
);
}

/**
* @param non-empty-string $key
*/
public function default(string $key, mixed $value): self
{
if (!\in_array($key, $this->optional, true)) {
throw new \LogicException("No optional key $key defined");
}

$defaults = $this->defaults;
/** @psalm-suppress MixedAssignment */
$defaults[$key] = $value;

return new self(
$this->constraints,
$this->optional,
$defaults,
$this->rename,
$this->message,
);
}

/**
* @param non-empty-string $from
* @param non-empty-string $to
*/
public function rename(string $from, string $to): self
{
$rename = $this->rename;
$rename[$from] = $to;

return new self(
$this->constraints,
$this->optional,
$this->defaults,
$rename,
$this->message,
);
}

/**
* @param callable(non-empty-string): non-empty-string $message
*/
public function withKeyFailure(callable $message): self
{
return new self($this->constraints, $this->optional, $message);
return new self(
$this->constraints,
$this->optional,
$this->defaults,
$this->rename,
$message,
);
}

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

$validation = $validation->and(
$keyValidation->and($ofType)($value),
static function($array, $value) use ($key, $optional) {
function($array, $value) use ($key, $optional) {
$concreteKey = $this->rename[$key] ?? $key;

if ($value !== $optional) {
/** @psalm-suppress MixedAssignment */
$array[$key] = $value;
$array[$concreteKey] = $value;
} else if (\array_key_exists($key, $this->defaults)) {
/** @psalm-suppress MixedAssignment */
$array[$concreteKey] = $this->defaults[$key];
}

return $array;
Expand Down

0 comments on commit 8a1d02d

Please sign in to comment.