diff options
Diffstat (limited to 'vendor/bakame/http-structured-fields/src/Validation')
8 files changed, 675 insertions, 0 deletions
diff --git a/vendor/bakame/http-structured-fields/src/Validation/ErrorCode.php b/vendor/bakame/http-structured-fields/src/Validation/ErrorCode.php new file mode 100644 index 000000000..3f6ed43ae --- /dev/null +++ b/vendor/bakame/http-structured-fields/src/Validation/ErrorCode.php @@ -0,0 +1,26 @@ +<?php + +namespace Bakame\Http\StructuredFields\Validation; + +/** + * General Error Code-. + * + * When adding new codes the name MUST be prefixed with + * a `@` to avoid conflicting with parameters keys. + */ +enum ErrorCode: string +{ + case ItemFailedParsing = '@item.failed.parsing'; + case ItemValueFailedValidation = '@item.value.failed.validation'; + case ParametersFailedParsing = '@parameters.failed.parsing'; + case ParametersMissingConstraints = '@parameters.missing.constraints'; + case ParametersFailedCriteria = '@parameters.failed.criteria'; + + /** + * @return array<string> + */ + public static function list(): array + { + return array_map(fn (self $case) => $case->value, self::cases()); + } +} diff --git a/vendor/bakame/http-structured-fields/src/Validation/ItemValidator.php b/vendor/bakame/http-structured-fields/src/Validation/ItemValidator.php new file mode 100644 index 000000000..21d8cdd2f --- /dev/null +++ b/vendor/bakame/http-structured-fields/src/Validation/ItemValidator.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); + +namespace Bakame\Http\StructuredFields\Validation; + +use Bakame\Http\StructuredFields\Item; +use Bakame\Http\StructuredFields\StructuredFieldProvider; +use Bakame\Http\StructuredFields\SyntaxError; +use Stringable; + +/** + * Structured field Item validator. + * + * @phpstan-import-type SfType from StructuredFieldProvider + */ +final class ItemValidator +{ + /** @var callable(SfType): (string|bool) */ + private mixed $valueConstraint; + private ParametersValidator $parametersConstraint; + + /** + * @param callable(SfType): (string|bool) $valueConstraint + */ + private function __construct( + callable $valueConstraint, + ParametersValidator $parametersConstraint, + ) { + $this->valueConstraint = $valueConstraint; + $this->parametersConstraint = $parametersConstraint; + } + + public static function new(): self + { + return new self(fn (mixed $value) => false, ParametersValidator::new()); + } + + /** + * Validates the Item value. + * + * On success populate the result item property + * On failure populates the result errors property + * + * @param callable(SfType): (string|bool) $constraint + */ + public function value(callable $constraint): self + { + return new self($constraint, $this->parametersConstraint); + } + + /** + * Validates the Item parameters as a whole. + * + * On failure populates the result errors property + */ + public function parameters(ParametersValidator $constraint): self + { + return new self($this->valueConstraint, $constraint); + } + + public function __invoke(Item|Stringable|string $item): bool|string + { + $result = $this->validate($item); + + return $result->isSuccess() ? true : (string) $result->errors; + } + + /** + * Validates the structured field Item. + */ + public function validate(Item|Stringable|string $item): Result + { + $violations = new ViolationList(); + if (!$item instanceof Item) { + try { + $item = Item::fromHttpValue($item); + } catch (SyntaxError $exception) { + $violations->add(ErrorCode::ItemFailedParsing->value, new Violation('The item string could not be parsed.', previous: $exception)); + + return Result::failed($violations); + } + } + + try { + $itemValue = $item->value($this->valueConstraint); + } catch (Violation $exception) { + $itemValue = null; + $violations->add(ErrorCode::ItemValueFailedValidation->value, $exception); + } + + $validate = $this->parametersConstraint->validate($item->parameters()); + $violations->addAll($validate->errors); + if ($violations->isNotEmpty()) { + return Result::failed($violations); + } + + /** @var ValidatedParameters $validatedParameters */ + $validatedParameters = $validate->data; + + return Result::success(new ValidatedItem($itemValue, $validatedParameters)); + } +} diff --git a/vendor/bakame/http-structured-fields/src/Validation/ParametersValidator.php b/vendor/bakame/http-structured-fields/src/Validation/ParametersValidator.php new file mode 100644 index 000000000..e851b9686 --- /dev/null +++ b/vendor/bakame/http-structured-fields/src/Validation/ParametersValidator.php @@ -0,0 +1,244 @@ +<?php + +declare(strict_types=1); + +namespace Bakame\Http\StructuredFields\Validation; + +use Bakame\Http\StructuredFields\Parameters; +use Bakame\Http\StructuredFields\StructuredFieldProvider; +use Bakame\Http\StructuredFields\SyntaxError; +use Stringable; + +/** + * Structured field Item validator. + * + * @phpstan-import-type SfType from StructuredFieldProvider + * + * @phpstan-type SfParameterKeyRule array{validate?:callable(SfType): (bool|string), required?:bool|string, default?:SfType|null} + * @phpstan-type SfParameterIndexRule array{validate?:callable(SfType, string): (bool|string), required?:bool|string, default?:array{0:string, 1:SfType}|array{}} + */ +final class ParametersValidator +{ + public const USE_KEYS = 1; + public const USE_INDICES = 2; + + /** @var ?callable(Parameters): (string|bool) */ + private mixed $criteria; + private int $type; + /** @var array<string, SfParameterKeyRule>|array<int, SfParameterIndexRule> */ + private array $filterConstraints; + + /** + * @param ?callable(Parameters): (string|bool) $criteria + * @param array<string, SfParameterKeyRule>|array<int, SfParameterIndexRule> $filterConstraints + */ + private function __construct( + ?callable $criteria = null, + int $type = self::USE_KEYS, + array $filterConstraints = [], + ) { + $this->criteria = $criteria; + $this->type = $type; + $this->filterConstraints = $filterConstraints; + } + + public static function new(): self + { + return new self(); + } + + /** + * Validates the Item parameters as a whole. + * + * On failure populates the result errors property + * + * @param ?callable(Parameters): (string|bool) $criteria + */ + public function filterByCriteria(?callable $criteria, int $type = self::USE_KEYS): self + { + return new self($criteria, [] === $this->filterConstraints ? $type : $this->type, $this->filterConstraints); + } + + /** + * Validate each parameters value per name. + * + * On success populate the result item property + * On failure populates the result errors property + * + * @param array<string, SfParameterKeyRule> $constraints + */ + public function filterByKeys(array $constraints): self + { + return new self($this->criteria, self::USE_KEYS, $constraints); + } + + /** + * Validate each parameters value per indices. + * + * On success populate the result item property + * On failure populates the result errors property + * + * @param array<int, SfParameterIndexRule> $constraints + */ + public function filterByIndices(array $constraints): self + { + return new self($this->criteria, self::USE_INDICES, $constraints); + } + + public function __invoke(Parameters|Stringable|string $parameters): bool|string + { + $result = $this->validate($parameters); + + return $result->isSuccess() ? true : (string) $result->errors; + } + + /** + * Validates the structured field Item. + */ + public function validate(Parameters|Stringable|string $parameters): Result + { + $violations = new ViolationList(); + if (!$parameters instanceof Parameters) { + try { + $parameters = Parameters::fromHttpValue($parameters); + } catch (SyntaxError $exception) { + $violations->add(ErrorCode::ParametersFailedParsing->value, new Violation('The parameters string could not be parsed.', previous: $exception)); + + return Result::failed($violations); + } + } + + if ([] === $this->filterConstraints && null === $this->criteria) { + $violations->add(ErrorCode::ParametersMissingConstraints->value, new Violation('The parameters constraints are missing.')); + } + + $parsedParameters = new ValidatedParameters(); + if ([] !== $this->filterConstraints) { + $parsedParameters = match ($this->type) { + self::USE_INDICES => $this->validateByIndices($parameters), + default => $this->validateByKeys($parameters), + }; + + if ($parsedParameters->isFailed()) { + $violations->addAll($parsedParameters->errors); + } else { + $parsedParameters = $parsedParameters->data; + } + } + + $errorMessage = $this->validateByCriteria($parameters); + if (!is_bool($errorMessage)) { + $violations->add(ErrorCode::ParametersFailedCriteria->value, new Violation($errorMessage)); + } + + /** @var ValidatedParameters $parsedParameters */ + $parsedParameters = $parsedParameters ?? new ValidatedParameters(); + if ([] === $this->filterConstraints && true === $errorMessage) { + $parsedParameters = new ValidatedParameters(match ($this->type) { + self::USE_KEYS => $this->toAssociative($parameters), + default => $this->toList($parameters), + }); + } + + return match ($violations->isNotEmpty()) { + true => Result::failed($violations), + default => Result::success($parsedParameters), + }; + } + + private function validateByCriteria(Parameters $parameters): bool|string + { + if (null === $this->criteria) { + return true; + } + + $errorMessage = ($this->criteria)($parameters); + if (true === $errorMessage) { + return true; + } + + if (!is_string($errorMessage) || '' === trim($errorMessage)) { + $errorMessage = 'The parameters constraints are not met.'; + } + + return $errorMessage; + } + + /** + * Validate the current parameter object using its keys and return the parsed values and the errors. + * + * @return Result<ValidatedParameters>|Result<null> + */ + private function validateByKeys(Parameters $parameters): Result /* @phpstan-ignore-line */ + { + $data = []; + $violations = new ViolationList(); + /** + * @var string $key + * @var SfParameterKeyRule $rule + */ + foreach ($this->filterConstraints as $key => $rule) { + try { + $data[$key] = $parameters->valueByKey($key, $rule['validate'] ?? null, $rule['required'] ?? false, $rule['default'] ?? null); + } catch (Violation $exception) { + $violations[$key] = $exception; + } + } + + return match ($violations->isNotEmpty()) { + true => Result::failed($violations), + default => Result::success(new ValidatedParameters($data)), + }; + } + + /** + * Validate the current parameter object using its indices and return the parsed values and the errors. + */ + public function validateByIndices(Parameters $parameters): Result + { + $data = []; + $violations = new ViolationList(); + /** + * @var int $index + * @var SfParameterIndexRule $rule + */ + foreach ($this->filterConstraints as $index => $rule) { + try { + $data[$index] = $parameters->valueByIndex($index, $rule['validate'] ?? null, $rule['required'] ?? false, $rule['default'] ?? []); + } catch (Violation $exception) { + $violations[$index] = $exception; + } + } + + return match ($violations->isNotEmpty()) { + true => Result::failed($violations), + default => Result::success(new ValidatedParameters($data)), + }; + } + + /** + * @return array<string,SfType> + */ + private function toAssociative(Parameters $parameters): array + { + $assoc = []; + foreach ($parameters as $parameter) { + $assoc[$parameter[0]] = $parameter[1]->value(); + } + + return $assoc; + } + + /** + * @return array<int, array{0:string, 1:SfType}> + */ + private function toList(Parameters $parameters): array + { + $list = []; + foreach ($parameters as $index => $parameter) { + $list[$index] = [$parameter[0], $parameter[1]->value()]; + } + + return $list; + } +} diff --git a/vendor/bakame/http-structured-fields/src/Validation/Result.php b/vendor/bakame/http-structured-fields/src/Validation/Result.php new file mode 100644 index 000000000..f765efda0 --- /dev/null +++ b/vendor/bakame/http-structured-fields/src/Validation/Result.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +namespace Bakame\Http\StructuredFields\Validation; + +final class Result +{ + private function __construct( + public readonly ValidatedParameters|ValidatedItem|null $data, + public readonly ViolationList $errors, + ) { + } + + public function isSuccess(): bool + { + return $this->errors->isEmpty(); + } + + public function isFailed(): bool + { + return $this->errors->isNotEmpty(); + } + + public static function success(ValidatedItem|ValidatedParameters $data): self + { + return new self($data, new ViolationList()); + } + + public static function failed(ViolationList $errors): self + { + return new self(null, $errors); + } +} diff --git a/vendor/bakame/http-structured-fields/src/Validation/ValidatedItem.php b/vendor/bakame/http-structured-fields/src/Validation/ValidatedItem.php new file mode 100644 index 000000000..df61a1aa8 --- /dev/null +++ b/vendor/bakame/http-structured-fields/src/Validation/ValidatedItem.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Bakame\Http\StructuredFields\Validation; + +use Bakame\Http\StructuredFields\Bytes; +use Bakame\Http\StructuredFields\DisplayString; +use Bakame\Http\StructuredFields\Token; +use DateTimeImmutable; + +final class ValidatedItem +{ + public function __construct( + public readonly Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool|null $value, + public readonly ValidatedParameters $parameters = new ValidatedParameters(), + ) { + } +} diff --git a/vendor/bakame/http-structured-fields/src/Validation/ValidatedParameters.php b/vendor/bakame/http-structured-fields/src/Validation/ValidatedParameters.php new file mode 100644 index 000000000..c7f5b5eb6 --- /dev/null +++ b/vendor/bakame/http-structured-fields/src/Validation/ValidatedParameters.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); + +namespace Bakame\Http\StructuredFields\Validation; + +use ArrayAccess; +use Bakame\Http\StructuredFields\ForbiddenOperation; +use Bakame\Http\StructuredFields\InvalidOffset; +use Bakame\Http\StructuredFields\StructuredFieldProvider; +use Countable; +use Iterator; +use IteratorAggregate; + +/** + * @phpstan-import-type SfType from StructuredFieldProvider + * + * @implements ArrayAccess<array-key, array{0:string, 1:SfType}|array{}|SfType|null> + * @implements IteratorAggregate<array-key, array{0:string, 1:SfType}|array{}|SfType|null> + */ +final class ValidatedParameters implements ArrayAccess, Countable, IteratorAggregate +{ + /** + * @param array<array-key, array{0:string, 1:SfType}|array{}|SfType|null> $values + */ + public function __construct( + private readonly array $values = [], + ) { + } + + public function count(): int + { + return count($this->values); + } + + public function getIterator(): Iterator + { + yield from $this->values; + } + + public function offsetExists($offset): bool + { + return array_key_exists($offset, $this->values); + } + + public function offsetGet($offset): mixed + { + return $this->offsetExists($offset) ? $this->values[$offset] : throw InvalidOffset::dueToMemberNotFound($offset); + } + + public function offsetUnset(mixed $offset): void + { + throw new ForbiddenOperation(self::class.' instance can not be updated using '.ArrayAccess::class.' methods.'); + } + + public function offsetSet(mixed $offset, mixed $value): void + { + throw new ForbiddenOperation(self::class.' instance can not be updated using '.ArrayAccess::class.' methods.'); + } + + /** + * @return array<array-key, array{0:string, 1:SfType}|array{}|SfType|null> + */ + public function all(): array + { + return $this->values; + } +} diff --git a/vendor/bakame/http-structured-fields/src/Validation/Violation.php b/vendor/bakame/http-structured-fields/src/Validation/Violation.php new file mode 100644 index 000000000..ec666de98 --- /dev/null +++ b/vendor/bakame/http-structured-fields/src/Validation/Violation.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace Bakame\Http\StructuredFields\Validation; + +use Bakame\Http\StructuredFields\StructuredFieldError; +use LogicException; + +final class Violation extends LogicException implements StructuredFieldError +{ +} diff --git a/vendor/bakame/http-structured-fields/src/Validation/ViolationList.php b/vendor/bakame/http-structured-fields/src/Validation/ViolationList.php new file mode 100644 index 000000000..8cbf5e741 --- /dev/null +++ b/vendor/bakame/http-structured-fields/src/Validation/ViolationList.php @@ -0,0 +1,169 @@ +<?php + +declare(strict_types=1); + +namespace Bakame\Http\StructuredFields\Validation; + +use ArrayAccess; +use Bakame\Http\StructuredFields\InvalidOffset; +use Countable; +use Iterator; +use IteratorAggregate; +use Stringable; +use TypeError; + +use function array_filter; +use function array_map; +use function count; +use function implode; +use function is_int; + +use const ARRAY_FILTER_USE_BOTH; + +/** + * @implements IteratorAggregate<array-key,Violation> + * @implements ArrayAccess<array-key,Violation> + */ +final class ViolationList implements IteratorAggregate, Countable, ArrayAccess, Stringable +{ + /** @var array<Violation> */ + private array $errors = []; + + /** + * @param iterable<array-key, Violation> $errors + */ + public function __construct(iterable $errors = []) + { + $this->addAll($errors); + } + + public function count(): int + { + return count($this->errors); + } + + public function getIterator(): Iterator + { + yield from $this->errors; + } + + public function __toString(): string + { + return implode(PHP_EOL, array_map(fn (Violation $e): string => $e->getMessage(), $this->errors)); + } + + /** + * @return array<array-key, string> + */ + public function summary(): array + { + return array_map(fn (Violation $e): string => $e->getMessage(), $this->errors); + } + + public function isEmpty(): bool + { + return [] === $this->errors; + } + + public function isNotEmpty(): bool + { + return ! $this->isEmpty(); + } + + /** + * @param string|int $offset + */ + public function offsetExists(mixed $offset): bool + { + return $this->has($offset); + } + + /** + * @param string|int $offset + * + * @return Violation + */ + public function offsetGet(mixed $offset): mixed + { + return $this->get($offset); + } + + /** + * @param string|int $offset + */ + public function offsetUnset(mixed $offset): void + { + unset($this->errors[$offset]); + } + + /** + * @param string|int|null $offset + * @param Violation $value + */ + public function offsetSet(mixed $offset, mixed $value): void + { + if (null === $offset) { + throw new TypeError('null can not be used as a valid offset value.'); + } + $this->add($offset, $value); + } + + public function has(string|int $offset): bool + { + if (is_int($offset)) { + return null !== $this->filterIndex($offset); + } + + return array_key_exists($offset, $this->errors); + } + + public function get(string|int $offset): Violation + { + return $this->errors[$this->filterIndex($offset) ?? throw InvalidOffset::dueToIndexNotFound($offset)]; + } + + public function add(string|int $offset, Violation $error): void + { + $this->errors[$offset] = $error; + } + + /** + * @param iterable<array-key, Violation> $errors + */ + public function addAll(iterable $errors): void + { + foreach ($errors as $offset => $error) { + $this->add($offset, $error); + } + } + + private function filterIndex(string|int $index, int|null $max = null): string|int|null + { + if (!is_int($index)) { + return $index; + } + + $max ??= count($this->errors); + + return match (true) { + [] === $this->errors, + 0 > $max + $index, + 0 > $max - $index - 1 => null, + 0 > $index => $max + $index, + default => $index, + }; + } + + /** + * @param callable(Violation, array-key): bool $callback + */ + public function filter(callable $callback): self + { + return new self(array_filter($this->errors, $callback, ARRAY_FILTER_USE_BOTH)); + } + + public function toException(): Violation + { + return new Violation((string) $this); + } +} |