Skip to content

Commit

Permalink
Add path abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
SerafimArts committed Jul 3, 2024
1 parent 5e30061 commit a3c0853
Show file tree
Hide file tree
Showing 17 changed files with 297 additions and 34 deletions.
2 changes: 1 addition & 1 deletion src/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public function __construct(
protected readonly ?bool $strictTypes = null,
/**
* If this option contains {@see true}, then objects are converted to
* associative arrays, otherwise anonymous {@see object} will be returned.
* associative arrays, otherwise anonymous {@see ObjectEntry} will be returned.
*/
protected readonly ?bool $objectsAsArrays = null,
/**
Expand Down
25 changes: 25 additions & 0 deletions src/Context/JsonPath.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace TypeLang\Mapper\Context;

use TypeLang\Mapper\Context\Path\ArrayIndexEntry;
use TypeLang\Mapper\Context\Path\ObjectPropertyEntry;

final class JsonPath extends Path
{
public function __toString(): string
{
$result = '$';

foreach ($this->only([ArrayIndexEntry::class, ObjectPropertyEntry::class]) as $entry) {
$result .= match (true) {
$entry instanceof ArrayIndexEntry => "[$entry]",
$entry instanceof ObjectPropertyEntry => ".$entry",
};
}

return $result;
}
}
51 changes: 31 additions & 20 deletions src/Context/LocalContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@
namespace TypeLang\Mapper\Context;

use TypeLang\Mapper\Context;
use TypeLang\Mapper\Context\Path\ArrayIndexEntry;
use TypeLang\Mapper\Context\Path\EntryInterface;
use TypeLang\Mapper\Context\Path\ObjectPropertyEntry;

/**
* Mutable local bypass context.
*/
final class LocalContext extends Context
{
/**
* @var list<non-empty-string|int>
*/
private array $stack = [];
private readonly PathInterface $path;

final public function __construct(
private readonly Direction $direction,
?bool $strictTypes = null,
?bool $objectsAsArrays = null,
?bool $detailedTypes = null,
) {
$this->path = new Path();

parent::__construct(
strictTypes: $strictTypes,
objectsAsArrays: $objectsAsArrays,
Expand All @@ -42,18 +44,12 @@ public function merge(?Context $context): self
return $this;
}

$result = new self(
return new self(
direction: $context instanceof self ? $context->direction : $this->direction,
strictTypes: $context->strictTypes ?? $this->strictTypes,
objectsAsArrays: $context->objectsAsArrays ?? $this->objectsAsArrays,
detailedTypes: $context->detailedTypes ?? $this->detailedTypes,
);

if ($context instanceof self) {
$result->stack = $context->stack;
}

return $result;
}

/**
Expand Down Expand Up @@ -83,27 +79,42 @@ public function getDirection(): Direction
/**
* @return list<non-empty-string|int>
*/
public function getPath(): array
public function getPathAsSegmentsArray(): array
{
return $this->stack;
$result = [];

foreach ($this->path as $entry) {
switch (true) {
case $entry instanceof ArrayIndexEntry:
$result[] = $entry->index;
break;

case $entry instanceof ObjectPropertyEntry:
$result[] = $entry->value;
break;
}
}

return $result;
}

public function getPath(): PathInterface
{
return $this->path;
}

/**
* @param non-empty-string|int $item
*
* @return $this
*/
public function enter(string|int $item): self
public function enter(EntryInterface $item): self
{
$this->stack[] = $item;
$this->path->enter($item);

return $this;
}

public function leave(): void
{
if ($this->stack !== []) {
\array_pop($this->stack);
}
$this->path->leave();
}
}
91 changes: 91 additions & 0 deletions src/Context/Path.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace TypeLang\Mapper\Context;

use TypeLang\Mapper\Context\Path\ArrayIndexEntry;
use TypeLang\Mapper\Context\Path\EntryInterface;
use TypeLang\Mapper\Context\Path\ObjectPropertyEntry;

/**
* @template-implements \IteratorAggregate<array-key, EntryInterface>
*/
class Path implements PathInterface, \IteratorAggregate
{
public function __construct(
/**
* @var list<EntryInterface>
*/
private array $entries = [],
) {}

public function enter(EntryInterface $entry): void
{
$this->entries[] = $entry;
}

public function leave(): void
{
if ($this->entries !== []) {
\array_pop($this->entries);
}
}

public function contains(mixed $value): bool
{
foreach ($this->entries as $entry) {
if ($entry->match($value)) {
return true;
}
}

return false;
}

/**
* @param list<class-string<EntryInterface>> $classes
*/
private function match(EntryInterface $entry, array $classes): bool
{
foreach ($classes as $class) {
if ($entry instanceof $class) {
return true;
}
}

return false;
}

/**
* @template T of EntryInterface
* @param list<class-string<T>> $classes
* @return list<T>
*/
public function only(array $classes): array
{
$result = [];

foreach ($this->entries as $entry) {
if ($this->match($entry, $classes)) {
$result[] = $entry;
}
}

/** @var list<T> */
return $result;
}

public function getIterator(): \Traversable
{
return new \ArrayIterator($this->entries);
}

/**
* @return int<0, max>
*/
public function count(): int
{
return \count($this->entries);
}
}
22 changes: 22 additions & 0 deletions src/Context/Path/ArrayIndexEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace TypeLang\Mapper\Context\Path;

final class ArrayIndexEntry extends Entry
{
/**
* @param int|non-empty-string $index
*/
public function __construct(
public readonly int|string $index,
) {
parent::__construct((string) $this->index);
}

public function match(mixed $value): bool
{
return $this->index === $value;
}
}
25 changes: 25 additions & 0 deletions src/Context/Path/Entry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace TypeLang\Mapper\Context\Path;

abstract class Entry implements EntryInterface
{
/**
* @param non-empty-string $value
*/
public function __construct(
public readonly string $value,
) {}

public function match(mixed $value): bool
{
return $this->value === $value;
}

public function __toString(): string
{
return $this->value;
}
}
17 changes: 17 additions & 0 deletions src/Context/Path/EntryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace TypeLang\Mapper\Context\Path;

interface EntryInterface extends \Stringable
{
public function match(mixed $value): bool;

/**
* Returns string representation of the path entry.
*
* @return non-empty-string
*/
public function __toString(): string;
}
16 changes: 16 additions & 0 deletions src/Context/Path/ObjectEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace TypeLang\Mapper\Context\Path;

final class ObjectEntry extends Entry
{
/**
* @param class-string $class
*/
public function __construct(string $class)
{
parent::__construct($class);
}
}
7 changes: 7 additions & 0 deletions src/Context/Path/ObjectPropertyEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace TypeLang\Mapper\Context\Path;

final class ObjectPropertyEntry extends Entry {}
19 changes: 19 additions & 0 deletions src/Context/PathInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace TypeLang\Mapper\Context;

use TypeLang\Mapper\Context\Path\EntryInterface;

/**
* @template-extends \Traversable<array-key, EntryInterface>
*/
interface PathInterface extends \Traversable, \Countable
{
public function enter(EntryInterface $entry): void;

public function leave(): void;

public function contains(mixed $value): bool;
}
2 changes: 1 addition & 1 deletion src/Exception/Mapping/InvalidValueException.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static function becauseInvalidValueGiven(
expectedType: $expectedType,
actualType: $actualType,
actualValue: $actualValue,
path: $context->getPath(),
path: $context->getPathAsSegmentsArray(),
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Exception/Mapping/MissingRequiredFieldException.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static function becauseFieldIsMissing(
template: 'Object of type {{expected}} requires field {{field}} in {{path}}',
expectedType: $expectedType,
field: $field,
path: $context->getPath(),
path: $context->getPathAsSegmentsArray(),
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/Type/ArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace TypeLang\Mapper\Type;

use TypeLang\Mapper\Context\LocalContext;
use TypeLang\Mapper\Context\Path\ArrayIndexEntry;
use TypeLang\Mapper\Exception\Mapping\InvalidValueException;
use TypeLang\Mapper\Exception\TypeNotFoundException;
use TypeLang\Mapper\Registry\RegistryInterface;
Expand Down Expand Up @@ -94,7 +95,6 @@ public function supportsCasting(mixed $value, LocalContext $context): bool
/**
* @return array<array-key, mixed>
* @throws InvalidValueException
* @throws TypeNotFoundException
*/
public function cast(mixed $value, RegistryInterface $types, LocalContext $context): array
{
Expand All @@ -103,7 +103,7 @@ public function cast(mixed $value, RegistryInterface $types, LocalContext $conte
$result = [];

foreach ($value as $index => $item) {
$context->enter($index);
$context->enter(new ArrayIndexEntry($index));

$result[$this->key->cast($index, $types, $context)]
= $this->value->cast($item, $types, $context);
Expand Down
Loading

0 comments on commit a3c0853

Please sign in to comment.