aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/bakame/http-structured-fields/src/Parameters.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/bakame/http-structured-fields/src/Parameters.php')
-rw-r--r--vendor/bakame/http-structured-fields/src/Parameters.php763
1 files changed, 763 insertions, 0 deletions
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);
+ }
+}