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
This commit is contained in:
Asmir Mustafic 2018-06-10 09:56:38 +02:00 committed by Guilhem N
parent cf0857af64
commit ab005c4129
9 changed files with 311 additions and 4 deletions

View File

@ -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);

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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()) {

View File

@ -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``

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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
*/