feat: introduce composite types

Composite types are composed of other types and must now implement a
method to recursively traverse all sub-types.
This commit is contained in:
Romain Canon 2022-05-05 18:29:27 +02:00
parent 5f9d41cf35
commit 892f3831c2
24 changed files with 451 additions and 15 deletions

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Type;
/** @api */
interface CombiningType extends Type
interface CombiningType extends CompositeType
{
public function isMatchedBy(Type $other): bool;

View File

@ -7,7 +7,7 @@ namespace CuyZ\Valinor\Type;
use CuyZ\Valinor\Type\Types\ArrayKeyType;
/** @api */
interface CompositeTraversableType extends Type
interface CompositeTraversableType extends CompositeType
{
public function keyType(): ArrayKeyType;

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Type;
/** @api */
interface CompositeType extends Type
{
/**
* @return iterable<Type>
*/
public function traverse(): iterable;
}

View File

@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Type;
/** @api */
interface TraversableType extends Type
{
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Type\Types;
use CuyZ\Valinor\Type\CompositeTraversableType;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;
use function is_array;
@ -98,6 +99,15 @@ final class ArrayType implements CompositeTraversableType
return $this->subType;
}
public function traverse(): iterable
{
yield $this->subType;
if ($this->subType instanceof CompositeType) {
yield from $this->subType->traverse();
}
}
public function __toString(): string
{
return $this->signature;

View File

@ -5,12 +5,13 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Type\Types;
use CuyZ\Valinor\Type\ObjectType;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;
use function is_a;
/** @api */
final class ClassType implements ObjectType
final class ClassType implements ObjectType, CompositeType
{
/** @var class-string */
private string $className;
@ -64,6 +65,17 @@ final class ClassType implements ObjectType
return is_a($this->className, $other->className(), true);
}
public function traverse(): iterable
{
foreach ($this->generics as $type) {
yield $type;
if ($type instanceof CompositeType) {
yield from $type->traverse();
}
}
}
public function __toString(): string
{
return empty($this->generics)

View File

@ -6,6 +6,7 @@ namespace CuyZ\Valinor\Type\Types;
use CuyZ\Valinor\Type\CombiningType;
use CuyZ\Valinor\Type\ObjectType;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;
use function implode;
@ -65,6 +66,17 @@ final class IntersectionType implements CombiningType
return true;
}
public function traverse(): iterable
{
foreach ($this->types as $type) {
yield $type;
if ($type instanceof CompositeType) {
yield from $type->traverse();
}
}
}
/**
* @return ObjectType[]
*/

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Type\Types;
use CuyZ\Valinor\Type\CompositeTraversableType;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;
use function is_iterable;
@ -84,6 +85,15 @@ final class IterableType implements CompositeTraversableType
return $this->subType;
}
public function traverse(): iterable
{
yield $this->subType;
if ($this->subType instanceof CompositeType) {
yield from $this->subType->traverse();
}
}
public function __toString(): string
{
return $this->signature;

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Type\Types;
use CuyZ\Valinor\Type\CompositeTraversableType;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;
use function is_array;
@ -91,6 +92,15 @@ final class ListType implements CompositeTraversableType
return $this->subType;
}
public function traverse(): iterable
{
yield $this->subType;
if ($this->subType instanceof CompositeType) {
yield from $this->subType->traverse();
}
}
public function __toString(): string
{
return $this->signature;

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Type\Types;
use CuyZ\Valinor\Type\CompositeTraversableType;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;
use function is_array;
@ -94,6 +95,15 @@ final class NonEmptyArrayType implements CompositeTraversableType
return $this->subType;
}
public function traverse(): iterable
{
yield $this->subType;
if ($this->subType instanceof CompositeType) {
yield from $this->subType->traverse();
}
}
public function __toString(): string
{
return $this->signature;

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Type\Types;
use CuyZ\Valinor\Type\CompositeTraversableType;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;
use function count;
@ -99,6 +100,15 @@ final class NonEmptyListType implements CompositeTraversableType
return $this->subType;
}
public function traverse(): iterable
{
yield $this->subType;
if ($this->subType instanceof CompositeType) {
yield from $this->subType->traverse();
}
}
public function __toString(): string
{
return $this->signature;

View File

@ -6,7 +6,7 @@ namespace CuyZ\Valinor\Type\Types;
use CuyZ\Valinor\Type\CompositeTraversableType;
use CuyZ\Valinor\Type\Parser\Exception\Iterable\ShapedArrayElementDuplicatedKey;
use CuyZ\Valinor\Type\TraversableType;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;
use function array_diff;
@ -18,7 +18,7 @@ use function in_array;
use function is_array;
/** @api */
final class ShapedArrayType implements TraversableType
final class ShapedArrayType implements CompositeType
{
/** @var ShapedArrayElement[] */
private array $elements;
@ -120,6 +120,17 @@ final class ShapedArrayType implements TraversableType
return true;
}
public function traverse(): iterable
{
foreach ($this->elements as $element) {
yield $type = $element->type();
if ($type instanceof CompositeType) {
yield from $type->traverse();
}
}
}
/**
* @return ShapedArrayElement[]
*/

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Type\Types;
use CuyZ\Valinor\Type\CombiningType;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\Exception\ForbiddenMixedType;
@ -82,6 +83,17 @@ final class UnionType implements CombiningType
return false;
}
public function traverse(): iterable
{
foreach ($this->types as $type) {
yield $type;
if ($type instanceof CompositeType) {
yield from $type->traverse();
}
}
}
public function types(): array
{
return $this->types;

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Fake\Type;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Type;
final class FakeCompositeType implements CompositeType
{
/** @var Type[] */
private array $types;
public function __construct(Type ...$types)
{
$this->types = $types;
}
public function traverse(): iterable
{
yield from $this->types;
}
public function accepts($value): bool
{
return true;
}
public function matches(Type $other): bool
{
return true;
}
public function __toString(): string
{
return 'FakeCompositeType';
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Fake\Type;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\ObjectType;
use CuyZ\Valinor\Type\Type;
use stdClass;
final class FakeObjectCompositeType implements ObjectType, CompositeType
{
/** @var class-string */
private string $className;
/** @var array<string, Type> */
private array $generics;
/**
* @param class-string $className
* @param array<string, Type> $generics
*/
public function __construct(string $className = stdClass::class, array $generics = [])
{
$this->className = $className;
$this->generics = $generics;
}
public function className(): string
{
return $this->className;
}
public function generics(): array
{
return $this->generics;
}
public function accepts($value): bool
{
return true;
}
public function matches(Type $other): bool
{
return true;
}
public function traverse(): iterable
{
yield from $this->generics;
}
public function __toString(): string
{
return $this->className;
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Types;
use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Types\ArrayKeyType;
use CuyZ\Valinor\Type\Types\ArrayType;
@ -134,6 +135,7 @@ final class ArrayTypeTest extends TestCase
self::assertTrue($arrayType->matches($iterableType));
}
public function test_does_not_match_invalid_iterable_type(): void
{
$typeA = new FakeType();
@ -180,4 +182,26 @@ final class ArrayTypeTest extends TestCase
self::assertFalse(ArrayType::native()->matches($unionType));
}
public function test_traverse_type_yields_sub_type(): void
{
$subType = new FakeType();
$type = new ArrayType(ArrayKeyType::default(), $subType);
self::assertCount(1, $type->traverse());
self::assertContains($subType, $type->traverse());
}
public function test_traverse_type_yields_types_recursively(): void
{
$subType = new FakeType();
$compositeType = new FakeCompositeType($subType);
$type = new ArrayType(ArrayKeyType::default(), $compositeType);
self::assertCount(2, $type->traverse());
self::assertContains($subType, $type->traverse());
self::assertContains($compositeType, $type->traverse());
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Types;
use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Types\ClassType;
use CuyZ\Valinor\Type\Types\MixedType;
@ -106,4 +107,31 @@ final class ClassTypeTest extends TestCase
self::assertFalse($classType->matches($unionType));
}
public function test_traverse_type_yields_sub_types(): void
{
$subTypeA = new FakeType();
$subTypeB = new FakeType();
$type = new ClassType(stdClass::class, [
'TemplateA' => $subTypeA,
'TemplateB' => $subTypeB,
]);
self::assertCount(2, $type->traverse());
self::assertContains($subTypeA, $type->traverse());
self::assertContains($subTypeB, $type->traverse());
}
public function test_traverse_type_yields_types_recursively(): void
{
$subType = new FakeType();
$compositeType = new FakeCompositeType($subType);
$type = new ClassType(stdClass::class, ['Template' => $compositeType]);
self::assertCount(2, $type->traverse());
self::assertContains($subType, $type->traverse());
self::assertContains($compositeType, $type->traverse());
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Types;
use CuyZ\Valinor\Tests\Fake\Type\FakeObjectCompositeType;
use CuyZ\Valinor\Tests\Fake\Type\FakeObjectType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Types\IntersectionType;
@ -133,4 +134,32 @@ final class IntersectionTypeTest extends TestCase
self::assertFalse($intersectionType->matches($unionType));
}
public function test_traverse_type_yields_sub_types(): void
{
$objectTypeA = new FakeObjectType();
$objectTypeB = new FakeObjectType();
$type = new IntersectionType($objectTypeA, $objectTypeB);
self::assertCount(2, $type->traverse());
self::assertContains($objectTypeA, $type->traverse());
self::assertContains($objectTypeB, $type->traverse());
}
public function test_traverse_type_yields_types_recursively(): void
{
$subTypeA = new FakeType();
$subTypeB = new FakeType();
$objectTypeA = new FakeObjectCompositeType(stdClass::class, ['Template' => $subTypeA]);
$objectTypeB = new FakeObjectCompositeType(stdClass::class, ['Template' => $subTypeB]);
$type = new IntersectionType($objectTypeA, $objectTypeB);
self::assertCount(4, $type->traverse());
self::assertContains($subTypeA, $type->traverse());
self::assertContains($subTypeB, $type->traverse());
self::assertContains($objectTypeA, $type->traverse());
self::assertContains($objectTypeB, $type->traverse());
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Types;
use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Types\ArrayKeyType;
use CuyZ\Valinor\Type\Types\NativeStringType;
@ -146,4 +147,26 @@ final class IterableTypeTest extends TestCase
self::assertFalse(IterableType::native()->matches($unionType));
}
public function test_traverse_type_yields_sub_type(): void
{
$subType = new FakeType();
$type = new IterableType(ArrayKeyType::default(), $subType);
self::assertCount(1, $type->traverse());
self::assertContains($subType, $type->traverse());
}
public function test_traverse_type_yields_types_recursively(): void
{
$subType = new FakeType();
$compositeType = new FakeCompositeType($subType);
$type = new IterableType(ArrayKeyType::default(), $compositeType);
self::assertCount(2, $type->traverse());
self::assertContains($subType, $type->traverse());
self::assertContains($compositeType, $type->traverse());
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Types;
use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Types\ArrayKeyType;
use CuyZ\Valinor\Type\Types\ArrayType;
@ -188,4 +189,26 @@ final class ListTypeTest extends TestCase
self::assertFalse(ListType::native()->matches($unionType));
}
public function test_traverse_type_yields_sub_type(): void
{
$subType = new FakeType();
$type = new ListType($subType);
self::assertCount(1, $type->traverse());
self::assertContains($subType, $type->traverse());
}
public function test_traverse_type_yields_types_recursively(): void
{
$subType = new FakeType();
$compositeType = new FakeCompositeType($subType);
$type = new ListType($compositeType);
self::assertCount(2, $type->traverse());
self::assertContains($subType, $type->traverse());
self::assertContains($compositeType, $type->traverse());
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Types;
use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Types\ArrayKeyType;
use CuyZ\Valinor\Type\Types\MixedType;
@ -149,4 +150,26 @@ final class NonEmptyArrayTypeTest extends TestCase
self::assertFalse(NonEmptyArrayType::native()->matches($unionType));
}
public function test_traverse_type_yields_sub_type(): void
{
$subType = new FakeType();
$type = new NonEmptyArrayType(ArrayKeyType::default(), $subType);
self::assertCount(1, $type->traverse());
self::assertContains($subType, $type->traverse());
}
public function test_traverse_type_yields_types_recursively(): void
{
$subType = new FakeType();
$compositeType = new FakeCompositeType($subType);
$type = new NonEmptyArrayType(ArrayKeyType::default(), $compositeType);
self::assertCount(2, $type->traverse());
self::assertContains($subType, $type->traverse());
self::assertContains($compositeType, $type->traverse());
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Types;
use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Types\ArrayKeyType;
use CuyZ\Valinor\Type\Types\ArrayType;
@ -235,4 +236,26 @@ final class NonEmptyListTypeTest extends TestCase
self::assertFalse(NonEmptyListType::native()->matches($unionType));
}
public function test_traverse_type_yields_sub_type(): void
{
$subType = new FakeType();
$type = new NonEmptyListType($subType);
self::assertCount(1, $type->traverse());
self::assertContains($subType, $type->traverse());
}
public function test_traverse_type_yields_types_recursively(): void
{
$subType = new FakeType();
$compositeType = new FakeCompositeType($subType);
$type = new NonEmptyListType($compositeType);
self::assertCount(2, $type->traverse());
self::assertContains($subType, $type->traverse());
self::assertContains($compositeType, $type->traverse());
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Types;
use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Parser\Exception\Iterable\ShapedArrayElementDuplicatedKey;
use CuyZ\Valinor\Type\Types\ArrayKeyType;
@ -168,4 +169,38 @@ final class ShapedArrayTypeTest extends TestCase
self::assertFalse($this->type->matches($unionType));
}
public function test_traverse_type_yields_sub_types(): void
{
$subTypeA = new FakeType();
$subTypeB = new FakeType();
$type = new ShapedArrayType(
new ShapedArrayElement(new StringValueType('foo'), $subTypeA),
new ShapedArrayElement(new StringValueType('bar'), $subTypeB),
);
self::assertCount(2, $type->traverse());
self::assertContains($subTypeA, $type->traverse());
self::assertContains($subTypeB, $type->traverse());
}
public function test_traverse_type_yields_types_recursively(): void
{
$subTypeA = new FakeType();
$subTypeB = new FakeType();
$compositeTypeA = new FakeCompositeType($subTypeA);
$compositeTypeB = new FakeCompositeType($subTypeB);
$type = new ShapedArrayType(
new ShapedArrayElement(new StringValueType('foo'), $compositeTypeA),
new ShapedArrayElement(new StringValueType('bar'), $compositeTypeB),
);
self::assertCount(4, $type->traverse());
self::assertContains($subTypeA, $type->traverse());
self::assertContains($subTypeB, $type->traverse());
self::assertContains($compositeTypeA, $type->traverse());
self::assertContains($compositeTypeB, $type->traverse());
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Type\Types;
use CuyZ\Valinor\Tests\Fake\Type\FakeCompositeType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Types\Exception\ForbiddenMixedType;
use CuyZ\Valinor\Type\Types\MixedType;
@ -135,4 +136,32 @@ final class UnionTypeTest extends TestCase
self::assertFalse($unionTypeA->matches($unionTypeB));
}
public function test_traverse_type_yields_sub_types(): void
{
$subTypeA = new FakeType();
$subTypeB = new FakeType();
$type = new UnionType($subTypeA, $subTypeB);
self::assertCount(2, $type->traverse());
self::assertContains($subTypeA, $type->traverse());
self::assertContains($subTypeB, $type->traverse());
}
public function test_traverse_type_yields_types_recursively(): void
{
$subTypeA = new FakeType();
$subTypeB = new FakeType();
$compositeTypeA = new FakeCompositeType($subTypeA);
$compositeTypeB = new FakeCompositeType($subTypeB);
$type = new UnionType($compositeTypeA, $compositeTypeB);
self::assertCount(4, $type->traverse());
self::assertContains($subTypeA, $type->traverse());
self::assertContains($subTypeB, $type->traverse());
self::assertContains($compositeTypeA, $type->traverse());
self::assertContains($compositeTypeB, $type->traverse());
}
}