diff --git a/src/MutableTypedMap.php b/src/MutableTypedMap.php new file mode 100644 index 0000000..391910e --- /dev/null +++ b/src/MutableTypedMap.php @@ -0,0 +1,144 @@ + $values + */ + private function __construct( + private array $values = [], + ) {} + + public static function create(TypedKVPair ...$kvPairs): self + { + $map = new self(); + + foreach ($kvPairs as $pair) { + $map->values[$pair->key->index] = $pair->value; + } + + return $map; + } + + public function with(TypedKey $key, mixed $value): static + { + $values = $this->values; + $values[$key->index] = $value; + + return new self($values); + } + + public function withAll(self $map): static + { + return new self(array_replace($this->values, $map->values)); + } + + public function without(TypedKey ...$keys): static + { + $values = $this->values; + + foreach ($keys as $key) { + unset($values[$key->index]); + } + + return new self($values); + } + + public function contains(TypedKey $key): bool + { + return \array_key_exists($key->index, $this->values); + } + + public function offsetExists(mixed $offset): bool + { + return \array_key_exists($offset->index, $this->values); + } + + /** + * @template V + * @param TypedKey $key + * @return V + */ + public function get(TypedKey $key): mixed + { + if (\array_key_exists($key->index, $this->values)) { + /** @var V */ + return $this->values[$key->index]; + } + + return $key->default($this); + } + + /** + * @template V + * @param TypedKey $offset + * @return V + */ + public function offsetGet(mixed $offset): mixed + { + if (\array_key_exists($offset->index, $this->values)) { + /** @var V */ + return $this->values[$offset->index]; + } + + return $offset->default($this); + } + + public function count(): int + { + return \count($this->values); + } + + /** + * @template V + * @param TypedKey $key + * @param V $value + */ + public function put(TypedKey $key, mixed $value): void + { + $this->values[$key->index] = $value; + } + + public function putAll(self $map): void + { + $this->values = array_replace($this->values, $map->values); + } + + public function remove(TypedKey ...$keys): void + { + foreach ($keys as $key) { + unset($this->values[$key->index]); + } + } + + /** + * @return list, non-empty-string, mixed}> + */ + public function __serialize(): array + { + return array_map( + static fn(TypedKey $key, mixed $value): array => [$key::class, $key->method, $value], + TypedKey::byIndexes(array_keys($this->values)), + $this->values, + ); + } + + /** + * @param list, non-empty-string, mixed}> $data + */ + public function __unserialize(array $data): void + { + foreach ($data as [$class, $method, $value]) { + $key = $class::$method(); + \assert($key instanceof $class); + $this->values[$key->index] = $value; + } + } +} diff --git a/src/TypedKVPair.php b/src/TypedKVPair.php new file mode 100644 index 0000000..d7e1308 --- /dev/null +++ b/src/TypedKVPair.php @@ -0,0 +1,21 @@ + $key + * @param V $value + */ + public function __construct( + public readonly TypedKey $key, + public readonly mixed $value, + ) {} +} diff --git a/src/TypedKey.php b/src/TypedKey.php new file mode 100644 index 0000000..baae4a5 --- /dev/null +++ b/src/TypedKey.php @@ -0,0 +1,101 @@ + + */ + private static array $keys = []; + + /** + * @internal + * @psalm-internal Typhoon\DataStructures + * @param list $indexes + * @return list + */ + final public static function byIndexes(array $indexes): array + { + $keys = array_values(self::$keys); + + return array_map( + static fn(int $index): self => $keys[$index] ?? throw new \LogicException(), + $indexes, + ); + } + + /** + * @template D + * @param ?callable(TypedMap): D $default + * @return static + */ + final protected static function init(?callable $default = null): static + { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1] ?? []; + + \assert( + isset($trace['class'], $trace['function']) && $trace['class'] === static::class && $trace['function'] !== '', + \sprintf('Invalid %s call', self::class), + ); + + $name = \sprintf('%s::%s', static::class, $trace['function']); + + \assert(!isset(self::$keys[$name]), \sprintf('Please ensure you memoize key in %s()', $name)); + + return self::$keys[$name] = new static( + index: \count(self::$keys), + method: $trace['function'], + default: $default ?? static fn(): never => throw new \LogicException(\sprintf('Key %s() does not have a default value', $name)), + ); + } + + /** + * @param non-negative-int $index + * @param non-empty-string $method + * @param callable(TypedMap): V $default + */ + final private function __construct( + public readonly int $index, + public readonly string $method, + private readonly mixed $default, + ) {} + + /** + * @return V + */ + final public function default(TypedMap $map): mixed + { + return ($this->default)($map); + } + + /** + * @return non-empty-string + */ + final public function toString(): string + { + return self::class . '::' . $this->method . '()'; + } + + final public function __serialize(): never + { + throw new \BadMethodCallException(\sprintf('%s does not support serialization', self::class)); + } + + final public function __unserialize(array $_data): never + { + throw new \BadMethodCallException(\sprintf('%s does not support deserialization', self::class)); + } + + final public function __clone() + { + throw new \BadMethodCallException(\sprintf('%s does not support cloning', self::class)); + } +} diff --git a/src/TypedMap.php b/src/TypedMap.php new file mode 100644 index 0000000..1419a93 --- /dev/null +++ b/src/TypedMap.php @@ -0,0 +1,62 @@ + + * @psalm-suppress UnusedClass + */ +abstract class TypedMap implements \ArrayAccess, \Countable +{ + public static function create(TypedKVPair ...$kvPairs): self + { + return MutableTypedMap::create(...$kvPairs); + } + + /** + * @template V + * @param TypedKey $key + * @param V $value + */ + abstract public function with(TypedKey $key, mixed $value): static; + + abstract public function withAll(MutableTypedMap $map): static; + + abstract public function without(TypedKey ...$keys): static; + + abstract public function contains(TypedKey $key): bool; + + abstract public function offsetExists(mixed $offset): bool; + + /** + * @template V + * @param TypedKey $key + * @return V + */ + abstract public function get(TypedKey $key): mixed; + + /** + * @template V + * @param TypedKey $offset + * @return V + */ + abstract public function offsetGet(mixed $offset): mixed; + + /** + * @return non-negative-int + */ + abstract public function count(): int; + + public function offsetSet(mixed $offset, mixed $value): never + { + throw new \BadMethodCallException(); + } + + public function offsetUnset(mixed $offset): never + { + throw new \BadMethodCallException(); + } +}