From 687b2f931a660ad0bc9c8c5625e7e6084d00cd7d Mon Sep 17 00:00:00 2001 From: Artem Henvald Date: Fri, 3 Dec 2021 20:15:37 +0200 Subject: [PATCH] Add base normalizer (#14) --- Serializer/CircularReferenceHandler.php | 31 +++++ .../ConstraintViolationListNormalizer.php | 79 ++++++++++++ .../Normalizer/JsonSchemaErrorNormalizer.php | 55 ++++++++ .../CircularReferenceHandlerTest.php | 39 ++++++ .../ConstraintViolationListNormalizerTest.php | 98 ++++++++++++++ .../JsonSchemaErrorNormalizerTest.php | 121 ++++++++++++++++++ 6 files changed, 423 insertions(+) create mode 100644 Serializer/CircularReferenceHandler.php create mode 100644 Serializer/Normalizer/ConstraintViolationListNormalizer.php create mode 100644 Serializer/Normalizer/JsonSchemaErrorNormalizer.php create mode 100644 Tests/Serializer/CircularReferenceHandlerTest.php create mode 100644 Tests/Serializer/Normalizer/ConstraintViolationListNormalizerTest.php create mode 100644 Tests/Serializer/Normalizer/JsonSchemaErrorNormalizerTest.php diff --git a/Serializer/CircularReferenceHandler.php b/Serializer/CircularReferenceHandler.php new file mode 100644 index 0000000..dfbbb8e --- /dev/null +++ b/Serializer/CircularReferenceHandler.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Serializer; + +/** + * CircularReferenceHandler. + */ +class CircularReferenceHandler +{ + /** + * @param mixed $object + * + * @return callable + */ + public function __invoke($object): callable + { + return static function () use ($object) { + return $object->getId(); + }; + } +} diff --git a/Serializer/Normalizer/ConstraintViolationListNormalizer.php b/Serializer/Normalizer/ConstraintViolationListNormalizer.php new file mode 100644 index 0000000..d57ea32 --- /dev/null +++ b/Serializer/Normalizer/ConstraintViolationListNormalizer.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Serializer\Normalizer; + +use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer as SymfonyConstraintViolationListNormalizer; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Validator\ConstraintViolationListInterface; + +/** + * ConstraintViolationListNormalizer. + */ +class ConstraintViolationListNormalizer implements NormalizerInterface +{ + private SymfonyConstraintViolationListNormalizer $symfonyConstraintViolationListNormalizer; + + /** + * @param SymfonyConstraintViolationListNormalizer $symfonyConstraintViolationListNormalizer + */ + public function __construct(SymfonyConstraintViolationListNormalizer $symfonyConstraintViolationListNormalizer) + { + $this->symfonyConstraintViolationListNormalizer = $symfonyConstraintViolationListNormalizer; + } + + /** + * @param mixed $data + * @param string|null $format + * + * @return bool + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof ConstraintViolationListInterface; + } + + /** + * Clear the "detail" field from prefixed property paths. + * + * From the parent class: + * { + * "detail": "propertyPath1: Error description 1\npropertyPath2: Error description 2", + * } + * After additional processing: + * { + * "detail": "Error description 1\nError description 2", + * } + * + * {@inheritdoc} + */ + public function normalize($object, string $format = null, array $context = []) + { + $result = $this->symfonyConstraintViolationListNormalizer->normalize($object, $format, $context); + + if (\is_array($result) && \array_key_exists('detail', $result) && $result['detail']) { + $messages = explode("\n", $result['detail']); + + foreach ($messages as &$message) { + $position = mb_strpos($message, ': '); + if (\is_int($position)) { + $message = mb_substr($message, $position + 2); + } + } + unset($message); + + $result['detail'] = implode("\n", $messages); + } + + return $result; + } +} diff --git a/Serializer/Normalizer/JsonSchemaErrorNormalizer.php b/Serializer/Normalizer/JsonSchemaErrorNormalizer.php new file mode 100644 index 0000000..7ef04c6 --- /dev/null +++ b/Serializer/Normalizer/JsonSchemaErrorNormalizer.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Serializer\Normalizer; + +use JsonSchema\Validator as JsonSchemaValidator; +use StfalconStudio\ApiBundle\Exception\RuntimeException; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * JsonSchemaErrorNormalizer. + */ +class JsonSchemaErrorNormalizer implements NormalizerInterface +{ + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof JsonSchemaValidator; + } + + /** + * @param JsonSchemaValidator|mixed $object + * @param string|null $format + * @param array $context + * + * @return array + */ + public function normalize($object, string $format = null, array $context = []): array + { + if (!$object instanceof JsonSchemaValidator) { + throw new RuntimeException(sprintf('Object of class %s is not instance of %s', \get_class($object), JsonSchemaValidator::class)); + } + + $data = []; + + foreach ($object->getErrors() as ['constraint' => $constraint, 'property' => $property, 'message' => $message]) { + $data[$constraint][] = [ + $property => $message, + ]; + } + + return $data; + } +} diff --git a/Tests/Serializer/CircularReferenceHandlerTest.php b/Tests/Serializer/CircularReferenceHandlerTest.php new file mode 100644 index 0000000..147dc08 --- /dev/null +++ b/Tests/Serializer/CircularReferenceHandlerTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Tests\Serializer; + +use PHPUnit\Framework\TestCase; +use StfalconStudio\ApiBundle\Model\UUID\UuidInterface; +use StfalconStudio\ApiBundle\Serializer\CircularReferenceHandler; + +final class CircularReferenceHandlerTest extends TestCase +{ + public function testHandleById(): void + { + $handler = new CircularReferenceHandler(); + + $object = $this + ->getMockBuilder(UuidInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['getId']) + ->getMock() + ; + + $object + ->expects(self::once()) + ->method('getId') + ; + + $handler($object)(); // Execute callback + } +} diff --git a/Tests/Serializer/Normalizer/ConstraintViolationListNormalizerTest.php b/Tests/Serializer/Normalizer/ConstraintViolationListNormalizerTest.php new file mode 100644 index 0000000..da5bc2e --- /dev/null +++ b/Tests/Serializer/Normalizer/ConstraintViolationListNormalizerTest.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Tests\Serializer\Normalizer; + +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use StfalconStudio\ApiBundle\Serializer\Normalizer\ConstraintViolationListNormalizer; +use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer as SymfonyConstraintViolationListNormalizer; +use Symfony\Component\Validator\ConstraintViolationListInterface; + +final class ConstraintViolationListNormalizerTest extends TestCase +{ + /** @var SymfonyConstraintViolationListNormalizer|MockObject */ + private $symfonyNormalizer; + + private ConstraintViolationListNormalizer $normalizer; + + protected function setUp(): void + { + $this->symfonyNormalizer = $this->createMock(SymfonyConstraintViolationListNormalizer::class); + $this->normalizer = new ConstraintViolationListNormalizer($this->symfonyNormalizer); + } + + protected function tearDown(): void + { + unset( + $this->symfonyNormalizer, + $this->normalizer, + ); + } + + /** + * @param string $originDetail + * @param string $resultDetail + * + * @dataProvider dataProviderForTestNormalize + */ + public function testNormalize(string $originDetail, string $resultDetail): void + { + $object = new \stdClass(); + $format = 'json'; + $context = ['some']; + + $this->symfonyNormalizer + ->expects(self::once()) + ->method('normalize') + ->with($object, $format, $context) + ->willReturn(['detail' => $originDetail]) + ; + + $result = (array) $this->normalizer->normalize($object, $format, $context); + + self::assertArrayHasKey('detail', $result); + self::assertSame($resultDetail, $result['detail']); + } + + public static function dataProviderForTestNormalize(): iterable + { + yield [ + 'origin_detail' => 'field1: Error description.', + 'result_detail' => 'Error description.', + ]; + yield [ + 'origin_detail' => "field1: Error description 1.\nfield2: Error description 2.", + 'result_detail' => "Error description 1.\nError description 2.", + ]; + yield [ + 'origin_detail' => 'Error description.', + 'result_detail' => 'Error description.', + ]; + yield [ + 'origin_detail' => "field1: Error :description 1.\nfield2: Error :description 2.", + 'result_detail' => "Error :description 1.\nError :description 2.", + ]; + } + + public function testNotSupportsNormalization(): void + { + self::assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + } + + public function testSupportsNormalization(): void + { + $error = $this->createMock(ConstraintViolationListInterface::class); + + self::assertTrue($this->normalizer->supportsNormalization($error)); + } +} diff --git a/Tests/Serializer/Normalizer/JsonSchemaErrorNormalizerTest.php b/Tests/Serializer/Normalizer/JsonSchemaErrorNormalizerTest.php new file mode 100644 index 0000000..82710b5 --- /dev/null +++ b/Tests/Serializer/Normalizer/JsonSchemaErrorNormalizerTest.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Tests\Serializer\Normalizer; + +use JsonSchema\Validator as JsonSchemaValidator; +use PHPUnit\Framework\TestCase; +use StfalconStudio\ApiBundle\Serializer\Normalizer\JsonSchemaErrorNormalizer; + +final class JsonSchemaErrorNormalizerTest extends TestCase +{ + private JsonSchemaValidator $jsonSchemaValidator; + + private JsonSchemaErrorNormalizer $normalizer; + + protected function setUp(): void + { + $this->normalizer = new JsonSchemaErrorNormalizer(); + $this->jsonSchemaValidator = new JsonSchemaValidator(); + } + + protected function tearDown(): void + { + unset( + $this->normalizer, + $this->jsonSchemaValidator, + ); + } + + public function testNormalizeWhenObjectIncorrect(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageMatches('/^Object of class .* is not instance of .*$/'); + + $this->normalizer->normalize($this->normalizer->normalize(new \stdClass())); + } + + public function testNormalizeWhenObjectWithoutErrors(): void + { + self::assertEmpty($this->normalizer->normalize($this->jsonSchemaValidator)); + } + + /** + * @param array $errors + * @param array $expected + * + * @dataProvider dataProviderForTestNormalizeWhenObjectWithErrors + */ + public function testNormalizeWhenObjectWithErrors(array $errors, array $expected): void + { + $this->jsonSchemaValidator->addErrors($errors); + + self::assertSame($expected, $this->normalizer->normalize($this->jsonSchemaValidator)); + } + + public static function dataProviderForTestNormalizeWhenObjectWithErrors(): iterable + { + yield [ + 'errors' => [ + [ + 'constraint' => 'NOT_NULL', + 'property' => 'email', + 'message' => 'This value should not be null.', + ], + ], + 'expected' => [ + 'NOT_NULL' => [ + ['email' => 'This value should not be null.'], + ], + ], + ]; + + yield [ + 'errors' => [ + [ + 'constraint' => 'NOT_NULL', + 'property' => 'email', + 'message' => 'This value should not be null.', + ], + [ + 'constraint' => 'IsTrue', + 'property' => 'is_active', + 'message' => 'This value should be true.', + ], + [ + 'constraint' => 'NOT_NULL', + 'property' => 'full_name', + 'message' => 'This value should not be null.', + ], + ], + 'expected' => [ + 'NOT_NULL' => [ + ['email' => 'This value should not be null.'], + ['full_name' => 'This value should not be null.'], + ], + 'IsTrue' => [ + ['is_active' => 'This value should be true.'], + ], + ], + ]; + } + + public function testDoesNotSupportNormalization(): void + { + self::assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + } + + public function testSupportsNormalization(): void + { + self::assertTrue($this->normalizer->supportsNormalization($this->jsonSchemaValidator)); + } +}