Valinor/tests/Integration/Mapping/InterfaceInferringMappingTest.php
Romain Canon 1b0ff39af6 feat!: handle exhaustive list of interface inferring
It is now mandatory to list all possible class-types that can be
inferred by the mapper. This change is a step towards the library being
able to deliver powerful new features such as compiling a mapper for
better performance.

BREAKING CHANGE: the existing calls to `MapperBuilder::infer` that could
return several class-names must now add a signature to the callback. The
callbacks that require no parameter and always return the same
class-name can remain unchanged.

For instance:

```php
$builder = (new \CuyZ\Valinor\MapperBuilder())
    // Can remain unchanged
    ->infer(SomeInterface::class, fn () => SomeImplementation::class);
```

```php
$builder = (new \CuyZ\Valinor\MapperBuilder())
    ->infer(
        SomeInterface::class,
        fn (string $type) => match($type) {
            'first' => ImplementationA::class,
            'second' => ImplementationB::class,
            default => throw new DomainException("Unhandled `$type`.")
        }
    )
    // …should be modified with:
    ->infer(
        SomeInterface::class,
        /** @return class-string<ImplementationA|ImplementationB> */
        fn (string $type) => match($type) {
            'first' => ImplementationA::class,
            'second' => ImplementationB::class,
            default => throw new DomainException("Unhandled `$type`.")
        }
    );
```
2022-06-17 18:03:27 +02:00

393 lines
15 KiB
PHP

