From b08b89030ce1101145ecc6a208eb23dd79f3fa7c Mon Sep 17 00:00:00 2001 From: Romain Canon Date: Mon, 2 Sep 2024 14:23:26 +0200 Subject: [PATCH] feat: improve object constructors parameters types inferring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collision system that checks object constructors parameters types is now way more clever, as it no longer checks for parameters' names only. Types are now also checked, and only true collision will be detected, for instance when two constructors share a parameter with the same name and type. Note that when two parameters share the same name, the following type priority operates: 1. Non-scalar type 2. Integer type 3. Float type 4. String type 5. Boolean type With this change, the code below is now valid: ```php final readonly class Money { private function __construct( public int $value, ) {} #[\CuyZ\Valinor\Mapper\Object\Constructor] public static function fromInt(int $value): self { return new self($value); } #[\CuyZ\Valinor\Mapper\Object\Constructor] public static function fromString(string $value): self { if (! preg_match('/^\d+€$/', $value)) { throw new \InvalidArgumentException('Invalid money format'); } return new self((int)rtrim($value, '€')); } } $mapper = (new \CuyZ\Valinor\MapperBuilder())->mapper(); $mapper->map(Money::class, 42); // ✅ $mapper->map(Money::class, '42€'); // ✅ ``` --- src/Library/Container.php | 10 +- src/Mapper/Object/Arguments.php | 35 +- src/Mapper/Object/ArgumentsValues.php | 26 +- .../Exception/ObjectBuildersCollision.php | 11 +- .../Exception/SeveralObjectBuildersFound.php | 24 -- .../Factory/CollisionObjectBuilderFactory.php | 69 ---- .../Factory/DateTimeObjectBuilderFactory.php | 1 + .../DateTimeZoneObjectBuilderFactory.php | 1 + .../Object/Factory/ObjectBuilderFactory.php | 2 +- .../ReflectionObjectBuilderFactory.php | 5 - .../Factory/SortingObjectBuilderFactory.php | 109 ++++++ src/Mapper/Object/FilteredObjectBuilder.php | 106 ------ .../Tree/Builder/CasterProxyNodeBuilder.php | 26 +- .../Builder/FilteredObjectNodeBuilder.php | 68 ---- .../Tree/Builder/InterfaceNodeBuilder.php | 52 +-- src/Mapper/Tree/Builder/ObjectNodeBuilder.php | 81 +++- .../Tree/Builder/ShapedArrayNodeBuilder.php | 5 +- src/Mapper/Tree/Builder/TreeNode.php | 29 +- src/Mapper/Tree/Builder/UnionNodeBuilder.php | 30 +- src/Mapper/Tree/Shell.php | 22 ++ src/Utility/TypeHelper.php | 20 +- .../ConstructorRegistrationMappingTest.php | 351 ++++++++++++------ tests/Unit/Mapper/Tree/ShellTest.php | 9 + tests/Unit/Utility/TypeHelperTest.php | 14 + 24 files changed, 582 insertions(+), 524 deletions(-) delete mode 100644 src/Mapper/Object/Exception/SeveralObjectBuildersFound.php delete mode 100644 src/Mapper/Object/Factory/CollisionObjectBuilderFactory.php create mode 100644 src/Mapper/Object/Factory/SortingObjectBuilderFactory.php delete mode 100644 src/Mapper/Object/FilteredObjectBuilder.php delete mode 100644 src/Mapper/Tree/Builder/FilteredObjectNodeBuilder.php diff --git a/src/Library/Container.php b/src/Library/Container.php index f453fc43..488f9d0d 100644 --- a/src/Library/Container.php +++ b/src/Library/Container.php @@ -18,7 +18,7 @@ use CuyZ\Valinor\Definition\Repository\Reflection\ReflectionFunctionDefinitionRepository; use CuyZ\Valinor\Mapper\ArgumentsMapper; use CuyZ\Valinor\Mapper\Object\Factory\CacheObjectBuilderFactory; -use CuyZ\Valinor\Mapper\Object\Factory\CollisionObjectBuilderFactory; +use CuyZ\Valinor\Mapper\Object\Factory\SortingObjectBuilderFactory; use CuyZ\Valinor\Mapper\Object\Factory\ConstructorObjectBuilderFactory; use CuyZ\Valinor\Mapper\Object\Factory\DateTimeObjectBuilderFactory; use CuyZ\Valinor\Mapper\Object\Factory\DateTimeZoneObjectBuilderFactory; @@ -37,7 +37,6 @@ use CuyZ\Valinor\Mapper\Tree\Builder\NodeBuilder; use CuyZ\Valinor\Mapper\Tree\Builder\NullNodeBuilder; use CuyZ\Valinor\Mapper\Tree\Builder\ObjectImplementations; -use CuyZ\Valinor\Mapper\Tree\Builder\FilteredObjectNodeBuilder; use CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder; use CuyZ\Valinor\Mapper\Tree\Builder\ScalarNodeBuilder; use CuyZ\Valinor\Mapper\Tree\Builder\ShapedArrayNodeBuilder; @@ -116,7 +115,6 @@ public function __construct(Settings $settings) ObjectType::class => new ObjectNodeBuilder( $this->get(ClassDefinitionRepository::class), $this->get(ObjectBuilderFactory::class), - $this->get(FilteredObjectNodeBuilder::class), ), ]); @@ -126,8 +124,6 @@ public function __construct(Settings $settings) $builder, $this->get(ObjectImplementations::class), $this->get(ClassDefinitionRepository::class), - $this->get(ObjectBuilderFactory::class), - $this->get(FilteredObjectNodeBuilder::class), new FunctionsContainer( $this->get(FunctionDefinitionRepository::class), $settings->customConstructors @@ -152,8 +148,6 @@ public function __construct(Settings $settings) return new ErrorCatcherNodeBuilder($builder, $settings->exceptionFilter); }, - FilteredObjectNodeBuilder::class => fn () => new FilteredObjectNodeBuilder(), - ObjectImplementations::class => fn () => new ObjectImplementations( new FunctionsContainer( $this->get(FunctionDefinitionRepository::class), @@ -172,7 +166,7 @@ public function __construct(Settings $settings) $factory = new ConstructorObjectBuilderFactory($factory, $settings->nativeConstructors, $constructors); $factory = new DateTimeZoneObjectBuilderFactory($factory, $this->get(FunctionDefinitionRepository::class)); $factory = new DateTimeObjectBuilderFactory($factory, $settings->supportedDateFormats, $this->get(FunctionDefinitionRepository::class)); - $factory = new CollisionObjectBuilderFactory($factory); + $factory = new SortingObjectBuilderFactory($factory); if (! $settings->allowPermissiveTypes) { $factory = new StrictTypesObjectBuilderFactory($factory); diff --git a/src/Mapper/Object/Arguments.php b/src/Mapper/Object/Arguments.php index 023e390d..67d06f68 100644 --- a/src/Mapper/Object/Arguments.php +++ b/src/Mapper/Object/Arguments.php @@ -12,8 +12,10 @@ use IteratorAggregate; use Traversable; +use function array_keys; use function array_map; use function array_values; +use function count; /** * @internal @@ -22,19 +24,21 @@ */ final class Arguments implements IteratorAggregate, Countable { - /** @var Argument[] */ - private array $arguments; + /** @var array */ + private array $arguments = []; public function __construct(Argument ...$arguments) { - $this->arguments = $arguments; + foreach ($arguments as $argument) { + $this->arguments[$argument->name()] = $argument; + } } public static function fromParameters(Parameters $parameters): self { return new self(...array_map( fn (ParameterDefinition $parameter) => Argument::fromParameter($parameter), - array_values([...$parameters]) + [...$parameters], )); } @@ -42,24 +46,29 @@ public static function fromProperties(Properties $properties): self { return new self(...array_map( fn (PropertyDefinition $property) => Argument::fromProperty($property), - array_values([...$properties]) + [...$properties], )); } public function at(int $index): Argument { - return $this->arguments[$index]; + return array_values($this->arguments)[$index]; } - public function has(string $name): bool + /** + * @return list + */ + public function names(): array { - foreach ($this->arguments as $argument) { - if ($argument->name() === $name) { - return true; - } - } + return array_keys($this->arguments); + } - return false; + /** + * @return array + */ + public function toArray(): array + { + return $this->arguments; } public function count(): int diff --git a/src/Mapper/Object/ArgumentsValues.php b/src/Mapper/Object/ArgumentsValues.php index 407112d9..9b595de3 100644 --- a/src/Mapper/Object/ArgumentsValues.php +++ b/src/Mapper/Object/ArgumentsValues.php @@ -4,7 +4,6 @@ namespace CuyZ\Valinor\Mapper\Object; -use CuyZ\Valinor\Mapper\Object\Exception\InvalidSource; use CuyZ\Valinor\Mapper\Tree\Shell; use CuyZ\Valinor\Type\CompositeTraversableType; use CuyZ\Valinor\Type\Types\ArrayKeyType; @@ -27,6 +26,8 @@ final class ArgumentsValues implements IteratorAggregate private Arguments $arguments; + private bool $hasInvalidValue = false; + private bool $forInterface = false; private bool $hadSingleArgument = false; @@ -42,7 +43,7 @@ public static function forInterface(Arguments $arguments, Shell $shell): self $self->forInterface = true; if (count($arguments) > 0) { - $self = $self->transform($shell); + $self->transform($shell); } return $self; @@ -51,11 +52,16 @@ public static function forInterface(Arguments $arguments, Shell $shell): self public static function forClass(Arguments $arguments, Shell $shell): self { $self = new self($arguments); - $self = $self->transform($shell); + $self->transform($shell); return $self; } + public function hasInvalidValue(): bool + { + return $this->hasInvalidValue; + } + public function hasValue(string $name): bool { return array_key_exists($name, $this->value); @@ -71,18 +77,18 @@ public function hadSingleArgument(): bool return $this->hadSingleArgument; } - private function transform(Shell $shell): self + private function transform(Shell $shell): void { - $clone = clone $this; - $transformedValue = $this->transformValueForSingleArgument($shell); if (! is_array($transformedValue)) { - throw new InvalidSource($transformedValue, $this->arguments); + $this->hasInvalidValue = true; + + return; } if ($transformedValue !== $shell->value()) { - $clone->hadSingleArgument = true; + $this->hadSingleArgument = true; } foreach ($this->arguments as $argument) { @@ -93,9 +99,7 @@ private function transform(Shell $shell): self } } - $clone->value = $transformedValue; - - return $clone; + $this->value = $transformedValue; } private function transformValueForSingleArgument(Shell $shell): mixed diff --git a/src/Mapper/Object/Exception/ObjectBuildersCollision.php b/src/Mapper/Object/Exception/ObjectBuildersCollision.php index 697fed7c..02648a98 100644 --- a/src/Mapper/Object/Exception/ObjectBuildersCollision.php +++ b/src/Mapper/Object/Exception/ObjectBuildersCollision.php @@ -4,23 +4,16 @@ namespace CuyZ\Valinor\Mapper\Object\Exception; -use CuyZ\Valinor\Definition\ClassDefinition; use CuyZ\Valinor\Mapper\Object\ObjectBuilder; use RuntimeException; -use function array_map; -use function implode; - /** @internal */ final class ObjectBuildersCollision extends RuntimeException { - public function __construct(ClassDefinition $class, ObjectBuilder ...$builders) + public function __construct(ObjectBuilder $builderA, ObjectBuilder $builderB) { - $constructors = array_map(fn (ObjectBuilder $builder) => $builder->signature(), $builders); - $constructors = implode('`, `', $constructors); - parent::__construct( - "A collision was detected between the following constructors of the class `{$class->type->toString()}`: `$constructors`.", + "A type collision was detected between the constructors `{$builderA->signature()}` and `{$builderB->signature()}`.", 1654955787 ); } diff --git a/src/Mapper/Object/Exception/SeveralObjectBuildersFound.php b/src/Mapper/Object/Exception/SeveralObjectBuildersFound.php deleted file mode 100644 index 732bb66e..00000000 --- a/src/Mapper/Object/Exception/SeveralObjectBuildersFound.php +++ /dev/null @@ -1,24 +0,0 @@ -body, 1642787246); - } - - public function body(): string - { - return $this->body; - } -} diff --git a/src/Mapper/Object/Factory/CollisionObjectBuilderFactory.php b/src/Mapper/Object/Factory/CollisionObjectBuilderFactory.php deleted file mode 100644 index 702a278b..00000000 --- a/src/Mapper/Object/Factory/CollisionObjectBuilderFactory.php +++ /dev/null @@ -1,69 +0,0 @@ -delegate->for($class); - - $sortedBuilders = []; - - foreach ($builders as $builder) { - $sortedBuilders[count($builder->describeArguments())][] = $builder; - } - - foreach ($sortedBuilders as $argumentsCount => $buildersList) { - if (count($buildersList) <= 1) { - continue; - } - - if ($argumentsCount <= 1) { - throw new ObjectBuildersCollision($class, ...$buildersList); - } - - // @phpstan-ignore-next-line // false positive - while (($current = array_shift($buildersList)) && count($buildersList) > 0) { - $arguments = $current->describeArguments(); - - do { - $other = current($buildersList); - - $collisions = 0; - - foreach ($arguments as $argumentA) { - $name = $argumentA->name(); - - foreach ($other->describeArguments() as $argumentB) { - if ($argumentB->name() === $name) { - $collisions++; - // @infection-ignore-all - break; - } - } - } - - if ($collisions >= count($arguments)) { - throw new ObjectBuildersCollision($class, $current, $other); - } - } while (next($buildersList)); - } - } - - return $builders; - } -} diff --git a/src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php b/src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php index fa5613ac..86269286 100644 --- a/src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php +++ b/src/Mapper/Object/Factory/DateTimeObjectBuilderFactory.php @@ -48,6 +48,7 @@ public function for(ClassDefinition $class): array $builders[] = $this->internalDateTimeBuilder($class->type); } + /** @var non-empty-list */ return $builders; } diff --git a/src/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php b/src/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php index cc17713f..0e929d03 100644 --- a/src/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php +++ b/src/Mapper/Object/Factory/DateTimeZoneObjectBuilderFactory.php @@ -57,6 +57,7 @@ public function for(ClassDefinition $class): array $builders[] = $this->defaultBuilder($class->type); } + /** @var non-empty-list */ return $builders; } diff --git a/src/Mapper/Object/Factory/ObjectBuilderFactory.php b/src/Mapper/Object/Factory/ObjectBuilderFactory.php index 760f3d42..a641b363 100644 --- a/src/Mapper/Object/Factory/ObjectBuilderFactory.php +++ b/src/Mapper/Object/Factory/ObjectBuilderFactory.php @@ -11,7 +11,7 @@ interface ObjectBuilderFactory { /** - * @return list + * @return non-empty-list */ public function for(ClassDefinition $class): array; } diff --git a/src/Mapper/Object/Factory/ReflectionObjectBuilderFactory.php b/src/Mapper/Object/Factory/ReflectionObjectBuilderFactory.php index 187f11bc..0242b452 100644 --- a/src/Mapper/Object/Factory/ReflectionObjectBuilderFactory.php +++ b/src/Mapper/Object/Factory/ReflectionObjectBuilderFactory.php @@ -6,17 +6,12 @@ use CuyZ\Valinor\Definition\ClassDefinition; use CuyZ\Valinor\Mapper\Object\ReflectionObjectBuilder; -use CuyZ\Valinor\Utility\Reflection\Reflection; /** @internal */ final class ReflectionObjectBuilderFactory implements ObjectBuilderFactory { public function for(ClassDefinition $class): array { - if (Reflection::enumExists($class->name)) { - return []; - } - return [new ReflectionObjectBuilder($class)]; } } diff --git a/src/Mapper/Object/Factory/SortingObjectBuilderFactory.php b/src/Mapper/Object/Factory/SortingObjectBuilderFactory.php new file mode 100644 index 00000000..e7dea8c8 --- /dev/null +++ b/src/Mapper/Object/Factory/SortingObjectBuilderFactory.php @@ -0,0 +1,109 @@ +delegate->for($class); + + $sortedByArgumentsNumber = []; + $sortedByPriority = []; + + foreach ($builders as $builder) { + $sortedByArgumentsNumber[$builder->describeArguments()->count()][] = $builder; + } + + krsort($sortedByArgumentsNumber); + + foreach ($sortedByArgumentsNumber as $sortedBuilders) { + usort($sortedBuilders, $this->sortObjectBuilders(...)); + + $sortedByPriority = array_merge($sortedByPriority, $sortedBuilders); + } + + return $sortedByPriority; + } + + private function sortObjectBuilders(ObjectBuilder $builderA, ObjectBuilder $builderB): int + { + $argumentsA = $builderA->describeArguments()->toArray(); + $argumentsB = $builderB->describeArguments()->toArray(); + + $sharedArguments = array_keys(array_intersect_key($argumentsA, $argumentsB)); + + $winner = null; + + foreach ($sharedArguments as $name) { + $typeA = $argumentsA[$name]->type(); + $typeB = $argumentsB[$name]->type(); + + $score = $this->sortTypes($typeA, $typeB); + + if ($score === 0) { + continue; + } + + $newWinner = $score === 1 ? $builderB : $builderA; + + if ($winner && $winner !== $newWinner) { + throw new ObjectBuildersCollision($builderA, $builderB); + } + + $winner = $newWinner; + } + + if ($winner === null && count($sharedArguments) === count($argumentsA)) { + throw new ObjectBuildersCollision($builderA, $builderB); + } + + // @infection-ignore-all / Incrementing or decrementing sorting value makes no sense, so we ignore it. + return $winner === $builderA ? -1 : 1; + } + + private function sortTypes(Type $typeA, Type $typeB): int + { + if ($typeA instanceof ScalarType && $typeB instanceof ScalarType) { + return TypeHelper::typePriority($typeB) <=> TypeHelper::typePriority($typeA); + } + + if (! $typeA instanceof ScalarType) { + // @infection-ignore-all / Decrementing sorting value makes no sense, so we ignore it. + return -1; + } + + return 1; + } +} diff --git a/src/Mapper/Object/FilteredObjectBuilder.php b/src/Mapper/Object/FilteredObjectBuilder.php deleted file mode 100644 index acabadfe..00000000 --- a/src/Mapper/Object/FilteredObjectBuilder.php +++ /dev/null @@ -1,106 +0,0 @@ -delegate = $this->filterBuilder($source, ...$builders); - } - - public static function from(mixed $source, ObjectBuilder ...$builders): ObjectBuilder - { - if (count($builders) === 1) { - return $builders[0]; - } - - return new self($source, ...$builders); - } - - public function describeArguments(): Arguments - { - return $this->delegate->describeArguments(); - } - - public function build(array $arguments): object - { - return $this->delegate->build($arguments); - } - - public function signature(): string - { - return $this->delegate->signature(); - } - - private function filterBuilder(mixed $source, ObjectBuilder ...$builders): ObjectBuilder - { - if (count($builders) === 1) { - return reset($builders); - } - - /** @var non-empty-list $builders */ - $constructors = []; - - foreach ($builders as $builder) { - $filledNumber = $this->filledArguments($builder, $source); - - if ($filledNumber === false) { - continue; - } - - $constructors[$filledNumber][] = $builder; - } - - ksort($constructors); - - $constructorsWithMostArguments = array_pop($constructors) ?: []; - - if (count($constructorsWithMostArguments) === 0) { - throw new CannotFindObjectBuilder($builders); - } - - if (count($constructorsWithMostArguments) > 1) { - throw new SeveralObjectBuildersFound(); - } - - return $constructorsWithMostArguments[0]; - } - - /** - * @return false|int<0, max> - */ - private function filledArguments(ObjectBuilder $builder, mixed $source): false|int - { - $arguments = $builder->describeArguments(); - - if (! is_array($source)) { - return count($arguments) === 1 ? 1 : false; - } - - /** @infection-ignore-all */ - $filled = 0; - - foreach ($arguments as $argument) { - if (isset($source[$argument->name()])) { - $filled++; - } elseif ($argument->isRequired()) { - return false; - } - } - - return $filled; - } -} diff --git a/src/Mapper/Tree/Builder/CasterProxyNodeBuilder.php b/src/Mapper/Tree/Builder/CasterProxyNodeBuilder.php index 9a50c340..71f9d086 100644 --- a/src/Mapper/Tree/Builder/CasterProxyNodeBuilder.php +++ b/src/Mapper/Tree/Builder/CasterProxyNodeBuilder.php @@ -20,28 +20,36 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode if ($shell->hasValue()) { $value = $shell->value(); - if ($this->typeAcceptsValue($shell->type(), $value)) { - return TreeNode::leaf($shell, $value); + $typeAcceptingValue = $this->typeAcceptingValue($shell->type(), $value); + + if ($typeAcceptingValue) { + return TreeNode::leaf($shell->withType($typeAcceptingValue), $value); } } return $this->delegate->build($shell, $rootBuilder); } - private function typeAcceptsValue(Type $type, mixed $value): bool + private function typeAcceptingValue(Type $type, mixed $value): ?Type { if ($type instanceof UnionType) { foreach ($type->types() as $subType) { - if ($this->typeAcceptsValue($subType, $value)) { - return true; + if ($this->typeAcceptingValue($subType, $value)) { + return $subType; } } - return false; + return null; + } + + if ($type instanceof CompositeTraversableType || $type instanceof ShapedArrayType) { + return null; + } + + if ($type->accepts($value)) { + return $type; } - return ! $type instanceof CompositeTraversableType - && ! $type instanceof ShapedArrayType - && $type->accepts($value); + return null; } } diff --git a/src/Mapper/Tree/Builder/FilteredObjectNodeBuilder.php b/src/Mapper/Tree/Builder/FilteredObjectNodeBuilder.php deleted file mode 100644 index 5eb70e07..00000000 --- a/src/Mapper/Tree/Builder/FilteredObjectNodeBuilder.php +++ /dev/null @@ -1,68 +0,0 @@ -describeArguments(), $shell); - - $children = $this->children($shell, $arguments, $rootBuilder); - - $object = $this->buildObject($builder, $children); - - return $arguments->hadSingleArgument() - ? TreeNode::flattenedBranch($shell, $object, $children[0]) - : TreeNode::branch($shell, $object, $children); - } - - /** - * @return array - */ - private function children(Shell $shell, ArgumentsValues $arguments, RootNodeBuilder $rootBuilder): array - { - $children = []; - - foreach ($arguments as $argument) { - $name = $argument->name(); - $type = $argument->type(); - $attributes = $argument->attributes(); - - $child = $shell->child($name, $type, $attributes); - - if ($arguments->hasValue($name)) { - $child = $child->withValue($arguments->getValue($name)); - } - - $children[] = $rootBuilder->build($child); - } - - return $children; - } - - /** - * @param TreeNode[] $children - */ - private function buildObject(ObjectBuilder $builder, array $children): ?object - { - $arguments = []; - - foreach ($children as $child) { - if (! $child->isValid()) { - return null; - } - - $arguments[$child->name()] = $child->value(); - } - - return $builder->build($arguments); - } -} diff --git a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php index 73c9b7e2..17b5a449 100644 --- a/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php +++ b/src/Mapper/Tree/Builder/InterfaceNodeBuilder.php @@ -8,8 +8,7 @@ use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository; use CuyZ\Valinor\Mapper\Object\Arguments; use CuyZ\Valinor\Mapper\Object\ArgumentsValues; -use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory; -use CuyZ\Valinor\Mapper\Object\FilteredObjectBuilder; +use CuyZ\Valinor\Mapper\Object\Exception\InvalidSource; use CuyZ\Valinor\Mapper\Tree\Exception\CannotInferFinalClass; use CuyZ\Valinor\Mapper\Tree\Exception\CannotResolveObjectType; use CuyZ\Valinor\Mapper\Tree\Exception\InterfaceHasBothConstructorAndInfer; @@ -27,8 +26,6 @@ public function __construct( private NodeBuilder $delegate, private ObjectImplementations $implementations, private ClassDefinitionRepository $classDefinitionRepository, - private ObjectBuilderFactory $objectBuilderFactory, - private FilteredObjectNodeBuilder $filteredObjectNodeBuilder, private FunctionsContainer $constructors, ) {} @@ -69,7 +66,13 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode throw new CannotInferFinalClass($type, $function); } - $children = $this->children($shell, $arguments, $rootBuilder); + $argumentsValues = ArgumentsValues::forInterface($arguments, $shell); + + if ($argumentsValues->hasInvalidValue()) { + throw new InvalidSource($shell->value(), $arguments); + } + + $children = $this->children($shell, $argumentsValues, $rootBuilder); $values = []; @@ -87,12 +90,10 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode throw UserlandError::from($exception); } - $class = $this->classDefinitionRepository->for($classType); - $objectBuilder = FilteredObjectBuilder::from($shell->value(), ...$this->objectBuilderFactory->for($class)); + $shell = $shell->withType($classType); + $shell = $shell->withAllowedSuperfluousKeys($arguments->names()); - $shell = $this->transformSourceForClass($shell, $arguments, $objectBuilder->describeArguments()); - - return $this->filteredObjectNodeBuilder->build($objectBuilder, $shell, $rootBuilder); + return $this->delegate->build($shell, $rootBuilder); } private function constructorRegisteredFor(Type $type): bool @@ -106,40 +107,11 @@ private function constructorRegisteredFor(Type $type): bool return false; } - private function transformSourceForClass(Shell $shell, Arguments $interfaceArguments, Arguments $classArguments): Shell - { - $value = $shell->value(); - - if (! is_array($value)) { - return $shell; - } - - foreach ($interfaceArguments as $argument) { - $name = $argument->name(); - - if (array_key_exists($name, $value) && ! $classArguments->has($name)) { - unset($value[$name]); - } - } - - if (count($classArguments) === 1 && count($value) === 1) { - $name = $classArguments->at(0)->name(); - - if (array_key_exists($name, $value)) { - $value = $value[$name]; - } - } - - return $shell->withValue($value); - } - /** * @return array */ - private function children(Shell $shell, Arguments $arguments, RootNodeBuilder $rootBuilder): array + private function children(Shell $shell, ArgumentsValues $arguments, RootNodeBuilder $rootBuilder): array { - $arguments = ArgumentsValues::forInterface($arguments, $shell); - $children = []; foreach ($arguments as $argument) { diff --git a/src/Mapper/Tree/Builder/ObjectNodeBuilder.php b/src/Mapper/Tree/Builder/ObjectNodeBuilder.php index 007de11a..88455bb0 100644 --- a/src/Mapper/Tree/Builder/ObjectNodeBuilder.php +++ b/src/Mapper/Tree/Builder/ObjectNodeBuilder.php @@ -5,13 +5,16 @@ namespace CuyZ\Valinor\Mapper\Tree\Builder; use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository; +use CuyZ\Valinor\Mapper\Object\ArgumentsValues; +use CuyZ\Valinor\Mapper\Object\Exception\CannotFindObjectBuilder; +use CuyZ\Valinor\Mapper\Object\Exception\InvalidSource; use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory; -use CuyZ\Valinor\Mapper\Object\FilteredObjectBuilder; +use CuyZ\Valinor\Mapper\Object\ObjectBuilder; use CuyZ\Valinor\Mapper\Tree\Shell; - use CuyZ\Valinor\Type\ObjectType; use function assert; +use function count; /** @internal */ final class ObjectNodeBuilder implements NodeBuilder @@ -19,7 +22,6 @@ final class ObjectNodeBuilder implements NodeBuilder public function __construct( private ClassDefinitionRepository $classDefinitionRepository, private ObjectBuilderFactory $objectBuilderFactory, - private FilteredObjectNodeBuilder $filteredObjectNodeBuilder, ) {} public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode @@ -34,8 +36,77 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode } $class = $this->classDefinitionRepository->for($type); - $objectBuilder = FilteredObjectBuilder::from($shell->value(), ...$this->objectBuilderFactory->for($class)); + $builders = $this->objectBuilderFactory->for($class); + + foreach ($builders as $builder) { + $argumentsValues = ArgumentsValues::forClass($builder->describeArguments(), $shell); + + if ($argumentsValues->hasInvalidValue()) { + if (count($builders) === 1) { + return TreeNode::error($shell, new InvalidSource($shell->value(), $builder->describeArguments())); + } + + continue; + } + + $children = $this->children($shell, $argumentsValues, $rootBuilder); + + $object = $this->buildObject($builder, $children); + + if ($argumentsValues->hadSingleArgument()) { + $node = TreeNode::flattenedBranch($shell, $object, $children[0]); + } else { + $node = TreeNode::branch($shell, $object, $children); + $node = $node->checkUnexpectedKeys(); + } + + if ($node->isValid() || count($builders) === 1) { + return $node; + } + } + + throw new CannotFindObjectBuilder($builders); + } + + /** + * @return list + */ + private function children(Shell $shell, ArgumentsValues $arguments, RootNodeBuilder $rootBuilder): array + { + $children = []; + + foreach ($arguments as $argument) { + $name = $argument->name(); + $type = $argument->type(); + $attributes = $argument->attributes(); + + $child = $shell->child($name, $type, $attributes); + + if ($arguments->hasValue($name)) { + $child = $child->withValue($arguments->getValue($name)); + } + + $children[] = $rootBuilder->build($child); + } + + return $children; + } + + /** + * @param list $children + */ + private function buildObject(ObjectBuilder $builder, array $children): ?object + { + $arguments = []; + + foreach ($children as $child) { + if (! $child->isValid()) { + return null; + } + + $arguments[$child->name()] = $child->value(); + } - return $this->filteredObjectNodeBuilder->build($objectBuilder, $shell, $rootBuilder); + return $builder->build($arguments); } } diff --git a/src/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php b/src/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php index e270f36a..fb0448ff 100644 --- a/src/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php +++ b/src/Mapper/Tree/Builder/ShapedArrayNodeBuilder.php @@ -30,7 +30,10 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode $array = $this->buildArray($children); - return TreeNode::branch($shell, $array, $children); + $node = TreeNode::branch($shell, $array, $children); + $node = $node->checkUnexpectedKeys(); + + return $node; } /** diff --git a/src/Mapper/Tree/Builder/TreeNode.php b/src/Mapper/Tree/Builder/TreeNode.php index 3b37d5ee..afc72587 100644 --- a/src/Mapper/Tree/Builder/TreeNode.php +++ b/src/Mapper/Tree/Builder/TreeNode.php @@ -12,9 +12,10 @@ use CuyZ\Valinor\Type\Type; use Throwable; +use function array_diff; +use function array_keys; use function array_map; use function assert; -use function count; use function is_array; /** @internal */ @@ -130,6 +131,23 @@ public function node(): Node return $this->buildNode($this); } + public function checkUnexpectedKeys(): self + { + $value = $this->shell->value(); + + if ($this->shell->allowSuperfluousKeys() || ! is_array($value)) { + return $this; + } + + $diff = array_diff(array_keys($value), array_keys($this->children), $this->shell->allowedSuperfluousKeys()); + + if ($diff !== []) { + return $this->withMessage(new UnexpectedKeysInSource($value, $this->children)); + } + + return $this; + } + private function check(): void { foreach ($this->children as $child) { @@ -138,17 +156,8 @@ private function check(): void } } - $value = $this->shell->value(); $type = $this->shell->type(); - if (! $this->shell->allowSuperfluousKeys() - && is_array($value) - && count($value) > count($this->children) - ) { - $this->valid = false; - $this->messages[] = new UnexpectedKeysInSource($value, $this->children); - } - if ($this->valid && ! $type->accepts($this->value)) { $this->valid = false; $this->messages[] = new InvalidNodeValue($type); diff --git a/src/Mapper/Tree/Builder/UnionNodeBuilder.php b/src/Mapper/Tree/Builder/UnionNodeBuilder.php index 4126135b..845d5df9 100644 --- a/src/Mapper/Tree/Builder/UnionNodeBuilder.php +++ b/src/Mapper/Tree/Builder/UnionNodeBuilder.php @@ -8,18 +8,17 @@ use CuyZ\Valinor\Mapper\Tree\Exception\TooManyResolvedTypesFromUnion; use CuyZ\Valinor\Mapper\Tree\Shell; use CuyZ\Valinor\Type\ClassType; -use CuyZ\Valinor\Type\FloatType; -use CuyZ\Valinor\Type\IntegerType; use CuyZ\Valinor\Type\ScalarType; -use CuyZ\Valinor\Type\StringType; use CuyZ\Valinor\Type\Types\InterfaceType; use CuyZ\Valinor\Type\Types\NullType; use CuyZ\Valinor\Type\Types\ShapedArrayType; use CuyZ\Valinor\Type\Types\UnionType; +use CuyZ\Valinor\Utility\TypeHelper; use function count; use function krsort; use function reset; +use function usort; /** @internal */ final class UnionNodeBuilder implements NodeBuilder @@ -91,28 +90,11 @@ public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode return $first[0]; } } elseif ($scalars !== []) { - // Sorting the scalar types by priority: int, float, string, bool. - $sorted = []; - - foreach ($scalars as $node) { - if ($node->type() instanceof IntegerType) { - $sorted[IntegerType::class] = $node; - } elseif ($node->type() instanceof FloatType) { - $sorted[FloatType::class] = $node; - } elseif ($node->type() instanceof StringType) { - $sorted[StringType::class] = $node; - } - } - - if (isset($sorted[IntegerType::class])) { - return $sorted[IntegerType::class]; - } elseif (isset($sorted[FloatType::class])) { - return $sorted[FloatType::class]; - } elseif (isset($sorted[StringType::class])) { - return $sorted[StringType::class]; - } + usort( + $scalars, + fn (TreeNode $a, TreeNode $b): int => TypeHelper::typePriority($b->type()) <=> TypeHelper::typePriority($a->type()), + ); - // @infection-ignore-all / We know this is a boolean, so we don't need to mutate the index return $scalars[0]; } diff --git a/src/Mapper/Tree/Shell.php b/src/Mapper/Tree/Shell.php index 32c2d131..cec31b4d 100644 --- a/src/Mapper/Tree/Shell.php +++ b/src/Mapper/Tree/Shell.php @@ -32,6 +32,9 @@ final class Shell private self $parent; + /** @var list */ + private array $allowedSuperfluousKeys = []; + private function __construct(Settings $settings, Type $type) { if ($type instanceof UnresolvableType) { @@ -135,6 +138,25 @@ public function attributes(): Attributes return $this->attributes ?? Attributes::empty(); } + /** + * @param list $allowedSuperfluousKeys + */ + public function withAllowedSuperfluousKeys(array $allowedSuperfluousKeys): self + { + $clone = clone $this; + $clone->allowedSuperfluousKeys = $allowedSuperfluousKeys; + + return $clone; + } + + /** + * @return list + */ + public function allowedSuperfluousKeys(): array + { + return $this->allowedSuperfluousKeys; + } + public function path(): string { if (! isset($this->parent)) { diff --git a/src/Utility/TypeHelper.php b/src/Utility/TypeHelper.php index 262ca036..51024413 100644 --- a/src/Utility/TypeHelper.php +++ b/src/Utility/TypeHelper.php @@ -6,9 +6,13 @@ use CuyZ\Valinor\Mapper\Object\Argument; use CuyZ\Valinor\Mapper\Object\Arguments; +use CuyZ\Valinor\Type\BooleanType; use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\FixedType; +use CuyZ\Valinor\Type\FloatType; +use CuyZ\Valinor\Type\IntegerType; use CuyZ\Valinor\Type\ObjectType; +use CuyZ\Valinor\Type\StringType; use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\EnumType; use CuyZ\Valinor\Type\Types\MixedType; @@ -17,6 +21,20 @@ /** @internal */ final class TypeHelper { + /** + * Sorting the scalar types by priority: int, float, string, bool. + */ + public static function typePriority(Type $type): int + { + return match (true) { + $type instanceof IntegerType => 4, + $type instanceof FloatType => 3, + $type instanceof StringType => 2, + $type instanceof BooleanType => 1, + default => 0, + }; + } + public static function dump(Type $type, bool $surround = true): string { if ($type instanceof EnumType) { @@ -51,7 +69,7 @@ function (Argument $argument) { return $argument->isRequired() ? "$name: $signature" : "$name?: $signature"; }, - [...$arguments] + [...$arguments], ); return '`array{' . implode(', ', $parameters) . '}`'; diff --git a/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php b/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php index 4e157da8..9282923f 100644 --- a/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php +++ b/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php @@ -20,6 +20,7 @@ use DateTime; use DateTimeImmutable; use DateTimeInterface; +use PHPUnit\Framework\Attributes\DataProvider; use stdClass; final class ConstructorRegistrationMappingTest extends IntegrationTestCase @@ -288,26 +289,6 @@ public function test_registered_native_constructor_is_called_if_registered_and_o self::assertSame(1337, $result->bar); } - public function test_registered_constructor_is_used_when_not_the_first_nor_last_one(): void - { - $object = new stdClass(); - - try { - $result = $this->mapperBuilder() - ->registerConstructor(fn (): DateTime => new DateTime()) - // This constructor is surrounded by other ones to ensure it is - // still used correctly. - ->registerConstructor(fn (): stdClass => $object) - ->registerConstructor(fn (): DateTimeImmutable => new DateTimeImmutable()) - ->mapper() - ->map(stdClass::class, []); - } catch (MappingError $error) { - $this->mappingFail($error); - } - - self::assertSame($object, $result); - } - public function test_registered_constructor_with_one_argument_is_used(): void { try { @@ -353,102 +334,6 @@ public function test_registered_constructor_with_several_arguments_is_used(): vo self::assertSame(1337.404, $result->float); } - public function test_registered_constructors_for_same_class_are_filtered_correctly(): void - { - $mapper = $this->mapperBuilder() - // Basic constructor - ->registerConstructor(function (string $foo): stdClass { - $class = new stdClass(); - $class->foo = $foo; - - return $class; - }) - // Constructor with two parameters - ->registerConstructor(function (string $foo, int $bar): stdClass { - $class = new stdClass(); - $class->foo = $foo; - $class->bar = $bar; - - return $class; - }) - // Constructor with optional parameter - ->registerConstructor(function (string $foo, int $bar, float $baz, string $fiz = 'fiz'): stdClass { - $class = new stdClass(); - $class->foo = $foo; - $class->bar = $bar; - $class->baz = $baz; - $class->fiz = $fiz; - - return $class; - }) - ->mapper(); - - try { - $resultA = $mapper->map(stdClass::class, 'foo'); - - $resultB = $mapper->map(stdClass::class, [ - 'foo' => 'foo', - 'bar' => 42, - ]); - - $resultC = $mapper->map(stdClass::class, [ - 'foo' => 'foo', - 'bar' => 42, - 'baz' => 1337.404, - ]); - } catch (MappingError $error) { - $this->mappingFail($error); - } - - self::assertSame('foo', $resultA->foo); - - self::assertSame('foo', $resultB->foo); - self::assertSame(42, $resultB->bar); - - self::assertSame('foo', $resultC->foo); - self::assertSame(42, $resultC->bar); - self::assertSame(1337.404, $resultC->baz); - self::assertSame('fiz', $resultC->fiz); - } - - public function test_several_constructors_with_same_arguments_number_are_filtered_correctly(): void - { - $mapper = $this->mapperBuilder() - ->registerConstructor(function (string $foo, string $bar): stdClass { - $class = new stdClass(); - $class->foo = $foo; - $class->bar = $bar; - - return $class; - }) - ->registerConstructor(function (string $foo, string $baz): stdClass { - $class = new stdClass(); - $class->foo = $foo; - $class->baz = $baz; - - return $class; - })->mapper(); - - try { - $resultA = $mapper->map(stdClass::class, [ - 'foo' => 'foo', - 'bar' => 'bar', - ]); - - $resultB = $mapper->map(stdClass::class, [ - 'foo' => 'foo', - 'baz' => 'baz', - ]); - } catch (MappingError $error) { - $this->mappingFail($error); - } - - self::assertSame('foo', $resultA->foo); - self::assertSame('bar', $resultA->bar); - self::assertSame('foo', $resultB->foo); - self::assertSame('baz', $resultB->baz); - } - public function test_inherited_static_constructor_is_used_to_map_child_class(): void { $class = (new class () { @@ -475,11 +360,222 @@ public function test_inherited_static_constructor_is_used_to_map_child_class(): self::assertSame(1337, $result->someOtherChild->bar); } + /** + * @param list $constructors + * @param array $data + */ + #[DataProvider('constructors_are_sorted_and_filtered_correctly_data_provider')] + public function test_constructors_are_sorted_and_filtered_correctly(array $constructors, array $data): void + { + $mapperBuilder = $this->mapperBuilder()->registerConstructor(...$constructors); + + try { + foreach ($data as $value) { + $result = $mapperBuilder->mapper()->map(stdClass::class, $value['value']); + + self::assertSame($value['expected'], $result); + + // Also testing with allowed superfluous keys to be sure that + // constructors with fewer arguments are taken into account but + //filtered correctly. + $result = $mapperBuilder->allowSuperfluousKeys()->mapper()->map(stdClass::class, $value['value']); + + self::assertSame($value['expected'], $result); + } + } catch (MappingError $error) { + $this->mappingFail($error); + } + } + + public static function constructors_are_sorted_and_filtered_correctly_data_provider(): iterable + { + $resultA = new stdClass(); + $resultB = new stdClass(); + $resultC = new stdClass(); + + yield 'constructor is used when surrounded by other constructors' => [ + 'constructors' => [ + fn (): DateTime => new DateTime(), + // This constructor is surrounded by other ones to ensure it is + // still used correctly. + fn (): stdClass => $resultA, + fn (): DateTimeImmutable => new DateTimeImmutable(), + ], + 'data' => [ + [ + 'value' => [], + 'expected' => $resultA, + ], + ], + ]; + + yield 'constructors for same class are sorted properly' => [ + 'constructors' => [ + // Basic constructor + fn (string $foo): stdClass => $resultA, + // Constructor with two parameters + fn (string $foo, int $bar): stdClass => $resultB, + // Constructor with optional parameter + fn (string $foo, int $bar, float $baz, string $fiz = 'fiz'): stdClass => $resultC, + ], + 'data' => [ + 'string source' => [ + 'value' => 'foo', + 'expected' => $resultA, + ], + 'foo and bar values' => [ + 'value' => [ + 'foo' => 'foo', + 'bar' => 42, + ], + 'expected' => $resultB, + ], + 'foo and bar and baz values' => [ + 'value' => [ + 'foo' => 'foo', + 'bar' => 42, + 'baz' => 1337.0, + ], + 'expected' => $resultC, + ], + ], + ]; + + yield 'constructors for same class with same arguments number but different types' => [ + 'constructors' => [ + fn (string $foo, string $bar): stdClass => $resultA, + fn (string $foo, string $fiz): stdClass => $resultB, + ], + 'data' => [ + 'foo and bar' => [ + 'value' => [ + 'foo' => 'foo', + 'bar' => 'bar', + ], + 'expected' => $resultA, + ], + 'foo and fiz' => [ + 'value' => [ + 'foo' => 'foo', + 'fiz' => 'fiz', + ], + 'expected' => $resultB, + ], + ], + ]; + + yield 'constructors with same parameter name but different types' => [ + 'constructors' => [ + fn (string $value): stdClass => $resultA, + fn (float $value): stdClass => $resultB, + fn (int $value): stdClass => $resultC, + ], + 'data' => [ + 'string source' => [ + 'value' => 'foo', + 'expected' => $resultA, + ], + 'float source' => [ + 'value' => 404.0, + 'expected' => $resultB, + ], + 'integer source' => [ + 'value' => 1337, + 'expected' => $resultC, + ], + ], + ]; + + yield 'constructors with same named parameter use integer over float' => [ + 'constructors' => [ + fn (float $value): stdClass => $resultA, + fn (int $value): stdClass => $resultB, + ], + 'data' => [ + [ + 'value' => 1337, + 'expected' => $resultB, + ], + ], + ]; + + yield 'constructors with same named parameters names use integer over float' => [ + 'constructors' => [ + fn (float $valueA, float $valueB): stdClass => $resultA, + fn (int $valueA, int $valueB): stdClass => $resultB, + ], + 'data' => [ + [ + 'value' => [ + 'valueA' => 42, + 'valueB' => 1337, + ], + 'expected' => $resultB, + ], + ], + ]; + + yield 'constructors with same parameter name but second one is either float or integer' => [ + 'constructors' => [ + fn (int $valueA, float $valueB): stdClass => $resultA, + fn (int $valueA, int $valueB): stdClass => $resultB, + ], + 'data' => [ + 'integer and float' => [ + 'value' => [ + 'valueA' => 42, + 'valueB' => 1337.0, + ], + 'expected' => $resultA, + ], + 'integer and integer' => [ + 'value' => [ + 'valueA' => 42, + 'valueB' => 1337, + ], + 'expected' => $resultB, + ], + ], + ]; + + yield 'constructor with non scalar argument has priority over those with scalar (non scalar constructor is registered first)' => [ + 'constructors' => [ + fn (int $valueA, SimpleObject $valueB): stdClass => $resultA, + fn (int $valueA, string $valueB): stdClass => $resultB, + ], + 'data' => [ + [ + 'value' => [ + 'valueA' => 42, + 'valueB' => 'foo', + ], + 'expected' => $resultA, + ], + ], + ]; + + yield 'constructor with non scalar argument has priority over those with scalar (non scalar constructor is registered last)' => [ + 'constructors' => [ + fn (int $valueA, string $valueB): stdClass => $resultA, + fn (int $valueA, SimpleObject $valueB): stdClass => $resultB, + ], + 'data' => [ + [ + 'value' => [ + 'valueA' => 42, + 'valueB' => 'foo', + ], + 'expected' => $resultB, + ], + ], + ]; + } + public function test_identical_registered_constructors_with_no_argument_throws_exception(): void { $this->expectException(ObjectBuildersCollision::class); $this->expectExceptionCode(1654955787); - $this->expectExceptionMessageMatches('/A collision was detected between the following constructors of the class `stdClass`: `Closure .*`, `Closure .*`\./'); + $this->expectExceptionMessageMatches('/A type collision was detected between the constructors `Closure .*` and `Closure .*`\./'); $this->mapperBuilder() ->registerConstructor( @@ -495,12 +591,27 @@ public function test_identical_registered_constructors_with_one_argument_throws_ { $this->expectException(ObjectBuildersCollision::class); $this->expectExceptionCode(1654955787); - $this->expectExceptionMessageMatches('/A collision was detected between the following constructors of the class `stdClass`: `Closure .*`, `Closure .*`\./'); + $this->expectExceptionMessageMatches('/A type collision was detected between the constructors `Closure .*` and `Closure .*`\./'); $this->mapperBuilder() ->registerConstructor( fn (int $int): stdClass => new stdClass(), - fn (float $float): stdClass => new stdClass(), + fn (int $int): stdClass => new stdClass(), + ) + ->mapper() + ->map(stdClass::class, []); + } + + public function test_constructors_with_colliding_arguments_throws_exception(): void + { + $this->expectException(ObjectBuildersCollision::class); + $this->expectExceptionCode(1654955787); + $this->expectExceptionMessageMatches('/A type collision was detected between the constructors `Closure .*` and `Closure .*`\./'); + + $this->mapperBuilder() + ->registerConstructor( + fn (int $valueA, float $valueB): stdClass => new stdClass(), + fn (float $valueA, int $valueB): stdClass => new stdClass(), ) ->mapper() ->map(stdClass::class, []); @@ -510,7 +621,7 @@ public function test_identical_registered_constructors_with_several_argument_thr { $this->expectException(ObjectBuildersCollision::class); $this->expectExceptionCode(1654955787); - $this->expectExceptionMessage('A collision was detected between the following constructors of the class `stdClass`: `CuyZ\Valinor\Tests\Integration\Mapping\constructorA()`, `CuyZ\Valinor\Tests\Integration\Mapping\constructorB()`.'); + $this->expectExceptionMessage('A type collision was detected between the constructors `CuyZ\Valinor\Tests\Integration\Mapping\constructorA()` and `CuyZ\Valinor\Tests\Integration\Mapping\constructorB()`.'); $this->mapperBuilder() ->registerConstructor( diff --git a/tests/Unit/Mapper/Tree/ShellTest.php b/tests/Unit/Mapper/Tree/ShellTest.php index 96cb70b0..a8b243c1 100644 --- a/tests/Unit/Mapper/Tree/ShellTest.php +++ b/tests/Unit/Mapper/Tree/ShellTest.php @@ -42,6 +42,15 @@ public function test_change_type_changes_type(): void self::assertSame($typeB, $shellB->type()); } + public function test_allows_superfluous_keys(): void + { + $shellA = Shell::root(new Settings(), new FakeType(), []); + $shellB = $shellA->withAllowedSuperfluousKeys(['foo', 'bar']); + + self::assertNotSame($shellA, $shellB); + self::assertSame(['foo', 'bar'], $shellB->allowedSuperfluousKeys()); + } + public function test_change_value_changes_value(): void { $valueA = 'foo'; diff --git a/tests/Unit/Utility/TypeHelperTest.php b/tests/Unit/Utility/TypeHelperTest.php index f509b1cd..75a8b5f9 100644 --- a/tests/Unit/Utility/TypeHelperTest.php +++ b/tests/Unit/Utility/TypeHelperTest.php @@ -9,12 +9,26 @@ use CuyZ\Valinor\Tests\Fake\Definition\FakeParameterDefinition; use CuyZ\Valinor\Tests\Fake\Type\FakeObjectType; use CuyZ\Valinor\Tests\Fake\Type\FakeType; +use CuyZ\Valinor\Type\Types\ArrayType; +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\UnionType; use CuyZ\Valinor\Utility\TypeHelper; use PHPUnit\Framework\TestCase; final class TypeHelperTest extends TestCase { + public function test_types_have_correct_priorities(): void + { + self::assertSame(4, TypeHelper::typePriority(NativeIntegerType::get())); + self::assertSame(3, TypeHelper::typePriority(NativeFloatType::get())); + self::assertSame(2, TypeHelper::typePriority(NativeStringType::get())); + self::assertSame(1, TypeHelper::typePriority(NativeBooleanType::get())); + self::assertSame(0, TypeHelper::typePriority(ArrayType::native())); + } + public function test_arguments_dump_is_correct(): void { $typeA = FakeType::permissive();