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