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