aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/bakame/http-structured-fields
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/bakame/http-structured-fields')
-rw-r--r--vendor/bakame/http-structured-fields/LICENSE21
-rw-r--r--vendor/bakame/http-structured-fields/composer.json106
-rw-r--r--vendor/bakame/http-structured-fields/src/Bytes.php83
-rw-r--r--vendor/bakame/http-structured-fields/src/DataType.php45
-rw-r--r--vendor/bakame/http-structured-fields/src/Dictionary.php788
-rw-r--r--vendor/bakame/http-structured-fields/src/DisplayString.php103
-rw-r--r--vendor/bakame/http-structured-fields/src/ForbiddenOperation.php11
-rw-r--r--vendor/bakame/http-structured-fields/src/Ietf.php73
-rw-r--r--vendor/bakame/http-structured-fields/src/InnerList.php478
-rw-r--r--vendor/bakame/http-structured-fields/src/InvalidArgument.php11
-rw-r--r--vendor/bakame/http-structured-fields/src/InvalidOffset.php38
-rw-r--r--vendor/bakame/http-structured-fields/src/Item.php502
-rw-r--r--vendor/bakame/http-structured-fields/src/Key.php53
-rw-r--r--vendor/bakame/http-structured-fields/src/Member.php116
-rw-r--r--vendor/bakame/http-structured-fields/src/MissingFeature.php13
-rw-r--r--vendor/bakame/http-structured-fields/src/OuterList.php430
-rw-r--r--vendor/bakame/http-structured-fields/src/ParameterAccess.php260
-rw-r--r--vendor/bakame/http-structured-fields/src/Parameters.php763
-rw-r--r--vendor/bakame/http-structured-fields/src/Parser.php523
-rw-r--r--vendor/bakame/http-structured-fields/src/StructuredFieldError.php11
-rw-r--r--vendor/bakame/http-structured-fields/src/StructuredFieldProvider.php28
-rw-r--r--vendor/bakame/http-structured-fields/src/SyntaxError.php11
-rw-r--r--vendor/bakame/http-structured-fields/src/Token.php52
-rw-r--r--vendor/bakame/http-structured-fields/src/Type.php88
-rw-r--r--vendor/bakame/http-structured-fields/src/Validation/ErrorCode.php26
-rw-r--r--vendor/bakame/http-structured-fields/src/Validation/ItemValidator.php103
-rw-r--r--vendor/bakame/http-structured-fields/src/Validation/ParametersValidator.php244
-rw-r--r--vendor/bakame/http-structured-fields/src/Validation/Result.php34
-rw-r--r--vendor/bakame/http-structured-fields/src/Validation/ValidatedItem.php19
-rw-r--r--vendor/bakame/http-structured-fields/src/Validation/ValidatedParameters.php68
-rw-r--r--vendor/bakame/http-structured-fields/src/Validation/Violation.php12
-rw-r--r--vendor/bakame/http-structured-fields/src/Validation/ViolationList.php169
32 files changed, 5282 insertions, 0 deletions
diff --git a/vendor/bakame/http-structured-fields/LICENSE b/vendor/bakame/http-structured-fields/LICENSE
new file mode 100644
index 000000000..225c502ed
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Nyamagana Butera
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/bakame/http-structured-fields/composer.json b/vendor/bakame/http-structured-fields/composer.json
new file mode 100644
index 000000000..9a8241ee2
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/composer.json
@@ -0,0 +1,106 @@
+{
+ "name": "bakame/http-structured-fields",
+ "description": "A PHP library that parses, validates and serializes HTTP structured fields according to RFC9561 and RFC8941",
+ "type": "library",
+ "keywords": [
+ "http",
+ "http headers",
+ "http trailers",
+ "headers",
+ "trailers",
+ "structured fields",
+ "structured headers",
+ "structured trailers",
+ "structured values",
+ "parser",
+ "serializer",
+ "validation",
+ "rfc8941",
+ "rfc9651"
+ ],
+ "license": "MIT",
+ "authors": [
+ {
+ "name" : "Ignace Nyamagana Butera",
+ "email" : "nyamsprod@gmail.com",
+ "homepage" : "https://github.com/nyamsprod/",
+ "role" : "Developer"
+ }
+ ],
+ "support": {
+ "docs": "https://github.com/bakame-php/http-structured-fields",
+ "issues": "https://github.com/bakame-php/http-structured-fields/issues",
+ "source": "https://github.com/bakame-php/http-structured-fields"
+ },
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nyamsprod"
+ }
+ ],
+ "require": {
+ "php" : "^8.1"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.65.0",
+ "httpwg/structured-field-tests": "*@dev",
+ "phpstan/phpstan": "^2.0.3",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0.1",
+ "phpstan/phpstan-deprecation-rules": "^2.0.1",
+ "phpunit/phpunit": "^10.5.38 || ^11.5.0",
+ "symfony/var-dumper": "^6.4.15 || ^v7.2.0",
+ "bakame/aide-base32": "dev-main",
+ "phpbench/phpbench": "^1.3.1"
+ },
+ "autoload": {
+ "psr-4": {
+ "Bakame\\Http\\StructuredFields\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Bakame\\Http\\StructuredFields\\": "tests/"
+ }
+ },
+ "scripts": {
+ "benchmark": "phpbench run --report=default",
+ "phpcs": "php-cs-fixer fix --dry-run --diff -vvv --allow-risky=yes --ansi",
+ "phpcs:fix": "php-cs-fixer fix -vvv --allow-risky=yes --ansi",
+ "phpstan": "phpstan analyse -c phpstan.neon --ansi --memory-limit 192M",
+ "phpunit": "XDEBUG_MODE=coverage phpunit --coverage-text",
+ "phpunit:min": "phpunit --no-coverage",
+ "test": [
+ "@phpunit",
+ "@phpstan",
+ "@phpcs"
+ ]
+ },
+ "scripts-descriptions": {
+ "benchmark": "Runs parser benchmark",
+ "phpstan": "Runs complete codebase static analysis",
+ "phpunit": "Runs unit and functional testing",
+ "phpcs": "Runs coding style testing",
+ "phpcs:fix": "Fix coding style issues",
+ "test": "Runs all tests"
+ },
+ "repositories": [
+ {
+ "type": "package",
+ "package": {
+ "name": "httpwg/structured-field-tests",
+ "version": "dev-main",
+ "source": {
+ "url": "https://github.com/httpwg/structured-field-tests.git",
+ "type": "git",
+ "reference": "main"
+ }
+ }
+ }
+ ],
+ "extra": {
+ "branch-alias": {
+ "dev-develop": "1.x-dev"
+ }
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/Bytes.php b/vendor/bakame/http-structured-fields/src/Bytes.php
new file mode 100644
index 000000000..aa747b76b
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/Bytes.php
@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use Stringable;
+use Throwable;
+
+use function base64_decode;
+use function base64_encode;
+use function preg_match;
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.3.5
+ */
+final class Bytes
+{
+ private function __construct(private readonly string $value)
+ {
+ }
+
+ /**
+ * Returns a new instance from a Base64 encoded string.
+ */
+ public static function fromEncoded(Stringable|string $encoded): self
+ {
+ $encoded = (string) $encoded;
+ if (1 !== preg_match('/^[a-z\d+\/=]*$/i', $encoded)) {
+ throw new SyntaxError('The byte sequence '.$encoded.' contains invalid characters.');
+ }
+
+ $decoded = base64_decode($encoded, true);
+ if (false === $decoded) {
+ throw new SyntaxError('Unable to base64 decode the byte sequence '.$encoded);
+ }
+
+ return new self($decoded);
+ }
+
+ public static function tryFromEncoded(Stringable|string $encoded): ?self
+ {
+ try {
+ return self::fromEncoded($encoded);
+ } catch (Throwable) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a new instance from a raw decoded string.
+ */
+ public static function fromDecoded(Stringable|string $decoded): self
+ {
+ return new self((string) $decoded);
+ }
+
+ /**
+ * Returns the decoded string.
+ */
+ public function decoded(): string
+ {
+ return $this->value;
+ }
+
+ /**
+ * Returns the base64 encoded string.
+ */
+ public function encoded(): string
+ {
+ return base64_encode($this->value);
+ }
+
+ public function equals(mixed $other): bool
+ {
+ return $other instanceof self && $other->value === $this->value;
+ }
+
+ public function type(): Type
+ {
+ return Type::Bytes;
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/DataType.php b/vendor/bakame/http-structured-fields/src/DataType.php
new file mode 100644
index 000000000..8cc5f5395
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/DataType.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use Exception;
+use Stringable;
+
+enum DataType: string
+{
+ case List = 'list';
+ case InnerList = 'innerlist';
+ case Parameters = 'parameters';
+ case Dictionary = 'dictionary';
+ case Item = 'item';
+
+ /**
+ * @throws SyntaxError|Exception
+ */
+ public function parse(Stringable|string $httpValue, Ietf $rfc = Ietf::Rfc9651): OuterList|InnerList|Parameters|Dictionary|Item
+ {
+ return match ($this) {
+ self::List => OuterList::fromHttpValue($httpValue, $rfc),
+ self::Dictionary => Dictionary::fromHttpValue($httpValue, $rfc),
+ self::Item => Item::fromHttpValue($httpValue, $rfc),
+ self::InnerList => InnerList::fromHttpValue($httpValue, $rfc),
+ self::Parameters => Parameters::fromHttpValue($httpValue, $rfc),
+ };
+ }
+
+ /**
+ * @throws SyntaxError|Exception
+ */
+ public function serialize(iterable $data, Ietf $rfc = Ietf::Rfc9651): string
+ {
+ return (match ($this) {
+ self::List => OuterList::fromPairs($data),
+ self::Dictionary => Dictionary::fromPairs($data),
+ self::Item => Item::fromPair([...$data]),
+ self::InnerList => InnerList::fromPair([...$data]),
+ self::Parameters => Parameters::fromPairs($data),
+ })->toHttpValue($rfc);
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/Dictionary.php b/vendor/bakame/http-structured-fields/src/Dictionary.php
new file mode 100644
index 000000000..8a318695c
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/Dictionary.php
@@ -0,0 +1,788 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use ArrayAccess;
+use Bakame\Http\StructuredFields\Validation\Violation;
+use CallbackFilterIterator;
+use Countable;
+use DateTimeInterface;
+use Exception;
+use Iterator;
+use IteratorAggregate;
+use Stringable;
+use Throwable;
+
+use function array_key_exists;
+use function array_keys;
+use function array_map;
+use function count;
+use function implode;
+use function is_bool;
+use function is_int;
+use function is_iterable;
+use function is_string;
+use function uasort;
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.2
+ *
+ * @phpstan-import-type SfMemberInput from StructuredFieldProvider
+ *
+ * @implements ArrayAccess<string, InnerList|Item>
+ * @implements IteratorAggregate<int, array{0:string, 1:InnerList|Item}>
+ */
+final class Dictionary implements ArrayAccess, Countable, IteratorAggregate
+{
+ /** @var array<string, InnerList|Item> */
+ private readonly array $members;
+
+ /**
+ * @param iterable<string, SfMemberInput> $members
+ */
+ private function __construct(iterable $members = [])
+ {
+ $filteredMembers = [];
+ foreach ($members as $key => $member) {
+ $filteredMembers[Key::from($key)->value] = Member::innerListOrItem($member);
+ }
+
+ $this->members = $filteredMembers;
+ }
+
+ /**
+ * Returns a new instance.
+ */
+ public static function new(): self
+ {
+ return new self();
+ }
+
+ /**
+ * Returns a new instance from an associative iterable construct.
+ *
+ * its keys represent the dictionary entry name
+ * its values represent the dictionary entry value
+ *
+ * @param StructuredFieldProvider|iterable<string, SfMemberInput> $members
+ */
+ public static function fromAssociative(StructuredFieldProvider|iterable $members): self
+ {
+ if ($members instanceof StructuredFieldProvider) {
+ $structuredField = $members->toStructuredField();
+
+ return match (true) {
+ $structuredField instanceof Dictionary,
+ $structuredField instanceof Parameters => new self($structuredField->toAssociative()),
+ default => throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a structured field container; '.$structuredField::class.' given.'),
+ };
+ }
+
+ return new self($members);
+ }
+
+ /**
+ * Returns a new instance from a pair iterable construct.
+ *
+ * Each member is composed of an array with two elements
+ * the first member represents the instance entry key
+ * the second member represents the instance entry value
+ *
+ * @param StructuredFieldProvider|Dictionary|Parameters|iterable<array{0:string, 1?:SfMemberInput}> $pairs
+ */
+ public static function fromPairs(StructuredFieldProvider|Dictionary|Parameters|iterable $pairs): self
+ {
+ if ($pairs instanceof StructuredFieldProvider) {
+ $pairs = $pairs->toStructuredField();
+ }
+
+ if (!is_iterable($pairs)) {
+ throw new InvalidArgument('The "'.$pairs::class.'" instance can not be used for creating a .'.self::class.' structured field.');
+ }
+
+ return match (true) {
+ $pairs instanceof Dictionary,
+ $pairs instanceof Parameters => new self($pairs->toAssociative()),
+ default => new self((function (iterable $pairs) {
+ foreach ($pairs as [$key, $member]) {
+ yield $key => Member::innerListOrItemFromPair($member);
+ }
+ })($pairs)),
+ };
+ }
+
+ /**
+ * Returns an instance from an HTTP textual representation compliant with RFC9651.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.2
+ *
+ * @throws StructuredFieldError|Throwable
+ */
+ public static function fromRfc9651(Stringable|string $httpValue): self
+ {
+ return self::fromHttpValue($httpValue, Ietf::Rfc9651);
+ }
+
+ /**
+ * Returns an instance from an HTTP textual representation compliant with RFC8941.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc8941.html#section-3.2
+ *
+ * @throws StructuredFieldError|Throwable
+ */
+ public static function fromRfc8941(Stringable|string $httpValue): self
+ {
+ return self::fromHttpValue($httpValue, Ietf::Rfc8941);
+ }
+
+ /**
+ * Returns an instance from an HTTP textual representation.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.2
+ *
+ * @throws StructuredFieldError|Exception If the string is not a valid
+ */
+ public static function fromHttpValue(Stringable|string $httpValue, Ietf $rfc = Ietf::Rfc9651): self
+ {
+ return self::fromPairs((new Parser($rfc))->parseDictionary($httpValue)); /* @phpstan-ignore-line */
+ }
+
+ public function toRfc9651(): string
+ {
+ return $this->toHttpValue(Ietf::Rfc9651);
+ }
+
+ public function toRfc8941(): string
+ {
+ return $this->toHttpValue(Ietf::Rfc8941);
+ }
+
+ public function toHttpValue(Ietf $rfc = Ietf::Rfc9651): string
+ {
+ $formatter = static fn (Item|InnerList $member, string $offset): string => match (true) {
+ $member instanceof Item && true === $member->value() => $offset.$member->parameters()->toHttpValue($rfc),
+ default => $offset.'='.$member->toHttpValue($rfc),
+ };
+
+ return implode(', ', array_map($formatter, $this->members, array_keys($this->members)));
+ }
+
+ public function __toString(): string
+ {
+ return $this->toHttpValue();
+ }
+
+ public function equals(mixed $other): bool
+ {
+ return $other instanceof self && $other->toHttpValue() === $this->toHttpValue();
+ }
+
+ /**
+ * Apply the callback if the given "condition" is (or resolves to) true.
+ *
+ * @param (callable($this): bool)|bool $condition
+ * @param callable($this): (self|null) $onSuccess
+ * @param ?callable($this): (self|null) $onFail
+ */
+ public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
+ {
+ if (!is_bool($condition)) {
+ $condition = $condition($this);
+ }
+
+ return match (true) {
+ $condition => $onSuccess($this),
+ null !== $onFail => $onFail($this),
+ default => $this,
+ } ?? $this;
+ }
+
+ public function count(): int
+ {
+ return count($this->members);
+ }
+
+ /**
+ * Tells whether the instance contains no members.
+ */
+ public function isEmpty(): bool
+ {
+ return !$this->isNotEmpty();
+ }
+
+ /**
+ * Tells whether the instance contains any members.
+ */
+ public function isNotEmpty(): bool
+ {
+ return [] !== $this->members;
+ }
+
+ /**
+ * @return Iterator<string, InnerList|Item>
+ */
+ public function toAssociative(): Iterator
+ {
+ yield from $this->members;
+ }
+
+ /**
+ * Returns an iterable construct of dictionary pairs.
+ *
+ * @return Iterator<int, array{0:string, 1:InnerList|Item}>
+ */
+ public function getIterator(): Iterator
+ {
+ foreach ($this->members as $index => $member) {
+ yield [$index, $member];
+ }
+ }
+
+ /**
+ * Returns an ordered list of the instance keys.
+ *
+ * @return array<string>
+ */
+ public function keys(): array
+ {
+ return array_keys($this->members);
+ }
+
+ /**
+ * @return array<int>
+ */
+ public function indices(): array
+ {
+ return array_keys($this->keys());
+ }
+
+ /**
+ * Tells whether the instance contain a members at the specified offsets.
+ */
+ public function hasKeys(string ...$keys): bool
+ {
+ foreach ($keys as $key) {
+ if (!array_key_exists($key, $this->members)) {
+ return false;
+ }
+ }
+
+ return [] !== $keys;
+ }
+
+ /**
+ * Returns true only if the instance only contains the listed keys, false otherwise.
+ *
+ * @param array<string> $keys
+ */
+ public function allowedKeys(array $keys): bool
+ {
+ foreach ($this->members as $key => $member) {
+ if (!in_array($key, $keys, true)) {
+ return false;
+ }
+ }
+
+ return [] !== $keys;
+ }
+
+ /**
+ * @param ?callable(Item|InnerList): (bool|string) $validate
+ *
+ * @throws InvalidOffset|Violation|StructuredFieldError
+ */
+ public function getByKey(string $key, ?callable $validate = null): Item|InnerList
+ {
+ $value = $this->members[$key] ?? throw InvalidOffset::dueToKeyNotFound($key);
+ if (null === $validate || true === ($exceptionMessage = $validate($value))) {
+ return $value;
+ }
+
+ if (!is_string($exceptionMessage) || '' === trim($exceptionMessage)) {
+ $exceptionMessage = "The parameter '{key}' whose value is '{value}' failed validation.";
+ }
+
+ throw new Violation(strtr($exceptionMessage, ['{key}' => $key, '{value}' => $value->toHttpValue()]));
+ }
+
+ /**
+ * Tells whether a pair is attached to the given index position.
+ */
+ public function hasIndices(int ...$indexes): bool
+ {
+ $max = count($this->members);
+ foreach ($indexes as $index) {
+ if (null === $this->filterIndex($index, $max)) {
+ return false;
+ }
+ }
+
+ return [] !== $indexes;
+ }
+
+ /**
+ * Filters and format instance index.
+ */
+ private function filterIndex(int $index, ?int $max = null): ?int
+ {
+ $max ??= count($this->members);
+
+ return match (true) {
+ [] === $this->members,
+ 0 > $max + $index,
+ 0 > $max - $index - 1 => null,
+ 0 > $index => $max + $index,
+ default => $index,
+ };
+ }
+
+ /**
+ * Returns the item or the inner-list and its key as attached to the given
+ * collection according to their index position otherwise throw.
+ *
+ * @param ?callable(Item|InnerList, string): (bool|string) $validate
+ *
+ * @throws InvalidOffset|Violation|StructuredFieldError
+ *
+ * @return array{0:string, 1:InnerList|Item}
+ */
+ public function getByIndex(int $index, ?callable $validate = null): array
+ {
+ $foundOffset = $this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index);
+ foreach ($this as $offset => $pair) {
+ if ($offset === $foundOffset) {
+ break;
+ }
+ }
+
+ if (!isset($pair)) {
+ throw InvalidOffset::dueToIndexNotFound($index);
+ }
+
+ if (null === $validate || true === ($exceptionMessage = $validate($pair[1], $pair[0]))) {
+ return $pair;
+ }
+
+ if (!is_string($exceptionMessage) || '' === trim($exceptionMessage)) {
+ $exceptionMessage = "The member at position '{index}' whose key is '{key}' with the value '{value}' failed validation.";
+ }
+
+ throw new Violation(strtr($exceptionMessage, ['{index}' => $index, '{key}' => $pair[0], '{value}' => $pair[1]->toHttpValue()]));
+ }
+
+ /**
+ * Returns the key associated with the given index or null otherwise.
+ */
+ public function indexByKey(string $key): ?int
+ {
+ foreach ($this as $index => $member) {
+ if ($key === $member[0]) {
+ return $index;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the index associated with the given key or null otherwise.
+ */
+ public function keyByIndex(int $index): ?string
+ {
+ $index = $this->filterIndex($index);
+ if (null === $index) {
+ return null;
+ }
+
+ foreach ($this as $offset => $member) {
+ if ($offset === $index) {
+ return $member[0];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the first member whether it is an item or an inner-list and its key as attached to the given
+ * collection according to their index position otherwise returns an empty array.
+ *
+ * @return array{0:string, 1:InnerList|Item}|array{}
+ */
+ public function first(): array
+ {
+ try {
+ return $this->getByIndex(0);
+ } catch (StructuredFieldError) {
+ return [];
+ }
+ }
+
+ /**
+ * Returns the first member whether it is an item or an inner-list and its key as attached to the given
+ * collection according to their index position otherwise returns an empty array.
+ *
+ * @return array{0:string, 1:InnerList|Item}|array{}
+ */
+ public function last(): array
+ {
+ try {
+ return $this->getByIndex(-1);
+ } catch (StructuredFieldError) {
+ return [];
+ }
+ }
+
+ /**
+ * Adds a member at the end of the instance otherwise updates the value associated with the key if already present.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param SfMemberInput $member
+ *
+ * @throws SyntaxError If the string key is not a valid
+ */
+ public function add(
+ string $key,
+ iterable|StructuredFieldProvider|Dictionary|Parameters|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): self {
+ $members = $this->members;
+ $members[Key::from($key)->value] = Member::innerListOrItem($member);
+
+ return $this->newInstance($members);
+ }
+
+ /**
+ * @param array<string, InnerList|Item> $members
+ */
+ private function newInstance(array $members): self
+ {
+ foreach ($members as $offset => $member) {
+ if (!isset($this->members[$offset]) || !$this->members[$offset]->equals($member)) {
+ return new self($members);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Deletes members associated with the list of submitted keys.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ */
+ private function remove(string|int ...$offsets): self
+ {
+ if ([] === $this->members || [] === $offsets) {
+ return $this;
+ }
+
+ $keys = array_keys($this->members);
+ $max = count($keys);
+ $reducer = fn (array $carry, string|int $key): array => match (true) {
+ is_string($key) && (false !== ($position = array_search($key, $keys, true))),
+ is_int($key) && (null !== ($position = $this->filterIndex($key, $max))) => [$position => true] + $carry,
+ default => $carry,
+ };
+
+ $indices = array_reduce($offsets, $reducer, []);
+
+ return match (true) {
+ [] === $indices => $this,
+ $max === count($indices) => self::new(),
+ default => self::fromPairs((function (array $offsets) {
+ foreach ($this->getIterator() as $offset => $pair) {
+ if (!array_key_exists($offset, $offsets)) {
+ yield $pair;
+ }
+ }
+ })($indices)),
+ };
+ }
+
+ /**
+ * Deletes members associated with the list using the member pair offset.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ */
+ public function removeByIndices(int ...$indices): self
+ {
+ return $this->remove(...$indices);
+ }
+
+ /**
+ * Deletes members associated with the list using the member key.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ */
+ public function removeByKeys(string ...$keys): self
+ {
+ return $this->remove(...$keys);
+ }
+
+ /**
+ * Adds a member at the end of the instance and deletes any previous reference to the key if present.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param SfMemberInput $member
+ * @throws SyntaxError If the string key is not a valid
+ */
+ public function append(
+ string $key,
+ iterable|StructuredFieldProvider|Dictionary|Parameters|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): self {
+ $members = $this->members;
+ unset($members[$key]);
+
+ return $this->newInstance([...$members, Key::from($key)->value => Member::innerListOrItem($member)]);
+ }
+
+ /**
+ * Adds a member at the beginning of the instance and deletes any previous reference to the key if present.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param SfMemberInput $member
+ *
+ * @throws SyntaxError If the string key is not a valid
+ */
+ public function prepend(
+ string $key,
+ iterable|StructuredFieldProvider|Dictionary|Parameters|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): self {
+ $members = $this->members;
+ unset($members[$key]);
+
+ return $this->newInstance([Key::from($key)->value => Member::innerListOrItem($member), ...$members]);
+ }
+
+ /**
+ * Inserts pairs at the end of the container.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param array{0:string, 1:SfMemberInput} ...$pairs
+ */
+ public function push(array ...$pairs): self
+ {
+ return match (true) {
+ [] === $pairs => $this,
+ default => self::fromPairs((function (iterable $pairs) {
+ yield from $this->getIterator();
+ yield from $pairs;
+ })($pairs)),
+ };
+ }
+
+ /**
+ * Inserts pairs at the beginning of the container.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param array{0:string, 1:SfMemberInput} ...$pairs
+ */
+ public function unshift(array ...$pairs): self
+ {
+ return match (true) {
+ [] === $pairs => $this,
+ default => self::fromPairs((function (iterable $pairs) {
+ yield from $pairs;
+ yield from $this->getIterator();
+ })($pairs)),
+ };
+ }
+
+ /**
+ * Insert a member pair using its offset.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param array{0:string, 1:SfMemberInput} ...$members
+ */
+ public function insert(int $index, array ...$members): self
+ {
+ $offset = $this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index);
+
+ return match (true) {
+ [] === $members => $this,
+ 0 === $offset => $this->unshift(...$members),
+ count($this->members) === $offset => $this->push(...$members),
+ default => (function (Iterator $newMembers) use ($offset, $members) {
+ $newMembers = iterator_to_array($newMembers);
+ array_splice($newMembers, $offset, 0, $members);
+
+ return self::fromPairs($newMembers);
+ })($this->getIterator()),
+ };
+ }
+
+ /**
+ * Replace a member pair using its offset.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param array{0:string, 1:SfMemberInput} $pair
+ */
+ public function replace(int $index, array $pair): self
+ {
+ $offset = $this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index);
+ $pair[1] = Member::innerListOrItem($pair[1]);
+ $pairs = iterator_to_array($this->getIterator());
+
+ return match (true) {
+ $pairs[$offset][0] === $pair[0] && $pairs[$offset][1]->equals($pair[1]) => $this,
+ default => self::fromPairs(array_replace($pairs, [$offset => $pair])),
+ };
+ }
+
+ /**
+ * Merges multiple instances using iterable associative structures.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param StructuredFieldProvider|Dictionary|Parameters|iterable<string, SfMemberInput> ...$others
+ */
+ public function mergeAssociative(StructuredFieldProvider|iterable ...$others): self
+ {
+ $members = $this->members;
+ foreach ($others as $other) {
+ if ($other instanceof StructuredFieldProvider) {
+ $other = $other->toStructuredField();
+ if (!is_iterable($other)) {
+ throw new InvalidArgument('The "'.$other::class.'" instance can not be used for creating a .'.self::class.' structured field.');
+ }
+ }
+
+ if ($other instanceof self || $other instanceof Parameters) {
+ $other = $other->toAssociative();
+ }
+
+ foreach ($other as $key => $value) {
+ $members[$key] = $value;
+ }
+ }
+
+ return new self($members);
+ }
+
+ /**
+ * Merges multiple instances using iterable pairs.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param StructuredFieldProvider|Dictionary|Parameters|iterable<array{0:string, 1:SfMemberInput}> ...$others
+ */
+ public function mergePairs(StructuredFieldProvider|Dictionary|Parameters|iterable ...$others): self
+ {
+ $members = $this->members;
+ foreach ($others as $other) {
+ if (!$other instanceof self) {
+ $other = self::fromPairs($other);
+ }
+ foreach ($other->toAssociative() as $key => $value) {
+ $members[$key] = $value;
+ }
+ }
+
+ return new self($members);
+ }
+
+ /**
+ * @param string $offset
+ */
+ public function offsetExists(mixed $offset): bool
+ {
+ return $this->hasKeys($offset);
+ }
+
+ /**
+ * @param string $offset
+ *
+ * @throws StructuredFieldError
+ */
+ public function offsetGet(mixed $offset): InnerList|Item
+ {
+ return $this->getByKey($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.');
+ }
+
+ /**
+ * Run a map over each container members.
+ *
+ * @template TMap
+ *
+ * @param callable(array{0:string, 1:Item|InnerList}, int): TMap $callback
+ *
+ * @return Iterator<TMap>
+ */
+ public function map(callable $callback): Iterator
+ {
+ foreach ($this as $offset => $member) {
+ yield ($callback)($member, $offset);
+ }
+ }
+
+ /**
+ * @param callable(TInitial|null, array{0:string, 1:Item|InnerList}, int): TInitial $callback
+ * @param TInitial|null $initial
+ *
+ * @template TInitial
+ *
+ * @return TInitial|null
+ */
+ public function reduce(callable $callback, mixed $initial = null): mixed
+ {
+ foreach ($this as $offset => $pair) {
+ $initial = $callback($initial, $pair, $offset);
+ }
+
+ return $initial;
+ }
+
+ /**
+ * Run a filter over each container members.
+ *
+ * @param callable(array{0:string, 1:InnerList|Item}, int): bool $callback
+ */
+ public function filter(callable $callback): self
+ {
+ return self::fromPairs(new CallbackFilterIterator($this, $callback));
+ }
+
+ /**
+ * Sort a container by value using a callback.
+ *
+ * @param callable(array{0:string, 1:InnerList|Item}, array{0:string, 1:InnerList|Item}): int $callback
+ */
+ public function sort(callable $callback): self
+ {
+ $members = iterator_to_array($this);
+ uasort($members, $callback);
+
+ return self::fromPairs($members);
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/DisplayString.php b/vendor/bakame/http-structured-fields/src/DisplayString.php
new file mode 100644
index 000000000..361f91ec2
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/DisplayString.php
@@ -0,0 +1,103 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use Stringable;
+use Throwable;
+
+use function preg_match;
+use function preg_replace_callback;
+use function rawurldecode;
+use function rawurlencode;
+use function str_contains;
+
+/**
+ * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-sfbis#section-4.2.10
+ */
+final class DisplayString
+{
+ private function __construct(private readonly string $value)
+ {
+ }
+
+ public static function tryFromEncoded(Stringable|string $encoded): ?self
+ {
+ try {
+ return self::fromEncoded($encoded);
+ } catch (Throwable) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a new instance from a Base64 encoded string.
+ */
+ public static function fromEncoded(Stringable|string $encoded): self
+ {
+ $encoded = (string) $encoded;
+
+ if (1 === preg_match('/[^\x20-\x7E]/i', $encoded)) {
+ throw new SyntaxError('The display string '.$encoded.' contains invalid characters.');
+ }
+
+ if (!str_contains($encoded, '%')) {
+ return new self($encoded);
+ }
+
+ if (1 === preg_match('/%(?![0-9a-f]{2})/', $encoded)) {
+ throw new SyntaxError('The display string '.$encoded.' contains invalid utf-8 encoded sequence.');
+ }
+
+ $decoded = (string) preg_replace_callback(
+ ',%[a-f0-9]{2},',
+ fn (array $matches): string => rawurldecode($matches[0]),
+ $encoded
+ );
+
+ if (1 !== preg_match('//u', $decoded)) {
+ throw new SyntaxError('The display string '.$encoded.' contains invalid characters.');
+ }
+
+ return new self($decoded);
+ }
+
+ /**
+ * Returns a new instance from a raw decoded string.
+ */
+ public static function fromDecoded(Stringable|string $decoded): self
+ {
+ return new self((string) $decoded);
+ }
+
+ /**
+ * Returns the decoded string.
+ */
+ public function decoded(): string
+ {
+ return $this->value;
+ }
+
+ /**
+ * Returns the base64 encoded string.
+ */
+ public function encoded(): string
+ {
+ return (string) preg_replace_callback(
+ '/[%"\x00-\x1F\x7F-\xFF]/',
+ static fn (array $matches): string => strtolower(rawurlencode($matches[0])),
+ $this->value
+ );
+ }
+
+ public function equals(mixed $other): bool
+ {
+ return $other instanceof self && $other->value === $this->value;
+ }
+
+ public function type(): Type
+ {
+ return Type::DisplayString;
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/ForbiddenOperation.php b/vendor/bakame/http-structured-fields/src/ForbiddenOperation.php
new file mode 100644
index 000000000..f1edbdef4
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/ForbiddenOperation.php
@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use LogicException;
+
+final class ForbiddenOperation extends LogicException implements StructuredFieldError
+{
+}
diff --git a/vendor/bakame/http-structured-fields/src/Ietf.php b/vendor/bakame/http-structured-fields/src/Ietf.php
new file mode 100644
index 000000000..a98e16e20
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/Ietf.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use DateTimeImmutable;
+use DateTimeZone;
+
+enum Ietf
+{
+ case Rfc8941;
+ case Rfc9651;
+
+ public function uri(): string
+ {
+ return match ($this) {
+ self::Rfc9651 => 'https://www.rfc-editor.org/rfc/rfc9651.html',
+ self::Rfc8941 => 'https://www.rfc-editor.org/rfc/rfc8941.html',
+ };
+ }
+
+ public function publishedAt(): DateTimeImmutable
+ {
+ return new DateTimeImmutable(match ($this) {
+ self::Rfc9651 => '2024-09-01',
+ self::Rfc8941 => '2021-02-01',
+ }, new DateTimeZone('UTC'));
+ }
+
+ public function isActive(): bool
+ {
+ return self::Rfc9651 === $this;
+ }
+
+ public function isObsolete(): bool
+ {
+ return !$this->isActive();
+ }
+
+ public function supports(mixed $value): bool
+ {
+ if ($value instanceof StructuredFieldProvider) {
+ $value = $value->toStructuredField();
+ }
+
+ if ($value instanceof OuterList ||
+ $value instanceof InnerList ||
+ $value instanceof Dictionary ||
+ $value instanceof Parameters ||
+ $value instanceof Item
+ ) {
+ try {
+ $value->toHttpValue($this);
+
+ return true;
+ } catch (MissingFeature) {
+ return false;
+ }
+ }
+
+ if (!$value instanceof Type) {
+ $value = Type::tryFromVariable($value);
+ }
+
+ return match ($value) {
+ null => false,
+ Type::DisplayString,
+ Type::Date => self::Rfc8941 !== $this,
+ default => true,
+ };
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/InnerList.php b/vendor/bakame/http-structured-fields/src/InnerList.php
new file mode 100644
index 000000000..8a9a3e734
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/InnerList.php
@@ -0,0 +1,478 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use ArrayAccess;
+use Bakame\Http\StructuredFields\Validation\Violation;
+use Countable;
+use DateTimeInterface;
+use Iterator;
+use IteratorAggregate;
+use Stringable;
+
+use function array_filter;
+use function array_is_list;
+use function array_map;
+use function array_replace;
+use function array_splice;
+use function array_values;
+use function count;
+use function implode;
+use function uasort;
+
+use const ARRAY_FILTER_USE_BOTH;
+use const ARRAY_FILTER_USE_KEY;
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.1.1
+ *
+ * @phpstan-import-type SfType from StructuredFieldProvider
+ * @phpstan-import-type SfTypeInput from StructuredFieldProvider
+ * @phpstan-import-type SfItemInput from StructuredFieldProvider
+ * @phpstan-import-type SfItemPair from StructuredFieldProvider
+ * @phpstan-import-type SfInnerListPair from StructuredFieldProvider
+ * @phpstan-import-type SfParameterInput from StructuredFieldProvider
+ *
+ * @implements ArrayAccess<int, Item>
+ * @implements IteratorAggregate<int, Item>
+ */
+final class InnerList implements ArrayAccess, Countable, IteratorAggregate
+{
+ use ParameterAccess;
+
+ /** @var list<Item> */
+ private readonly array $members;
+ private readonly Parameters $parameters;
+
+ /**
+ * @param iterable<SfItemInput|SfItemPair> $members
+ */
+ private function __construct(iterable $members, ?Parameters $parameters = null)
+ {
+ $this->members = array_map(Member::item(...), array_values([...$members]));
+ $this->parameters = $parameters ?? Parameters::new();
+ }
+
+ /**
+ * Returns an instance from an HTTP textual representation.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.1
+ */
+ public static function fromHttpValue(Stringable|string $httpValue, Ietf $rfc = Ietf::Rfc9651): self
+ {
+ return self::fromPair((new Parser($rfc))->parseInnerList($httpValue));
+ }
+
+ /**
+ * Returns a new instance with an iter.
+ *
+ * @param iterable<SfItemInput> $value
+ * @param Parameters|iterable<string, SfItemInput> $parameters
+ */
+ public static function fromAssociative(
+ iterable $value,
+ StructuredFieldProvider|Parameters|iterable $parameters
+ ): self {
+ if ($parameters instanceof StructuredFieldProvider) {
+ $parameters = $parameters->toStructuredField();
+ if (!$parameters instanceof Parameters) {
+ throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a '.Parameters::class.'; '.$parameters::class.' given.');
+ }
+ }
+
+ if (!$parameters instanceof Parameters) {
+ return new self($value, Parameters::fromAssociative($parameters));
+ }
+
+ return new self($value, $parameters);
+ }
+
+ /**
+ * @param array{0:iterable<SfItemInput>, 1?:Parameters|SfParameterInput}|array<mixed> $pair
+ */
+ public static function fromPair(array $pair = []): self
+ {
+ if ([] === $pair) {
+ return self::new();
+ }
+
+ if (!array_is_list($pair) || 2 < count($pair)) {
+ throw new SyntaxError('The pair must be represented by an non-empty array as a list containing at most 2 members.');
+ }
+
+ if (1 === count($pair)) {
+ return new self($pair[0]);
+ }
+
+ if ($pair[1] instanceof StructuredFieldProvider) {
+ $pair[1] = $pair[1]->toStructuredField();
+ if (!$pair[1] instanceof Parameters) {
+ throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a '.Parameters::class.'; '.$pair[1]::class.' given.');
+ }
+ }
+
+ if (!$pair[1] instanceof Parameters) {
+ return new self($pair[0], Parameters::fromPairs($pair[1]));
+ }
+
+ return new self($pair[0], $pair[1]);
+ }
+
+ /**
+ * Returns a new instance.
+ *
+ * @param StructuredFieldProvider|Item|SfTypeInput|SfItemPair ...$members
+ */
+ public static function new(
+ StructuredFieldProvider|Item|Token|Bytes|DisplayString|DateTimeInterface|array|string|int|float|bool ...$members
+ ): self {
+ return new self($members);
+ }
+
+ public static function fromRfc9651(Stringable|string $httpValue): self
+ {
+ return self::fromHttpValue($httpValue, Ietf::Rfc9651);
+ }
+
+ public static function fromRfc8941(Stringable|string $httpValue): self
+ {
+ return self::fromHttpValue($httpValue, Ietf::Rfc8941);
+ }
+
+ public function toHttpValue(Ietf $rfc = Ietf::Rfc9651): string
+ {
+ return '('.implode(' ', array_map(fn (Item $value): string => $value->toHttpValue($rfc), $this->members)).')'.$this->parameters->toHttpValue($rfc);
+ }
+
+ public function toRfc9651(): string
+ {
+ return $this->toHttpValue(Ietf::Rfc9651);
+ }
+
+ public function toRfc8941(): string
+ {
+ return $this->toHttpValue(Ietf::Rfc8941);
+ }
+
+ public function __toString(): string
+ {
+ return $this->toHttpValue();
+ }
+
+ public function equals(mixed $other): bool
+ {
+ return $other instanceof self && $other->toHttpValue() === $this->toHttpValue();
+ }
+
+ /**
+ * Apply the callback if the given "condition" is (or resolves to) true.
+ *
+ * @param (callable($this): bool)|bool $condition
+ * @param callable($this): (self|null) $onSuccess
+ * @param ?callable($this): (self|null) $onFail
+ */
+ public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
+ {
+ if (!is_bool($condition)) {
+ $condition = $condition($this);
+ }
+
+ return match (true) {
+ $condition => $onSuccess($this),
+ null !== $onFail => $onFail($this),
+ default => $this,
+ } ?? $this;
+ }
+
+ /**
+ * @return array{0:list<Item>, 1:Parameters}
+ */
+ public function toPair(): array
+ {
+ return [$this->members, $this->parameters];
+ }
+
+ public function getIterator(): Iterator
+ {
+ yield from $this->members;
+ }
+
+ public function count(): int
+ {
+ return count($this->members);
+ }
+
+ public function isEmpty(): bool
+ {
+ return !$this->isNotEmpty();
+ }
+
+ public function isNotEmpty(): bool
+ {
+ return [] !== $this->members;
+ }
+
+ /**
+ * @return array<int>
+ */
+ public function indices(): array
+ {
+ return array_keys($this->members);
+ }
+
+ public function hasIndices(int ...$indices): bool
+ {
+ $max = count($this->members);
+ foreach ($indices as $offset) {
+ if (null === $this->filterIndex($offset, $max)) {
+ return false;
+ }
+ }
+
+ return [] !== $indices;
+ }
+
+ private function filterIndex(int $index, ?int $max = null): ?int
+ {
+ $max ??= count($this->members);
+
+ return match (true) {
+ [] === $this->members,
+ 0 > $max + $index,
+ 0 > $max - $index - 1 => null,
+ 0 > $index => $max + $index,
+ default => $index,
+ };
+ }
+
+ /**
+ * @param ?callable(Item): (bool|string) $validate
+ *
+ * @throws SyntaxError|Violation|StructuredFieldError
+ */
+ public function getByIndex(int $index, ?callable $validate = null): Item
+ {
+ $value = $this->members[$this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index)];
+ if (null === $validate) {
+ return $value;
+ }
+
+ if (true === ($exceptionMessage = $validate($value))) {
+ return $value;
+ }
+
+ if (!is_string($exceptionMessage) || '' === trim($exceptionMessage)) {
+ $exceptionMessage = "The item at '{index}' whose value is '{value}' failed validation.";
+ }
+
+ throw new Violation(strtr($exceptionMessage, ['{index}' => $index, '{value}' => $value->toHttpValue()]));
+ }
+
+ public function first(): ?Item
+ {
+ return $this->members[0] ?? null;
+ }
+
+ public function last(): ?Item
+ {
+ return $this->members[$this->filterIndex(-1)] ?? null;
+ }
+
+ public function withParameters(StructuredFieldProvider|Parameters $parameters): static
+ {
+ if ($parameters instanceof StructuredFieldProvider) {
+ $parameters = $parameters->toStructuredField();
+ if (!$parameters instanceof Parameters) {
+ throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a '.Parameters::class.'; '.$parameters::class.' given.');
+ }
+ }
+
+ return $this->parameters->equals($parameters) ? $this : new self($this->members, $parameters);
+ }
+
+ /**
+ * Inserts members at the beginning of the list.
+ */
+ public function unshift(
+ StructuredFieldProvider|OuterList|Dictionary|InnerList|Parameters|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool ...$members
+ ): self {
+ $membersToAdd = array_reduce(
+ $members,
+ function (array $carry, $member) {
+ if ($member instanceof StructuredFieldProvider) {
+ $member = $member->toStructuredField();
+ }
+
+ return [...$carry, ...$member instanceof InnerList ? [...$member] : [$member]];
+ },
+ []
+ );
+
+ return match (true) {
+ [] === $membersToAdd => $this,
+ default => new self([...array_values($membersToAdd), ...$this->members], $this->parameters),
+ };
+ }
+
+ /**
+ * Inserts members at the end of the list.
+ */
+ public function push(
+ StructuredFieldProvider|OuterList|Dictionary|InnerList|Parameters|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool ...$members
+ ): self {
+ $membersToAdd = array_reduce(
+ $members,
+ function (array $carry, $member) {
+ if ($member instanceof StructuredFieldProvider) {
+ $member = $member->toStructuredField();
+ }
+
+ return [...$carry, ...$member instanceof InnerList ? [...$member] : [$member]];
+ },
+ []
+ );
+
+ return match (true) {
+ [] === $membersToAdd => $this,
+ default => new self([...$this->members, ...array_values($membersToAdd)], $this->parameters),
+ };
+ }
+
+ /**
+ * Inserts members starting at the given index.
+ *
+ * @throws InvalidOffset If the index does not exist
+ */
+ public function insert(
+ int $index,
+ StructuredFieldProvider|OuterList|Dictionary|InnerList|Parameters|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool ...$members
+ ): self {
+ $offset = $this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index);
+
+ return match (true) {
+ 0 === $offset => $this->unshift(...$members),
+ count($this->members) === $offset => $this->push(...$members),
+ [] === $members => $this,
+ default => (function (array $newMembers) use ($offset, $members) {
+ array_splice($newMembers, $offset, 0, $members);
+
+ return new self($newMembers, $this->parameters);
+ })($this->members),
+ };
+ }
+
+ public function replace(
+ int $index,
+ StructuredFieldProvider|OuterList|Dictionary|InnerList|Parameters|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): self {
+ $offset = $this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index);
+ $member = Member::item($member);
+
+ return match (true) {
+ $member->equals($this->members[$offset]) => $this,
+ default => new self(array_replace($this->members, [$offset => $member]), $this->parameters),
+ };
+ }
+
+ public function removeByIndices(int ...$indices): self
+ {
+ $max = count($this->members);
+ $indices = array_filter(
+ array_map(fn (int $index): ?int => $this->filterIndex($index, $max), $indices),
+ fn (?int $index): bool => null !== $index
+ );
+
+ return match (true) {
+ [] === $indices => $this,
+ count($indices) === $max => self::new(),
+ default => new self(array_filter(
+ $this->members,
+ fn (int $offset): bool => !in_array($offset, $indices, true),
+ ARRAY_FILTER_USE_KEY
+ ), $this->parameters),
+ };
+ }
+
+ /**
+ * @param int $offset
+ */
+ public function offsetExists(mixed $offset): bool
+ {
+ return $this->hasIndices($offset);
+ }
+
+ /**
+ * @param int $offset
+ */
+ public function offsetGet(mixed $offset): Item
+ {
+ return $this->getByIndex($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.');
+ }
+
+ /**
+ * @param callable(Item, int): TMap $callback
+ *
+ * @template TMap
+ *
+ * @return Iterator<TMap>
+ */
+ public function map(callable $callback): Iterator
+ {
+ foreach ($this->members as $offset => $member) {
+ yield ($callback)($member, $offset);
+ }
+ }
+
+ /**
+ * @param callable(TInitial|null, Item, int=): TInitial $callback
+ * @param TInitial|null $initial
+ *
+ * @template TInitial
+ *
+ * @return TInitial|null
+ */
+ public function reduce(callable $callback, mixed $initial = null): mixed
+ {
+ foreach ($this->members as $offset => $member) {
+ $initial = $callback($initial, $member, $offset);
+ }
+
+ return $initial;
+ }
+
+ /**
+ * @param callable(Item, int): bool $callback
+ */
+ public function filter(callable $callback): self
+ {
+ $members = array_filter($this->members, $callback, ARRAY_FILTER_USE_BOTH);
+ if ($members === $this->members) {
+ return $this;
+ }
+
+ return new self($members, $this->parameters);
+ }
+
+ /**
+ * @param callable(Item, Item): int $callback
+ */
+ public function sort(callable $callback): self
+ {
+ $members = $this->members;
+ uasort($members, $callback);
+
+ return new self($members, $this->parameters);
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/InvalidArgument.php b/vendor/bakame/http-structured-fields/src/InvalidArgument.php
new file mode 100644
index 000000000..6c7c3344f
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/InvalidArgument.php
@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use InvalidArgumentException;
+
+final class InvalidArgument extends InvalidArgumentException implements StructuredFieldError
+{
+}
diff --git a/vendor/bakame/http-structured-fields/src/InvalidOffset.php b/vendor/bakame/http-structured-fields/src/InvalidOffset.php
new file mode 100644
index 000000000..ab878d98d
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/InvalidOffset.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use OutOfBoundsException;
+
+final class InvalidOffset extends OutOfBoundsException implements StructuredFieldError
+{
+ private function __construct(string $message)
+ {
+ parent::__construct($message);
+ }
+
+ public static function dueToIndexNotFound(string|int $index): self
+ {
+ if (is_string($index)) {
+ return new self('The member index can not be the string "'.$index.'".');
+ }
+
+ return new self('No member exists with the index "'.$index.'".');
+ }
+
+ public static function dueToKeyNotFound(string|int $key): self
+ {
+ if (is_int($key)) {
+ return new self('The member key can not be the integer "'.$key.'".');
+ }
+
+ return new self('No member exists with the key "'.$key.'".');
+ }
+
+ public static function dueToMemberNotFound(string|int $offset): self
+ {
+ return new self('No member exists with the '.(is_int($offset) ? 'index' : 'key').' "'.$offset.'".');
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/Item.php b/vendor/bakame/http-structured-fields/src/Item.php
new file mode 100644
index 000000000..6d0434ee5
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/Item.php
@@ -0,0 +1,502 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use BadMethodCallException;
+use Bakame\Http\StructuredFields\Validation\Violation;
+use DateTimeImmutable;
+use DateTimeInterface;
+use DateTimeZone;
+use Exception;
+use ReflectionMethod;
+use Stringable;
+use Throwable;
+use TypeError;
+
+use function array_is_list;
+use function count;
+use function in_array;
+
+use const JSON_PRESERVE_ZERO_FRACTION;
+use const PHP_ROUND_HALF_EVEN;
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.3
+ *
+ * @phpstan-import-type SfType from StructuredFieldProvider
+ * @phpstan-import-type SfItemInput from StructuredFieldProvider
+ * @phpstan-import-type SfItemPair from StructuredFieldProvider
+ * @phpstan-import-type SfTypeInput from StructuredFieldProvider
+ *
+ * @method static ?Item tryFromPair(array{0: SfItemInput, 1?: Parameters|iterable<array{0:string, 1:SfItemInput}>}|array<mixed> $pair) try to create a new instance from a Pair
+ * @method static ?Item tryFromRfc9651(Stringable|string $httpValue) try to create a new instance from a string using RFC9651
+ * @method static ?Item tryFromRfc8941(Stringable|string $httpValue) try to create a new instance from a string using RFC8941
+ * @method static ?Item tryFromHttpValue(Stringable|string $httpValue) try to create a new instance from a string
+ * @method static ?Item tryFromAssociative(Bytes|Token|DisplayString|DateTimeInterface|string|int|float|bool $value, StructuredFieldProvider|Parameters|iterable<string, SfItemInput> $parameters) try to create a new instance from a value and a parameters as associative construct
+ * @method static ?Item tryNew(mixed $value) try to create a new bare instance from a value
+ * @method static ?Item tryFromEncodedBytes(Stringable|string $value) try to create a new instance from an encoded byte sequence
+ * @method static ?Item tryFromDecodedBytes(Stringable|string $value) try to create a new instance from a decoded byte sequence
+ * @method static ?Item tryFromEncodedDisplayString(Stringable|string $value) try to create a new instance from an encoded display string
+ * @method static ?Item tryFromDecodedDisplayString(Stringable|string $value) try to create a new instance from a decoded display string
+ * @method static ?Item tryFromToken(Stringable|string $value) try to create a new instance from a token string
+ * @method static ?Item tryFromTimestamp(int $timestamp) try to create a new instance from a timestamp
+ * @method static ?Item tryFromDateFormat(string $format, string $datetime) try to create a new instance from a date format
+ * @method static ?Item tryFromDateString(string $datetime, DateTimeZone|string|null $timezone = null) try to create a new instance from a date string
+ * @method static ?Item tryFromDate(DateTimeInterface $datetime) try to create a new instance from a DateTimeInterface object
+ * @method static ?Item tryFromDecimal(int|float $value) try to create a new instance from a float
+ * @method static ?Item tryFromInteger(int|float $value) try to create a new instance from an integer
+ */
+final class Item
+{
+ use ParameterAccess;
+
+ private readonly Token|Bytes|DisplayString|DateTimeImmutable|int|float|string|bool $value;
+ private readonly Parameters $parameters;
+ private readonly Type $type;
+
+ private function __construct(Token|Bytes|DisplayString|DateTimeInterface|int|float|string|bool $value, ?Parameters $parameters = null)
+ {
+ if ($value instanceof DateTimeInterface && !$value instanceof DateTimeImmutable) {
+ $value = DateTimeImmutable::createFromInterface($value);
+ }
+
+ $this->value = $value;
+ $this->parameters = $parameters ?? Parameters::new();
+ $this->type = Type::fromVariable($value);
+ }
+
+ /**
+ * @throws BadMethodCallException
+ */
+ public static function __callStatic(string $name, array $arguments): ?self /* @phpstan-ignore-line */
+ {
+ if (!str_starts_with($name, 'try')) {
+ throw new BadMethodCallException('The method "'.self::class.'::'.$name.'" does not exist.');
+ }
+
+ $namedConstructor = lcfirst(substr($name, strlen('try')));
+ if (!method_exists(self::class, $namedConstructor)) {
+ throw new BadMethodCallException('The method "'.self::class.'::'.$name.'" does not exist.');
+ }
+
+ $method = new ReflectionMethod(self::class, $namedConstructor);
+ if (!$method->isPublic() || !$method->isStatic()) {
+ throw new BadMethodCallException('The method "'.self::class.'::'.$name.'" can not be accessed directly.');
+ }
+
+ try {
+ return self::$namedConstructor(...$arguments); /* @phpstan-ignore-line */
+ } catch (Throwable) { /* @phpstan-ignore-line */
+ return null;
+ }
+ }
+
+ public static function fromRfc9651(Stringable|string $httpValue): self
+ {
+ return self::fromHttpValue($httpValue, Ietf::Rfc9651);
+ }
+
+ public static function fromRfc8941(Stringable|string $httpValue): self
+ {
+ return self::fromHttpValue($httpValue, Ietf::Rfc8941);
+ }
+
+ /**
+ * Returns a new instance from an HTTP Header or Trailer value string
+ * in compliance with a published RFC.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.3
+ *
+ * @throws SyntaxError|Exception If the HTTP value can not be parsed
+ */
+ public static function fromHttpValue(Stringable|string $httpValue, Ietf $rfc = Ietf::Rfc9651): self
+ {
+ return self::fromPair((new Parser($rfc))->parseItem($httpValue));
+ }
+
+ /**
+ * Returns a new instance from a value type and an iterable of key-value parameters.
+ *
+ * @param StructuredFieldProvider|Parameters|iterable<string, SfItemInput> $parameters
+ *
+ * @throws SyntaxError If the value or the parameters are not valid
+ */
+ public static function fromAssociative(
+ Bytes|Token|DisplayString|DateTimeInterface|string|int|float|bool $value,
+ StructuredFieldProvider|Parameters|iterable $parameters
+ ): self {
+ if ($parameters instanceof StructuredFieldProvider) {
+ $parameters = $parameters->toStructuredField();
+ if ($parameters instanceof Parameters) {
+ return new self($value, $parameters);
+ }
+
+ throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a '.Parameters::class.'; '.$parameters::class.' given.');
+ }
+
+ if (!$parameters instanceof Parameters) {
+ return new self($value, Parameters::fromAssociative($parameters));
+ }
+
+ return new self($value, $parameters);
+ }
+
+ /**
+ * @param array{0: SfItemInput, 1?: Parameters|iterable<array{0:string, 1:SfItemInput}>}|array<mixed> $pair
+ *
+ * @throws SyntaxError If the pair or its content is not valid.
+ */
+ public static function fromPair(array $pair): self
+ {
+ $nbElements = count($pair);
+ if (!in_array($nbElements, [1, 2], true) || !array_is_list($pair)) {
+ throw new SyntaxError('The pair must be represented by an non-empty array as a list containing exactly 1 or 2 members.');
+ }
+
+ if (1 === $nbElements) {
+ return new self($pair[0]);
+ }
+
+ if ($pair[1] instanceof StructuredFieldProvider) {
+ $pair[1] = $pair[1]->toStructuredField();
+ if ($pair[1] instanceof Parameters) {
+ return new self($pair[0], Parameters::fromPairs($pair[1]));
+ }
+
+ throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a '.Parameters::class.'; '.$pair[1]::class.' given.');
+ }
+
+ if (!$pair[1] instanceof Parameters) {
+ return new self($pair[0], Parameters::fromPairs($pair[1]));
+ }
+
+ return new self($pair[0], $pair[1]);
+ }
+
+ /**
+ * Returns a new bare instance from value.
+ *
+ * @param SfItemPair|SfItemInput $value
+ *
+ * @throws SyntaxError|TypeError If the value is not valid.
+ */
+ public static function new(mixed $value): self
+ {
+ if (is_array($value)) {
+ return self::fromPair($value);
+ }
+
+ return new self($value); /* @phpstan-ignore-line */
+ }
+
+ /**
+ * Returns a new instance from a string.
+ *
+ * @throws SyntaxError if the string is invalid
+ */
+ public static function fromString(Stringable|string $value): self
+ {
+ return new self((string)$value);
+ }
+
+ /**
+ * Returns a new instance from an encoded byte sequence and an iterable of key-value parameters.
+ *
+ * @throws SyntaxError if the sequence is invalid
+ */
+ public static function fromEncodedBytes(Stringable|string $value): self
+ {
+ return new self(Bytes::fromEncoded($value));
+ }
+
+ /**
+ * Returns a new instance from a decoded byte sequence and an iterable of key-value parameters.
+ *
+ * @throws SyntaxError if the sequence is invalid
+ */
+ public static function fromDecodedBytes(Stringable|string $value): self
+ {
+ return new self(Bytes::fromDecoded($value));
+ }
+
+ /**
+ * Returns a new instance from an encoded byte sequence and an iterable of key-value parameters.
+ *
+ * @throws SyntaxError if the sequence is invalid
+ */
+ public static function fromEncodedDisplayString(Stringable|string $value): self
+ {
+ return new self(DisplayString::fromEncoded($value));
+ }
+
+ /**
+ * Returns a new instance from a decoded byte sequence and an iterable of key-value parameters.
+ *
+ * @throws SyntaxError if the sequence is invalid
+ */
+ public static function fromDecodedDisplayString(Stringable|string $value): self
+ {
+ return new self(DisplayString::fromDecoded($value));
+ }
+
+ /**
+ * Returns a new instance from a Token and an iterable of key-value parameters.
+ *
+ * @throws SyntaxError if the token is invalid
+ */
+ public static function fromToken(Stringable|string $value): self
+ {
+ return new self(Token::fromString($value));
+ }
+
+ /**
+ * Returns a new instance from a timestamp and an iterable of key-value parameters.
+ *
+ * @throws SyntaxError if the timestamp value is not supported
+ */
+ public static function fromTimestamp(int $timestamp): self
+ {
+ return new self((new DateTimeImmutable())->setTimestamp($timestamp));
+ }
+
+ /**
+ * Returns a new instance from a date format its date string representation and an iterable of key-value parameters.
+ *
+ * @throws SyntaxError if the format is invalid
+ */
+ public static function fromDateFormat(string $format, string $datetime): self
+ {
+ try {
+ $value = DateTimeImmutable::createFromFormat($format, $datetime);
+ } catch (Exception $exception) {
+ throw new SyntaxError('The date notation `'.$datetime.'` is incompatible with the date format `'.$format.'`.', 0, $exception);
+ }
+
+ if (!$value instanceof DateTimeImmutable) {
+ throw new SyntaxError('The date notation `'.$datetime.'` is incompatible with the date format `'.$format.'`.');
+ }
+
+ return new self($value);
+ }
+
+ /**
+ * Returns a new instance from a string parsable by DateTimeImmutable constructor, an optional timezone and an iterable of key-value parameters.
+ *
+ * @throws SyntaxError if the format is invalid
+ */
+ public static function fromDateString(string $datetime, DateTimeZone|string|null $timezone = null): self
+ {
+ $timezone ??= date_default_timezone_get();
+ if (!$timezone instanceof DateTimeZone) {
+ try {
+ $timezone = new DateTimeZone($timezone);
+ } catch (Throwable $exception) {
+ throw new SyntaxError('The timezone could not be instantiated.', 0, $exception);
+ }
+ }
+
+ try {
+ return new self(new DateTimeImmutable($datetime, $timezone));
+ } catch (Throwable $exception) {
+ throw new SyntaxError('Unable to create a '.DateTimeImmutable::class.' instance with the date notation `'.$datetime.'.`', 0, $exception);
+ }
+ }
+
+ /**
+ * Returns a new instance from a DateTineInterface implementing object.
+ *
+ * @throws SyntaxError if the format is invalid
+ */
+ public static function fromDate(DateTimeInterface $datetime): self
+ {
+ return new self($datetime);
+ }
+
+ /**
+ * Returns a new instance from a float value.
+ *
+ * @throws SyntaxError if the format is invalid
+ */
+ public static function fromDecimal(int|float $value): self
+ {
+ return new self((float)$value);
+ }
+
+ /**
+ * Returns a new instance from an integer value.
+ *
+ * @throws SyntaxError if the format is invalid
+ */
+ public static function fromInteger(int|float $value): self
+ {
+ return new self((int)$value);
+ }
+
+ /**
+ * Returns a new instance for the boolean true type.
+ */
+ public static function true(): self
+ {
+ return new self(true);
+ }
+
+ /**
+ * Returns a new instance for the boolean false type.
+ */
+ public static function false(): self
+ {
+ return new self(false);
+ }
+
+ /**
+ * Returns the underlying value.
+ * If a validation rule is provided, an exception will be thrown
+ * if the validation rules does not return true.
+ *
+ * if the validation returns false then a default validation message will be return; otherwise the submitted message string will be returned as is.
+ *
+ * @param ?callable(SfType): (string|bool) $validate
+ *
+ * @throws Violation
+ */
+ public function value(?callable $validate = null): Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool
+ {
+ if (null === $validate) {
+ return $this->value;
+ }
+
+ $exceptionMessage = $validate($this->value);
+ if (true === $exceptionMessage) {
+ return $this->value;
+ }
+
+ if (!is_string($exceptionMessage) || '' === trim($exceptionMessage)) {
+ $exceptionMessage = "The item value '{value}' failed validation.";
+ }
+
+ throw new Violation(strtr($exceptionMessage, ['{value}' => $this->serialize()]));
+ }
+
+ public function type(): Type
+ {
+ return $this->type;
+ }
+
+ /**
+ * Serialize the Item value according to RFC8941.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.1
+ */
+ public function toHttpValue(Ietf $rfc = Ietf::Rfc9651): string
+ {
+ return $this->serialize($rfc).$this->parameters->toHttpValue($rfc);
+ }
+
+ /**
+ * Serialize the Item value according to RFC8941.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.1
+ */
+ private function serialize(Ietf $rfc = Ietf::Rfc9651): string
+ {
+ return match (true) {
+ !$rfc->supports($this->type) => throw MissingFeature::dueToLackOfSupport($this->type, $rfc),
+ $this->value instanceof DateTimeImmutable => '@'.$this->value->getTimestamp(),
+ $this->value instanceof Token => $this->value->toString(),
+ $this->value instanceof Bytes => ':'.$this->value->encoded().':',
+ $this->value instanceof DisplayString => '%"'.$this->value->encoded().'"',
+ is_int($this->value) => (string) $this->value,
+ is_float($this->value) => (string) json_encode(round($this->value, 3, PHP_ROUND_HALF_EVEN), JSON_PRESERVE_ZERO_FRACTION),
+ $this->value,
+ false === $this->value => '?'.($this->value ? '1' : '0'),
+ default => '"'.preg_replace('/(["\\\])/', '\\\$1', $this->value).'"',
+ };
+ }
+
+ public function toRfc9651(): string
+ {
+ return $this->toHttpValue(Ietf::Rfc9651);
+ }
+
+ public function toRfc8941(): string
+ {
+ return $this->toHttpValue(Ietf::Rfc8941);
+ }
+
+ public function __toString(): string
+ {
+ return $this->toHttpValue();
+ }
+
+ /**
+ * @return array{0:SfItemInput, 1:Parameters}
+ */
+ public function toPair(): array
+ {
+ return [$this->value, $this->parameters];
+ }
+
+ public function equals(mixed $other): bool
+ {
+ return $other instanceof self && $other->toHttpValue() === $this->toHttpValue();
+ }
+
+ /**
+ * Apply the callback if the given "condition" is (or resolves to) true.
+ *
+ * @param (callable($this): bool)|bool $condition
+ * @param callable($this): (self|null) $onSuccess
+ * @param ?callable($this): (self|null) $onFail
+ */
+ public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
+ {
+ if (!is_bool($condition)) {
+ $condition = $condition($this);
+ }
+
+ return match (true) {
+ $condition => $onSuccess($this),
+ null !== $onFail => $onFail($this),
+ default => $this,
+ } ?? $this;
+ }
+
+ /**
+ * Returns a new instance with the newly associated value.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified value change.
+ *
+ * @throws SyntaxError If the value is invalid or not supported
+ */
+ public function withValue(DateTimeInterface|Bytes|Token|DisplayString|string|int|float|bool $value): self
+ {
+ $isEqual = match (true) {
+ $this->value instanceof Bytes,
+ $this->value instanceof Token,
+ $this->value instanceof DisplayString => $this->value->equals($value),
+ $this->value instanceof DateTimeInterface && $value instanceof DateTimeInterface => $value->getTimestamp() === $this->value->getTimestamp(),
+ default => $value === $this->value,
+ };
+
+ if ($isEqual) {
+ return $this;
+ }
+
+ return new self($value, $this->parameters);
+ }
+
+ public function withParameters(StructuredFieldProvider|Parameters $parameters): static
+ {
+ if ($parameters instanceof StructuredFieldProvider) {
+ $parameters = $parameters->toStructuredField();
+ if (!$parameters instanceof Parameters) {
+ throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a '.Parameters::class.'; '.$parameters::class.' given.');
+ }
+ }
+
+ return $this->parameters->equals($parameters) ? $this : new self($this->value, $parameters);
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/Key.php b/vendor/bakame/http-structured-fields/src/Key.php
new file mode 100644
index 000000000..e1971426a
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/Key.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Bakame\Http\StructuredFields;
+
+use Stringable;
+
+use function preg_match;
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.1.2
+ * @internal normalize HTTP field key
+ */
+final class Key
+{
+ private function __construct(public readonly string $value)
+ {
+ }
+
+ /**
+ * @throws SyntaxError If the string is not a valid HTTP value field key
+ */
+ public static function from(Stringable|string|int $httpValue): self
+ {
+ $key = (string) $httpValue;
+ $instance = self::fromStringBeginning($key);
+ if ($instance->value !== $key) {
+ throw new SyntaxError('No valid http value key could be extracted from "'.$httpValue.'".');
+ }
+
+ return $instance;
+ }
+
+ public static function tryFrom(Stringable|string|int $httpValue): ?self
+ {
+ try {
+ return self::from($httpValue);
+ } catch (SyntaxError $e) {
+ return null;
+ }
+ }
+
+ /**
+ * @throws SyntaxError If the string does not start with a valid HTTP value field key
+ */
+ public static function fromStringBeginning(string $httpValue): self
+ {
+ if (1 !== preg_match('/^(?<key>[a-z*][a-z\d.*_-]*)/', $httpValue, $found)) {
+ throw new SyntaxError('No valid http value key could be extracted from "'.$httpValue.'".');
+ }
+
+ return new self($found['key']);
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/Member.php b/vendor/bakame/http-structured-fields/src/Member.php
new file mode 100644
index 000000000..ee315b078
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/Member.php
@@ -0,0 +1,116 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use function count;
+use function in_array;
+use function is_array;
+use function is_iterable;
+
+/**
+ * @phpstan-import-type SfMemberInput from StructuredFieldProvider
+ * @phpstan-import-type SfItemInput from StructuredFieldProvider
+ * @phpstan-import-type SfItemPair from StructuredFieldProvider
+ * @phpstan-import-type SfInnerListPair from StructuredFieldProvider
+ * @phpstan-import-type SfTypeInput from StructuredFieldProvider
+ *
+ * @internal Validate containers member
+ */
+final class Member
+{
+ /**
+ * @param SfMemberInput $value
+ */
+ public static function innerListOrItem(mixed $value): InnerList|Item
+ {
+ if ($value instanceof StructuredFieldProvider) {
+ $value = $value->toStructuredField();
+ if ($value instanceof Item || $value instanceof InnerList) {
+ return $value;
+ }
+
+ throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a '.Item::class.' or an '.InnerList::class.'; '.$value::class.' given.');
+ }
+
+ return match (true) {
+ $value instanceof InnerList,
+ $value instanceof Item => $value,
+ is_iterable($value) => InnerList::new(...$value),
+ default => Item::new($value),
+ };
+ }
+
+ public static function innerListOrItemFromPair(mixed $value): InnerList|Item
+ {
+ if ($value instanceof StructuredFieldProvider) {
+ $value = $value->toStructuredField();
+ if ($value instanceof Item || $value instanceof InnerList) {
+ return $value;
+ }
+
+ throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a '.Item::class.' or an '.InnerList::class.'; '.$value::class.' given.');
+ }
+
+ if ($value instanceof InnerList || $value instanceof Item) {
+ return $value;
+ }
+
+ if (!is_array($value)) {
+ if (is_iterable($value)) {
+ throw new SyntaxError('The value must be an Item value not an iterable.');
+ }
+
+ return Item::new($value); /* @phpstan-ignore-line */
+ }
+
+ if (!array_is_list($value)) {
+ throw new SyntaxError('The pair must be represented by an array as a list.');
+ }
+
+ if ([] === $value) {
+ return InnerList::new();
+ }
+
+ if (!in_array(count($value), [1, 2], true)) {
+ throw new SyntaxError('The pair first member represents its value; the second member is its associated parameters.');
+ }
+
+ return is_iterable($value[0]) ? InnerList::fromPair($value) : Item::fromPair($value);
+ }
+
+ /**
+ * @param SfItemInput|SfItemPair $value
+ */
+ public static function item(mixed $value): Item
+ {
+ if ($value instanceof StructuredFieldProvider) {
+ $value = $value->toStructuredField();
+ if (!$value instanceof Item) {
+ throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a '.Item::class.'; '.$value::class.' given.');
+ }
+
+ return $value;
+ }
+
+ if ($value instanceof Item) {
+ return $value;
+ }
+
+ return Item::new($value);
+ }
+
+ /**
+ * @param SfItemInput|SfItemPair $value
+ */
+ public static function bareItem(mixed $value): Item
+ {
+ $bareItem = self::item($value);
+ if ($bareItem->parameters()->isNotEmpty()) {
+ throw new InvalidArgument('The "'.$bareItem::class.'" instance is not a Bare Item.');
+ }
+
+ return $bareItem;
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/MissingFeature.php b/vendor/bakame/http-structured-fields/src/MissingFeature.php
new file mode 100644
index 000000000..03bd8c05f
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/MissingFeature.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+final class MissingFeature extends SyntaxError
+{
+ public static function dueToLackOfSupport(Type $type, Ietf $rfc): self
+ {
+ return new self('The \''.$type->value.'\' type is not handled by '.strtoupper($rfc->name));
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/OuterList.php b/vendor/bakame/http-structured-fields/src/OuterList.php
new file mode 100644
index 000000000..644f1990e
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/OuterList.php
@@ -0,0 +1,430 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use ArrayAccess;
+use Bakame\Http\StructuredFields\Validation\Violation;
+use Countable;
+use DateTimeInterface;
+use Exception;
+use Iterator;
+use IteratorAggregate;
+use Stringable;
+
+use function array_filter;
+use function array_map;
+use function array_replace;
+use function array_splice;
+use function array_values;
+use function count;
+use function implode;
+use function is_iterable;
+use function uasort;
+
+use const ARRAY_FILTER_USE_BOTH;
+use const ARRAY_FILTER_USE_KEY;
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#name-lists
+ *
+ * @phpstan-import-type SfMemberInput from StructuredFieldProvider
+ * @phpstan-import-type SfInnerListPair from StructuredFieldProvider
+ * @phpstan-import-type SfItemPair from StructuredFieldProvider
+ *
+ * @implements ArrayAccess<int, InnerList|Item>
+ * @implements IteratorAggregate<int, InnerList|Item>
+ */
+final class OuterList implements ArrayAccess, Countable, IteratorAggregate
+{
+ /** @var list<InnerList|Item> */
+ private readonly array $members;
+
+ /**
+ * @param SfMemberInput ...$members
+ */
+ private function __construct(
+ iterable|StructuredFieldProvider|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool ...$members
+ ) {
+ $this->members = array_map(Member::innerListOrItem(...), array_values([...$members]));
+ }
+
+ /**
+ * Returns an instance from an HTTP textual representation.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.1
+ *
+ * @throws SyntaxError|Exception
+ */
+ public static function fromHttpValue(Stringable|string $httpValue, Ietf $rfc = Ietf::Rfc9651): self
+ {
+ return self::fromPairs((new Parser($rfc))->parseList($httpValue)); /* @phpstan-ignore-line */
+ }
+
+ /**
+ * @param StructuredFieldProvider|iterable<SfInnerListPair|SfItemPair> $pairs
+ */
+ public static function fromPairs(StructuredFieldProvider|iterable $pairs): self
+ {
+ if ($pairs instanceof StructuredFieldProvider) {
+ $pairs = $pairs->toStructuredField();
+ }
+
+ if (!is_iterable($pairs)) {
+ throw new InvalidArgument('The "'.$pairs::class.'" instance can not be used for creating a .'.self::class.' structured field.');
+ }
+
+ return match (true) {
+ $pairs instanceof OuterList,
+ $pairs instanceof InnerList => new self($pairs),
+ default => new self(...(function (iterable $pairs) {
+ foreach ($pairs as $member) {
+ yield Member::innerListOrItemFromPair($member);
+ }
+ })($pairs)),
+ };
+ }
+
+ /**
+ * @param StructuredFieldProvider|SfInnerListPair|SfItemPair|SfMemberInput ...$members
+ */
+ public static function new(iterable|StructuredFieldProvider|InnerList|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool ...$members): self
+ {
+ return self::fromPairs($members); /* @phpstan-ignore-line*/
+ }
+
+ public static function fromRfc9651(Stringable|string $httpValue): self
+ {
+ return self::fromHttpValue($httpValue, Ietf::Rfc9651);
+ }
+
+ public static function fromRfc8941(Stringable|string $httpValue): self
+ {
+ return self::fromHttpValue($httpValue, Ietf::Rfc8941);
+ }
+
+ public function toHttpValue(Ietf $rfc = Ietf::Rfc9651): string
+ {
+ return implode(', ', array_map(fn (Item|InnerList $member): string => $member->toHttpValue($rfc), $this->members));
+ }
+
+ public function toRfc9651(): string
+ {
+ return $this->toHttpValue(Ietf::Rfc9651);
+ }
+
+ public function toRfc8941(): string
+ {
+ return $this->toHttpValue(Ietf::Rfc8941);
+ }
+
+ public function __toString(): string
+ {
+ return $this->toHttpValue();
+ }
+
+ public function equals(mixed $other): bool
+ {
+ return $other instanceof self && $other->toHttpValue() === $this->toHttpValue();
+ }
+
+ /**
+ * Apply the callback if the given "condition" is (or resolves to) true.
+ *
+ * @param (callable($this): bool)|bool $condition
+ * @param callable($this): (self|null) $onSuccess
+ * @param ?callable($this): (self|null) $onFail
+ */
+ public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
+ {
+ if (!is_bool($condition)) {
+ $condition = $condition($this);
+ }
+
+ return match (true) {
+ $condition => $onSuccess($this),
+ null !== $onFail => $onFail($this),
+ default => $this,
+ } ?? $this;
+ }
+
+ public function getIterator(): Iterator
+ {
+ yield from $this->members;
+ }
+
+ public function count(): int
+ {
+ return count($this->members);
+ }
+
+ public function isEmpty(): bool
+ {
+ return !$this->isNotEmpty();
+ }
+
+ public function isNotEmpty(): bool
+ {
+ return [] !== $this->members;
+ }
+
+ /**
+ * @return array<int>
+ */
+ public function indices(): array
+ {
+ return array_keys($this->members);
+ }
+
+ public function hasIndices(int ...$indices): bool
+ {
+ $max = count($this->members);
+ foreach ($indices as $index) {
+ if (null === $this->filterIndex($index, $max)) {
+ return false;
+ }
+ }
+
+ return [] !== $indices;
+ }
+
+ private function filterIndex(int $index, ?int $max = null): ?int
+ {
+ $max ??= count($this->members);
+
+ return match (true) {
+ [] === $this->members,
+ 0 > $max + $index,
+ 0 > $max - $index - 1 => null,
+ 0 > $index => $max + $index,
+ default => $index,
+ };
+ }
+
+ /**
+ * @param ?callable(InnerList|Item): (bool|string) $validate
+ *
+ * @throws SyntaxError|Violation|StructuredFieldError
+ */
+ public function getByIndex(int $index, ?callable $validate = null): InnerList|Item
+ {
+ $value = $this->members[$this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index)];
+ if (null === $validate) {
+ return $value;
+ }
+
+ if (true === ($exceptionMessage = $validate($value))) {
+ return $value;
+ }
+
+ if (!is_string($exceptionMessage) || '' === trim($exceptionMessage)) {
+ $exceptionMessage = "The member at position '{index}' whose value is '{value}' failed validation.";
+ }
+
+ throw new Violation(strtr($exceptionMessage, ['{index}' => $index, '{value}' => $value->toHttpValue()]));
+ }
+
+ public function first(): InnerList|Item|null
+ {
+ return $this->members[0] ?? null;
+ }
+
+ public function last(): InnerList|Item|null
+ {
+ return $this->members[$this->filterIndex(-1)] ?? null;
+ }
+
+ /**
+ * Inserts members at the beginning of the list.
+ *
+ * @param SfMemberInput ...$members
+ */
+ public function unshift(
+ StructuredFieldProvider|InnerList|Item|iterable|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool ...$members
+ ): self {
+ $membersToAdd = array_reduce(
+ $members,
+ function (array $carry, $member) {
+ if ($member instanceof StructuredFieldProvider) {
+ $member = $member->toStructuredField();
+ }
+
+ return [...$carry, ...$member instanceof InnerList ? [...$member] : [$member]];
+ },
+ []
+ );
+
+ return match (true) {
+ [] === $membersToAdd => $this,
+ default => new self(...array_values($membersToAdd), ...$this->members),
+ };
+ }
+
+ /**
+ * Inserts members at the end of the list.
+ *
+ * @param SfMemberInput ...$members
+ */
+ public function push(
+ iterable|StructuredFieldProvider|InnerList|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool ...$members
+ ): self {
+ $membersToAdd = array_reduce(
+ $members,
+ function (array $carry, $member) {
+ if ($member instanceof StructuredFieldProvider) {
+ $member = $member->toStructuredField();
+ }
+
+ return [...$carry, ...$member instanceof InnerList ? [...$member] : [$member]];
+ },
+ []
+ );
+
+ return match (true) {
+ [] === $membersToAdd => $this,
+ default => new self(...$this->members, ...array_values($membersToAdd)),
+ };
+ }
+
+ /**
+ * Inserts members starting at the given index.
+ *
+ * @param SfMemberInput ...$members
+ *
+ * @throws InvalidOffset If the index does not exist
+ */
+ public function insert(
+ int $index,
+ iterable|StructuredFieldProvider|InnerList|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool ...$members
+ ): self {
+ $offset = $this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index);
+
+ return match (true) {
+ 0 === $offset => $this->unshift(...$members),
+ count($this->members) === $offset => $this->push(...$members),
+ [] === $members => $this,
+ default => (function (array $newMembers) use ($offset, $members) {
+ array_splice($newMembers, $offset, 0, $members);
+
+ return new self(...$newMembers);
+ })($this->members),
+ };
+ }
+
+ /**
+ * @param SfMemberInput $member
+ */
+ public function replace(
+ int $index,
+ iterable|StructuredFieldProvider|InnerList|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): self {
+ $offset = $this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index);
+ $member = Member::innerListOrItem($member);
+
+ return match (true) {
+ $member->equals($this->members[$offset]) => $this,
+ default => new self(...array_replace($this->members, [$offset => $member])),
+ };
+ }
+
+ public function removeByIndices(int ...$indices): self
+ {
+ $max = count($this->members);
+ $offsets = array_filter(
+ array_map(fn (int $index): ?int => $this->filterIndex($index, $max), $indices),
+ fn (?int $index): bool => null !== $index
+ );
+
+ return match (true) {
+ [] === $offsets => $this,
+ $max === count($offsets) => new self(),
+ default => new self(...array_filter(
+ $this->members,
+ fn (int $index): bool => !in_array($index, $offsets, true),
+ ARRAY_FILTER_USE_KEY
+ )),
+ };
+ }
+
+ /**
+ * @param int $offset
+ */
+ public function offsetExists(mixed $offset): bool
+ {
+ return $this->hasIndices($offset);
+ }
+
+ /**
+ * @param int $offset
+ */
+ public function offsetGet(mixed $offset): InnerList|Item
+ {
+ return $this->getByIndex($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.');
+ }
+
+ /**
+ * @param callable(InnerList|Item, int): TMap $callback
+ *
+ * @template TMap
+ *
+ * @return Iterator<TMap>
+ */
+ public function map(callable $callback): Iterator
+ {
+ foreach ($this->members as $offset => $member) {
+ yield ($callback)($member, $offset);
+ }
+ }
+
+ /**
+ * @param callable(TInitial|null, InnerList|Item, int): TInitial $callback
+ * @param TInitial|null $initial
+ *
+ * @template TInitial
+ *
+ * @return TInitial|null
+ */
+ public function reduce(callable $callback, mixed $initial = null): mixed
+ {
+ foreach ($this->members as $offset => $member) {
+ $initial = $callback($initial, $member, $offset);
+ }
+
+ return $initial;
+ }
+
+ /**
+ * @param callable(InnerList|Item, int): bool $callback
+ */
+ public function filter(callable $callback): self
+ {
+ $members = array_filter($this->members, $callback, ARRAY_FILTER_USE_BOTH);
+ if ($members === $this->members) {
+ return $this;
+ }
+
+ return new self(...$members);
+ }
+
+ /**
+ * @param callable(InnerList|Item, InnerList|Item): int $callback
+ */
+ public function sort(callable $callback): self
+ {
+ $members = $this->members;
+ uasort($members, $callback);
+
+ return new self(...$members);
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/ParameterAccess.php b/vendor/bakame/http-structured-fields/src/ParameterAccess.php
new file mode 100644
index 000000000..9c71f0955
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/ParameterAccess.php
@@ -0,0 +1,260 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use Bakame\Http\StructuredFields\Validation\Violation;
+use DateTimeImmutable;
+use DateTimeInterface;
+
+/**
+ * Common manipulation methods used when interacting with an object
+ * with a Parameters instance attached to it.
+ *
+ * @phpstan-import-type SfType from StructuredFieldProvider
+ * @phpstan-import-type SfItemInput from StructuredFieldProvider
+ */
+trait ParameterAccess
+{
+ /**
+ * Returns a copy of the associated parameter instance.
+ */
+ public function parameters(): Parameters
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * Returns the member value or null if no members value exists.
+ *
+ * @param ?callable(SfType): (bool|string) $validate
+ *
+ * @throws Violation if the validation fails
+ *
+ * @return SfType|null
+ */
+ public function parameterByKey(
+ string $key,
+ ?callable $validate = null,
+ bool|string $required = false,
+ Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool|null $default = null
+ ): Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool|null {
+ return $this->parameters->valueByKey($key, $validate, $required, $default);
+ }
+
+ /**
+ * Returns the member value and key as pair or an empty array if no members value exists.
+ *
+ * @param ?callable(SfType, string): (bool|string) $validate
+ * @param array{0:string, 1:SfType}|array{} $default
+ *
+ * @throws Violation if the validation fails
+ *
+ * @return array{0:string, 1:SfType}|array{}
+ */
+ public function parameterByIndex(
+ int $index,
+ ?callable $validate = null,
+ bool|string $required = false,
+ array $default = []
+ ): array {
+ return $this->parameters->valueByIndex($index, $validate, $required, $default);
+ }
+
+ /**
+ * Returns a new instance with the newly associated parameter instance.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ */
+ abstract public function withParameters(Parameters $parameters): static;
+
+ /**
+ * Adds a member if its key is not present at the of the associated parameter instance or update the instance at the given key.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ *
+ * @param StructuredFieldProvider|Item|SfType $member
+ *
+ * @throws SyntaxError If the string key is not a valid
+ */
+ public function addParameter(
+ string $key,
+ StructuredFieldProvider|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): static {
+ return $this->withParameters($this->parameters()->add($key, $member));
+ }
+
+ /**
+ * Adds a member at the start of the associated parameter instance and deletes any previous reference to the key if present.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ *
+ * @param StructuredFieldProvider|Item|SfType $member
+ *
+ * @throws SyntaxError If the string key is not a valid
+ */
+ public function prependParameter(
+ string $key,
+ StructuredFieldProvider|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): static {
+ return $this->withParameters($this->parameters()->prepend($key, $member));
+ }
+
+ /**
+ * Adds a member at the end of the associated parameter instance and deletes any previous reference to the key if present.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ *
+ * @param StructuredFieldProvider|Item|SfType $member
+ *
+ * @throws SyntaxError If the string key is not a valid
+ */
+ public function appendParameter(
+ string $key,
+ StructuredFieldProvider|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): static {
+ return $this->withParameters($this->parameters()->append($key, $member));
+ }
+
+ /**
+ * Removes all parameters members associated with the list of submitted keys in the associated parameter instance.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ */
+ public function withoutAnyParameter(): static
+ {
+ return $this->withParameters(Parameters::new());
+ }
+
+ /**
+ * Inserts pair at the end of the member list.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ *
+ * @param array{0:string, 1:SfItemInput} ...$pairs
+ */
+ public function pushParameters(array ...$pairs): static
+ {
+ return $this->withParameters($this->parameters()->push(...$pairs));
+ }
+
+ /**
+ * Inserts pair at the beginning of the member list.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ *
+ * @param array{0:string, 1:SfItemInput} ...$pairs
+ */
+ public function unshiftParameters(array ...$pairs): static
+ {
+ return $this->withParameters($this->parameters()->unshift(...$pairs));
+ }
+
+ /**
+ * Delete member based on their key.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ */
+ public function withoutParameterByKeys(string ...$keys): static
+ {
+ return $this->withParameters($this->parameters()->removeByKeys(...$keys));
+ }
+
+ /**
+ * Delete member based on their offsets.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ */
+ public function withoutParameterByIndices(int ...$indices): static
+ {
+ return $this->withParameters($this->parameters()->removeByIndices(...$indices));
+ }
+
+ /**
+ * Inserts members at the specified index.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ *
+ * @param array{0:string, 1:SfType} ...$pairs
+ */
+ public function insertParameters(int $index, array ...$pairs): static
+ {
+ return $this->withParameters($this->parameters()->insert($index, ...$pairs));
+ }
+
+ /**
+ * Replace the member at the specified index.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ *
+ * @param array{0:string, 1:SfType} $pair
+ */
+ public function replaceParameter(int $index, array $pair): static
+ {
+ return $this->withParameters($this->parameters()->replace($index, $pair));
+ }
+
+ /**
+ * Sort the object parameters by value using a callback.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ *
+ * @param callable(array{0:string, 1:Item}, array{0:string, 1:Item}): int $callback
+ */
+ public function sortParameters(callable $callback): static
+ {
+ return $this->withParameters($this->parameters()->sort($callback));
+ }
+
+ /**
+ * Filter the object parameters using a callback.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified parameter change.
+ *
+ * @param callable(array{0:string, 1:Item}, int): bool $callback
+ */
+ public function filterParameters(callable $callback): static
+ {
+ return $this->withParameters($this->parameters()->filter($callback));
+ }
+
+ /**
+ * Merges multiple instances using iterable pairs.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param StructuredFieldProvider|Parameters|Dictionary|iterable<array{0:string, 1:SfItemInput}> ...$others
+ */
+ public function mergeParametersByPairs(...$others): static
+ {
+ return $this->withParameters($this->parameters()->mergePairs(...$others));
+ }
+
+ /**
+ * Merges multiple instances using iterable associative.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified changes.
+ *
+ * @param StructuredFieldProvider|Dictionary|Parameters|iterable<string, SfItemInput> ...$others
+ */
+ public function mergeParametersByAssociative(...$others): static
+ {
+ return $this->withParameters($this->parameters()->mergeAssociative(...$others));
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/Parameters.php b/vendor/bakame/http-structured-fields/src/Parameters.php
new file mode 100644
index 000000000..9fd9a4e2b
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/Parameters.php
@@ -0,0 +1,763 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use ArrayAccess;
+use Bakame\Http\StructuredFields\Validation\Violation;
+use CallbackFilterIterator;
+use Countable;
+use DateTimeImmutable;
+use DateTimeInterface;
+use Exception;
+use Iterator;
+use IteratorAggregate;
+use Stringable;
+
+use function array_key_exists;
+use function array_keys;
+use function array_map;
+use function array_replace;
+use function count;
+use function implode;
+use function is_int;
+use function is_string;
+use function trim;
+use function uasort;
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.1.2
+ *
+ * @phpstan-import-type SfItemInput from StructuredFieldProvider
+ * @phpstan-import-type SfType from StructuredFieldProvider
+ *
+ * @implements ArrayAccess<string, Item>
+ * @implements IteratorAggregate<int, array{0:string, 1:Item}>
+ */
+final class Parameters implements ArrayAccess, Countable, IteratorAggregate
+{
+ /** @var array<string, Item> */
+ private readonly array $members;
+
+ /**
+ * @param iterable<string, SfItemInput> $members
+ */
+ private function __construct(iterable $members = [])
+ {
+ $filteredMembers = [];
+ foreach ($members as $key => $member) {
+ $filteredMembers[Key::from($key)->value] = Member::bareItem($member);
+ }
+
+ $this->members = $filteredMembers;
+ }
+
+ /**
+ * Returns a new instance.
+ */
+ public static function new(): self
+ {
+ return new self();
+ }
+
+ /**
+ * Returns a new instance from an associative iterable construct.
+ *
+ * its keys represent the dictionary entry key
+ * its values represent the dictionary entry value
+ *
+ * @param StructuredFieldProvider|iterable<string, SfItemInput> $members
+ */
+ public static function fromAssociative(StructuredFieldProvider|iterable $members): self
+ {
+ if ($members instanceof StructuredFieldProvider) {
+ $structuredField = $members->toStructuredField();
+
+ return match (true) {
+ $structuredField instanceof Dictionary,
+ $structuredField instanceof Parameters => new self($structuredField->toAssociative()),
+ default => throw new InvalidArgument('The '.StructuredFieldProvider::class.' must provide a structured field container; '.$structuredField::class.' given.'),
+ };
+ }
+
+ return new self($members);
+ }
+
+ /**
+ * Returns a new instance from a pair iterable construct.
+ *
+ * Each member is composed of an array with two elements
+ * the first member represents the instance entry key
+ * the second member represents the instance entry value
+ *
+ * @param StructuredFieldProvider|iterable<array{0:string, 1:SfItemInput}> $pairs
+ */
+ public static function fromPairs(StructuredFieldProvider|iterable $pairs): self
+ {
+ if ($pairs instanceof StructuredFieldProvider) {
+ $pairs = $pairs->toStructuredField();
+ }
+
+ if (!is_iterable($pairs)) {
+ throw new InvalidArgument('The "'.$pairs::class.'" instance can not be used for creating a .'.self::class.' structured field.');
+ }
+
+ return match (true) {
+ $pairs instanceof Parameters,
+ $pairs instanceof Dictionary => new self($pairs->toAssociative()),
+ default => new self((function (iterable $pairs) {
+ foreach ($pairs as [$key, $member]) {
+ yield $key => $member;
+ }
+ })($pairs)),
+ };
+ }
+
+ /**
+ * Returns an instance from an HTTP textual representation.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.1.2
+ *
+ * @throws SyntaxError|Exception If the string is not a valid
+ */
+ public static function fromHttpValue(Stringable|string $httpValue, Ietf $rfc = Ietf::Rfc9651): self
+ {
+ return self::fromPairs((new Parser($rfc))->parseParameters($httpValue)); /* @phpstan-ignore-line */
+ }
+
+ public static function fromRfc9651(Stringable|string $httpValue): self
+ {
+ return self::fromHttpValue($httpValue, Ietf::Rfc9651);
+ }
+
+ public static function fromRfc8941(Stringable|string $httpValue): self
+ {
+ return self::fromHttpValue($httpValue, Ietf::Rfc8941);
+ }
+
+ public function toHttpValue(Ietf $rfc = Ietf::Rfc9651): string
+ {
+ $formatter = static fn (Item $member, string $offset): string => match ($member->value()) {
+ true => ';'.$offset,
+ default => ';'.$offset.'='.$member->toHttpValue($rfc),
+ };
+
+ return implode('', array_map($formatter, $this->members, array_keys($this->members)));
+ }
+
+ public function toRfc9651(): string
+ {
+ return $this->toHttpValue(Ietf::Rfc9651);
+ }
+
+ public function toRfc8941(): string
+ {
+ return $this->toHttpValue(Ietf::Rfc8941);
+ }
+
+ public function __toString(): string
+ {
+ return $this->toHttpValue();
+ }
+
+ public function equals(mixed $other): bool
+ {
+ return $other instanceof self && $other->toHttpValue() === $this->toHttpValue();
+ }
+
+ /**
+ * Apply the callback if the given "condition" is (or resolves to) true.
+ *
+ * @param (callable($this): bool)|bool $condition
+ * @param callable($this): (self|null) $onSuccess
+ * @param ?callable($this): (self|null) $onFail
+ */
+ public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): self
+ {
+ if (!is_bool($condition)) {
+ $condition = $condition($this);
+ }
+
+ return match (true) {
+ $condition => $onSuccess($this),
+ null !== $onFail => $onFail($this),
+ default => $this,
+ } ?? $this;
+ }
+
+ public function count(): int
+ {
+ return count($this->members);
+ }
+
+ public function isEmpty(): bool
+ {
+ return !$this->isNotEmpty();
+ }
+
+ public function isNotEmpty(): bool
+ {
+ return [] !== $this->members;
+ }
+
+ /**
+ * @return Iterator<string, Item>
+ */
+ public function toAssociative(): Iterator
+ {
+ yield from $this->members;
+ }
+
+ /**
+ * @return Iterator<int, array{0:string, 1:Item}>
+ */
+ public function getIterator(): Iterator
+ {
+ foreach ($this->members as $index => $member) {
+ yield [$index, $member];
+ }
+ }
+
+ /**
+ * @return array<string>
+ */
+ public function keys(): array
+ {
+ return array_keys($this->members);
+ }
+
+ /**
+ * Tells whether the instance contain a members at the specified offsets.
+ */
+ public function hasKeys(string ...$keys): bool
+ {
+ foreach ($keys as $key) {
+ if (!array_key_exists($key, $this->members)) {
+ return false;
+ }
+ }
+
+ return [] !== $keys;
+ }
+
+ /**
+ * @param ?callable(SfType): (bool|string) $validate
+ *
+ * @throws Violation|InvalidOffset
+ */
+ public function getByKey(string $key, ?callable $validate = null): Item
+ {
+ $value = $this->members[$key] ?? throw InvalidOffset::dueToKeyNotFound($key);
+ if (null === $validate || true === ($exceptionMessage = $validate($value->value()))) {
+ return $value;
+ }
+
+ if (!is_string($exceptionMessage) || '' === trim($exceptionMessage)) {
+ $exceptionMessage = "The parameter '{key}' whose value is '{value}' failed validation.";
+ }
+
+ throw new Violation(strtr($exceptionMessage, ['{key}' => $key, '{value}' => $value->toHttpValue()]));
+ }
+
+ /**
+ * @return array<int>
+ */
+ public function indices(): array
+ {
+ return array_keys($this->keys());
+ }
+
+ public function hasIndices(int ...$indices): bool
+ {
+ $max = count($this->members);
+ foreach ($indices as $index) {
+ if (null === $this->filterIndex($index, $max)) {
+ return false;
+ }
+ }
+
+ return [] !== $indices;
+ }
+
+ /**
+ * Filters and format instance index.
+ */
+ private function filterIndex(int $index, int|null $max = null): int|null
+ {
+ $max ??= count($this->members);
+
+ return match (true) {
+ [] === $this->members,
+ 0 > $max + $index,
+ 0 > $max - $index - 1 => null,
+ 0 > $index => $max + $index,
+ default => $index,
+ };
+ }
+
+ /**
+ * @param ?callable(SfType, string): (bool|string) $validate
+ *
+ * @throws InvalidOffset|Violation
+ *
+ * @return array{0:string, 1:Item}
+ */
+ public function getByIndex(int $index, ?callable $validate = null): array
+ {
+ $foundOffset = $this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index);
+ foreach ($this as $offset => $pair) {
+ if ($offset === $foundOffset) {
+ break;
+ }
+ }
+
+ if (!isset($pair)) {
+ throw InvalidOffset::dueToIndexNotFound($index);
+ }
+
+ if (null === $validate || true === ($exceptionMessage = $validate($pair[1]->value(), $pair[0]))) {
+ return $pair;
+ }
+
+ if (!is_string($exceptionMessage) || '' === trim($exceptionMessage)) {
+ $exceptionMessage = "The parameter at position '{index}' whose key is '{key}' with the value '{value}' failed validation.";
+ }
+
+ throw new Violation(strtr($exceptionMessage, ['{index}' => $index, '{key}' => $pair[0], '{value}' => $pair[1]->toHttpValue()]));
+ }
+
+ /**
+ * Returns the key associated with the given index or null otherwise.
+ */
+ public function indexByKey(string $key): ?int
+ {
+ foreach ($this as $index => $member) {
+ if ($key === $member[0]) {
+ return $index;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the index associated with the given key or null otherwise.
+ */
+ public function keyByIndex(int $index): ?string
+ {
+ $index = $this->filterIndex($index);
+ if (null === $index) {
+ return null;
+ }
+
+ foreach ($this as $offset => $member) {
+ if ($offset === $index) {
+ return $member[0];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return array{0:string, 1:Item}|array{}
+ */
+ public function first(): array
+ {
+ try {
+ return $this->getByIndex(0);
+ } catch (InvalidOffset) {
+ return [];
+ }
+ }
+
+ /**
+ * @return array{0:string, 1:Item}|array{}
+ */
+ public function last(): array
+ {
+ try {
+ return $this->getByIndex(-1);
+ } catch (InvalidOffset) {
+ return [];
+ }
+ }
+
+ /**
+ * Returns true only if the instance only contains the listed keys, false otherwise.
+ *
+ * @param array<string> $keys
+ */
+ public function allowedKeys(array $keys): bool
+ {
+ foreach ($this->members as $key => $member) {
+ if (!in_array($key, $keys, true)) {
+ return false;
+ }
+ }
+
+ return [] !== $keys;
+ }
+
+ /**
+ * Returns the member value or null if no members value exists.
+ *
+ * @param ?callable(SfType): (bool|string) $validate
+ *
+ * @throws Violation if the validation fails
+ *
+ * @return SfType|null
+ */
+ public function valueByKey(
+ string $key,
+ ?callable $validate = null,
+ bool|string $required = false,
+ Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool|null $default = null
+ ): Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool|null {
+ if (null !== $default && null === Type::tryFromVariable($default)) {
+ throw new SyntaxError('The default parameter is invalid.');
+ }
+
+ try {
+ return $this->getByKey($key, $validate)->value();
+ } catch (InvalidOffset $exception) {
+ if (false === $required) {
+ return $default;
+ }
+
+ $message = $required;
+ if (!is_string($message) || '' === trim($message)) {
+ $message = "The required parameter '{key}' is missing.";
+ }
+
+ throw new Violation(strtr($message, ['{key}' => $key]), previous: $exception);
+ }
+ }
+
+ /**
+ * Returns the member value and key as pair or an empty array if no members value exists.
+ *
+ * @param ?callable(SfType, string): (bool|string) $validate
+ * @param array{0:string, 1:SfType}|array{} $default
+ *
+ * @throws Violation if the validation fails
+ *
+ * @return array{0:string, 1:SfType}|array{}
+ */
+ public function valueByIndex(int $index, ?callable $validate = null, bool|string $required = false, array $default = []): array
+ {
+ $default = match (true) {
+ [] === $default => [],
+ !array_is_list($default) => throw new SyntaxError('The pair must be represented by an array as a list.'), /* @phpstan-ignore-line */
+ 2 !== count($default) => throw new SyntaxError('The pair first member is the key; its second member is its value.'), /* @phpstan-ignore-line */
+ null === ($key = Key::tryFrom($default[0])?->value) => throw new SyntaxError('The pair first member is invalid.'),
+ null === ($value = Item::tryNew($default[1])?->value()) => throw new SyntaxError('The pair second member is invalid.'),
+ default => [$key, $value],
+ };
+
+ try {
+ $tuple = $this->getByIndex($index, $validate);
+
+ return [$tuple[0], $tuple[1]->value()];
+ } catch (InvalidOffset $exception) {
+ if (false === $required) {
+ return $default;
+ }
+
+ $message = $required;
+ if (!is_string($message) || '' === trim($message)) {
+ $message = "The required parameter at position '{index}' is missing.";
+ }
+
+ throw new Violation(strtr($message, ['{index}' => $index]), previous: $exception);
+ }
+ }
+
+ /**
+ * @param StructuredFieldProvider|Item|SfType $member
+ */
+ public function add(
+ string $key,
+ StructuredFieldProvider|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): self {
+ $key = Key::from($key)->value;
+ $member = Member::bareItem($member);
+ $oldMember = $this->members[$key] ?? null;
+ if (null === $oldMember || !$oldMember->equals($member)) {
+ $members = $this->members;
+ $members[$key] = $member;
+
+ return new self($members);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param array<string, Item> $members
+ */
+ private function newInstance(array $members): self
+ {
+ foreach ($members as $offset => $member) {
+ if (!isset($this->members[$offset]) || !$this->members[$offset]->equals($member)) {
+ return new self($members);
+ }
+ }
+
+ return $this;
+ }
+
+ private function remove(string|int ...$offsets): self
+ {
+ if ([] === $this->members || [] === $offsets) {
+ return $this;
+ }
+
+ $keys = array_keys($this->members);
+ $max = count($keys);
+ $reducer = fn (array $carry, string|int $key): array => match (true) {
+ is_string($key) && (false !== ($position = array_search($key, $keys, true))),
+ is_int($key) && (null !== ($position = $this->filterIndex($key, $max))) => [$position => true] + $carry,
+ default => $carry,
+ };
+
+ $indices = array_reduce($offsets, $reducer, []);
+
+ return match (true) {
+ [] === $indices => $this,
+ $max === count($indices) => self::new(),
+ default => self::fromPairs((function (array $offsets) {
+ foreach ($this as $offset => $pair) {
+ if (!array_key_exists($offset, $offsets)) {
+ yield $pair;
+ }
+ }
+ })($indices)),
+ };
+ }
+
+ public function removeByIndices(int ...$indices): self
+ {
+ return $this->remove(...$indices);
+ }
+
+ public function removeByKeys(string ...$keys): self
+ {
+ return $this->remove(...$keys);
+ }
+
+ /**
+ * @param StructuredFieldProvider|Item|SfType $member
+ */
+ public function append(
+ string $key,
+ StructuredFieldProvider|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): self {
+ $key = Key::from($key)->value;
+ $member = Member::bareItem($member);
+ $members = $this->members;
+ unset($members[$key]);
+ $members[$key] = $member;
+
+ return $this->newInstance($members);
+ }
+
+ /**
+ * @param StructuredFieldProvider|Item|SfType $member
+ */
+ public function prepend(
+ string $key,
+ StructuredFieldProvider|Item|Token|Bytes|DisplayString|DateTimeInterface|string|int|float|bool $member
+ ): self {
+ $key = Key::from($key)->value;
+ $member = Member::bareItem($member);
+ $members = $this->members;
+ unset($members[$key]);
+
+ return $this->newInstance([$key => $member, ...$members]);
+ }
+
+ /**
+ * @param array{0:string, 1:SfItemInput} ...$pairs
+ */
+ public function push(array ...$pairs): self
+ {
+ return match (true) {
+ [] === $pairs => $this,
+ default => self::fromPairs((function (iterable $pairs) {
+ yield from $this->getIterator();
+ yield from $pairs;
+ })($pairs)),
+ };
+ }
+
+ /**
+ * @param array{0:string, 1:SfItemInput} ...$pairs
+ */
+ public function unshift(array ...$pairs): self
+ {
+ return match (true) {
+ [] === $pairs => $this,
+ default => self::fromPairs((function (iterable $pairs) {
+ yield from $pairs;
+ yield from $this->getIterator();
+ })($pairs)),
+ };
+ }
+
+ /**
+ * @param array{0:string, 1:SfItemInput} ...$members
+ */
+ public function insert(int $index, array ...$members): self
+ {
+ $offset = $this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index);
+
+ return match (true) {
+ [] === $members => $this,
+ 0 === $offset => $this->unshift(...$members),
+ count($this->members) === $offset => $this->push(...$members),
+ default => (function (Iterator $newMembers) use ($offset, $members) {
+ $newMembers = iterator_to_array($newMembers);
+ array_splice($newMembers, $offset, 0, $members);
+
+ return self::fromPairs($newMembers);
+ })($this->getIterator()),
+ };
+ }
+
+ /**
+ * @param array{0:string, 1:SfItemInput} $pair
+ */
+ public function replace(int $index, array $pair): self
+ {
+ $offset = $this->filterIndex($index) ?? throw InvalidOffset::dueToIndexNotFound($index);
+ $pair[1] = Member::bareItem($pair[1]);
+ $pairs = iterator_to_array($this);
+
+ return match (true) {
+ $pairs[$offset][0] === $pair[0] && $pairs[$offset][1]->equals($pair[1]) => $this,
+ default => self::fromPairs(array_replace($pairs, [$offset => $pair])),
+ };
+ }
+
+ /**
+ * @param StructuredFieldProvider|Dictionary|Parameters|iterable<string, SfItemInput> ...$others
+ */
+ public function mergeAssociative(StructuredFieldProvider|iterable ...$others): self
+ {
+ $members = $this->members;
+ foreach ($others as $other) {
+ if ($other instanceof StructuredFieldProvider) {
+ $other = $other->toStructuredField();
+ if (!$other instanceof Dictionary && !$other instanceof Parameters) {
+ throw new InvalidArgument('The "'.$other::class.'" instance can not be used for creating a .'.self::class.' structured field.');
+ }
+ }
+
+ if ($other instanceof self || $other instanceof Dictionary) {
+ $other = $other->toAssociative();
+ }
+
+ foreach ($other as $key => $value) {
+ $members[$key] = $value;
+ }
+ }
+
+ return new self($members);
+ }
+
+ /**
+ * @param StructuredFieldProvider|Parameters|Dictionary|iterable<array{0:string, 1:SfItemInput}> ...$others
+ */
+ public function mergePairs(Dictionary|Parameters|StructuredFieldProvider|iterable ...$others): self
+ {
+ $members = $this->members;
+ foreach ($others as $other) {
+ if (!$other instanceof self) {
+ $other = self::fromPairs($other);
+ }
+ foreach ($other->toAssociative() as $key => $value) {
+ $members[$key] = $value;
+ }
+ }
+
+ return new self($members);
+ }
+
+ /**
+ * @param string $offset
+ */
+ public function offsetExists(mixed $offset): bool
+ {
+ return $this->hasKeys($offset);
+ }
+
+ /**
+ * @param string $offset
+ */
+ public function offsetGet(mixed $offset): Item
+ {
+ return $this->getByKey($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.');
+ }
+
+ /**
+ * @param callable(array{0:string, 1:Item}, int): TMap $callback
+ *
+ * @template TMap
+ *
+ * @return Iterator<TMap>
+ */
+ public function map(callable $callback): Iterator
+ {
+ foreach ($this as $offset => $pair) {
+ yield ($callback)($pair, $offset);
+ }
+ }
+
+ /**
+ * @param callable(TInitial|null, array{0:string, 1:Item}, int): TInitial $callback
+ * @param TInitial|null $initial
+ *
+ * @template TInitial
+ *
+ * @return TInitial|null
+ */
+ public function reduce(callable $callback, mixed $initial = null): mixed
+ {
+ foreach ($this as $offset => $pair) {
+ $initial = $callback($initial, $pair, $offset);
+ }
+
+ return $initial;
+ }
+
+ /**
+ * @param callable(array{0:string, 1:Item}, int): bool $callback
+ */
+ public function filter(callable $callback): self
+ {
+ return self::fromPairs(new CallbackFilterIterator($this->getIterator(), $callback));
+ }
+
+ /**
+ * @param callable(array{0:string, 1:Item}, array{0:string, 1:Item}): int $callback
+ */
+ public function sort(callable $callback): self
+ {
+ $members = iterator_to_array($this);
+ uasort($members, $callback);
+
+ return self::fromPairs($members);
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/Parser.php b/vendor/bakame/http-structured-fields/src/Parser.php
new file mode 100644
index 000000000..4c7448c15
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/Parser.php
@@ -0,0 +1,523 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use DateTimeImmutable;
+use Exception;
+use Stringable;
+
+use function in_array;
+use function ltrim;
+use function preg_match;
+use function str_contains;
+use function strlen;
+use function substr;
+use function trim;
+
+/**
+ * A class to parse HTTP Structured Fields from their HTTP textual representation according to RFC8941.
+ *
+ * Based on gapple\StructuredFields\Parser class in Structured Field Values for PHP v1.0.0.
+ *
+ * @link https://github.com/gapple/structured-fields/blob/v1.0.0/src/Parser.php
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2
+ *
+ * @see Dictionary::fromHttpValue()
+ * @see Parameters::fromHttpValue()
+ * @see OuterList::fromHttpValue()
+ * @see InnerList::fromHttpValue()
+ * @see Item::fromHttpValue()
+ *
+ * @internal Do not use directly this class as it's behaviour and return type
+ * MAY change significantly even during a major release cycle.
+ *
+ * @phpstan-type SfValue Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool
+ * @phpstan-type SfParameter array<array{0:string, 1:SfValue}>
+ * @phpstan-type SfItem array{0:SfValue, 1: SfParameter}
+ * @phpstan-type SfInnerList array{0:array<SfItem>, 1: SfParameter}
+ */
+final class Parser
+{
+ private const REGEXP_BYTES = '/^(?<sequence>:(?<byte>[a-z\d+\/=]*):)/i';
+ private const REGEXP_BOOLEAN = '/^\?[01]/';
+ private const REGEXP_DATE = '/^@(?<date>-?\d{1,15})(?:[^\d.]|$)/';
+ private const REGEXP_DECIMAL = '/^-?\d{1,12}\.\d{1,3}$/';
+ private const REGEXP_INTEGER = '/^-?\d{1,15}$/';
+ private const REGEXP_TOKEN = "/^(?<token>[a-z*][a-z\d:\/!#\$%&'*+\-.^_`|~]*)/i";
+ private const REGEXP_INVALID_CHARACTERS = "/[\r\t\n]|[^\x20-\x7E]/";
+ private const REGEXP_VALID_NUMBER = '/^(?<number>-?\d+(?:\.\d+)?)(?:[^\d.]|$)/';
+ private const REGEXP_VALID_SPACE = '/^(?<space>,[ \t]*)/';
+ private const FIRST_CHARACTER_RANGE_NUMBER = '-1234567890';
+ private const FIRST_CHARACTER_RANGE_TOKEN = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ*';
+
+ public function __construct(private readonly Ietf $rfc)
+ {
+ }
+
+ /**
+ * Returns an Item as a PHP list array from an HTTP textual representation.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-item
+ *
+ *
+ * @throws Exception|SyntaxError
+ *
+ * @return SfItem
+ */
+ public function parseItem(Stringable|string $httpValue): array
+ {
+ $remainder = trim((string) $httpValue, ' ');
+ if ('' === $remainder || 1 === preg_match(self::REGEXP_INVALID_CHARACTERS, $remainder)) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for an item contains invalid characters.");
+ }
+
+ [$value, $offset] = $this->extractValue($remainder);
+ $remainder = substr($remainder, $offset);
+ if ('' !== $remainder && !str_contains($remainder, ';')) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for an item contains invalid characters.");
+ }
+
+ return [$value, $this->parseParameters($remainder)]; /* @phpstan-ignore-line */
+ }
+
+ /**
+ * Returns a Parameters ordered map container as a PHP list array from an HTTP textual representation.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.1.2
+ *
+ * @throws SyntaxError|Exception
+ *
+ * @return array<SfParameter>
+ */
+ public function parseParameters(Stringable|string $httpValue): array
+ {
+ $remainder = trim((string) $httpValue);
+ [$parameters, $offset] = $this->extractParametersValues($remainder);
+ if (strlen($remainder) !== $offset) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for Parameters contains invalid characters.");
+ }
+
+ return $parameters; /* @phpstan-ignore-line */
+ }
+
+ /**
+ * Returns an ordered list represented as a PHP list array from an HTTP textual representation.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.1
+ *
+ * @throws SyntaxError|Exception
+ *
+ * @return array<SfInnerList|SfItem>
+ */
+ public function parseList(Stringable|string $httpValue): array
+ {
+ $list = [];
+ $remainder = ltrim((string) $httpValue, ' ');
+ while ('' !== $remainder) {
+ [$list[], $offset] = $this->extractItemOrInnerList($remainder);
+ $remainder = self::removeCommaSeparatedWhiteSpaces($remainder, $offset);
+ }
+
+ return $list;
+ }
+
+ /**
+ * Returns a Dictionary represented as a PHP list array from an HTTP textual representation.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.2
+ *
+ * @throws SyntaxError|Exception
+ *
+ * @return array<array{0:string, 1:SfInnerList|SfItem}>
+ */
+ public function parseDictionary(Stringable|string $httpValue): array
+ {
+ $map = [];
+ $remainder = ltrim((string) $httpValue, ' ');
+ while ('' !== $remainder) {
+ $key = Key::fromStringBeginning($remainder)->value;
+ $remainder = substr($remainder, strlen($key));
+ if ('' === $remainder || '=' !== $remainder[0]) {
+ $remainder = '=?1'.$remainder;
+ }
+ $member = [$key];
+
+ [$member[1], $offset] = $this->extractItemOrInnerList(substr($remainder, 1));
+ $remainder = self::removeCommaSeparatedWhiteSpaces($remainder, ++$offset);
+ $map[] = $member;
+ }
+
+ return $map;
+ }
+
+ /**
+ * Returns an inner list represented as a PHP list array from an HTTP textual representation.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.1.2
+ *
+ * @throws SyntaxError|Exception
+ *
+ * @return SfInnerList
+ */
+ public function parseInnerList(Stringable|string $httpValue): array
+ {
+ $remainder = ltrim((string) $httpValue, ' ');
+ if ('(' !== $remainder[0]) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a inner list is missing a parenthesis.");
+ }
+
+ [$list, $offset] = $this->extractInnerList($remainder);
+ $remainder = self::removeOptionalWhiteSpaces(substr($remainder, $offset));
+ if ('' !== $remainder) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a inner list contains invalid data.");
+ }
+
+ return $list;
+ }
+
+ /**
+ * Filter optional white spaces before and after comma.
+ *
+ * @see https://tools.ietf.org/html/rfc7230#section-3.2.3
+ */
+ private static function removeCommaSeparatedWhiteSpaces(string $remainder, int $offset): string
+ {
+ $remainder = self::removeOptionalWhiteSpaces(substr($remainder, $offset));
+ if ('' === $remainder) {
+ return '';
+ }
+
+ if (1 !== preg_match(self::REGEXP_VALID_SPACE, $remainder, $found)) {
+ throw new SyntaxError('The HTTP textual representation is missing an excepted comma.');
+ }
+
+ $remainder = substr($remainder, strlen($found['space']));
+
+ if ('' === $remainder) {
+ throw new SyntaxError('The HTTP textual representation has an unexpected end of line.');
+ }
+
+ return $remainder;
+ }
+
+ /**
+ * Remove optional white spaces before field value.
+ *
+ * @see https://tools.ietf.org/html/rfc7230#section-3.2.3
+ */
+ private static function removeOptionalWhiteSpaces(string $httpValue): string
+ {
+ return ltrim($httpValue, " \t");
+ }
+
+ /**
+ * Returns an item or an inner list as a PHP list array from an HTTP textual representation.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.1.1
+ *
+ * @throws SyntaxError|Exception
+ *
+ * @return array{0: SfInnerList|SfItem, 1:int}
+ */
+ private function extractItemOrInnerList(string $httpValue): array
+ {
+ if ('(' === $httpValue[0]) {
+ return $this->extractInnerList($httpValue);
+ }
+
+ [$item, $remainder] = $this->extractItem($httpValue);
+
+ return [$item, strlen($httpValue) - strlen($remainder)];
+ }
+
+ /**
+ * Returns an inner list represented as a PHP list array from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.1.2
+ *
+ * @throws SyntaxError|Exception
+ *
+ * @return array{0: SfInnerList, 1 :int}
+ */
+ private function extractInnerList(string $httpValue): array
+ {
+ $list = [];
+ $remainder = substr($httpValue, 1);
+ while ('' !== $remainder) {
+ $remainder = ltrim($remainder, ' ');
+
+ if (')' === $remainder[0]) {
+ $remainder = substr($remainder, 1);
+ [$parameters, $offset] = $this->extractParametersValues($remainder);
+ $remainder = substr($remainder, $offset);
+
+ return [[$list, $parameters], strlen($httpValue) - strlen($remainder)];
+ }
+
+ [$list[], $remainder] = $this->extractItem($remainder);
+
+ if ('' !== $remainder && !in_array($remainder[0], [' ', ')'], true)) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a inner list is using invalid characters.");
+ }
+ }
+
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a inner list has an unexpected end of line.");
+ }
+
+ /**
+ * Returns an item represented as a PHP array from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @throws SyntaxError|Exception
+ *
+ * @return array{0:SfItem, 1:string}
+ */
+ private function extractItem(string $remainder): array
+ {
+ [$value, $offset] = $this->extractValue($remainder);
+ $remainder = substr($remainder, $offset);
+ [$parameters, $offset] = $this->extractParametersValues($remainder);
+
+ return [[$value, $parameters], substr($remainder, $offset)];
+ }
+
+ /**
+ * Returns an item value from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.3.1
+ *
+ * @throws SyntaxError|Exception
+ *
+ * @return array{0:SfValue, 1:int}
+ */
+ private function extractValue(string $httpValue): array
+ {
+ return match (true) {
+ '"' === $httpValue[0] => self::extractString($httpValue),
+ ':' === $httpValue[0] => self::extractBytes($httpValue),
+ '?' === $httpValue[0] => self::extractBoolean($httpValue),
+ '@' === $httpValue[0] => self::extractDate($httpValue, $this->rfc),
+ str_starts_with($httpValue, '%"') => self::extractDisplayString($httpValue, $this->rfc),
+ str_contains(self::FIRST_CHARACTER_RANGE_NUMBER, $httpValue[0]) => self::extractNumber($httpValue),
+ str_contains(self::FIRST_CHARACTER_RANGE_TOKEN, $httpValue[0]) => self::extractToken($httpValue),
+ default => throw new SyntaxError("The HTTP textual representation \"$httpValue\" for an item value is unknown or unsupported."),
+ };
+ }
+
+ /**
+ * Returns a parameters container represented as a PHP associative array from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.3.2
+ *
+ * @throws SyntaxError|Exception
+ *
+ * @return array{0:SfParameter, 1:int}
+ */
+ private function extractParametersValues(Stringable|string $httpValue): array
+ {
+ $map = [];
+ $httpValue = (string) $httpValue;
+ $remainder = $httpValue;
+ while ('' !== $remainder && ';' === $remainder[0]) {
+ $remainder = ltrim(substr($remainder, 1), ' ');
+ $key = Key::fromStringBeginning($remainder)->value;
+ $member = [$key, true];
+ $remainder = substr($remainder, strlen($key));
+ if ('' !== $remainder && '=' === $remainder[0]) {
+ $remainder = substr($remainder, 1);
+ [$member[1], $offset] = $this->extractValue($remainder);
+ $remainder = substr($remainder, $offset);
+ }
+
+ $map[] = $member;
+ }
+
+ return [$map, strlen($httpValue) - strlen($remainder)];
+ }
+
+ /**
+ * Returns a boolean from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.8
+ *
+ * @return array{0:bool, 1:int}
+ */
+ private static function extractBoolean(string $httpValue): array
+ {
+ return match (1) {
+ preg_match(self::REGEXP_BOOLEAN, $httpValue) => ['1' === $httpValue[1], 2],
+ default => throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a Boolean contains invalid characters."),
+ };
+ }
+
+ /**
+ * Returns an int or a float from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.4
+ *
+ * @return array{0:int|float, 1:int}
+ */
+ private static function extractNumber(string $httpValue): array
+ {
+ if (1 !== preg_match(self::REGEXP_VALID_NUMBER, $httpValue, $found)) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a Number contains invalid characters.");
+ }
+
+ return match (1) {
+ preg_match(self::REGEXP_DECIMAL, $found['number']) => [(float) $found['number'], strlen($found['number'])],
+ preg_match(self::REGEXP_INTEGER, $found['number']) => [(int) $found['number'], strlen($found['number'])],
+ default => throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a Number contains too much digit."),
+ };
+ }
+
+ /**
+ * Returns DateTimeImmutable instance from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @see https://httpwg.org/http-extensions/draft-ietf-httpbis-sfbis.html#name-dates
+ *
+ * @throws SyntaxError
+ * @throws Exception
+ *
+ * @return array{0:DateTimeImmutable, 1:int}
+ */
+ private static function extractDate(string $httpValue, Ietf $rfc): array
+ {
+ if (!$rfc->supports(Type::Date)) {
+ throw MissingFeature::dueToLackOfSupport(Type::Date, $rfc);
+ }
+
+ if (1 !== preg_match(self::REGEXP_DATE, $httpValue, $found)) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a Date contains invalid characters.");
+ }
+
+ return [new DateTimeImmutable('@'.$found['date']), strlen($found['date']) + 1];
+ }
+
+ /**
+ * Returns a string from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.5
+ *
+ * @return array{0:string, 1:int}
+ */
+ private static function extractString(string $httpValue): array
+ {
+ $offset = 1;
+ $remainder = substr($httpValue, $offset);
+ $output = '';
+
+ if (1 === preg_match(self::REGEXP_INVALID_CHARACTERS, $remainder)) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a String contains an invalid end string.");
+ }
+
+ while ('' !== $remainder) {
+ $char = $remainder[0];
+ $offset += 1;
+
+ if ('"' === $char) {
+ return [$output, $offset];
+ }
+
+ $remainder = substr($remainder, 1);
+
+ if ('\\' !== $char) {
+ $output .= $char;
+ continue;
+ }
+
+ $char = $remainder[0] ?? '';
+ $offset += 1;
+ $remainder = substr($remainder, 1);
+
+ if (!in_array($char, ['"', '\\'], true)) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a String contains an invalid end string.");
+ }
+
+ $output .= $char;
+ }
+
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a String contains an invalid end string.");
+ }
+
+ /**
+ * Returns a string from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-sfbis#section-4.2.10
+ *
+ * @return array{0:DisplayString, 1:int}
+ */
+ private static function extractDisplayString(string $httpValue, Ietf $rfc): array
+ {
+ if (!$rfc->supports(Type::DisplayString)) {
+ throw MissingFeature::dueToLackOfSupport(Type::DisplayString, $rfc);
+ }
+
+ $offset = 2;
+ $remainder = substr($httpValue, $offset);
+ $output = '';
+
+ if (1 === preg_match(self::REGEXP_INVALID_CHARACTERS, $remainder)) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a DisplayString contains an invalid character string.");
+ }
+
+ while ('' !== $remainder) {
+ $char = $remainder[0];
+ $offset += 1;
+
+ if ('"' === $char) {
+ return [DisplayString::fromEncoded($output), $offset];
+ }
+
+ $remainder = substr($remainder, 1);
+ if ('%' !== $char) {
+ $output .= $char;
+ continue;
+ }
+
+ $octet = substr($remainder, 0, 2);
+ $offset += 2;
+ if (1 === preg_match('/^[0-9a-f]]{2}$/', $octet)) {
+ throw new SyntaxError("The HTTP textual representation '$httpValue' for a DisplayString contains uppercased percent encoding sequence.");
+ }
+
+ $remainder = substr($remainder, 2);
+ $output .= $char.$octet;
+ }
+
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a DisplayString contains an invalid end string.");
+ }
+
+ /**
+ * Returns a Token from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.6
+ *
+ * @return array{0:Token, 1:int}
+ */
+ private static function extractToken(string $httpValue): array
+ {
+ preg_match(self::REGEXP_TOKEN, $httpValue, $found);
+
+ $token = $found['token'] ?? throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a Token contains invalid characters.");
+
+ return [Token::fromString($token), strlen($token)];
+ }
+
+ /**
+ * Returns a Byte Sequence from an HTTP textual representation and the consumed offset in a tuple.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-4.2.7
+ *
+ * @return array{0:Bytes, 1:int}
+ */
+ private static function extractBytes(string $httpValue): array
+ {
+ if (1 !== preg_match(self::REGEXP_BYTES, $httpValue, $found)) {
+ throw new SyntaxError("The HTTP textual representation \"$httpValue\" for a Byte Sequence contains invalid characters.");
+ }
+
+ return [Bytes::fromEncoded($found['byte']), strlen($found['sequence'])];
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/StructuredFieldError.php b/vendor/bakame/http-structured-fields/src/StructuredFieldError.php
new file mode 100644
index 000000000..fa823f6c8
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/StructuredFieldError.php
@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use Throwable;
+
+interface StructuredFieldError extends Throwable
+{
+}
diff --git a/vendor/bakame/http-structured-fields/src/StructuredFieldProvider.php b/vendor/bakame/http-structured-fields/src/StructuredFieldProvider.php
new file mode 100644
index 000000000..fd9e2be99
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/StructuredFieldProvider.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use DateTimeImmutable;
+use DateTimeInterface;
+
+/**
+ * @phpstan-type SfType Bytes|Token|DisplayString|DateTimeImmutable|string|int|float|bool
+ * @phpstan-type SfTypeInput SfType|DateTimeInterface
+ * @phpstan-type SfList InnerList|OuterList
+ * @phpstan-type SfOrderedMap Dictionary|Parameters
+ * @phpstan-type SfDataType SfList|SfOrderedMap|Item
+ * @phpstan-type SfItemInput SfTypeInput|SfDataType|StructuredFieldProvider
+ * @phpstan-type SfMemberInput iterable<SfItemInput>|SfItemInput
+ * @phpstan-type SfParameterInput iterable<array{0:string, 1?:SfItemInput}>
+ * @phpstan-type SfInnerListPair array{0:iterable<SfItemInput>, 1?:Parameters|SfParameterInput}
+ * @phpstan-type SfItemPair array{0:SfTypeInput, 1?:Parameters|SfParameterInput}
+ */
+interface StructuredFieldProvider
+{
+ /**
+ * Returns one of the StructuredField Data Type class.
+ */
+ public function toStructuredField(): Dictionary|InnerList|Item|OuterList|Parameters;
+}
diff --git a/vendor/bakame/http-structured-fields/src/SyntaxError.php b/vendor/bakame/http-structured-fields/src/SyntaxError.php
new file mode 100644
index 000000000..304671fe9
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/SyntaxError.php
@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use InvalidArgumentException;
+
+class SyntaxError extends InvalidArgumentException implements StructuredFieldError
+{
+}
diff --git a/vendor/bakame/http-structured-fields/src/Token.php b/vendor/bakame/http-structured-fields/src/Token.php
new file mode 100644
index 000000000..88f41dbe8
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/Token.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use Stringable;
+use Throwable;
+
+use function preg_match;
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#name-tokens
+ */
+final class Token
+{
+ private function __construct(private readonly string $value)
+ {
+ if (1 !== preg_match("/^([a-z*][a-z\d:\/!#\$%&'*+\-.^_`|~]*)$/i", $this->value)) {
+ throw new SyntaxError('The token '.$this->value.' contains invalid characters.');
+ }
+ }
+
+ public function toString(): string
+ {
+ return $this->value;
+ }
+
+ public static function tryFromString(Stringable|string $value): ?self
+ {
+ try {
+ return self::fromString($value);
+ } catch (Throwable) {
+ return null;
+ }
+ }
+
+ public static function fromString(Stringable|string $value): self
+ {
+ return new self((string)$value);
+ }
+
+ public function equals(mixed $other): bool
+ {
+ return $other instanceof self && $other->value === $this->value;
+ }
+
+ public function type(): Type
+ {
+ return Type::Token;
+ }
+}
diff --git a/vendor/bakame/http-structured-fields/src/Type.php b/vendor/bakame/http-structured-fields/src/Type.php
new file mode 100644
index 000000000..44eb8764d
--- /dev/null
+++ b/vendor/bakame/http-structured-fields/src/Type.php
@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Bakame\Http\StructuredFields;
+
+use DateTimeInterface;
+
+use function abs;
+use function floor;
+use function gettype;
+use function is_float;
+use function is_int;
+use function is_object;
+use function is_string;
+use function preg_match;
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc9651.html#section-3.3
+ */
+enum Type: string
+{
+ private const MAXIMUM_INT = 999_999_999_999_999;
+ private const MAXIMUM_FLOAT = 999_999_999_999;
+
+ case Integer = 'integer';
+ case Decimal = 'decimal';
+ case String = 'string';
+ case Token = 'token';
+ case Bytes = 'binary';
+ case DisplayString = 'displaystring';
+ case Boolean = 'boolean';
+ case Date = 'date';
+
+ public function equals(mixed $other): bool
+ {
+ return match (true) {
+ $other instanceof Item => $other->type() === $this,
+ default => $other instanceof self && $other === $this,
+ };
+ }
+
+ public function isOneOf(mixed ...$other): bool
+ {
+ foreach ($other as $item) {
+ if ($this->equals($item)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @throws SyntaxError if the value can not be resolved into a supported HTTP structured field data type
+ */
+ public static function fromVariable(Item|Token|DisplayString|Bytes|DateTimeInterface|int|float|bool|string $value): self
+ {
+ return self::tryFromVariable($value) ?? throw new SyntaxError(match (true) {
+ $value instanceof DateTimeInterface => 'The integer representation of a date is limited to 15 digits for a HTTP structured field date type.',
+ is_int($value) => 'The integer is limited to 15 digits for a HTTP structured field integer type.',
+ is_float($value) => 'The integer portion of decimals is limited to 12 digits for a HTTP structured field decimal type.',
+ is_string($value) => 'The string contains characters that are invalid for a HTTP structured field string type',
+ default => (is_object($value) ? 'An instance of "'.$value::class.'"' : 'A value of type "'.gettype($value).'"').' can not be used as an HTTP structured field value type.',
+ });
+ }
+
+ public static function tryFromVariable(mixed $variable): ?self
+ {
+ return match (true) {
+ $variable instanceof Item,
+ $variable instanceof Token,
+ $variable instanceof DisplayString,
+ $variable instanceof Bytes => $variable->type(),
+ $variable instanceof DateTimeInterface && self::MAXIMUM_INT >= abs($variable->getTimestamp()) => Type::Date,
+ is_int($variable) && self::MAXIMUM_INT >= abs($variable) => Type::Integer,
+ is_float($variable) && self::MAXIMUM_FLOAT >= abs(floor($variable)) => Type::Decimal,
+ is_bool($variable) => Type::Boolean,
+ is_string($variable) && 1 !== preg_match('/[^\x20-\x7f]/', $variable) => Type::String,
+ default => null,
+ };
+ }
+
+ public function supports(mixed $value): bool
+ {
+ return self::tryFromVariable($value)?->equals($this) ?? false;
+ }
+}
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);
+ }
+}