Skip to content

Commit

Permalink
Update the validation engine of wrapper-based rules
Browse files Browse the repository at this point in the history
Signed-off-by: Henrique Moody <[email protected]>
  • Loading branch information
henriquemoody committed Feb 22, 2024
1 parent e341fef commit 99dc872
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 598 deletions.
61 changes: 61 additions & 0 deletions library/Helpers/CanExtractRules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <[email protected]>
* SPDX-License-Identifier: MIT
*/

declare(strict_types=1);

namespace Respect\Validation\Helpers;

use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Validatable;
use Respect\Validation\Validator;

use function array_map;
use function count;
use function current;
use function sprintf;

trait CanExtractRules
{
private function extractSingle(Validatable $rule, string $class): Validatable
{
if ($rule instanceof Validator) {
return $this->extractSingleFromValidator($rule, $class);
}

if (!$rule instanceof $class) {
throw new ComponentException(sprintf(
'Could not extract rule %s from %s',
$class,
$rule::class,
));
}

return $rule;
}

/**
* @param array<Validatable> $rules
*
* @return array<Validatable>
*/
private function extractMany(array $rules, string $class): array
{
return array_map(fn (Validatable $rule) => $this->extractSingle($rule, $class), $rules);
}

private function extractSingleFromValidator(Validator $rule, string $class): Validatable
{
$rules = $rule->getRules();
if (count($rules) !== 1) {
throw new ComponentException(sprintf(
'Validator must contain exactly one rule'
));
}

return $this->extractSingle(current($rules), $class);
}
}
55 changes: 0 additions & 55 deletions library/Rules/AbstractWrapper.php

This file was deleted.

159 changes: 38 additions & 121 deletions library/Rules/KeySet.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,153 +9,70 @@

namespace Respect\Validation\Rules;

use Respect\Validation\Attributes\ExceptionClass;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Exceptions\NonOmissibleValidationException;
use Respect\Validation\Helpers\CanBindEvaluateRule;
use Respect\Validation\Helpers\CanExtractRules;
use Respect\Validation\Message\Template;
use Respect\Validation\NonNegatable;
use Respect\Validation\Result;
use Respect\Validation\Validatable;

use function array_key_exists;
use function array_diff;
use function array_keys;
use function array_map;
use function array_values;
use function count;
use function current;
use function is_array;

