From ab005c41298f1847750c2d47b4d34e11aa23080e Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Sun, 10 Jun 2018 09:56:38 +0200 Subject: [PATCH] Implement alternative naming system via configuration (#1312) * implement alternative naming system via configuration * use strict comparison * test di configs * rever * test naming aliases are applied * set "default" as default area * test names are passed to generators * cs formatting * added extra check for built-int types * cs * added documentation about alternative names * allow to create the same alias in two different areas * document and test better aliasing strategy * specify that the last matching rule is used * Make last matching rule wins * Fix documentation --- ApiDocGenerator.php | 9 ++- DependencyInjection/Configuration.php | 17 +++++ DependencyInjection/NelmioApiDocExtension.php | 20 +++++ Model/ModelRegistry.php | 30 +++++++- Resources/doc/alternative_names.rst | 28 +++++++ Resources/doc/index.rst | 1 + .../DependencyInjection/ConfigurationTest.php | 68 +++++++++++++++++ .../NelmioApiDocExtensionTest.php | 68 +++++++++++++++++ Tests/Model/ModelRegistryTest.php | 74 ++++++++++++++++++- 9 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 Resources/doc/alternative_names.rst diff --git a/ApiDocGenerator.php b/ApiDocGenerator.php index 4b7b796..4000be1 100644 --- a/ApiDocGenerator.php +++ b/ApiDocGenerator.php @@ -28,6 +28,8 @@ final class ApiDocGenerator private $cacheItemPool; + private $alternativeNames = []; + /** * @param DescriberInterface[]|iterable $describers * @param ModelDescriberInterface[]|iterable $modelDescribers @@ -40,6 +42,11 @@ final class ApiDocGenerator $this->cacheItemId = $cacheItemId; } + public function setAlternativeNames(array $alternativeNames) + { + $this->alternativeNames = $alternativeNames; + } + public function generate(): Swagger { if (null !== $this->swagger) { @@ -54,7 +61,7 @@ final class ApiDocGenerator } $this->swagger = new Swagger(); - $modelRegistry = new ModelRegistry($this->modelDescribers, $this->swagger); + $modelRegistry = new ModelRegistry($this->modelDescribers, $this->swagger, $this->alternativeNames); foreach ($this->describers as $describer) { if ($describer instanceof ModelRegistryAwareInterface) { $describer->setModelRegistry($modelRegistry); diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 9588e65..1c04e4a 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -85,6 +85,23 @@ final class Configuration implements ConfigurationInterface ->children() ->booleanNode('use_jms')->defaultFalse()->end() ->end() + ->children() + ->arrayNode('names') + ->prototype('array') + ->children() + ->scalarNode('alias')->isRequired()->end() + ->scalarNode('type')->isRequired()->end() + ->arrayNode('groups') + ->defaultValue([]) + ->prototype('scalar')->end() + ->end() + ->arrayNode('areas') + ->defaultValue([]) + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->end() ->end() ->end(); diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index ae1619d..1522712 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -61,8 +61,11 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI $container->setParameter('nelmio_api_doc.areas', array_keys($config['areas'])); foreach ($config['areas'] as $area => $areaConfig) { + $nameAliases = $this->findNameAliases($config['models']['names'], $area); + $container->register(sprintf('nelmio_api_doc.generator.%s', $area), ApiDocGenerator::class) ->setPublic(false) + ->addMethodCall('setAlternativeNames', [$nameAliases]) ->setArguments([ new TaggedIteratorArgument(sprintf('nelmio_api_doc.describer.%s', $area)), new TaggedIteratorArgument('nelmio_api_doc.model_describer'), @@ -152,4 +155,21 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI // Import the base configuration $container->getDefinition('nelmio_api_doc.describers.config')->replaceArgument(0, $config['documentation']); } + + private function findNameAliases(array $names, string $area): array + { + $nameAliases = array_filter($names, function (array $aliasInfo) use ($area) { + return empty($aliasInfo['areas']) || in_array($area, $aliasInfo['areas'], true); + }); + + $aliases = []; + foreach ($nameAliases as $nameAlias) { + $aliases[$nameAlias['alias']] = [ + 'type' => $nameAlias['type'], + 'groups' => $nameAlias['groups'], + ]; + } + + return $aliases; + } } diff --git a/Model/ModelRegistry.php b/Model/ModelRegistry.php index 9d9d741..1d1a96d 100644 --- a/Model/ModelRegistry.php +++ b/Model/ModelRegistry.php @@ -19,6 +19,8 @@ use Symfony\Component\PropertyInfo\Type; final class ModelRegistry { + private $alternativeNames = []; + private $unregistered = []; private $models = []; @@ -34,10 +36,11 @@ final class ModelRegistry * * @internal */ - public function __construct($modelDescribers, Swagger $api) + public function __construct($modelDescribers, Swagger $api, array $alternativeNames = []) { $this->modelDescribers = $modelDescribers; $this->api = $api; + $this->alternativeNames = array_reverse($alternativeNames); // last rule wins } public function register(Model $model): string @@ -95,7 +98,9 @@ final class ModelRegistry private function generateModelName(Model $model): string { $definitions = $this->api->getDefinitions(); - $base = $name = $this->getTypeShortName($model->getType()); + + $name = $base = $this->getAlternativeName($model) ?? $this->getTypeShortName($model->getType()); + $i = 1; while ($definitions->has($name)) { ++$i; @@ -105,6 +110,27 @@ final class ModelRegistry return $name; } + /** + * @param Model $model + * + * @return string|null + */ + private function getAlternativeName(Model $model) + { + $type = $model->getType(); + foreach ($this->alternativeNames as $alternativeName => $criteria) { + if ( + Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && + $type->getClassName() === $criteria['type'] && + $criteria['groups'] == $model->getGroups() + ) { + return $alternativeName; + } + } + + return null; + } + private function getTypeShortName(Type $type): string { if (null !== $type->getCollectionValueType()) { diff --git a/Resources/doc/alternative_names.rst b/Resources/doc/alternative_names.rst new file mode 100644 index 0000000..720b7aa --- /dev/null +++ b/Resources/doc/alternative_names.rst @@ -0,0 +1,28 @@ +Alternative Names +================= + +NelmioApiDoc generates automatically the model names but the ``nelmio_api_doc.models.names`` option allows to +customize the names for some models. + +Configuration +------------- + +You can define alternative names for each group and area combination, the last matching rule is used: + +.. code-block:: yaml + + nelmio_api_doc: + models: + names: + - { alias: MainUser, type: App\Entity\User} + - { alias: MainUser_light, type: App\Entity\User, groups: [light] } + - { alias: MainUser_secret, type: App\Entity\User, areas: [private] } + - { alias: MainUser, type: App\Entity\User, groups: [standard], areas: [private] } + + +In this case the class ``App\Entity\User`` will be aliased into: + +- ``MainUser`` when no more detailed rules are specified +- ``MainUser_light`` when the group is equal to ``light`` +- ``MainUser_secret`` for the ``private`` area +- ``MainUser`` for the ``private`` area when the group is equal to ``standard`` diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index 957a163..a6cac48 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -302,6 +302,7 @@ If you need more complex features, take a look at: :maxdepth: 1 areas + alternative_names faq .. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 14203ac..f354465 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -37,6 +37,74 @@ class ConfigurationTest extends TestCase $this->assertSame($areas, $config['areas']); } + public function testAlternativeNames() + { + $processor = new Processor(); + $config = $processor->processConfiguration(new Configuration(), [[ + 'models' => [ + 'names' => [ + [ + 'alias' => 'Foo1', + 'type' => 'App\Foo', + 'groups' => ['group'], + ], + [ + 'alias' => 'Foo2', + 'type' => 'App\Foo', + 'groups' => [], + ], + [ + 'alias' => 'Foo3', + 'type' => 'App\Foo', + ], + [ + 'alias' => 'Foo4', + 'type' => 'App\Foo', + 'groups' => ['group'], + 'areas' => ['internal'], + ], + [ + 'alias' => 'Foo1', + 'type' => 'App\Foo', + 'areas' => ['internal'], + ], + ], + ], + ]]); + $this->assertEquals([ + [ + 'alias' => 'Foo1', + 'type' => 'App\Foo', + 'groups' => ['group'], + 'areas' => [], + ], + [ + 'alias' => 'Foo2', + 'type' => 'App\Foo', + 'groups' => [], + 'areas' => [], + ], + [ + 'alias' => 'Foo3', + 'type' => 'App\Foo', + 'groups' => [], + 'areas' => [], + ], + [ + 'alias' => 'Foo4', + 'type' => 'App\\Foo', + 'groups' => ['group'], + 'areas' => ['internal'], + ], + [ + 'alias' => 'Foo1', + 'type' => 'App\\Foo', + 'groups' => [], + 'areas' => ['internal'], + ], + ], $config['models']['names']); + } + /** * @group legacy * @expectedException \InvalidArgumentException diff --git a/Tests/DependencyInjection/NelmioApiDocExtensionTest.php b/Tests/DependencyInjection/NelmioApiDocExtensionTest.php index 8fce626..a2b3a25 100644 --- a/Tests/DependencyInjection/NelmioApiDocExtensionTest.php +++ b/Tests/DependencyInjection/NelmioApiDocExtensionTest.php @@ -17,6 +17,74 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; class NelmioApiDocExtensionTest extends TestCase { + public function testNameAliasesArePassedToModelRegistry() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.bundles', []); + $extension = new NelmioApiDocExtension(); + $extension->load([[ + 'areas' => [ + 'default' => ['path_patterns' => ['/foo'], 'host_patterns' => []], + 'commercial' => ['path_patterns' => ['/internal'], 'host_patterns' => []], + ], + 'models' => [ + 'names' => [ + [ // Test1 alias for all the areas + 'alias' => 'Test1', + 'type' => 'App\Test', + ], + [ // Foo1 alias for all the areas + 'alias' => 'Foo1', + 'type' => 'App\Foo', + ], + [ // overwrite Foo1 alias for all the commercial area + 'alias' => 'Foo1', + 'type' => 'App\Bar', + 'areas' => ['commercial'], + ], + ], + ], + ]], $container); + + $methodCalls = $container->getDefinition('nelmio_api_doc.generator.default')->getMethodCalls(); + $foundMethodCall = false; + foreach ($methodCalls as $methodCall) { + if ('setAlternativeNames' === $methodCall[0]) { + $this->assertEquals([ + 'Foo1' => [ + 'type' => 'App\\Foo', + 'groups' => [], + ], + 'Test1' => [ + 'type' => 'App\\Test', + 'groups' => [], + ], + ], $methodCall[1][0]); + $foundMethodCall = true; + } + } + $this->assertTrue($foundMethodCall); + + $methodCalls = $container->getDefinition('nelmio_api_doc.generator.commercial')->getMethodCalls(); + $foundMethodCall = false; + foreach ($methodCalls as $methodCall) { + if ('setAlternativeNames' === $methodCall[0]) { + $this->assertEquals([ + 'Foo1' => [ + 'type' => 'App\\Bar', + 'groups' => [], + ], + 'Test1' => [ + 'type' => 'App\\Test', + 'groups' => [], + ], + ], $methodCall[1][0]); + $foundMethodCall = true; + } + } + $this->assertTrue($foundMethodCall); + } + public function testMergesRootKeysFromMultipleConfigurations() { $container = new ContainerBuilder(); diff --git a/Tests/Model/ModelRegistryTest.php b/Tests/Model/ModelRegistryTest.php index 56e900e..5f8769c 100644 --- a/Tests/Model/ModelRegistryTest.php +++ b/Tests/Model/ModelRegistryTest.php @@ -11,7 +11,6 @@ namespace Nelmio\ApiDocBundle\Tests\Model; -use EXSyst\Component\Swagger\Schema; use EXSyst\Component\Swagger\Swagger; use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\Model\ModelRegistry; @@ -20,6 +19,79 @@ use Symfony\Component\PropertyInfo\Type; class ModelRegistryTest extends TestCase { + public function testNameAliasingNotAppliedForCollections() + { + $alternativeNames = [ + 'Foo1' => [ + 'type' => self::class, + 'groups' => ['group1'], + ], + ]; + $registry = new ModelRegistry([], new Swagger(), $alternativeNames); + $type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true); + + $this->assertEquals('#/definitions/array', $registry->register(new Model($type, ['group1']))); + } + + /** + * @dataProvider getNameAlternatives + * + * @param $expected + */ + public function testNameAliasingForObjects(string $expected, $groups, array $alternativeNames) + { + $registry = new ModelRegistry([], new Swagger(), $alternativeNames); + $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class); + + $this->assertEquals($expected, $registry->register(new Model($type, $groups))); + } + + public function getNameAlternatives() + { + return [ + [ + '#/definitions/ModelRegistryTest', + null, + [ + 'Foo1' => [ + 'type' => self::class, + 'groups' => ['group1'], + ], + ], + ], + [ + '#/definitions/Foo1', + ['group1'], + [ + 'Foo1' => [ + 'type' => self::class, + 'groups' => ['group1'], + ], + ], + ], + [ + '#/definitions/Foo1', + ['group1', 'group2'], + [ + 'Foo1' => [ + 'type' => self::class, + 'groups' => ['group1', 'group2'], + ], + ], + ], + [ + '#/definitions/Foo1', + null, + [ + 'Foo1' => [ + 'type' => self::class, + 'groups' => [], + ], + ], + ], + ]; + } + /** * @dataProvider unsupportedTypesProvider */