fix: handle scalar value casting in union types only in flexible mode

This commit is contained in:
Romain Canon 2022-11-06 23:58:17 +01:00
parent 92a41a1564
commit 752ad9d12e
17 changed files with 151 additions and 387 deletions

View File

@ -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,

View File

@ -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;

View File

@ -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);
}
} }

View File

@ -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;

View File

@ -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);
} }

View File

@ -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}.',
], ],

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
} }

View File

@ -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;

View File

@ -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;
} }

View File

@ -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);
} }
} }

View File

@ -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);
}
}

View File

@ -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);
}
}