-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
328 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Typhoon\DataStructures; | ||
|
||
/** | ||
* @api | ||
*/ | ||
final class MutableTypedMap extends TypedMap | ||
{ | ||
/** | ||
* @param array<non-negative-int, mixed> $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<V> $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<V> $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<V> $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<array{class-string<TypedKey>, 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<array{class-string<TypedKey>, 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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Typhoon\DataStructures; | ||
|
||
/** | ||
* @api | ||
* @template V | ||
*/ | ||
final class TypedKVPair | ||
{ | ||
/** | ||
* @param TypedKey<V> $key | ||
* @param V $value | ||
*/ | ||
public function __construct( | ||
public readonly TypedKey $key, | ||
public readonly mixed $value, | ||
) {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Typhoon\DataStructures; | ||
|
||
/** | ||
* @api | ||
* @template V | ||
* @psalm-consistent-templates | ||
*/ | ||
abstract class TypedKey | ||
{ | ||
/** | ||
* @var array<non-empty-string, self> | ||
*/ | ||
private static array $keys = []; | ||
|
||
/** | ||
* @internal | ||
* @psalm-internal Typhoon\DataStructures | ||
* @param list<non-negative-int> $indexes | ||
* @return list<self> | ||
*/ | ||
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<D> | ||
*/ | ||
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Typhoon\DataStructures; | ||
|
||
/** | ||
* @api | ||
* @implements \ArrayAccess<TypedKey, mixed> | ||
* @psalm-suppress UnusedClass | ||
*/ | ||
abstract class TypedMap implements \ArrayAccess, \Countable | ||
{ | ||
public static function create(TypedKVPair ...$kvPairs): self | ||
{ | ||
return MutableTypedMap::create(...$kvPairs); | ||
} | ||
|
||
/** | ||
* @template V | ||
* @param TypedKey<V> $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<V> $key | ||
* @return V | ||
*/ | ||
abstract public function get(TypedKey $key): mixed; | ||
|
||
/** | ||
* @template V | ||
* @param TypedKey<V> $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(); | ||
} | ||
} |