Skip to content

Commit

Permalink
fix: handle scalar value casting in union types only in flexible mode
Browse files Browse the repository at this point in the history
  • Loading branch information
romm committed Nov 7, 2022
1 parent 92a41a1 commit 752ad9d
Show file tree
Hide file tree
Showing 17 changed files with 151 additions and 387 deletions.
4 changes: 1 addition & 3 deletions src/Library/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@
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;
Expand Down Expand Up @@ -102,7 +100,7 @@ public function __construct(Settings $settings)
ScalarType::class => new ScalarNodeBuilder($settings->flexible),
]);

$builder = new UnionNodeBuilder($builder, new UnionNullNarrower(new UnionScalarNarrower()));
$builder = new UnionNodeBuilder($builder, $settings->flexible);

$builder = new ClassNodeBuilder(
$builder,
Expand Down
2 changes: 1 addition & 1 deletion src/Mapper/Tree/Builder/ObjectImplementations.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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;
Expand All @@ -14,7 +15,6 @@
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;
Expand Down
48 changes: 43 additions & 5 deletions src/Mapper/Tree/Builder/UnionNodeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@

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
Expand All @@ -29,8 +35,40 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Resolver\Exception;
namespace CuyZ\Valinor\Mapper\Tree\Exception;

use RuntimeException;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand All @@ -32,9 +35,15 @@ public function __construct(UnionType $unionType)
),
];

$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);
}
Expand Down
3 changes: 3 additions & 0 deletions src/Mapper/Tree/Message/DefaultMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}.',
],
Expand Down
44 changes: 0 additions & 44 deletions src/Type/Resolver/Exception/UnionTypeDoesNotAllowNull.php

This file was deleted.

15 changes: 0 additions & 15 deletions src/Type/Resolver/Union/UnionNarrower.php

This file was deleted.

55 changes: 0 additions & 55 deletions src/Type/Resolver/Union/UnionNullNarrower.php

This file was deleted.

43 changes: 0 additions & 43 deletions src/Type/Resolver/Union/UnionScalarNarrower.php

This file was deleted.

25 changes: 0 additions & 25 deletions tests/Fake/Type/Resolver/Union/FakeUnionNarrower.php

This file was deleted.

9 changes: 9 additions & 0 deletions tests/Integration/Mapping/Fixture/NativeUnionValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

// @PHP8.0 move inside \CuyZ\Valinor\Tests\Integration\Mapping\UnionValuesMappingTest
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants;
use DateTimeInterface;

class NativeUnionValues
{
Expand All @@ -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;

Expand All @@ -51,6 +56,8 @@ public function __construct(
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'
) {
Expand All @@ -62,6 +69,8 @@ public function __construct(
$this->nullableWithNull = $nullableWithNull;
$this->intOrLiteralTrue = $intOrLiteralTrue;
$this->intOrLiteralFalse = $intOrLiteralFalse;
$this->dateTimeOrNull = $dateTimeOrNull;
$this->nullOrDateTime = $nullOrDateTime;
$this->constantWithStringValue = $constantWithStringValue;
$this->constantWithIntegerValue = $constantWithIntegerValue;
}
Expand Down
Loading

0 comments on commit 752ad9d

Please sign in to comment.