From e974721d8c8d6008ae811c107bd8e7db942fe2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Barto=C5=A1?= Date: Sun, 9 Jul 2023 22:42:47 +0200 Subject: [PATCH] ReflectionMetaSource: improve error messages, add tests --- src/Meta/Source/ReflectorMetaSource.php | 75 ++++++---- .../Definition/TargetLessRuleDefinition.php | 28 ++++ .../Definition/UnsupportedDefinition.php | 26 ++++ .../Doubles/Meta/FieldWithMultipleRulesVO.php | 18 +++ tests/Doubles/Meta/FieldWithNoRuleVO.php | 14 ++ tests/Doubles/Meta/RuleAboveClassVO.php | 14 ++ .../Meta/UnsupportedClassDefinitionVO.php | 14 ++ .../Meta/UnsupportedPropertyDefinitionVO.php | 14 ++ tests/Doubles/Meta/VariantFieldParentVO.php | 14 ++ tests/Doubles/Meta/VariantFieldVO.php | 13 ++ tests/Unit/Meta/MetaLoaderTest.php | 6 + .../Meta/Source/ReflectorMetaSourceTest.php | 128 ++++++++++++++++++ 12 files changed, 334 insertions(+), 30 deletions(-) create mode 100644 tests/Doubles/Definition/TargetLessRuleDefinition.php create mode 100644 tests/Doubles/Definition/UnsupportedDefinition.php create mode 100644 tests/Doubles/Meta/FieldWithMultipleRulesVO.php create mode 100644 tests/Doubles/Meta/FieldWithNoRuleVO.php create mode 100644 tests/Doubles/Meta/RuleAboveClassVO.php create mode 100644 tests/Doubles/Meta/UnsupportedClassDefinitionVO.php create mode 100644 tests/Doubles/Meta/UnsupportedPropertyDefinitionVO.php create mode 100644 tests/Doubles/Meta/VariantFieldParentVO.php create mode 100644 tests/Doubles/Meta/VariantFieldVO.php create mode 100644 tests/Unit/Meta/Source/ReflectorMetaSourceTest.php diff --git a/src/Meta/Source/ReflectorMetaSource.php b/src/Meta/Source/ReflectorMetaSource.php index 784e6a6..6ac64fd 100644 --- a/src/Meta/Source/ReflectorMetaSource.php +++ b/src/Meta/Source/ReflectorMetaSource.php @@ -3,6 +3,7 @@ namespace Orisai\ObjectMapper\Meta\Source; use Orisai\Exceptions\Logic\InvalidArgument; +use Orisai\Exceptions\Message; use Orisai\ObjectMapper\Callbacks\CallbackDefinition; use Orisai\ObjectMapper\Docs\DocDefinition; use Orisai\ObjectMapper\MappedObject; @@ -48,8 +49,8 @@ public function load(ReflectionClass $class): CompileMeta } return new CompileMeta( - $this->loadClassMeta($structures), - $this->loadPropertiesMeta($structures), + $this->loadClassMeta($class, $structures), + $this->loadPropertiesMeta($class, $structures), $sources, ); } @@ -67,9 +68,10 @@ private function getStructureGroup(ReflectionClass $class): StructureGroup } /** + * @param ReflectionClass $rootClass * @return list */ - private function loadClassMeta(StructureGroup $group): array + private function loadClassMeta(ReflectionClass $rootClass, StructureGroup $group): array { $resolved = []; foreach ($group->getClasses() as $class) { @@ -84,12 +86,16 @@ private function loadClassMeta(StructureGroup $group): array $definition = $this->checkDefinitionType($definition); if ($definition instanceof RuleDefinition) { - throw InvalidArgument::create() - ->withMessage(sprintf( - 'Rule definition %s (subtype of %s) cannot be used on class, only properties are allowed', + $message = Message::create() + ->withContext("Resolving metadata of mapped object '{$rootClass->getName()}'.") + ->withProblem(sprintf( + "Rule definition '%s' (subtype of '%s') cannot be used on class, only properties are allowed.", get_class($definition), RuleDefinition::class, )); + + throw InvalidArgument::create() + ->withMessage($message); } if ($definition instanceof CallbackDefinition) { @@ -121,9 +127,10 @@ private function loadClassMeta(StructureGroup $group): array } /** + * @param ReflectionClass $rootClass * @return list */ - private function loadPropertiesMeta(StructureGroup $group): array + private function loadPropertiesMeta(ReflectionClass $rootClass, StructureGroup $group): array { $resolved = []; foreach ($group->getGroupedProperties() as $groupedProperty) { @@ -132,9 +139,6 @@ private function loadPropertiesMeta(StructureGroup $group): array $reflector = $propertyStructure->getSource()->getReflector(); $definitions = $this->reader->readProperty($reflector, MetaDefinition::class); - $property = $propertyStructure->getContextReflector(); - $class = $property->getDeclaringClass(); - $callbacks = []; $docs = []; $modifiers = []; @@ -145,15 +149,18 @@ private function loadPropertiesMeta(StructureGroup $group): array if ($definition instanceof RuleDefinition) { if ($rule !== null) { + $message = Message::create() + ->withContext("Resolving metadata of mapped object '{$rootClass->getName()}'.") + ->withProblem( + "Property '{$propertyStructure->getSource()->toString()}' has" + . ' multiple rule definitions, but only one is allowed.', + ) + ->withSolution( + sprintf("Combine multiple with '%s' or '%s'.", AnyOf::class, AllOf::class), + ); + throw InvalidArgument::create() - ->withMessage(sprintf( - 'Mapped property %s::$%s has multiple expectation definitions, while only one is allowed. ' . - 'Combine multiple with %s or %s', - $class->getName(), - $property->getName(), - AnyOf::class, - AllOf::class, - )); + ->withMessage($message); } $rule = new RuleCompileMeta( @@ -183,11 +190,15 @@ private function loadPropertiesMeta(StructureGroup $group): array } if ($rule === null) { - throw InvalidArgument::create() - ->withMessage( - "Property {$class->getName()}::\${$property->getName()} has mapped object definition, " . - 'but no rule definition.', + $message = Message::create() + ->withContext("Resolving metadata of mapped object '{$rootClass->getName()}'.") + ->withProblem( + "Property '{$propertyStructure->getSource()->toString()}' has" + . ' mapped object definition, but no rule definition.', ); + + throw InvalidArgument::create() + ->withMessage($message); } $resolvedGroup[] = new FieldCompileMeta( @@ -203,7 +214,7 @@ private function loadPropertiesMeta(StructureGroup $group): array continue; } - $this->checkFieldInvariance($resolvedGroup); + $this->checkFieldInvariance($rootClass, $resolvedGroup); $resolved[] = $resolvedGroup[array_key_first($resolvedGroup)]; } @@ -223,7 +234,7 @@ private function checkDefinitionType(MetaDefinition $definition): MetaDefinition ) { throw InvalidArgument::create() ->withMessage(sprintf( - 'Definition %s (subtype of %s) should implement %s, %s %s or %s', + "Definition '%s' (subtype of '%s') should implement '%s', '%s', '%s' or '%s'.", get_class($definition), MetaDefinition::class, CallbackDefinition::class, @@ -237,19 +248,23 @@ private function checkDefinitionType(MetaDefinition $definition): MetaDefinition } /** + * @param ReflectionClass $rootClass * @param list $resolvedGroup */ - private function checkFieldInvariance(array $resolvedGroup): void + private function checkFieldInvariance(ReflectionClass $rootClass, array $resolvedGroup): void { $previousFieldMeta = null; foreach ($resolvedGroup as $fieldMeta) { if ($previousFieldMeta !== null && !$fieldMeta->hasEqualMeta($previousFieldMeta)) { - throw InvalidArgument::create() - ->withMessage( - "Definition of property '{$fieldMeta->getClass()->getContextReflector()->getName()}" - . "::\${$fieldMeta->getProperty()->getContextReflector()->getName()}'" - . " can't be changed but it differs from definition in '{$previousFieldMeta->getClass()->getContextReflector()->getName()}'.", + $message = Message::create() + ->withContext("Resolving metadata of mapped object '{$rootClass->getName()}'.") + ->withProblem( + "Definition of property '{$fieldMeta->getProperty()->getSource()->toString()}'" + . " can't be changed but it differs from definition '{$previousFieldMeta->getProperty()->getSource()->toString()}'.", ); + + throw InvalidArgument::create() + ->withMessage($message); } $previousFieldMeta = $fieldMeta; diff --git a/tests/Doubles/Definition/TargetLessRuleDefinition.php b/tests/Doubles/Definition/TargetLessRuleDefinition.php new file mode 100644 index 0000000..c05d15f --- /dev/null +++ b/tests/Doubles/Definition/TargetLessRuleDefinition.php @@ -0,0 +1,28 @@ +source = new AnnotationsMetaSource(); + } + + /** + * @param class-string $class + * + * @dataProvider provideUnsupportedDefinitionType + */ + public function testUnsupportedDefinitionType(string $class): void + { + $this->expectException(InvalidArgument::class); + $this->expectExceptionMessage( + "Definition 'Tests\Orisai\ObjectMapper\Doubles\Definition\UnsupportedDefinition' " + . "(subtype of 'Orisai\ObjectMapper\Meta\MetaDefinition') should implement " + . "'Orisai\ObjectMapper\Callbacks\CallbackDefinition', " + . "'Orisai\ObjectMapper\Docs\DocDefinition', " + . "'Orisai\ObjectMapper\Modifiers\ModifierDefinition' or " + . "'Orisai\ObjectMapper\Rules\RuleDefinition'.", + ); + + $this->source->load(new ReflectionClass($class)); + } + + public function provideUnsupportedDefinitionType(): Generator + { + yield [ + UnsupportedClassDefinitionVO::class, + ]; + + yield [ + UnsupportedPropertyDefinitionVO::class, + ]; + } + + public function testFieldInvariance(): void + { + $this->expectException(InvalidArgument::class); + $this->expectExceptionMessage( + <<<'MSG' +Context: Resolving metadata of mapped object + 'Tests\Orisai\ObjectMapper\Doubles\Meta\VariantFieldVO'. +Problem: Definition of property + 'Tests\Orisai\ObjectMapper\Doubles\Meta\VariantFieldVO->$field' can't + be changed but it differs from definition + 'Tests\Orisai\ObjectMapper\Doubles\Meta\VariantFieldParentVO->$field'. +MSG, + ); + + $this->source->load(new ReflectionClass(VariantFieldVO::class)); + } + + public function testRuleAboveClass(): void + { + $this->expectException(InvalidArgument::class); + $this->expectExceptionMessage( + <<<'MSG' +Context: Resolving metadata of mapped object + 'Tests\Orisai\ObjectMapper\Doubles\Meta\RuleAboveClassVO'. +Problem: Rule definition + 'Tests\Orisai\ObjectMapper\Doubles\Definition\TargetLessRuleDefinition' + (subtype of 'Orisai\ObjectMapper\Rules\RuleDefinition') cannot be used + on class, only properties are allowed. +MSG, + ); + + $this->source->load(new ReflectionClass(RuleAboveClassVO::class)); + } + + public function testFieldWithMultipleRules(): void + { + $this->expectException(InvalidArgument::class); + $this->expectExceptionMessage( + <<<'MSG' +Context: Resolving metadata of mapped object + 'Tests\Orisai\ObjectMapper\Doubles\Meta\FieldWithMultipleRulesVO'. +Problem: Property + 'Tests\Orisai\ObjectMapper\Doubles\Meta\FieldWithMultipleRulesVO->$field' + has multiple rule definitions, but only one is allowed. +Solution: Combine multiple with 'Orisai\ObjectMapper\Rules\AnyOf' or + 'Orisai\ObjectMapper\Rules\AllOf'. +MSG, + ); + + $this->source->load(new ReflectionClass(FieldWithMultipleRulesVO::class)); + } + + public function testFieldWithNoRule(): void + { + $this->expectException(InvalidArgument::class); + $this->expectExceptionMessage( + <<<'MSG' +Context: Resolving metadata of mapped object + 'Tests\Orisai\ObjectMapper\Doubles\Meta\FieldWithNoRuleVO'. +Problem: Property + 'Tests\Orisai\ObjectMapper\Doubles\Meta\FieldWithNoRuleVO->$field' has + mapped object definition, but no rule definition. +MSG, + ); + + $this->source->load(new ReflectionClass(FieldWithNoRuleVO::class)); + } + +}