From a5a13501e23aedb833c740598c506a583b27e3ba Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Thu, 7 Aug 2014 12:06:04 -0700 Subject: [PATCH] Alternate model naming strategy. --- DependencyInjection/Configuration.php | 1 + DependencyInjection/NelmioApiDocExtension.php | 1 + .../SwaggerConfigCompilerPass.php | 4 + Formatter/SwaggerFormatter.php | 176 ++----------- Swagger/ModelRegistry.php | 244 ++++++++++++++++++ 5 files changed, 273 insertions(+), 153 deletions(-) create mode 100644 Swagger/ModelRegistry.php diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 36af70a..09c776d 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -137,6 +137,7 @@ class Configuration implements ConfigurationInterface ->arrayNode('swagger') ->addDefaultsIfNotSet() ->children() + ->scalarNode('model_naming_strategy')->defaultValue('dot_notation')->end() ->scalarNode('api_base_path')->defaultValue('/api')->end() ->scalarNode('swagger_version')->defaultValue('1.2')->end() ->scalarNode('api_version')->defaultValue('0.1')->end() diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 87da66e..63e45c8 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -64,6 +64,7 @@ class NelmioApiDocExtension extends Extension $container->setParameter('nelmio_api_doc.swagger.swagger_version', $config['swagger']['swagger_version']); $container->setParameter('nelmio_api_doc.swagger.api_version', $config['swagger']['api_version']); $container->setParameter('nelmio_api_doc.swagger.info', $config['swagger']['info']); + $container->setParameter('nelmio_api_doc.swagger.model_naming_strategy', $config['swagger']['model_naming_strategy']); if ($config['cache']['enabled'] === true) { $arguments = $container->getDefinition('nelmio_api_doc.extractor.api_doc_extractor')->getArguments(); diff --git a/DependencyInjection/SwaggerConfigCompilerPass.php b/DependencyInjection/SwaggerConfigCompilerPass.php index 9e41910..16c4e9c 100644 --- a/DependencyInjection/SwaggerConfigCompilerPass.php +++ b/DependencyInjection/SwaggerConfigCompilerPass.php @@ -47,6 +47,10 @@ class SwaggerConfigCompilerPass implements CompilerPassInterface $authentication = $container->getParameter('nelmio_api_doc.sandbox.authentication'); + $formatter->setArguments(array( + $container->getParameter('nelmio_api_doc.swagger.model_naming_strategy'), + )); + if ($authentication !== null) { $formatter->addMethodCall('setAuthenticationConfig', array($authentication)); } diff --git a/Formatter/SwaggerFormatter.php b/Formatter/SwaggerFormatter.php index 4f8aab1..02f56ad 100644 --- a/Formatter/SwaggerFormatter.php +++ b/Formatter/SwaggerFormatter.php @@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\Formatter; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\DataTypes; +use Nelmio\ApiDocBundle\Swagger\ModelRegistry; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Router; use Symfony\Component\Routing\RouterInterface; @@ -54,6 +55,16 @@ class SwaggerFormatter implements FormatterInterface DataTypes::DATETIME => 'date-time', ); + /** + * @var \Nelmio\ApiDocBundle\Swagger\ModelRegistry + */ + protected $modelRegistry; + + public function __construct($namingStategy) + { + $this->modelRegistry = new ModelRegistry($namingStategy); + } + /** * @var array */ @@ -192,8 +203,6 @@ class SwaggerFormatter implements FormatterInterface $apiBag = array(); - $models = array(); - foreach ($collection as $item) { @@ -252,10 +261,7 @@ class SwaggerFormatter implements FormatterInterface $data = $apiDoc->toArray(); if (isset($data['parameters'])) { - $parameters = array_merge($parameters, $this->deriveParameters($data['parameters'], - $models, - $input['paramType'] - )); + $parameters = array_merge($parameters, $this->deriveParameters($data['parameters'], $input['paramType'])); } $responseMap = $apiDoc->getParsedResponseMap(); @@ -273,7 +279,7 @@ class SwaggerFormatter implements FormatterInterface $responseModel = array( 'code' => $statusCode, 'message' => $message, - 'responseModel' => $this->registerModel($prop['type']['class'], $prop['model'], '', $models), + 'responseModel' => $this->registerModel($prop['type']['class'], $prop['model'], ''), ); $responseMessages[$statusCode] = $responseModel; } @@ -315,7 +321,7 @@ class SwaggerFormatter implements FormatterInterface ); } - $apiDeclaration['models'] = $models; + $apiDeclaration['models'] = $this->modelRegistry->getModels(); return $apiDeclaration; } @@ -374,7 +380,7 @@ class SwaggerFormatter implements FormatterInterface * * @return array */ - protected function deriveParameters(array $input, array &$models, $paramType = 'form') + protected function deriveParameters(array $input, $paramType = 'form') { $parameters = array(); @@ -403,8 +409,7 @@ class SwaggerFormatter implements FormatterInterface $this->registerModel( $prop['subType'], isset($prop['children']) ? $prop['children'] : null, - $prop['description'] ?: $prop['dataType'], - $models + $prop['description'] ?: $prop['dataType'] ); break; @@ -414,8 +419,7 @@ class SwaggerFormatter implements FormatterInterface $ref = $this->registerModel( $prop['subType'], isset($prop['children']) ? $prop['children'] : null, - $prop['description'] ?: $prop['dataType'], - $models + $prop['description'] ?: $prop['dataType'] ); $items = array( '$ref' => $ref, @@ -477,150 +481,16 @@ class SwaggerFormatter implements FormatterInterface /** * Registers a model into the model array. Returns a unique identifier for the model to be used in `$ref` properties. * - * @param $className - * @param array $parameters + * @param $className + * @param array $parameters * @param string $description - * @param $models + * + * @internal param $models * @return mixed */ - public function registerModel($className, array $parameters = null, $description = '', &$models) + public function registerModel($className, array $parameters = null, $description = '') { - if (isset ($models[$className])) { - return $models[$className]['id']; - } - - /* - * Converts \Fully\Qualified\Class\Name to Fully.Qualified.Class.Name - */ - $id = preg_replace('#(\\\|[^A-Za-z0-9])#', '.', $className); - //Replace duplicate dots. - $id = preg_replace('/\.+/', '.', $id); - //Replace trailing dots. - $id = preg_replace('/^\./', '', $id); - - $model = array( - 'id' => $id, - 'description' => $description, - ); - - if (is_array($parameters)) { - - $required = array(); - $properties = array(); - - foreach ($parameters as $name => $prop) { - - $subParam = array(); - - if ($prop['actualType'] === DataTypes::MODEL) { - - $subParam['$ref'] = $this->registerModel( - $prop['subType'], - isset($prop['children']) ? $prop['children'] : null, - $prop['description'] ?: $prop['dataType'], - $models - ); - - } else { - - $type = null; - $format = null; - $items = null; - $enum = null; - $ref = null; - - if (isset($this->typeMap[$prop['actualType']])) { - $type = $this->typeMap[$prop['actualType']]; - } else{ - - switch ($prop['actualType']) { - case DataTypes::ENUM: - $type = 'string'; - if (isset($prop['format'])) { - $enum = array_keys(json_decode($prop['format'], true)); - } - break; - - case DataTypes::COLLECTION: - $type = 'array'; - - if ($prop['subType'] === DataTypes::MODEL) { - - } else { - - if ($prop['subType'] === null - || isset($this->typeMap[$prop['subType']])) { - $items = array( - 'type' => 'string', - ); - } elseif (!isset($this->typeMap[$prop['subType']])) { - $items = array( - '$ref' => - $this->registerModel( - $prop['subType'], - isset($prop['children']) ? $prop['children'] : null, - $prop['description'] ?: $prop['dataType'], - $models - ) - ); - } - } - /* @TODO: Handle recursion if subtype is a model. */ - break; - - case DataTypes::MODEL: - $ref = $this->registerModel( - $prop['subType'], - isset($prop['children']) ? $prop['children'] : null, - $prop['description'] ?: $prop['dataType'], - $models - ); - $type = $ref; - /* @TODO: Handle recursion. */ - break; - } - } - - if (isset($this->formatMap[$prop['actualType']])) { - $format = $this->formatMap[$prop['actualType']]; - } - - $subParam = array( - 'type' => $type, - 'description' => empty($prop['description']) === false ? (string) $prop['description'] : $prop['dataType'], - ); - - if ($format !== null) { - $subParam['format'] = $format; - } - - if ($enum !== null) { - $subParam['enum'] = $enum; - } - - if ($ref !== null) { - $subParam['$ref'] = $ref; - } - - if ($items !== null) { - $subParam['items'] = $items; - } - - if ($prop['required']) { - $required[] = $name; - } - - } - - $properties[$name] = $subParam; - } - - $model['properties'] = $properties; - $model['required'] = $required; - $models[$id] = $model; - } - - return $id; + return $this->modelRegistry->register($className, $parameters, $description); } /** diff --git a/Swagger/ModelRegistry.php b/Swagger/ModelRegistry.php new file mode 100644 index 0000000..eaa98f8 --- /dev/null +++ b/Swagger/ModelRegistry.php @@ -0,0 +1,244 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Nelmio\ApiDocBundle\Swagger; + +use Nelmio\ApiDocBundle\DataTypes; + +/** + * Class ModelRegistry + * + * @author Bez Hermoso + */ +class ModelRegistry +{ + /** + * @var array + */ + protected $namingStrategies = array( + 'dot_notation' => 'nameDotNotation', + 'last_segment_only' => 'nameLastSegmentOnly', + ); + + /** + * @var array + */ + protected $models = array(); + + protected $classes = array(); + + protected $classMap = array(); + + /** + * @var callable + */ + protected $namingStategy; + + protected $typeMap = array( + DataTypes::INTEGER => 'integer', + DataTypes::FLOAT => 'number', + DataTypes::STRING => 'string', + DataTypes::BOOLEAN => 'boolean', + DataTypes::FILE => 'string', + DataTypes::DATE => 'string', + DataTypes::DATETIME => 'string', + ); + + protected $formatMap = array( + DataTypes::INTEGER => 'int32', + DataTypes::FLOAT => 'float', + DataTypes::FILE => 'byte', + DataTypes::DATE => 'date', + DataTypes::DATETIME => 'date-time', + ); + + public function __construct($namingStrategy) + { + if (!isset($this->namingStrategies[$namingStrategy])) { + throw new \InvalidArgumentException(sprintf( + 'Invalid naming strategy. Choose from: %s', + json_encode(array_keys($this->namingStrategies)) + )); + } + + $this->namingStategy = array($this, $this->namingStrategies[$namingStrategy]); + } + + public function register($className, array $parameters = null, $description = '') + { + if (!isset($this->classes[$className])) { + $this->classes[$className] = array(); + } + + $id = call_user_func_array($this->namingStategy, array($className)); + + if (isset($this->models[$id])) { + return $id; + } + + $this->classes[$className][] = $id; + + $model = array( + 'id' => $id, + 'description' => $description, + ); + + if (is_array($parameters)) { + + $required = array(); + $properties = array(); + + foreach ($parameters as $name => $prop) { + + $subParam = array(); + + if ($prop['actualType'] === DataTypes::MODEL) { + + $subParam['$ref'] = $this->register( + $prop['subType'], + isset($prop['children']) ? $prop['children'] : null, + $prop['description'] ?: $prop['dataType'] + ); + + } else { + + $type = null; + $format = null; + $items = null; + $enum = null; + $ref = null; + + if (isset($this->typeMap[$prop['actualType']])) { + + $type = $this->typeMap[$prop['actualType']]; + + } else { + + switch ($prop['actualType']) { + case DataTypes::ENUM: + $type = 'string'; + if (isset($prop['format'])) { + $enum = array_keys(json_decode($prop['format'], true)); + } + break; + + case DataTypes::COLLECTION: + $type = 'array'; + + if ($prop['subType'] === DataTypes::MODEL) { + + } else { + + if ($prop['subType'] === null + || isset($this->typeMap[$prop['subType']])) { + $items = array( + 'type' => 'string', + ); + } elseif (!isset($this->typeMap[$prop['subType']])) { + $items = array( + '$ref' => + $this->register( + $prop['subType'], + isset($prop['children']) ? $prop['children'] : null, + $prop['description'] ?: $prop['dataType'] + ) + ); + } + } + /* @TODO: Handle recursion if subtype is a model. */ + break; + + case DataTypes::MODEL: + $ref = $this->register( + $prop['subType'], + isset($prop['children']) ? $prop['children'] : null, + $prop['description'] ?: $prop['dataType'] + ); + + $type = $ref; + /* @TODO: Handle recursion. */ + break; + } + } + + if (isset($this->formatMap[$prop['actualType']])) { + $format = $this->formatMap[$prop['actualType']]; + } + + $subParam = array( + 'type' => $type, + 'description' => empty($prop['description']) === false ? (string) $prop['description'] : $prop['dataType'], + ); + + if ($format !== null) { + $subParam['format'] = $format; + } + + if ($enum !== null) { + $subParam['enum'] = $enum; + } + + if ($ref !== null) { + $subParam['$ref'] = $ref; + } + + if ($items !== null) { + $subParam['items'] = $items; + } + + if ($prop['required']) { + $required[] = $name; + } + + } + + $properties[$name] = $subParam; + } + + $model['properties'] = $properties; + $model['required'] = $required; + $this->models[$id] = $model; + } + + return $id; + + } + + public function nameDotNotation($className) + { + /* + * Converts \Fully\Qualified\Class\Name to Fully.Qualified.Class.Name + */ + $id = preg_replace('#(\\\|[^A-Za-z0-9])#', '.', $className); + //Replace duplicate dots. + $id = preg_replace('/\.+/', '.', $id); + //Replace trailing dots. + $id = preg_replace('/^\./', '', $id); + + return $id; + + } + + public function nameLastSegmentOnly($className) + { + /* + * Converts \Fully\Qualified\ClassName to ClassName + */ + $segments = explode('\\', $className); + $id = end($segments); + + return $id; + } + + public function getModels() + { + return $this->models; + } +}