Valinor/tests/Integration/Mapping/InterfaceInferringMappingTest.php
Romain Canon 69ad3f4777 feat: allow injecting a cache implementation that is used by the mapper
The cache implementation that was previously injected in the mapper
builder must now be manually injected. This gives better control on when
the cache should be enabled, especially depending on which environment
the application is running.

The library provides a cache implementation out of the box, which saves
cache entries into the file system.

It is also possible to use any PSR-16 compliant implementation, as long
as it is capable of caching the entries handled by the library.

```php
$cache = new \CuyZ\Valinor\Cache\FileSystemCache('path/to/cache-dir');

(new \CuyZ\Valinor\MapperBuilder())
    ->withCache($cache)
    ->mapper()
    ->map(SomeClass::class, [/* … */]);
```
2022-05-23 20:28:02 +02:00

211 lines
7.9 KiB
PHP

<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Integration\Mapping;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidInterfaceResolverReturnType;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidTypeResolvedForInterface;
use CuyZ\Valinor\Mapper\Tree\Exception\ResolvedTypeForInterfaceIsNotAccepted;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
use CuyZ\Valinor\Type\Resolver\Exception\CannotResolveTypeFromInterface;
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_single_argument_works_properly(): void
{
try {
[$resultA, $resultB] = (new MapperBuilder())
->infer(SomeInterface::class, 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, 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_interface_implementation_throws_exception(): void
{
$this->expectException(CannotResolveTypeFromInterface::class);
$this->expectExceptionCode(1618049116);
$this->expectExceptionMessage('Impossible to resolve an implementation for the interface `' . SomeInterface::class . '`.');
(new MapperBuilder())
->mapper()
->map(SomeInterface::class, []);
}
public function test_invalid_interface_resolver_return_type_throws_exception(): void
{
$this->expectException(InvalidInterfaceResolverReturnType::class);
$this->expectExceptionCode(1630091260);
$this->expectExceptionMessage('Invalid value 42; it must be the name of a class that implements `DateTimeInterface`.');
(new MapperBuilder())
->infer(DateTimeInterface::class, fn () => 42)
->mapper()
->map(DateTimeInterface::class, []);
}
public function test_invalid_interface_resolved_return_type_throws_exception(): void
{
$this->expectException(InvalidTypeResolvedForInterface::class);
$this->expectExceptionCode(1618049224);
$this->expectExceptionMessage('Invalid type `int`; it must be the name of a class that implements `DateTimeInterface`.');
(new MapperBuilder())
->infer(DateTimeInterface::class, fn () => 'int')
->mapper()
->map(DateTimeInterface::class, []);
}
public function test_invalid_resolved_interface_implementation_throws_exception(): void
{
$this->expectException(ResolvedTypeForInterfaceIsNotAccepted::class);
$this->expectExceptionCode(1618049487);
$this->expectExceptionMessage('The implementation `stdClass` is not accepted by the interface `DateTimeInterface`.');
(new MapperBuilder())
->infer(DateTimeInterface::class, fn () => stdClass::class)
->mapper()
->map(DateTimeInterface::class, []);
}
public function test_invalid_source_throws_exception(): void
{
try {
(new MapperBuilder())
->infer(SomeInterface::class, fn (string $type, int $key): string => 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, fn (int $key): string => 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, function () {
// @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;
}