diff --git a/src/Internal/ArrayTypedMap.php b/src/Internal/ArrayTypedMap.php new file mode 100644 index 0000000..cb6505d --- /dev/null +++ b/src/Internal/ArrayTypedMap.php @@ -0,0 +1,88 @@ + + */ + private array $values = []; + + /** + * @return non-empty-string + */ + private static function keyToString(TypedKey $key): string + { + return $key::class . '::' . $key->name; + } + + protected function doPut(TypedKey $key, mixed $value): void + { + $this->values[self::keyToString($key)] = $value; + } + + public function putAll(TypedMap $map): void + { + if ($map instanceof self) { + $this->values = [...$this->values, ...$map->values]; + } else { + parent::putAll($map); + } + } + + public function contains(TypedKey $key): bool + { + return isset($this->values[self::keyToString($key)]); + } + + /** + * @template V + * @template D + * @param TypedKey $key + * @param callable(): D $or + * @return V|D + */ + public function getOr(TypedKey $key, callable $or): mixed + { + $keyString = self::keyToString($key); + + if (\array_key_exists($keyString, $this->values)) { + /** @var V */ + return $this->values[$keyString]; + } + + return $or(); + } + + public function remove(TypedKey ...$keys): void + { + foreach ($keys as $key) { + unset($this->values[self::keyToString($key)]); + } + } + + public function count(): int + { + return \count($this->values); + } + + protected function all(): \Traversable + { + foreach ($this->values as $keyString => $value) { + /** @var TypedKey $key */ + $key = \constant($keyString); + yield $key => $value; + } + } +} diff --git a/src/Internal/SplObjectStorageTypedMap.php b/src/Internal/SplObjectStorageTypedMap.php new file mode 100644 index 0000000..ebc9fcc --- /dev/null +++ b/src/Internal/SplObjectStorageTypedMap.php @@ -0,0 +1,83 @@ + + */ + private \SplObjectStorage $values; + + public function __construct() + { + /** @var \SplObjectStorage */ + $this->values = new \SplObjectStorage(); + } + + protected function doPut(TypedKey $key, mixed $value): void + { + $this->values->attach($key, $value); + } + + public function contains(TypedKey $key): bool + { + return $this->values->contains($key); + } + + /** + * @template V + * @template D + * @param TypedKey $key + * @param callable(): D $or + * @return V|D + */ + public function getOr(mixed $key, callable $or): mixed + { + if ($this->values->contains($key)) { + /** @var V */ + return $this->values->offsetGet($key); + } + + return $or(); + } + + public function remove(TypedKey ...$keys): void + { + foreach ($keys as $key) { + $this->values->detach($key); + } + } + + public function putAll(TypedMap $map): void + { + if ($map instanceof self) { + $this->values->addAll($map->values); + } else { + parent::putAll($map); + } + } + + public function count(): int + { + return \count($this->values); + } + + protected function all(): \Traversable + { + foreach ($this->values as $key) { + yield $key => $this->values->getInfo(); + } + } +} diff --git a/src/MutableTypedMap.php b/src/MutableTypedMap.php new file mode 100644 index 0000000..13b919a --- /dev/null +++ b/src/MutableTypedMap.php @@ -0,0 +1,87 @@ + $key + * @param V $value + */ + final public function with(TypedKey $key, mixed $value): static + { + if ($key instanceof OptionalTypedKey && $value === $key->default($this)) { + if ($this->contains($key)) { + $copy = clone $this; + $copy->remove($key); + + return $copy; + } + + return $this; + } + + $copy = clone $this; + $copy->doPut($key, $value); + + return $copy; + } + + final public function withAll(TypedMap $map): static + { + $copy = clone $this; + foreach ($map->all() as $key => $value) { + $copy->put($key, $value); + } + + return $copy; + } + + final public function without(TypedKey ...$keys): static + { + if ($keys === []) { + return $this; + } + + $copy = clone $this; + $copy->remove(...$keys); + + return $copy; + } + + /** + * @template V + * @param TypedKey $key + * @param V $value + */ + final public function put(TypedKey $key, mixed $value): void + { + if ($key instanceof OptionalTypedKey && $value === $key->default($this)) { + $this->remove($key); + } else { + $this->doPut($key, $value); + } + } + + /** + * @template V + * @param TypedKey $key + * @param V $value + */ + abstract protected function doPut(TypedKey $key, mixed $value): void; + + public function putAll(TypedMap $map): void + { + foreach ($map->all() as $key => $value) { + $this->put($key, $value); + } + } + + abstract public function remove(TypedKey ...$keys): void; +} diff --git a/src/OptionalTypedKey.php b/src/OptionalTypedKey.php new file mode 100644 index 0000000..5eecfb8 --- /dev/null +++ b/src/OptionalTypedKey.php @@ -0,0 +1,18 @@ + + */ +interface OptionalTypedKey extends TypedKey +{ + /** + * @return TValue + */ + public function default(TypedMap $map): mixed; +} diff --git a/src/TypedKey.php b/src/TypedKey.php new file mode 100644 index 0000000..bc91e9b --- /dev/null +++ b/src/TypedKey.php @@ -0,0 +1,11 @@ + + * @psalm-suppress UnusedClass + */ +abstract class TypedMap implements \ArrayAccess, \Countable +{ + public static function create(): static + { + /** @var static */ + return new ArrayTypedMap(); + } + + /** + * @template V + * @param TypedKey $key + * @param V $value + */ + abstract public function with(TypedKey $key, mixed $value): static; + + abstract public function withAll(self $map): static; + + abstract public function without(TypedKey ...$keys): static; + + abstract public function contains(TypedKey $key): bool; + + final public function offsetExists(mixed $offset): bool + { + return $this->contains($offset); + } + + /** + * @template V + * @template D + * @param TypedKey $key + * @param D $default + * @return V|D + */ + final public function get(TypedKey $key, mixed $default = null): mixed + { + return $this->getOr($key, static fn(): mixed => $default); + } + + /** + * @template V + * @template D + * @param TypedKey $key + * @param callable(): D $or + * @return V|D + */ + abstract public function getOr(TypedKey $key, callable $or): mixed; + + /** + * @template V + * @param TypedKey $offset + * @return V + * @throws KeyIsNotDefined + */ + final public function offsetGet(mixed $offset): mixed + { + return $this->getOr($offset, static function () use ($offset): void { throw new KeyIsNotDefined($offset); }); + } + + /** + * @return non-negative-int + */ + abstract public function count(): int; + + /** + * @return \Traversable + */ + abstract protected function all(): \Traversable; + + public function offsetSet(mixed $offset, mixed $value): never + { + throw new \BadMethodCallException(); + } + + public function offsetUnset(mixed $offset): never + { + throw new \BadMethodCallException(); + } +}