Skip to content

Commit 5c8df74

Browse files
committed
Fix #673: name converter for Swagger and NelmioApiDocBundle
1 parent 2d252cf commit 5c8df74

File tree

6 files changed

+156
-5
lines changed

6 files changed

+156
-5
lines changed

src/Bridge/NelmioApiDoc/Parser/ApiPlatformParser.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Nelmio\ApiDocBundle\DataTypes;
2121
use Nelmio\ApiDocBundle\Parser\ParserInterface;
2222
use Symfony\Component\PropertyInfo\Type;
23+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
2324

2425
/**
2526
* Extract input and output information for the NelmioApiDocBundle.
@@ -42,12 +43,14 @@ final class ApiPlatformParser implements ParserInterface
4243
private $resourceMetadataFactory;
4344
private $propertyNameCollectionFactory;
4445
private $propertyMetadataFactory;
46+
private $nameConverter;
4547

46-
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory)
48+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null)
4749
{
4850
$this->resourceMetadataFactory = $resourceMetadataFactory;
4951
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
5052
$this->propertyMetadataFactory = $propertyMetadataFactory;
53+
$this->nameConverter = $nameConverter;
5154
}
5255

5356
/**
@@ -171,7 +174,8 @@ private function getPropertyMetadata(ResourceMetadata $resourceMetadata, string
171174
($propertyMetadata->isReadable() && self::OUT_PREFIX === $io) ||
172175
($propertyMetadata->isWritable() && self::IN_PREFIX === $io)
173176
) {
174-
$data[$propertyName] = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, null, $visited);
177+
$normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName) : $propertyName;
178+
$data[$normalizedPropertyName] = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, null, $visited);
175179
}
176180
}
177181

src/Bridge/Symfony/Bundle/Resources/config/nelmio_api_doc.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
2020
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
2121
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
22+
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
2223

2324
<tag name="nelmio_api_doc.extractor.parser" />
2425
</service>

src/Bridge/Symfony/Bundle/Resources/config/swagger.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<argument type="service" id="api_platform.operation_path_resolver" />
1616
<argument type="service" id="api_platform.router" />
1717
<argument type="service" id="api_platform.filters" />
18+
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
1819

1920
<tag name="serializer.normalizer" priority="16" />
2021
</service>

src/Swagger/Serializer/DocumentationNormalizer.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
2424
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
2525
use Symfony\Component\PropertyInfo\Type;
26+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
2627
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2728

2829
/**
@@ -45,8 +46,9 @@ final class DocumentationNormalizer implements NormalizerInterface
4546
private $operationPathResolver;
4647
private $urlGenerator;
4748
private $filterCollection;
49+
private $nameConverter;
4850

49-
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator, FilterCollection $filterCollection = null)
51+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator, FilterCollection $filterCollection = null, NameConverterInterface $nameConverter = null)
5052
{
5153
$this->resourceMetadataFactory = $resourceMetadataFactory;
5254
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
@@ -56,6 +58,7 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
5658
$this->operationPathResolver = $operationPathResolver;
5759
$this->urlGenerator = $urlGenerator;
5860
$this->filterCollection = $filterCollection;
61+
$this->nameConverter = $nameConverter;
5962
}
6063

6164
/**
@@ -378,12 +381,13 @@ private function getDefinitionSchema(string $resourceClass, ResourceMetadata $re
378381
$options = isset($serializerContext['groups']) ? ['serializer_groups' => $serializerContext['groups']] : [];
379382
foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) {
380383
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
384+
$normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName) : $propertyName;
381385

382386
if ($propertyMetadata->isRequired()) {
383-
$definitionSchema['required'][] = $propertyName;
387+
$definitionSchema['required'][] = $normalizedPropertyName;
384388
}
385389

386-
$definitionSchema['properties'][$propertyName] = $this->getPropertySchema($propertyMetadata);
390+
$definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($propertyMetadata);
387391
}
388392

389393
return $definitionSchema;

tests/Bridge/NelmioApiDoc/Parser/ApiPlatformParserTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Nelmio\ApiDocBundle\Parser\ParserInterface;
2727
use Prophecy\Argument;
2828
use Symfony\Component\PropertyInfo\Type;
29+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
2930

3031
/**
3132
* @author Teoh Han Hui <teohhanhui@gmail.com>
@@ -359,4 +360,46 @@ public function testParseRelation()
359360
],
360361
], $actual);
361362
}
363+
364+
public function testParseWithNameConverter()
365+
{
366+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
367+
$resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('dummy', 'dummy', null, [], []))->shouldBeCalled();
368+
$resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal();
369+
370+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
371+
$propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::cetera())->willReturn(new PropertyNameCollection([
372+
'nameConverted',
373+
]))->shouldBeCalled();
374+
$propertyNameCollectionFactory = $propertyNameCollectionFactoryProphecy->reveal();
375+
376+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
377+
$nameConvertedPropertyMetadata = (new PropertyMetadata())
378+
->withType(new Type(Type::BUILTIN_TYPE_STRING, true))
379+
->withDescription('A converted name')
380+
->withReadable(true)
381+
->withWritable(true)
382+
->withRequired(false);
383+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'nameConverted')->willReturn($nameConvertedPropertyMetadata)->shouldBeCalled();
384+
$propertyMetadataFactory = $propertyMetadataFactoryProphecy->reveal();
385+
386+
$nameConverterProphecy = $this->prophesize(NameConverterInterface::class);
387+
$nameConverterProphecy->normalize('nameConverted')->willReturn('name_converted')->shouldBeCalled();
388+
$nameConverter = $nameConverterProphecy->reveal();
389+
390+
$apiPlatformParser = new ApiPlatformParser($resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter);
391+
392+
$actual = $apiPlatformParser->parse([
393+
'class' => sprintf('%s:%s:%s', ApiPlatformParser::OUT_PREFIX, Dummy::class, 'get'),
394+
]);
395+
396+
$this->assertEquals([
397+
'name_converted' => [
398+
'dataType' => DataTypes::STRING,
399+
'required' => false,
400+
'description' => 'A converted name',
401+
'readonly' => false,
402+
],
403+
], $actual);
404+
}
362405
}

tests/Swagger/Serializer/DocumentationNormalizerTest.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use ApiPlatform\Core\Tests\Fixtures\DummyFilter;
3030
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
3131
use Symfony\Component\PropertyInfo\Type;
32+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
3233

3334
/**
3435
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
@@ -235,6 +236,103 @@ public function testNormalize()
235236
$this->assertEquals($expected, $normalizer->normalize($documentation));
236237
}
237238

239+
public function testNormalizeWithNameConverter()
240+
{
241+
$documentation = new Documentation(new ResourceNameCollection([Dummy::class]), 'Dummy API', 'This is a dummy API', '1.2.3', ['jsonld' => ['application/ld+json']]);
242+
243+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
244+
$propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['name', 'nameConverted']));
245+
246+
$dummyMetadata = new ResourceMetadata('Dummy', 'This is a dummy.', null, ['get' => ['method' => 'GET']], [], []);
247+
248+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
249+
$resourceMetadataFactoryProphecy->create(Dummy::class)->shouldBeCalled()->willReturn($dummyMetadata);
250+
251+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
252+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a name.', true, true, null, null, false));
253+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'nameConverted')->shouldBeCalled()->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), 'This is a converted name.', true, true, null, null, false));
254+
255+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
256+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
257+
258+
$operationMethodResolverProphecy = $this->prophesize(OperationMethodResolverInterface::class);
259+
$operationMethodResolverProphecy->getItemOperationMethod(Dummy::class, 'get')->shouldBeCalled()->willReturn('GET');
260+
261+
$urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class);
262+
$urlGeneratorProphecy->generate('api_entrypoint')->willReturn('/app_dev.php/')->shouldBeCalled();
263+
264+
$nameConverterProphecy = $this->prophesize(NameConverterInterface::class);
265+
$nameConverterProphecy->normalize('name')->willReturn('name')->shouldBeCalled();
266+
$nameConverterProphecy->normalize('nameConverted')->willReturn('name_converted')->shouldBeCalled();
267+
268+
$operationPathResolver = new CustomOperationPathResolver(new UnderscoreOperationPathResolver());
269+
270+
$normalizer = new DocumentationNormalizer(
271+
$resourceMetadataFactoryProphecy->reveal(),
272+
$propertyNameCollectionFactoryProphecy->reveal(),
273+
$propertyMetadataFactoryProphecy->reveal(),
274+
$resourceClassResolverProphecy->reveal(),
275+
$operationMethodResolverProphecy->reveal(),
276+
$operationPathResolver,
277+
$urlGeneratorProphecy->reveal(),
278+
null,
279+
$nameConverterProphecy->reveal()
280+
);
281+
282+
$expected = [
283+
'swagger' => '2.0',
284+
'basePath' => '/app_dev.php/',
285+
'info' => [
286+
'title' => 'Dummy API',
287+
'description' => 'This is a dummy API',
288+
'version' => '1.2.3',
289+
],
290+
'paths' => new \ArrayObject([
291+
'/dummies/{id}' => [
292+
'get' => new \ArrayObject([
293+
'tags' => ['Dummy'],
294+
'operationId' => 'getDummyItem',
295+
'produces' => ['application/ld+json'],
296+
'summary' => 'Retrieves a Dummy resource.',
297+
'parameters' => [
298+
[
299+
'name' => 'id',
300+
'in' => 'path',
301+
'type' => 'integer',
302+
'required' => true,
303+
],
304+
],
305+
'responses' => [
306+
200 => [
307+
'description' => 'Dummy resource response',
308+
'schema' => ['$ref' => '#/definitions/Dummy'],
309+
],
310+
404 => ['description' => 'Resource not found'],
311+
],
312+
]),
313+
],
314+
]),
315+
'definitions' => new \ArrayObject([
316+
'Dummy' => new \ArrayObject([
317+
'type' => 'object',
318+
'description' => 'This is a dummy.',
319+
'properties' => [
320+
'name' => new \ArrayObject([
321+
'type' => 'string',
322+
'description' => 'This is a name.',
323+
]),
324+
'name_converted' => new \ArrayObject([
325+
'type' => 'string',
326+
'description' => 'This is a converted name.',
327+
]),
328+
],
329+
]),
330+
]),
331+
];
332+
333+
$this->assertEquals($expected, $normalizer->normalize($documentation));
334+
}
335+
238336
public function testNormalizeWithOnlyNormalizationGroups()
239337
{
240338
$title = 'Test API';

0 commit comments

Comments
 (0)