Skip to content

Commit

Permalink
Serialize openapi properly fix #3997
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Feb 5, 2021
1 parent b494acc commit 65ac528
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 42 deletions.
4 changes: 3 additions & 1 deletion src/Bridge/Symfony/Bundle/Resources/config/openapi.xml
Expand Up @@ -6,7 +6,9 @@

<services>
<service id="api_platform.openapi.normalizer" class="ApiPlatform\Core\OpenApi\Serializer\OpenApiNormalizer" public="false">
<argument type="service" id="serializer.normalizer.object" />
<argument type="service" id="api_platform.property_accessor"/>
<argument type="service" id="api_platform.property_info"/>

<!-- Just after the DocumentationNormalizer see swagger.xml -->
<tag name="serializer.normalizer" priority="-795" />
</service>
Expand Down
74 changes: 45 additions & 29 deletions src/OpenApi/Serializer/OpenApiNormalizer.php
Expand Up @@ -13,8 +13,10 @@

namespace ApiPlatform\Core\OpenApi\Serializer;

use ApiPlatform\Core\OpenApi\Model\Paths;
use ApiPlatform\Core\OpenApi\OpenApi;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

Expand All @@ -26,59 +28,73 @@ final class OpenApiNormalizer implements NormalizerInterface, CacheableSupportsM
public const FORMAT = 'json';
private const EXTENSION_PROPERTIES_KEY = 'extensionProperties';

private $decorated;
private $propertyAccessor;
private $propertyInfo;

public function __construct(NormalizerInterface $decorated)
public function __construct(PropertyAccessorInterface $propertyAccessor, PropertyInfoExtractorInterface $propertyInfo)
{
$this->decorated = $decorated;
$this->propertyAccessor = $propertyAccessor;
$this->propertyInfo = $propertyInfo;
}

/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = []): array
{
$context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] = true;
$context[AbstractObjectNormalizer::SKIP_NULL_VALUES] = true;

return $this->recursiveClean($this->decorated->normalize($object, $format, $context));
return $this->objectToArray($object);
}

private function recursiveClean($data): array
private function objectToArray($object)
{
foreach ($data as $key => $value) {
if (self::EXTENSION_PROPERTIES_KEY === $key) {
foreach ($data[self::EXTENSION_PROPERTIES_KEY] as $extensionPropertyKey => $extensionPropertyValue) {
$data[$extensionPropertyKey] = $extensionPropertyValue;
}
if (!\is_object($object)) {
return $object;
}

if ($object instanceof Paths) {
$paths = $object->getPaths();
ksort($paths);

return array_map([$this, 'objectToArray'], $paths);
}

if ($object instanceof \ArrayObject) {
return array_map([$this, 'objectToArray'], $object->getArrayCopy());
}

$array = [];
foreach ($this->propertyInfo->getProperties(\get_class($object)) as $property) {
$value = $this->propertyAccessor->getValue($object, $property);

if (\is_object($value)) {
$array[$property] = $this->objectToArray($value);
continue;
}

if ('schemas' === $key) {
if ($value) {
ksort($value);
if (self::EXTENSION_PROPERTIES_KEY === $property) {
foreach ($value as $extensionPropertyKey => $extensionPropertyValue) {
$array[$extensionPropertyKey] = $extensionPropertyValue;
}
continue;
}

// Side effect of using getPaths(): Paths which itself contains the array
if ('paths' === $key) {
$value = $data['paths'] = $data['paths']['paths'];
if ($value) {
ksort($value);
if (is_iterable($value)) {
$array[$property] = [];

foreach ($value as $key => $v) {
$array[$property][$key] = $this->objectToArray($v);
}
unset($data['paths']['paths']);
continue;
}

if (\is_array($value)) {
$data[$key] = $this->recursiveClean($value);
// arrays must stay even if empty
if (null === $value) {
continue;
}
}

unset($data[self::EXTENSION_PROPERTIES_KEY]);
$array[$property] = $value;
}

return $data;
return $array;
}

/**
Expand Down
4 changes: 0 additions & 4 deletions tests/Bridge/Symfony/Bundle/Command/OpenApiCommandTest.php
Expand Up @@ -56,8 +56,6 @@ public function testExecuteWithYaml()
$this->assertYaml($result);

$expected = <<<YAML
/dummy_cars:
get:
operationId: getDummyCarCollection
tags:
- DummyCar
Expand All @@ -66,8 +64,6 @@ public function testExecuteWithYaml()
$this->assertStringContainsString(str_replace(\PHP_EOL, "\n", $expected), $result, 'nested object should be present.');

$expected = <<<YAML
'/dummy_cars/{id}':
get:
operationId: getDummyCarItem
tags: []
YAML;
Expand Down
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Serializer\NameConverter;

use LogicException;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;

/**
Expand All @@ -23,6 +24,11 @@ class CustomConverter extends CamelCaseToSnakeCaseNameConverter
{
public function normalize($propertyName)
{
// OpenApi test, this should never be called
if ('extensionProperties' === $propertyName) {
throw new LogicException('OpenApi is not using the name converter.');
}

return 'nameConverted' === $propertyName ? parent::normalize($propertyName) : $propertyName;
}

Expand Down
22 changes: 14 additions & 8 deletions tests/OpenApi/Serializer/OpenApiNormalizerTest.php
Expand Up @@ -37,11 +37,12 @@
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Container\ContainerInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

class OpenApiNormalizerTest extends TestCase
{
Expand Down Expand Up @@ -157,13 +158,16 @@ public function testNormalize()
$openApi = $openApi->withExtensionProperty('key', 'Custom x-key value');
$openApi = $openApi->withExtensionProperty('x-value', 'Custom x-value value');

$reflectionExtractor = new ReflectionExtractor();
$propertyInfo = new PropertyInfoExtractor(
[$reflectionExtractor],
[$reflectionExtractor],
[],
[$reflectionExtractor],
[$reflectionExtractor]
);
$encoders = [new JsonEncoder()];
$normalizers = [new ObjectNormalizer()];

$serializer = new Serializer($normalizers, $encoders);
$normalizers[0]->setSerializer($serializer);

$normalizer = new OpenApiNormalizer($normalizers[0]);
$normalizer = new OpenApiNormalizer(PropertyAccess::createPropertyAccessor(), $propertyInfo);

$openApiAsArray = $normalizer->normalize($openApi);

Expand All @@ -185,5 +189,7 @@ public function testNormalize()

// Make sure things are sorted
$this->assertEquals(array_keys($openApiAsArray['paths']), ['/dummies', '/dummies/{id}', '/zorros', '/zorros/{id}']);
// Test name converter doesn't rename this property
$this->assertArrayHasKey('requestBody', $openApiAsArray['paths']['/dummies']['post']);
}
}

0 comments on commit 65ac528

Please sign in to comment.