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\TemplateParser;
|
||||
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\Types\ArrayType;
|
||||
use CuyZ\Valinor\Type\Types\IterableType;
|
||||
@ -102,7 +100,7 @@ final class Container
|
||||
ScalarType::class => new ScalarNodeBuilder($settings->flexible),
|
||||
]);
|
||||
|
||||
$builder = new UnionNodeBuilder($builder, new UnionNullNarrower(new UnionScalarNarrower()));
|
||||
$builder = new UnionNodeBuilder($builder, $settings->flexible);
|
||||
|
||||
$builder = new ClassNodeBuilder(
|
||||
$builder,
|
||||
|
@ -6,6 +6,7 @@ namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
||||
|
||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||
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\InvalidResolvedImplementationValue;
|
||||
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\Type\Parser\Exception\InvalidType;
|
||||
use CuyZ\Valinor\Type\Parser\TypeParser;
|
||||
use CuyZ\Valinor\Type\Resolver\Exception\CannotResolveObjectType;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\ClassStringType;
|
||||
use CuyZ\Valinor\Type\Types\ClassType;
|
||||
|
@ -4,21 +4,27 @@ declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Builder;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Tree\Exception\CannotResolveTypeFromUnion;
|
||||
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 function count;
|
||||
|
||||
/** @internal */
|
||||
final class UnionNodeBuilder implements NodeBuilder
|
||||
{
|
||||
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->unionNarrower = $unionNarrower;
|
||||
$this->flexible = $flexible;
|
||||
}
|
||||
|
||||
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
|
||||
@ -29,8 +35,40 @@ final class UnionNodeBuilder implements NodeBuilder
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
namespace CuyZ\Valinor\Type\Resolver\Exception;
|
||||
namespace CuyZ\Valinor\Mapper\Tree\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
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\HasParameters;
|
||||
@ -22,7 +22,10 @@ final class CannotResolveTypeFromUnion extends RuntimeException implements Error
|
||||
/** @var array<string, string> */
|
||||
private array $parameters;
|
||||
|
||||
public function __construct(UnionType $unionType)
|
||||
/**
|
||||
* @param mixed $source
|
||||
*/
|
||||
public function __construct($source, UnionType $unionType)
|
||||
{
|
||||
$this->parameters = [
|
||||
'allowed_types' => implode(
|
||||
@ -32,9 +35,15 @@ final class CannotResolveTypeFromUnion extends RuntimeException implements Error
|
||||
),
|
||||
];
|
||||
|
||||
$this->body = TypeHelper::containsObject($unionType)
|
||||
? 'Invalid value {source_value}.'
|
||||
: 'Value {source_value} does not match any of {allowed_types}.';
|
||||
if ($source === null) {
|
||||
$this->body = TypeHelper::containsObject($unionType)
|
||||
? '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);
|
||||
}
|
@ -14,6 +14,9 @@ interface DefaultMessage
|
||||
'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}.' => [
|
||||
'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
|
||||
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants;
|
||||
use DateTimeInterface;
|
||||
|
||||
class NativeUnionValues
|
||||
{
|
||||
@ -27,6 +28,10 @@ class NativeUnionValues
|
||||
/** @var int|false */
|
||||
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 */
|
||||
public string|int $constantWithStringValue = 1653398288;
|
||||
|
||||
@ -51,6 +56,8 @@ class NativeUnionValuesWithConstructor extends NativeUnionValues
|
||||
string|null $nullableWithNull = 'Schwifty!',
|
||||
int|bool $intOrLiteralTrue = 42,
|
||||
int|bool $intOrLiteralFalse = 42,
|
||||
DateTimeInterface|null $dateTimeOrNull = null,
|
||||
null|DateTimeInterface $nullOrDateTime = null,
|
||||
string|int $constantWithStringValue = 1653398288,
|
||||
string|int $constantWithIntegerValue = 'some string value'
|
||||
) {
|
||||
@ -62,6 +69,8 @@ class NativeUnionValuesWithConstructor extends NativeUnionValues
|
||||
$this->nullableWithNull = $nullableWithNull;
|
||||
$this->intOrLiteralTrue = $intOrLiteralTrue;
|
||||
$this->intOrLiteralFalse = $intOrLiteralFalse;
|
||||
$this->dateTimeOrNull = $dateTimeOrNull;
|
||||
$this->nullOrDateTime = $nullOrDateTime;
|
||||
$this->constantWithStringValue = $constantWithStringValue;
|
||||
$this->constantWithIntegerValue = $constantWithIntegerValue;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace CuyZ\Valinor\Tests\Integration\Mapping;
|
||||
|
||||
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\InvalidResolvedImplementationValue;
|
||||
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\InterfaceBInferer;
|
||||
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
|
||||
use CuyZ\Valinor\Type\Resolver\Exception\CannotResolveObjectType;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use DomainException;
|
||||
|
@ -6,10 +6,14 @@ namespace CuyZ\Valinor\Tests\Integration\Mapping\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\MappingError;
|
||||
use CuyZ\Valinor\MapperBuilder;
|
||||
use CuyZ\Valinor\Tests\Fixture\Enum\PureEnum;
|
||||
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\Mapping\Fixture\NativeUnionValues;
|
||||
use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\NativeUnionValuesWithConstructor;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
|
||||
final class UnionValuesMappingTest extends IntegrationTest
|
||||
{
|
||||
@ -24,6 +28,8 @@ final class UnionValuesMappingTest extends IntegrationTest
|
||||
'nullableWithNull' => null,
|
||||
'intOrLiteralTrue' => true,
|
||||
'intOrLiteralFalse' => false,
|
||||
'dateTimeOrNull' => 1667754013,
|
||||
'nullOrDateTime' => 1667754014,
|
||||
'constantWithStringValue' => 'some string value',
|
||||
'constantWithIntegerValue' => 1653398288,
|
||||
];
|
||||
@ -50,6 +56,10 @@ final class UnionValuesMappingTest extends IntegrationTest
|
||||
self::assertSame(null, $result->nullableWithNull);
|
||||
self::assertSame(true, $result->intOrLiteralTrue);
|
||||
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(1653398288, $result->constantWithIntegerValue);
|
||||
}
|
||||
@ -78,6 +88,60 @@ final class UnionValuesMappingTest extends IntegrationTest
|
||||
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
|
||||
@ -106,6 +170,12 @@ class UnionValues
|
||||
/** @var int|false */
|
||||
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 */
|
||||
public $constantWithStringValue = 1653398288;
|
||||
|
||||
@ -145,6 +215,8 @@ class UnionValuesWithConstructor extends UnionValues
|
||||
* @param string|null|float $nullableWithNull
|
||||
* @param int|true $intOrLiteralTrue
|
||||
* @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 $constantWithIntegerValue
|
||||
*/
|
||||
@ -157,6 +229,8 @@ class UnionValuesWithConstructor extends UnionValues
|
||||
$nullableWithNull = 'Schwifty!',
|
||||
$intOrLiteralTrue = 42,
|
||||
$intOrLiteralFalse = 42,
|
||||
$dateTimeOrNull = null,
|
||||
$nullOrDateTime = null,
|
||||
$constantWithStringValue = 1653398288,
|
||||
$constantWithIntegerValue = 'some string value'
|
||||
) {
|
||||
@ -168,6 +242,8 @@ class UnionValuesWithConstructor extends UnionValues
|
||||
$this->nullableWithNull = $nullableWithNull;
|
||||
$this->intOrLiteralTrue = $intOrLiteralTrue;
|
||||
$this->intOrLiteralFalse = $intOrLiteralFalse;
|
||||
$this->dateTimeOrNull = $dateTimeOrNull;
|
||||
$this->nullOrDateTime = $nullOrDateTime;
|
||||
$this->constantWithStringValue = $constantWithStringValue;
|
||||
$this->constantWithIntegerValue = $constantWithIntegerValue;
|
||||
}
|
||||
|
@ -368,8 +368,8 @@ final class FlexibleMappingTest extends IntegrationTest
|
||||
} catch (MappingError $exception) {
|
||||
$error = $exception->node()->messages()[0];
|
||||
|
||||
self::assertSame('1618742357', $error->code());
|
||||
self::assertSame("Cannot be empty and must be filled with a value matching type `bool|int|float`.", (string)$error);
|
||||
self::assertSame('1607027306', $error->code());
|
||||
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