<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Integration\Mapping;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidAbstractObjectName;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidResolvedImplementationValue;
use CuyZ\Valinor\Mapper\Tree\Exception\MissingObjectImplementationRegistration;
use CuyZ\Valinor\Mapper\Tree\Exception\ObjectImplementationCallbackError;
use CuyZ\Valinor\Mapper\Tree\Exception\ObjectImplementationNotRegistered;
use CuyZ\Valinor\Mapper\Tree\Exception\ResolvedImplementationIsNotAccepted;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
use CuyZ\Valinor\Type\Resolver\Exception\CannotResolveObjectType;
use DateTime;
use DateTimeInterface;
use DomainException;
use stdClass;
final class InterfaceInferringMappingTest extends IntegrationTest
{
public function test_override_date_time_interface_inferring_overrides_correctly(): void
{
try {
$result = (new MapperBuilder())
->infer(DateTimeInterface::class, fn () => DateTime::class)
->mapper()
->map(DateTimeInterface::class, 1645279176);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertInstanceOf(DateTime::class, $result);
self::assertSame('1645279176', $result->format('U'));
}
public function test_infer_interface_with_union_of_class_string_works_properly(): void
{
try {
$result = (new MapperBuilder())
->infer(
SomeInterface::class,
/** @return class-string<SomeClassThatInheritsInterfaceA>|class-string<SomeClassThatInheritsInterfaceB> */
fn (): string => SomeClassThatInheritsInterfaceA::class
)
->mapper()
->map(SomeInterface::class, 'foo');
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertInstanceOf(SomeClassThatInheritsInterfaceA::class, $result);
}
public function test_infer_interface_with_class_string_with_union_of_class_names_works_properly(): void
{
try {
$result = (new MapperBuilder())
->infer(
SomeInterface::class,
/** @return class-string<SomeClassThatInheritsInterfaceA|SomeClassThatInheritsInterfaceB> */
fn (): string => SomeClassThatInheritsInterfaceA::class
)
->mapper()
->map(SomeInterface::class, 'foo');
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertInstanceOf(SomeClassThatInheritsInterfaceA::class, $result);
}
public function test_infer_interface_with_single_argument_works_properly(): void
{
try {
[$resultA, $resultB] = (new MapperBuilder())
->infer(
SomeInterface::class,
/** @return class-string<SomeClassThatInheritsInterfaceA|SomeClassThatInheritsInterfaceB> */
function (string $type): string {
// @PHP8.0 use `match` with short closure
switch ($type) {
case 'classA-foo':
return SomeClassThatInheritsInterfaceA::class;
case 'classB-bar':
return SomeClassThatInheritsInterfaceB::class;
default:
self::fail("Type `$type` not handled.");
}
}
)
->mapper()
->map('list<' . SomeInterface::class . '>', [
'classA-foo',
['type' => 'classB-bar', 'valueB' => 'classB-bar'],
]);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertInstanceOf(SomeClassThatInheritsInterfaceA::class, $resultA);
self::assertInstanceOf(SomeClassThatInheritsInterfaceB::class, $resultB);
self::assertSame('classA-foo', $resultA->valueA);
self::assertSame('classB-bar', $resultB->valueB);
}
public function test_infer_interface_with_several_arguments_works_properly(): void
{
try {
[$resultA, $resultB] = (new MapperBuilder())
->infer(
SomeInterface::class,
/** @return class-string<SomeClassThatInheritsInterfaceA|SomeClassThatInheritsInterfaceB> */
function (string $type, int $key): string {
if ($type === 'classA' && $key === 42) {
return SomeClassThatInheritsInterfaceA::class;
} elseif ($type === 'classB' && $key === 1337) {
return SomeClassThatInheritsInterfaceB::class;
}
self::fail("Combinaison `$type` / `$key` not handled.");
}
)
->mapper()
->map('list<' . SomeInterface::class . '>', [
[
'type' => 'classA',
'key' => 42,
'valueA' => 'foo',
],
[
'type' => 'classB',
'key' => 1337,
'valueB' => 'bar',
],
]);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertInstanceOf(SomeClassThatInheritsInterfaceA::class, $resultA);
self::assertInstanceOf(SomeClassThatInheritsInterfaceB::class, $resultB);
self::assertSame('foo', $resultA->valueA);
self::assertSame('bar', $resultB->valueB);
}
public function test_unresolvable_implementation_throws_exception(): void
{
$this->expectException(CannotResolveObjectType::class);
$this->expectExceptionCode(1618049116);
$this->expectExceptionMessage('Impossible to resolve an implementation for `' . SomeInterface::class . '`.');
(new MapperBuilder())
->mapper()
->map(SomeInterface::class, []);
}
public function test_invalid_resolved_implementation_value_throws_exception(): void
{
$this->expectException(InvalidResolvedImplementationValue::class);
$this->expectExceptionCode(1630091260);
$this->expectExceptionMessage('Invalid value 42, expected a subtype of `DateTimeInterface`.');
(new MapperBuilder())
->infer(DateTimeInterface::class, fn () => 42)
->mapper()
->map(DateTimeInterface::class, []);
}
public function test_invalid_resolved_implementation_type_throws_exception(): void
{
$this->expectException(ResolvedImplementationIsNotAccepted::class);
$this->expectExceptionCode(1618049487);
$this->expectExceptionMessage('Invalid implementation type `int`, expected a subtype of `DateTimeInterface`.');
(new MapperBuilder())
->infer(DateTimeInterface::class, fn () => 'int')
->mapper()
->map(DateTimeInterface::class, []);
}
public function test_invalid_resolved_implementation_throws_exception(): void
{
$this->expectException(ResolvedImplementationIsNotAccepted::class);
$this->expectExceptionCode(1618049487);
$this->expectExceptionMessage('Invalid implementation type `stdClass`, expected a subtype of `DateTimeInterface`.');
(new MapperBuilder())
->infer(DateTimeInterface::class, fn () => stdClass::class)
->mapper()
->map(DateTimeInterface::class, []);
}
public function test_invalid_object_type_resolved_implementation_throws_exception(): void
{
$this->expectException(ResolvedImplementationIsNotAccepted::class);
$this->expectExceptionCode(1618049487);
$this->expectExceptionMessage('Invalid implementation type `DateTimeInterface`, expected a subtype of `DateTimeInterface`.');
(new MapperBuilder())
->infer(DateTimeInterface::class, fn () => DateTimeInterface::class)
->mapper()
->map(DateTimeInterface::class, []);
}
public function test_object_implementation_callback_error_throws_exception(): void
{
$exception = new DomainException('some error message', 1653990051);
$this->expectException(ObjectImplementationCallbackError::class);
$this->expectExceptionCode(1653983061);
$this->expectExceptionMessage('Error thrown when trying to get implementation of `DateTimeInterface`: some error message');
(new MapperBuilder())
->infer(
DateTimeInterface::class,
function () use ($exception) {
// @PHP8.0 use short closure
throw $exception;
}
)
->mapper()
->map(DateTimeInterface::class, []);
}
public function test_invalid_abstract_object_name_throws_exception(): void
{
$this->expectException(InvalidAbstractObjectName::class);
$this->expectExceptionCode(1653990369);
$this->expectExceptionMessage('Invalid interface or class name `invalid type`.');
(new MapperBuilder())
->infer('invalid type', fn () => stdClass::class) // @phpstan-ignore-line
->mapper()
->map(stdClass::class, []);
}
public function test_invalid_abstract_object_type_throws_exception(): void
{
$this->expectException(InvalidAbstractObjectName::class);
$this->expectExceptionCode(1653990369);
$this->expectExceptionMessage('Invalid interface or class name `string`.');
(new MapperBuilder())
->infer('string', fn () => stdClass::class) // @phpstan-ignore-line
->mapper()
->map(stdClass::class, []);
}
public function test_missing_object_implementation_registration_throws_exception(): void
{
$this->expectException(MissingObjectImplementationRegistration::class);
$this->expectExceptionCode(1653990549);
$this->expectExceptionMessage('No implementation of `' . SomeInterface::class . '` found with return type `mixed` of');
(new MapperBuilder())
->infer(
SomeInterface::class,
fn (string $type) => SomeClassThatInheritsInterfaceA::class
)
->mapper()
->map(SomeInterface::class, []);
}
public function test_invalid_union_object_implementation_registration_throws_exception(): void
{
$this->expectException(MissingObjectImplementationRegistration::class);
$this->expectExceptionCode(1653990549);
$this->expectExceptionMessage('No implementation of `' . SomeInterface::class . '` found with return type `string|int` of');
(new MapperBuilder())
->infer(
SomeInterface::class,
/** @return string|int */
fn () => SomeClassThatInheritsInterfaceA::class
)
->mapper()
->map(SomeInterface::class, []);
}
public function test_invalid_class_string_object_implementation_registration_throws_exception(): void
{
$this->expectException(MissingObjectImplementationRegistration::class);
$this->expectExceptionCode(1653990549);
$this->expectExceptionMessage('No implementation of `' . SomeInterface::class . '` found with return type `class-string` of');
(new MapperBuilder())
->infer(
SomeInterface::class,
/** @return class-string */
fn () => SomeClassThatInheritsInterfaceA::class
)
->mapper()
->map(SomeInterface::class, []);
}
public function test_object_implementation_not_registered_throws_exception(): void
{
$this->expectException(ObjectImplementationNotRegistered::class);
$this->expectExceptionCode(1653990989);
$this->expectExceptionMessage('Invalid implementation `' . SomeClassThatInheritsInterfaceC::class . '` for `' . SomeInterface::class . '`, it should be one of `' . SomeClassThatInheritsInterfaceA::class . '`, `' . SomeClassThatInheritsInterfaceB::class . '`.');
(new MapperBuilder())
->infer(
SomeInterface::class,
/** @return class-string<SomeClassThatInheritsInterfaceA|SomeClassThatInheritsInterfaceB> */
fn (): string => SomeClassThatInheritsInterfaceC::class
)
->mapper()
->map(SomeInterface::class, []);
}
public function test_invalid_source_throws_exception(): void
{
try {
(new MapperBuilder())
->infer(
SomeInterface::class,
/** @return class-string<SomeClassThatInheritsInterfaceA> */
fn (string $type, int $key) => SomeClassThatInheritsInterfaceA::class
)
->mapper()
->map(SomeInterface::class, 42);
} catch (MappingError $exception) {
$error = $exception->node()->messages()[0];
self::assertSame('1645283485', $error->code());
self::assertSame('Invalid value 42: it must be an array.', (string)$error);
}
}
public function test_invalid_source_value_throws_exception(): void
{
try {
(new MapperBuilder())
->infer(
SomeInterface::class,
/** @return class-string<SomeClassThatInheritsInterfaceA> */
fn (int $key) => SomeClassThatInheritsInterfaceA::class
)
->mapper()
->map(SomeInterface::class, 'foo');
} catch (MappingError $exception) {
$error = $exception->node()->children()['key']->messages()[0];
self::assertSame('1618736242', $error->code());
self::assertSame("Cannot cast 'foo' to `int`.", (string)$error);
}
}
public function test_exception_thrown_is_caught_and_throws_message_exception(): void
{
try {
(new MapperBuilder())
->infer(
DateTimeInterface::class,
/** @return class-string<DateTime> */
function (string $value) {
// @PHP8.0 use short closure
throw new DomainException('some error message', 1645303304);
}
)
->mapper()
->map(DateTimeInterface::class, 'foo');
} catch (MappingError $exception) {
$error = $exception->node()->messages()[0];
self::assertSame('1645303304', $error->code());
self::assertSame('some error message', (string)$error);
}
}
}
interface SomeInterface
{
}
final class SomeClassThatInheritsInterfaceA implements SomeInterface
{
public string $valueA;
}
final class SomeClassThatInheritsInterfaceB implements SomeInterface
{
public string $valueB;
}
final class SomeClassThatInheritsInterfaceC implements SomeInterface
{
}