mirror of
https://github.com/danog/Valinor.git
synced 2024-11-26 20:24:40 +01:00
fix: handle scalar value casting in union types only in flexible mode
This commit is contained in:
parent
92a41a1564
commit
752ad9d12e
@ -56,8 +56,6 @@ use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
|
|||||||
use CuyZ\Valinor\Type\Parser\Template\BasicTemplateParser;
|
use CuyZ\Valinor\Type\Parser\Template\BasicTemplateParser;
|
||||||
use CuyZ\Valinor\Type\Parser\Template\TemplateParser;
|
use CuyZ\Valinor\Type\Parser\Template\TemplateParser;
|
||||||
use CuyZ\Valinor\Type\Parser\TypeParser;
|
use CuyZ\Valinor\Type\Parser\TypeParser;
|
||||||
use CuyZ\Valinor\Type\Resolver\Union\UnionNullNarrower;
|
|
||||||
use CuyZ\Valinor\Type\Resolver\Union\UnionScalarNarrower;
|
|
||||||
use CuyZ\Valinor\Type\ScalarType;
|
use CuyZ\Valinor\Type\ScalarType;
|
||||||
use CuyZ\Valinor\Type\Types\ArrayType;
|
use CuyZ\Valinor\Type\Types\ArrayType;
|
||||||
use CuyZ\Valinor\Type\Types\IterableType;
|
use CuyZ\Valinor\Type\Types\IterableType;
|
||||||
@ -102,7 +100,7 @@ final class Container
|
|||||||
ScalarType::class => new ScalarNodeBuilder($settings->flexible),
|
ScalarType::class => new ScalarNodeBuilder($settings->flexible),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$builder = new UnionNodeBuilder($builder, new UnionNullNarrower(new UnionScalarNarrower()));
|
$builder = new UnionNodeBuilder($builder, $settings->flexible);
|
||||||
|
|
||||||
$builder = new ClassNodeBuilder(
|
$builder = new ClassNodeBuilder(
|
||||||
$builder,
|
$builder,
|
||||||
|
@ -6,6 +6,7 @@ namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
|||||||
|
|
||||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||||
use CuyZ\Valinor\Definition\FunctionsContainer;
|
use CuyZ\Valinor\Definition\FunctionsContainer;
|
||||||
|
use CuyZ\Valinor\Mapper\Tree\Exception\CannotResolveObjectType;
|
||||||
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidAbstractObjectName;
|
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidAbstractObjectName;
|
||||||
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidResolvedImplementationValue;
|
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidResolvedImplementationValue;
|
||||||
use CuyZ\Valinor\Mapper\Tree\Exception\MissingObjectImplementationRegistration;
|
use CuyZ\Valinor\Mapper\Tree\Exception\MissingObjectImplementationRegistration;
|
||||||
@ -14,7 +15,6 @@ use CuyZ\Valinor\Mapper\Tree\Exception\ObjectImplementationNotRegistered;
|
|||||||
use CuyZ\Valinor\Mapper\Tree\Exception\ResolvedImplementationIsNotAccepted;
|
use CuyZ\Valinor\Mapper\Tree\Exception\ResolvedImplementationIsNotAccepted;
|
||||||
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
|
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
|
||||||
use CuyZ\Valinor\Type\Parser\TypeParser;
|
use CuyZ\Valinor\Type\Parser\TypeParser;
|
||||||
use CuyZ\Valinor\Type\Resolver\Exception\CannotResolveObjectType;
|
|
||||||
use CuyZ\Valinor\Type\Type;
|
use CuyZ\Valinor\Type\Type;
|
||||||
use CuyZ\Valinor\Type\Types\ClassStringType;
|
use CuyZ\Valinor\Type\Types\ClassStringType;
|
||||||
use CuyZ\Valinor\Type\Types\ClassType;
|
use CuyZ\Valinor\Type\Types\ClassType;
|
||||||
|
@ -4,21 +4,27 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
||||||
|
|
||||||
|
use CuyZ\Valinor\Mapper\Tree\Exception\CannotResolveTypeFromUnion;
|
||||||
use CuyZ\Valinor\Mapper\Tree\Shell;
|
use CuyZ\Valinor\Mapper\Tree\Shell;
|
||||||
use CuyZ\Valinor\Type\Resolver\Union\UnionNarrower;
|
use CuyZ\Valinor\Type\EnumType;
|
||||||
|
use CuyZ\Valinor\Type\ScalarType;
|
||||||
|
use CuyZ\Valinor\Type\Type;
|
||||||
|
use CuyZ\Valinor\Type\Types\NullType;
|
||||||
use CuyZ\Valinor\Type\Types\UnionType;
|
use CuyZ\Valinor\Type\Types\UnionType;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
final class UnionNodeBuilder implements NodeBuilder
|
final class UnionNodeBuilder implements NodeBuilder
|
||||||
{
|
{
|
||||||
private NodeBuilder $delegate;
|
private NodeBuilder $delegate;
|
||||||
|
|
||||||
private UnionNarrower $unionNarrower;
|
private bool $flexible;
|
||||||
|
|
||||||
public function __construct(NodeBuilder $delegate, UnionNarrower $unionNarrower)
|
public function __construct(NodeBuilder $delegate, bool $flexible)
|
||||||
{
|
{
|
||||||
$this->delegate = $delegate;
|
$this->delegate = $delegate;
|
||||||
$this->unionNarrower = $unionNarrower;
|
$this->flexible = $flexible;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
|
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
|
||||||
@ -29,8 +35,40 @@ final class UnionNodeBuilder implements NodeBuilder
|
|||||||
return $this->delegate->build($shell, $rootBuilder);
|
return $this->delegate->build($shell, $rootBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
$narrowedType = $this->unionNarrower->narrow($type, $shell->value());
|
$narrowedType = $this->narrow($type, $shell->value());
|
||||||
|
|
||||||
return $rootBuilder->build($shell->withType($narrowedType));
|
return $rootBuilder->build($shell->withType($narrowedType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $source
|
||||||
|
*/
|
||||||
|
private function narrow(UnionType $type, $source): Type
|
||||||
|
{
|
||||||
|
$subTypes = $type->types();
|
||||||
|
|
||||||
|
if ($source !== null && count($subTypes) === 2) {
|
||||||
|
if ($subTypes[0] instanceof NullType) {
|
||||||
|
return $subTypes[1];
|
||||||
|
} elseif ($subTypes[1] instanceof NullType) {
|
||||||
|
return $subTypes[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($subTypes as $subType) {
|
||||||
|
if (! $subType instanceof ScalarType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->flexible && ! $subType instanceof EnumType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($subType->canCast($source)) {
|
||||||
|
return $subType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CannotResolveTypeFromUnion($source, $type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace CuyZ\Valinor\Type\Resolver\Exception;
|
namespace CuyZ\Valinor\Mapper\Tree\Exception;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace CuyZ\Valinor\Type\Resolver\Exception;
|
namespace CuyZ\Valinor\Mapper\Tree\Exception;
|
||||||
|
|
||||||
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
|
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
|
||||||
use CuyZ\Valinor\Mapper\Tree\Message\HasParameters;
|
use CuyZ\Valinor\Mapper\Tree\Message\HasParameters;
|
||||||
@ -22,7 +22,10 @@ final class CannotResolveTypeFromUnion extends RuntimeException implements Error
|
|||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
private array $parameters;
|
private array $parameters;
|
||||||
|
|
||||||
public function __construct(UnionType $unionType)
|
/**
|
||||||
|
* @param mixed $source
|
||||||
|
*/
|
||||||
|
public function __construct($source, UnionType $unionType)
|
||||||
{
|
{
|
||||||
$this->parameters = [
|
$this->parameters = [
|
||||||
'allowed_types' => implode(
|
'allowed_types' => implode(
|
||||||
@ -32,9 +35,15 @@ final class CannotResolveTypeFromUnion extends RuntimeException implements Error
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->body = TypeHelper::containsObject($unionType)
|
if ($source === null) {
|
||||||
? 'Invalid value {source_value}.'
|
$this->body = TypeHelper::containsObject($unionType)
|
||||||
: 'Value {source_value} does not match any of {allowed_types}.';
|
? 'Cannot be empty.'
|
||||||
|
: 'Cannot be empty and must be filled with a value matching any of {allowed_types}.';
|
||||||
|
} else {
|
||||||
|
$this->body = TypeHelper::containsObject($unionType)
|
||||||
|
? 'Invalid value {source_value}.'
|
||||||
|
: 'Value {source_value} does not match any of {allowed_types}.';
|
||||||
|
}
|
||||||
|
|
||||||
parent::__construct(StringFormatter::for($this), 1607027306);
|
parent::__construct(StringFormatter::for($this), 1607027306);
|
||||||
}
|
}
|
@ -14,6 +14,9 @@ interface DefaultMessage
|
|||||||
'Value {source_value} does not match any of {allowed_types}.' => [
|
'Value {source_value} does not match any of {allowed_types}.' => [
|
||||||
'en' => 'Value {source_value} does not match any of {allowed_types}.',
|
'en' => 'Value {source_value} does not match any of {allowed_types}.',
|
||||||
],
|
],
|
||||||
|
'Cannot be empty and must be filled with a value matching any of {allowed_types}.' => [
|
||||||
|
'en' => 'Cannot be empty and must be filled with a value matching any of {allowed_types}.',
|
||||||
|
],
|
||||||
'Value {source_value} does not match type {expected_type}.' => [
|
'Value {source_value} does not match type {expected_type}.' => [
|
||||||
'en' => 'Value {source_value} does not match type {expected_type}.',
|
'en' => 'Value {source_value} does not match type {expected_type}.',
|
||||||
],
|
],
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace CuyZ\Valinor\Type\Resolver\Exception;
|
|
||||||
|
|
||||||
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
|
|
||||||
use CuyZ\Valinor\Mapper\Tree\Message\HasParameters;
|
|
||||||
use CuyZ\Valinor\Type\Types\UnionType;
|
|
||||||
use CuyZ\Valinor\Utility\String\StringFormatter;
|
|
||||||
use CuyZ\Valinor\Utility\TypeHelper;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
final class UnionTypeDoesNotAllowNull extends RuntimeException implements ErrorMessage, HasParameters
|
|
||||||
{
|
|
||||||
private string $body;
|
|
||||||
|
|
||||||
/** @var array<string, string> */
|
|
||||||
private array $parameters;
|
|
||||||
|
|
||||||
public function __construct(UnionType $unionType)
|
|
||||||
{
|
|
||||||
$this->parameters = [
|
|
||||||
'expected_type' => TypeHelper::dump($unionType),
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->body = TypeHelper::containsObject($unionType)
|
|
||||||
? 'Cannot be empty.'
|
|
||||||
: 'Cannot be empty and must be filled with a value matching type {expected_type}.';
|
|
||||||
|
|
||||||
parent::__construct(StringFormatter::for($this), 1618742357);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function body(): string
|
|
||||||
{
|
|
||||||
return $this->body;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function parameters(): array
|
|
||||||
{
|
|
||||||
return $this->parameters;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace CuyZ\Valinor\Type\Resolver\Union;
|
|
||||||
|
|
||||||
use CuyZ\Valinor\Type\Type;
|
|
||||||
use CuyZ\Valinor\Type\Types\UnionType;
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
interface UnionNarrower
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param mixed $source
|
|
||||||
*/
|
|
||||||
public function narrow(UnionType $unionType, $source): Type;
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace CuyZ\Valinor\Type\Resolver\Union;
|
|
||||||
|
|
||||||
use CuyZ\Valinor\Type\Resolver\Exception\UnionTypeDoesNotAllowNull;
|
|
||||||
use CuyZ\Valinor\Type\Type;
|
|
||||||
use CuyZ\Valinor\Type\Types\NullType;
|
|
||||||
use CuyZ\Valinor\Type\Types\UnionType;
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
final class UnionNullNarrower implements UnionNarrower
|
|
||||||
{
|
|
||||||
private UnionNarrower $delegate;
|
|
||||||
|
|
||||||
public function __construct(UnionNarrower $delegate)
|
|
||||||
{
|
|
||||||
$this->delegate = $delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function narrow(UnionType $unionType, $source): Type
|
|
||||||
{
|
|
||||||
$allowsNull = $this->findNullType($unionType);
|
|
||||||
|
|
||||||
if ($source === null) {
|
|
||||||
if (! $allowsNull) {
|
|
||||||
throw new UnionTypeDoesNotAllowNull($unionType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NullType::get();
|
|
||||||
}
|
|
||||||
|
|
||||||
$subTypes = $unionType->types();
|
|
||||||
|
|
||||||
if ($allowsNull && count($subTypes) === 2) {
|
|
||||||
return $subTypes[0] instanceof NullType
|
|
||||||
? $subTypes[1]
|
|
||||||
: $subTypes[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->delegate->narrow($unionType, $source);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findNullType(UnionType $type): bool
|
|
||||||
{
|
|
||||||
foreach ($type->types() as $subType) {
|
|
||||||
if ($subType instanceof NullType) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace CuyZ\Valinor\Type\Resolver\Union;
|
|
||||||
|
|
||||||
use CuyZ\Valinor\Type\Resolver\Exception\CannotResolveTypeFromUnion;
|
|
||||||
use CuyZ\Valinor\Type\ScalarType;
|
|
||||||
use CuyZ\Valinor\Type\Type;
|
|
||||||
use CuyZ\Valinor\Type\Types\UnionType;
|
|
||||||
|
|
||||||
use function count;
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
final class UnionScalarNarrower implements UnionNarrower
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param mixed $source
|
|
||||||
*/
|
|
||||||
public function narrow(UnionType $unionType, $source): Type
|
|
||||||
{
|
|
||||||
$accepts = [];
|
|
||||||
$canCast = [];
|
|
||||||
|
|
||||||
foreach ($unionType->types() as $subType) {
|
|
||||||
if ($subType->accepts($source)) {
|
|
||||||
$accepts[] = $subType;
|
|
||||||
} elseif ($subType instanceof ScalarType && $subType->canCast($source)) {
|
|
||||||
$canCast[] = $subType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($accepts) === 1) {
|
|
||||||
return $accepts[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($canCast) === 1) {
|
|
||||||
return $canCast[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new CannotResolveTypeFromUnion($unionType);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace CuyZ\Valinor\Tests\Fake\Type\Resolver\Union;
|
|
||||||
|
|
||||||
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
|
|
||||||
use CuyZ\Valinor\Type\Resolver\Union\UnionNarrower;
|
|
||||||
use CuyZ\Valinor\Type\Type;
|
|
||||||
use CuyZ\Valinor\Type\Types\UnionType;
|
|
||||||
|
|
||||||
final class FakeUnionNarrower implements UnionNarrower
|
|
||||||
{
|
|
||||||
private Type $type;
|
|
||||||
|
|
||||||
public function narrow(UnionType $unionType, $source): Type
|
|
||||||
{
|
|
||||||
return $this->type ?? new FakeType();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function willReturn(Type $type): void
|
|
||||||
{
|
|
||||||
$this->type = $type;
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,6 +6,7 @@ namespace CuyZ\Valinor\Tests\Integration\Mapping\Fixture;
|
|||||||
|
|
||||||
// @PHP8.0 move inside \CuyZ\Valinor\Tests\Integration\Mapping\UnionValuesMappingTest
|
// @PHP8.0 move inside \CuyZ\Valinor\Tests\Integration\Mapping\UnionValuesMappingTest
|
||||||
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants;
|
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants;
|
||||||
|
use DateTimeInterface;
|
||||||
|
|
||||||
class NativeUnionValues
|
class NativeUnionValues
|
||||||
{
|
{
|
||||||
@ -27,6 +28,10 @@ class NativeUnionValues
|
|||||||
/** @var int|false */
|
/** @var int|false */
|
||||||
public int|bool $intOrLiteralFalse = 42;
|
public int|bool $intOrLiteralFalse = 42;
|
||||||
|
|
||||||
|
public DateTimeInterface|null $dateTimeOrNull = null;
|
||||||
|
|
||||||
|
public null|DateTimeInterface $nullOrDateTime = null;
|
||||||
|
|
||||||
/** @var ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A */
|
/** @var ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A */
|
||||||
public string|int $constantWithStringValue = 1653398288;
|
public string|int $constantWithStringValue = 1653398288;
|
||||||
|
|
||||||
@ -51,6 +56,8 @@ class NativeUnionValuesWithConstructor extends NativeUnionValues
|
|||||||
string|null $nullableWithNull = 'Schwifty!',
|
string|null $nullableWithNull = 'Schwifty!',
|
||||||
int|bool $intOrLiteralTrue = 42,
|
int|bool $intOrLiteralTrue = 42,
|
||||||
int|bool $intOrLiteralFalse = 42,
|
int|bool $intOrLiteralFalse = 42,
|
||||||
|
DateTimeInterface|null $dateTimeOrNull = null,
|
||||||
|
null|DateTimeInterface $nullOrDateTime = null,
|
||||||
string|int $constantWithStringValue = 1653398288,
|
string|int $constantWithStringValue = 1653398288,
|
||||||
string|int $constantWithIntegerValue = 'some string value'
|
string|int $constantWithIntegerValue = 'some string value'
|
||||||
) {
|
) {
|
||||||
@ -62,6 +69,8 @@ class NativeUnionValuesWithConstructor extends NativeUnionValues
|
|||||||
$this->nullableWithNull = $nullableWithNull;
|
$this->nullableWithNull = $nullableWithNull;
|
||||||
$this->intOrLiteralTrue = $intOrLiteralTrue;
|
$this->intOrLiteralTrue = $intOrLiteralTrue;
|
||||||
$this->intOrLiteralFalse = $intOrLiteralFalse;
|
$this->intOrLiteralFalse = $intOrLiteralFalse;
|
||||||
|
$this->dateTimeOrNull = $dateTimeOrNull;
|
||||||
|
$this->nullOrDateTime = $nullOrDateTime;
|
||||||
$this->constantWithStringValue = $constantWithStringValue;
|
$this->constantWithStringValue = $constantWithStringValue;
|
||||||
$this->constantWithIntegerValue = $constantWithIntegerValue;
|
$this->constantWithIntegerValue = $constantWithIntegerValue;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace CuyZ\Valinor\Tests\Integration\Mapping;
|
namespace CuyZ\Valinor\Tests\Integration\Mapping;
|
||||||
|
|
||||||
use CuyZ\Valinor\Mapper\MappingError;
|
use CuyZ\Valinor\Mapper\MappingError;
|
||||||
|
use CuyZ\Valinor\Mapper\Tree\Exception\CannotResolveObjectType;
|
||||||
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidAbstractObjectName;
|
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidAbstractObjectName;
|
||||||
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidResolvedImplementationValue;
|
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidResolvedImplementationValue;
|
||||||
use CuyZ\Valinor\Mapper\Tree\Exception\MissingObjectImplementationRegistration;
|
use CuyZ\Valinor\Mapper\Tree\Exception\MissingObjectImplementationRegistration;
|
||||||
@ -21,7 +22,6 @@ use CuyZ\Valinor\Tests\Fixture\Object\InterfaceWithDifferentNamespaces\Interface
|
|||||||
use CuyZ\Valinor\Tests\Fixture\Object\InterfaceWithDifferentNamespaces\InterfaceB;
|
use CuyZ\Valinor\Tests\Fixture\Object\InterfaceWithDifferentNamespaces\InterfaceB;
|
||||||
use CuyZ\Valinor\Tests\Fixture\Object\InterfaceWithDifferentNamespaces\InterfaceBInferer;
|
use CuyZ\Valinor\Tests\Fixture\Object\InterfaceWithDifferentNamespaces\InterfaceBInferer;
|
||||||
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
|
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
|
||||||
use CuyZ\Valinor\Type\Resolver\Exception\CannotResolveObjectType;
|
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use DomainException;
|
use DomainException;
|
||||||
|
@ -6,10 +6,14 @@ namespace CuyZ\Valinor\Tests\Integration\Mapping\Object;
|
|||||||
|
|
||||||
use CuyZ\Valinor\Mapper\MappingError;
|
use CuyZ\Valinor\Mapper\MappingError;
|
||||||
use CuyZ\Valinor\MapperBuilder;
|
use CuyZ\Valinor\MapperBuilder;
|
||||||
|
use CuyZ\Valinor\Tests\Fixture\Enum\PureEnum;
|
||||||
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants;
|
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants;
|
||||||
|
use CuyZ\Valinor\Tests\Fixture\Object\StringableObject;
|
||||||
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
|
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
|
||||||
use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\NativeUnionValues;
|
use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\NativeUnionValues;
|
||||||
use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\NativeUnionValuesWithConstructor;
|
use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\NativeUnionValuesWithConstructor;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
|
||||||
final class UnionValuesMappingTest extends IntegrationTest
|
final class UnionValuesMappingTest extends IntegrationTest
|
||||||
{
|
{
|
||||||
@ -24,6 +28,8 @@ final class UnionValuesMappingTest extends IntegrationTest
|
|||||||
'nullableWithNull' => null,
|
'nullableWithNull' => null,
|
||||||
'intOrLiteralTrue' => true,
|
'intOrLiteralTrue' => true,
|
||||||
'intOrLiteralFalse' => false,
|
'intOrLiteralFalse' => false,
|
||||||
|
'dateTimeOrNull' => 1667754013,
|
||||||
|
'nullOrDateTime' => 1667754014,
|
||||||
'constantWithStringValue' => 'some string value',
|
'constantWithStringValue' => 'some string value',
|
||||||
'constantWithIntegerValue' => 1653398288,
|
'constantWithIntegerValue' => 1653398288,
|
||||||
];
|
];
|
||||||
@ -50,6 +56,10 @@ final class UnionValuesMappingTest extends IntegrationTest
|
|||||||
self::assertSame(null, $result->nullableWithNull);
|
self::assertSame(null, $result->nullableWithNull);
|
||||||
self::assertSame(true, $result->intOrLiteralTrue);
|
self::assertSame(true, $result->intOrLiteralTrue);
|
||||||
self::assertSame(false, $result->intOrLiteralFalse);
|
self::assertSame(false, $result->intOrLiteralFalse);
|
||||||
|
self::assertInstanceOf(DateTimeInterface::class, $result->dateTimeOrNull);
|
||||||
|
self::assertInstanceOf(DateTimeInterface::class, $result->nullOrDateTime);
|
||||||
|
self::assertSame((new DateTimeImmutable('@1667754013'))->format('U'), $result->dateTimeOrNull->format('U'));
|
||||||
|
self::assertSame((new DateTimeImmutable('@1667754014'))->format('U'), $result->nullOrDateTime->format('U'));
|
||||||
self::assertSame('some string value', $result->constantWithStringValue);
|
self::assertSame('some string value', $result->constantWithStringValue);
|
||||||
self::assertSame(1653398288, $result->constantWithIntegerValue);
|
self::assertSame(1653398288, $result->constantWithIntegerValue);
|
||||||
}
|
}
|
||||||
@ -78,6 +88,60 @@ final class UnionValuesMappingTest extends IntegrationTest
|
|||||||
self::assertSame('fiz', $result->stringValueWithDoubleQuote);
|
self::assertSame('fiz', $result->stringValueWithDoubleQuote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_filled_source_value_is_casted_when_union_contains_three_types_including_null(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = (new MapperBuilder())
|
||||||
|
->flexible()
|
||||||
|
->mapper()
|
||||||
|
->map('null|int|string', new StringableObject('foo'));
|
||||||
|
} catch (MappingError $error) {
|
||||||
|
$this->mappingFail($error);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame('foo', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP >= 8.1
|
||||||
|
*/
|
||||||
|
public function test_enum_in_union_type_is_casted_properly(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = (new MapperBuilder())->mapper()->map('int|' . PureEnum::class, 'FOO');
|
||||||
|
} catch (MappingError $error) {
|
||||||
|
$this->mappingFail($error);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame(PureEnum::FOO, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_source_value_is_casted_when_other_type_cannot_be_caster(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = (new MapperBuilder())
|
||||||
|
->flexible()
|
||||||
|
->mapper()
|
||||||
|
->map('string[]|string', new StringableObject('foo'));
|
||||||
|
} catch (MappingError $error) {
|
||||||
|
$this->mappingFail($error);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame('foo', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invalid_value_is_not_casted_when_casting_mode_is_disabled(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(new MapperBuilder())->mapper()->map('string|float', 42);
|
||||||
|
} catch (MappingError $exception) {
|
||||||
|
$error = $exception->node()->messages()[0];
|
||||||
|
|
||||||
|
self::assertSame('1607027306', $error->code());
|
||||||
|
self::assertSame('Value 42 does not match any of `string`, `float`.', (string)$error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnionValues
|
class UnionValues
|
||||||
@ -106,6 +170,12 @@ class UnionValues
|
|||||||
/** @var int|false */
|
/** @var int|false */
|
||||||
public $intOrLiteralFalse = 42;
|
public $intOrLiteralFalse = 42;
|
||||||
|
|
||||||
|
/** @var DateTimeInterface|null */
|
||||||
|
public $dateTimeOrNull;
|
||||||
|
|
||||||
|
/** @var null|DateTimeInterface */
|
||||||
|
public $nullOrDateTime;
|
||||||
|
|
||||||
/** @var ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A */
|
/** @var ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A */
|
||||||
public $constantWithStringValue = 1653398288;
|
public $constantWithStringValue = 1653398288;
|
||||||
|
|
||||||
@ -145,6 +215,8 @@ class UnionValuesWithConstructor extends UnionValues
|
|||||||
* @param string|null|float $nullableWithNull
|
* @param string|null|float $nullableWithNull
|
||||||
* @param int|true $intOrLiteralTrue
|
* @param int|true $intOrLiteralTrue
|
||||||
* @param int|false $intOrLiteralFalse
|
* @param int|false $intOrLiteralFalse
|
||||||
|
* @param DateTimeInterface|null $dateTimeOrNull
|
||||||
|
* @param null|DateTimeInterface $nullOrDateTime
|
||||||
* @param ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A $constantWithStringValue
|
* @param ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A $constantWithStringValue
|
||||||
* @param ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A $constantWithIntegerValue
|
* @param ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A $constantWithIntegerValue
|
||||||
*/
|
*/
|
||||||
@ -157,6 +229,8 @@ class UnionValuesWithConstructor extends UnionValues
|
|||||||
$nullableWithNull = 'Schwifty!',
|
$nullableWithNull = 'Schwifty!',
|
||||||
$intOrLiteralTrue = 42,
|
$intOrLiteralTrue = 42,
|
||||||
$intOrLiteralFalse = 42,
|
$intOrLiteralFalse = 42,
|
||||||
|
$dateTimeOrNull = null,
|
||||||
|
$nullOrDateTime = null,
|
||||||
$constantWithStringValue = 1653398288,
|
$constantWithStringValue = 1653398288,
|
||||||
$constantWithIntegerValue = 'some string value'
|
$constantWithIntegerValue = 'some string value'
|
||||||
) {
|
) {
|
||||||
@ -168,6 +242,8 @@ class UnionValuesWithConstructor extends UnionValues
|
|||||||
$this->nullableWithNull = $nullableWithNull;
|
$this->nullableWithNull = $nullableWithNull;
|
||||||
$this->intOrLiteralTrue = $intOrLiteralTrue;
|
$this->intOrLiteralTrue = $intOrLiteralTrue;
|
||||||
$this->intOrLiteralFalse = $intOrLiteralFalse;
|
$this->intOrLiteralFalse = $intOrLiteralFalse;
|
||||||
|
$this->dateTimeOrNull = $dateTimeOrNull;
|
||||||
|
$this->nullOrDateTime = $nullOrDateTime;
|
||||||
$this->constantWithStringValue = $constantWithStringValue;
|
$this->constantWithStringValue = $constantWithStringValue;
|
||||||
$this->constantWithIntegerValue = $constantWithIntegerValue;
|
$this->constantWithIntegerValue = $constantWithIntegerValue;
|
||||||
}
|
}
|
||||||
|
@ -368,8 +368,8 @@ final class FlexibleMappingTest extends IntegrationTest
|
|||||||
} catch (MappingError $exception) {
|
} catch (MappingError $exception) {
|
||||||
$error = $exception->node()->messages()[0];
|
$error = $exception->node()->messages()[0];
|
||||||
|
|
||||||
self::assertSame('1618742357', $error->code());
|
self::assertSame('1607027306', $error->code());
|
||||||
self::assertSame("Cannot be empty and must be filled with a value matching type `bool|int|float`.", (string)$error);
|
self::assertSame('Cannot be empty and must be filled with a value matching any of `bool`, `int`, `float`.', (string)$error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace CuyZ\Valinor\Tests\Unit\Type\Resolver\Union;
|
|
||||||
|
|
||||||
use CuyZ\Valinor\Tests\Fake\Type\FakeObjectType;
|
|
||||||
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
|
|
||||||
use CuyZ\Valinor\Tests\Fake\Type\Resolver\Union\FakeUnionNarrower;
|
|
||||||
use CuyZ\Valinor\Type\Resolver\Exception\UnionTypeDoesNotAllowNull;
|
|
||||||
use CuyZ\Valinor\Type\Resolver\Union\UnionNullNarrower;
|
|
||||||
use CuyZ\Valinor\Type\Types\NullType;
|
|
||||||
use CuyZ\Valinor\Type\Types\UnionType;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
final class UnionNullNarrowerTest extends TestCase
|
|
||||||
{
|
|
||||||
private UnionNullNarrower $unionNullNarrower;
|
|
||||||
|
|
||||||
private FakeUnionNarrower $delegate;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->delegate = new FakeUnionNarrower();
|
|
||||||
|
|
||||||
$this->unionNullNarrower = new UnionNullNarrower($this->delegate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_null_value_with_union_type_allowing_null_returns_null_type(): void
|
|
||||||
{
|
|
||||||
$unionType = new UnionType(new FakeType(), NullType::get());
|
|
||||||
|
|
||||||
$type = $this->unionNullNarrower->narrow($unionType, null);
|
|
||||||
|
|
||||||
self::assertInstanceOf(NullType::class, $type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_union_not_containing_null_type_is_narrowed_by_delegate(): void
|
|
||||||
{
|
|
||||||
$type = new FakeType();
|
|
||||||
$this->delegate->willReturn($type);
|
|
||||||
$unionType = new UnionType(new FakeType(), new FakeType());
|
|
||||||
|
|
||||||
$narrowedType = $this->unionNullNarrower->narrow($unionType, 'foo');
|
|
||||||
|
|
||||||
self::assertSame($type, $narrowedType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_null_value_not_allowed_by_union_type_throws_exception(): void
|
|
||||||
{
|
|
||||||
$unionType = new UnionType(new FakeType(), new FakeType());
|
|
||||||
|
|
||||||
$this->expectException(UnionTypeDoesNotAllowNull::class);
|
|
||||||
$this->expectExceptionCode(1618742357);
|
|
||||||
$this->expectExceptionMessage("Cannot be empty and must be filled with a value matching type `{$unionType->toString()}`.");
|
|
||||||
|
|
||||||
$this->unionNullNarrower->narrow($unionType, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_null_value_not_allowed_by_union_type_containing_object_type_throws_exception(): void
|
|
||||||
{
|
|
||||||
$unionType = new UnionType(new FakeType(), new FakeObjectType());
|
|
||||||
|
|
||||||
$this->expectException(UnionTypeDoesNotAllowNull::class);
|
|
||||||
$this->expectExceptionCode(1618742357);
|
|
||||||
$this->expectExceptionMessage('Cannot be empty.');
|
|
||||||
|
|
||||||
$this->unionNullNarrower->narrow($unionType, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_non_null_value_for_union_type_with_null_type_at_left_returns_type_at_right(): void
|
|
||||||
{
|
|
||||||
$type = new FakeType();
|
|
||||||
$unionType = new UnionType($type, new NullType());
|
|
||||||
|
|
||||||
$narrowedType = $this->unionNullNarrower->narrow($unionType, 'foo');
|
|
||||||
|
|
||||||
self::assertSame($type, $narrowedType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_non_null_value_for_union_type_with_null_type_at_right_returns_type_at_left(): void
|
|
||||||
{
|
|
||||||
$type = new FakeType();
|
|
||||||
$unionType = new UnionType(new NullType(), $type);
|
|
||||||
|
|
||||||
$narrowedType = $this->unionNullNarrower->narrow($unionType, 'foo');
|
|
||||||
|
|
||||||
self::assertSame($type, $narrowedType);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace CuyZ\Valinor\Tests\Unit\Type\Resolver\Union;
|
|
||||||
|
|
||||||
use CuyZ\Valinor\Type\IntegerType;
|
|
||||||
use CuyZ\Valinor\Type\Resolver\Union\UnionScalarNarrower;
|
|
||||||
use CuyZ\Valinor\Type\StringType;
|
|
||||||
use CuyZ\Valinor\Type\Type;
|
|
||||||
use CuyZ\Valinor\Type\Types\NativeBooleanType;
|
|
||||||
use CuyZ\Valinor\Type\Types\NativeFloatType;
|
|
||||||
use CuyZ\Valinor\Type\Types\NativeIntegerType;
|
|
||||||
use CuyZ\Valinor\Type\Types\NativeStringType;
|
|
||||||
use CuyZ\Valinor\Type\Types\UndefinedObjectType;
|
|
||||||
use CuyZ\Valinor\Type\Types\UnionType;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
final class UnionScalarNarrowerTest extends TestCase
|
|
||||||
{
|
|
||||||
private UnionScalarNarrower $unionScalarNarrower;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->unionScalarNarrower = new UnionScalarNarrower();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider matching_types_are_resolved_data_provider
|
|
||||||
*
|
|
||||||
* @param mixed $source
|
|
||||||
* @param class-string<Type> $expectedType
|
|
||||||
*/
|
|
||||||
public function test_matching_types_are_resolved(UnionType $unionType, $source, string $expectedType): void
|
|
||||||
{
|
|
||||||
$type = $this->unionScalarNarrower->narrow($unionType, $source);
|
|
||||||
|
|
||||||
self::assertInstanceOf($expectedType, $type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function matching_types_are_resolved_data_provider(): iterable
|
|
||||||
{
|
|
||||||
$scalarUnion = new UnionType(
|
|
||||||
NativeIntegerType::get(),
|
|
||||||
NativeFloatType::get(),
|
|
||||||
NativeStringType::get(),
|
|
||||||
NativeBooleanType::get(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'int|float|string|bool with integer value' => [
|
|
||||||
'Union type' => $scalarUnion,
|
|
||||||
'Source' => 42,
|
|
||||||
'Expected type' => IntegerType::class,
|
|
||||||
],
|
|
||||||
'int|float|string|bool with float value' => [
|
|
||||||
'Union type' => $scalarUnion,
|
|
||||||
'Source' => 1337.42,
|
|
||||||
'Expected type' => NativeFloatType::class,
|
|
||||||
],
|
|
||||||
'int|float with stringed-float value' => [
|
|
||||||
'Union type' => new UnionType(NativeIntegerType::get(), NativeFloatType::get()),
|
|
||||||
'Source' => '1337.42',
|
|
||||||
'Expected type' => NativeFloatType::class,
|
|
||||||
],
|
|
||||||
'int|float|string|bool with string value' => [
|
|
||||||
'Union type' => $scalarUnion,
|
|
||||||
'Source' => 'foo',
|
|
||||||
'Expected type' => StringType::class,
|
|
||||||
],
|
|
||||||
'int|float|string|bool with boolean value' => [
|
|
||||||
'Union type' => $scalarUnion,
|
|
||||||
'Source' => true,
|
|
||||||
'Expected type' => NativeBooleanType::class,
|
|
||||||
],
|
|
||||||
'int|object with object value' => [
|
|
||||||
'Union type' => new UnionType(NativeIntegerType::get(), UndefinedObjectType::get()),
|
|
||||||
'Source' => new stdClass(),
|
|
||||||
'Expected type' => UndefinedObjectType::class,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_integer_type_is_narrowed_over_float_when_an_integer_value_is_given(): void
|
|
||||||
{
|
|
||||||
$unionType = new UnionType(NativeFloatType::get(), NativeIntegerType::get());
|
|
||||||
|
|
||||||
$type = $this->unionScalarNarrower->narrow($unionType, 42);
|
|
||||||
|
|
||||||
self::assertInstanceOf(IntegerType::class, $type);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user