#[ExceptionClass(NonOmissibleValidationException::class)]
#[Template(
'All of the required rules must pass for {{name}}',
'',
self::TEMPLATE_NONE,
'Must have keys {{missingKeys}} in {{name}}',
'Must not have keys {{missingKeys}} in {{name}}',
self::TEMPLATE_MISSING,
)]
#[Template(
'These rules must pass for {{name}}',
'',
self::TEMPLATE_SOME,
'Must not have keys {{extraKeys}} in {{name}}',
'Must have keys {{extraKeys}} in {{name}}',
self::TEMPLATE_EXTRA,
)]
#[Template(
'Must have keys {{keys}}',
'',
self::TEMPLATE_STRUCTURE,
)]
#[Template(
'Must not have keys {{extraKeys}}',
'',
self::TEMPLATE_STRUCTURE_EXTRA,
)]
final class KeySet extends AbstractWrapper implements NonNegatable
final class KeySet extends Wrapper
{
public const TEMPLATE_NONE = '__none__';
public const TEMPLATE_SOME = '__some__';
public const TEMPLATE_STRUCTURE = '__structure__';
public const TEMPLATE_STRUCTURE_EXTRA = '__structure_extra__';

/**
* @var mixed[]
*/
private readonly array $keys;

/**
* @var mixed[]
*/
private array $extraKeys = [];

/**
* @var Key[]
*/
private readonly array $keyRules;

public function __construct(Validatable ...$validatables)
{
$this->keyRules = array_map([$this, 'getKeyRule'], $validatables);
$this->keys = array_map([$this, 'getKeyReference'], $this->keyRules);

parent::__construct(new AllOf(...$this->keyRules));
}

public function assert(mixed $input): void
{
if (!$this->hasValidStructure($input)) {
throw $this->reportError($input);
}

parent::assert($input);
}

public function check(mixed $input): void
{
if (!$this->hasValidStructure($input)) {
throw $this->reportError($input);
}
use CanBindEvaluateRule;
use CanExtractRules;

parent::check($input);
}

public function validate(mixed $input): bool
{
if (!$this->hasValidStructure($input)) {
return false;
}

return parent::validate($input);
}
public const TEMPLATE_MISSING = '__missing__';
public const TEMPLATE_EXTRA = '__extra__';

/**
* @return array<string, mixed>
*/
public function getParams(): array
{
return [
'keys' => $this->keys,
'extraKeys' => $this->extraKeys,
];
}

protected function getStandardTemplate(mixed $input): string
{
if (count($this->extraKeys)) {
return self::TEMPLATE_STRUCTURE_EXTRA;
}

return KeySet::TEMPLATE_STRUCTURE;
}
/** @var array<string|int> */
private readonly array $keys;

private function getKeyRule(Validatable $validatable): Key
public function __construct(Validatable ...$rules)
{
if ($validatable instanceof Key) {
return $validatable;
}
/** @var array<Key> $keyRules */
$keyRules = $this->extractMany($rules, Key::class);

if (!$validatable instanceof AllOf || count($validatable->getRules()) !== 1) {
throw new ComponentException('KeySet rule accepts only Key rules');
}

return $this->getKeyRule(current($validatable->getRules()));
}
$this->keys = array_map(static fn(Key $rule) => $rule->getReference(), $keyRules);

private function getKeyReference(Key $rule): mixed
{
return $rule->getReference();
parent::__construct(new AllOf(...$keyRules));
}

private function hasValidStructure(mixed $input): bool
public function evaluate(mixed $input): Result
{
if (!is_array($input)) {
return false;
$result = $this->bindEvaluate(new ArrayType(), $this, $input);
if (!$result->isValid) {
return $result;
}

foreach ($this->keyRules as $keyRule) {
if (!array_key_exists($keyRule->getReference(), $input) && $keyRule->isMandatory()) {
return false;
}
$inputKeys = array_keys($input);

unset($input[$keyRule->getReference()]);
$missingKeys = array_diff($this->keys, $inputKeys);
if (count($missingKeys) > 0) {
return Result::failed($input, $this, self::TEMPLATE_MISSING)
->withParameters(['missingKeys' => array_values($missingKeys)]);
}

foreach ($input as $extraKey => &$ignoreValue) {
$this->extraKeys[] = $extraKey;
$extraKeys = array_diff($inputKeys, $this->keys);
if (count($extraKeys) > 0) {
return Result::failed($input, $this, self::TEMPLATE_EXTRA)
->withParameters(['extraKeys' => array_values($extraKeys)]);
}

return count($input) == 0;
return parent::evaluate($input);
}
}
38 changes: 8 additions & 30 deletions library/Rules/Nullable.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace Respect\Validation\Rules;

use Respect\Validation\Message\Template;
use Respect\Validation\Result;

#[Template(
'The value must be nullable',
Expand All @@ -21,43 +22,20 @@
'{{name}} must not be null',
self::TEMPLATE_NAMED,
)]
final class Nullable extends AbstractWrapper
final class Nullable extends Wrapper
{
public const TEMPLATE_NAMED = '__named__';

public function assert(mixed $input): void
public function evaluate(mixed $input): Result
{
if ($input === null) {
return;
if ($input !== null) {
return parent::evaluate($input);
}

parent::assert($input);
}

public function check(mixed $input): void
{
if ($input === null) {
return;
}

parent::check($input);
}

public function validate(mixed $input): bool
{
if ($input === null) {
return true;
}

return parent::validate($input);
}

protected function getStandardTemplate(mixed $input): string
{
if ($input || $this->getName()) {
return self::TEMPLATE_NAMED;
if ($this->getName()) {
return Result::passed($input, $this, self::TEMPLATE_NAMED);
}

return self::TEMPLATE_STANDARD;
return Result::passed($input, $this);
}
}
Loading

0 comments on commit 99dc872

Please sign in to comment.