Skip to content

Commit

Permalink
Add TypedMap
Browse files Browse the repository at this point in the history
  • Loading branch information
vudaltsov committed Sep 3, 2024
1 parent 3eadee0 commit 90d227e
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 0 deletions.
88 changes: 88 additions & 0 deletions src/Internal/ArrayTypedMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace Typhoon\DataStructures\Internal;

use Typhoon\DataStructures\MutableTypedMap;
use Typhoon\DataStructures\TypedKey;
use Typhoon\DataStructures\TypedMap;

/**
* @internal
* @psalm-internal Typhoon\DataStructures
*/
final class ArrayTypedMap extends MutableTypedMap
{
/**
* @var array<non-empty-string, mixed>
*/
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<V> $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;
}
}
}
83 changes: 83 additions & 0 deletions src/Internal/SplObjectStorageTypedMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Typhoon\DataStructures\Internal;

use Typhoon\DataStructures\MutableTypedMap;
use Typhoon\DataStructures\TypedKey;
use Typhoon\DataStructures\TypedMap;

/**
* @internal
* @psalm-internal Typhoon\DataStructures
* @psalm-suppress UnusedClass
*/
final class SplObjectStorageTypedMap extends MutableTypedMap
{
/**
* @var \SplObjectStorage<TypedKey, mixed>
*/
private \SplObjectStorage $values;

public function __construct()
{
/** @var \SplObjectStorage<TypedKey, mixed> */
$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<V> $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();
}
}
}
87 changes: 87 additions & 0 deletions src/MutableTypedMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Typhoon\DataStructures;

/**
* @api
*/
abstract class MutableTypedMap extends TypedMap
{
/**
* @template V
* @param TypedKey<V> $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<V> $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<V> $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;
}
18 changes: 18 additions & 0 deletions src/OptionalTypedKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Typhoon\DataStructures;

/**
* @api
* @template-covariant TValue
* @extends TypedKey<TValue>
*/
interface OptionalTypedKey extends TypedKey
{
/**
* @return TValue
*/
public function default(TypedMap $map): mixed;
}
11 changes: 11 additions & 0 deletions src/TypedKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Typhoon\DataStructures;

/**
* @api
* @template-covariant TValue
*/
interface TypedKey extends \UnitEnum {}
91 changes: 91 additions & 0 deletions src/TypedMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Typhoon\DataStructures;

use Typhoon\DataStructures\Internal\ArrayTypedMap;

/**
* @api
* @implements \ArrayAccess<TypedKey, mixed>
* @psalm-suppress UnusedClass
*/
abstract class TypedMap implements \ArrayAccess, \Countable
{
public static function create(): static
{
/** @var static */
return new ArrayTypedMap();
}

/**
* @template V
* @param TypedKey<V> $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<V> $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<V> $key
* @param callable(): D $or
* @return V|D
*/
abstract public function getOr(TypedKey $key, callable $or): mixed;

/**
* @template V
* @param TypedKey<V> $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<TypedKey, mixed>
*/
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();
}
}

0 comments on commit 90d227e

Please sign in to comment.