From 6f85aed33c0130101054680c29017fd8bfde43cc Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Tue, 17 Jun 2014 17:05:00 -0700 Subject: [PATCH 01/11] Swagger support: Unified data types [actualType and subType] Updated tests. JMS parsing fixes; updated {Validator,FormType}Parser, FOSRestHandler, and AbstractFormatter, and updated DataTypes enum. Modified dataType checking. Updated tests. Updated DataTypes enum. Quick fix and added doc comments. CS fixes. Refactored FormTypeParser to produce nested parameters. Updated tests accordingly. Logical and CS fixes. Sub-forms and more tests. Logical and CS fixes. Swagger support: created formatter. Configuration and resourcePath logic update. ApiDoc annotation update. Updated formatter and added tests. Parameter formatting. Added tests for SwaggerFormatter. Added option in annotation, and the corresponding logic for parsing the supplied values and processing them in the formatter. Routing update. Updated tests. Removed unused dependency and updated doc comments. Renamed 'responseModels' to 'responseMap' Update the resource filtering and formatting of response messages. Updated check for 200 response model. Ignore data_class and always use form-type to avoid conflicts. Fix: add 'type' even if '' is specified. Refactored responseMap; added parsedResponseMap. Added tests and updated some. Fix: add 'type' even if '' is specified. Initial commit of command. Finished logic for dumping files. Updated doc comment; added license and added more meaningful class comment. Array of models support. --- Annotation/ApiDoc.php | 71 ++ Command/SwaggerDumpCommand.php | 132 ++++ Controller/ApiDocController.php | 10 + DataTypes.php | 2 +- DependencyInjection/Configuration.php | 19 + DependencyInjection/NelmioApiDocExtension.php | 6 + .../SwaggerConfigCompilerPass.php | 43 ++ Extractor/ApiDocExtractor.php | 43 +- Formatter/SwaggerFormatter.php | 595 ++++++++++++++ NelmioApiDocBundle.php | 2 + Resources/config/formatters.xml | 2 + Resources/config/routing.yml | 2 +- Resources/config/swagger_routing.yml | 11 + Tests/Annotation/ApiDocTest.php | 37 + Tests/Extractor/ApiDocExtractorTest.php | 15 +- .../Controller/ResourceController.php | 85 ++ Tests/Fixtures/Form/SimpleType.php | 1 + Tests/Fixtures/app/config/default.yml | 11 + Tests/Fixtures/app/config/routing.yml | 42 + Tests/Formatter/MarkdownFormatterTest.php | 255 +++++- Tests/Formatter/SimpleFormatterTest.php | 627 +++++++++++++-- Tests/Formatter/SwaggerFormatterTest.php | 726 ++++++++++++++++++ 22 files changed, 2654 insertions(+), 83 deletions(-) create mode 100644 Command/SwaggerDumpCommand.php create mode 100644 DependencyInjection/SwaggerConfigCompilerPass.php create mode 100644 Formatter/SwaggerFormatter.php create mode 100644 Resources/config/swagger_routing.yml create mode 100644 Tests/Fixtures/Controller/ResourceController.php create mode 100644 Tests/Formatter/SwaggerFormatterTest.php diff --git a/Annotation/ApiDoc.php b/Annotation/ApiDoc.php index 3680ec5..90fc24a 100644 --- a/Annotation/ApiDoc.php +++ b/Annotation/ApiDoc.php @@ -135,6 +135,21 @@ class ApiDoc */ private $statusCodes = array(); + /** + * @var string|null + */ + private $resourceDescription = null; + + /** + * @var array + */ + private $responseMap = array(); + + /** + * @var array + */ + private $parsedResponseMap = array(); + /** * @var array */ @@ -241,6 +256,17 @@ class ApiDoc if (isset($data['https'])) { $this->https = $data['https']; } + + if (isset($data['resourceDescription'])) { + $this->resourceDescription = $data['resourceDescription']; + } + + if (isset($data['responseMap'])) { + $this->responseMap = $data['responseMap']; + if (isset($this->responseMap[200])) { + $this->output = $this->responseMap[200]; + } + } } /** @@ -606,6 +632,10 @@ class ApiDoc $data['tags'] = $tags; } + if ($resourceDescription = $this->resourceDescription) { + $data['resourceDescription'] = $resourceDescription; + } + $data['https'] = $this->https; $data['authentication'] = $this->authentication; $data['authenticationRoles'] = $this->authenticationRoles; @@ -613,4 +643,45 @@ class ApiDoc return $data; } + + /** + * @return null|string + */ + public function getResourceDescription() + { + return $this->resourceDescription; + } + + /** + * @return array + */ + public function getResponseMap() + { + if (!isset($this->responseMap[200]) && null !== $this->output) { + $this->responseMap[200] = $this->output; + } + + return $this->responseMap; + } + + /** + * @return array + */ + public function getParsedResponseMap() + { + return $this->parsedResponseMap; + } + + /** + * @param $model + * @param $type + * @param int $statusCode + */ + public function setResponseForStatusCode($model, $type, $statusCode = 200) + { + $this->parsedResponseMap[$statusCode] = array('type' => $type, 'model' => $model); + if ($statusCode == 200 && $this->response !== $model) { + $this->response = $model; + } + } } diff --git a/Command/SwaggerDumpCommand.php b/Command/SwaggerDumpCommand.php new file mode 100644 index 0000000..6b9e93d --- /dev/null +++ b/Command/SwaggerDumpCommand.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Nelmio\ApiDocBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Filesystem; + + +/** + * Symfony2 command to dump Swagger-compliant JSON files. + * + * @author Bez Hermoso + */ +class SwaggerDumpCommand extends ContainerAwareCommand +{ + protected function configure() + { + $this + ->setDescription('Dump Swagger-compliant JSON files.') + ->addOption('resource', '', InputOption::VALUE_OPTIONAL, 'A specific resource API declaration to dump.') + ->addOption('all', '', InputOption::VALUE_NONE, 'Dump resource list and all API declarations.') + ->addOption('list-only', '', InputOption::VALUE_NONE, 'Dump resource list only.') + ->addArgument('destination', InputOption::VALUE_REQUIRED, 'Directory to dump JSON files in.', null) + ->setName('api:swagger:dump'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $container = $this->getContainer(); + $destination = $input->getArgument('destination'); + + $rootDir = $container->get('kernel')->getRootDir(); + $rootDir = $rootDir . '/..'; + + if (null === $destination) { + $destination = realpath($rootDir . '/' . $destination); + } + + $fs = new Filesystem(); + + if (!$fs->exists($destination)) { + $fs->mkdir($destination); + } + + $destination = realpath($destination); + + if ($input->getOption('all') && $input->getOption('resource')) { + throw new \RuntimeException('Cannot selectively dump a resource with the --all flag.'); + } + + if ($input->getOption('list-only') && $input->getOption('resource')) { + throw new \RuntimeException('Cannot selectively dump a resource with the --list-only flag.'); + } + + if ($input->getOption('all') && $input->getOption('list-only')) { + throw new \RuntimeException('Cannot selectively dump resource list with the --all flag.'); + } + + $output->writeln(''); + $output->writeln('Reading annotations...'); + $extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor'); + $data = $extractor->all(); + + if ($input->getOption('list-only')) { + $this->dumpResourceList($destination, $data, $output); + } + + if (false != ($resource = $input->getOption('resource'))) { + $this->dumpApiDeclaration($destination, $data, $resource, $output); + } + + if ($input->getOption('all')) { + $formatter = $container->get('nelmio_api_doc.formatter.swagger_formatter'); + $this->dumpResourceList($destination, $data, $output); + $list = $formatter->format($data); + foreach ($list['apis'] as $api) { + $this->dumpApiDeclaration($destination, $data, substr($api['path'], 1), $output); + } + } + } + + protected function dumpResourceList($destination, array $data, OutputInterface $output) + { + $container = $this->getContainer(); + $formatter = $container->get('nelmio_api_doc.formatter.swagger_formatter'); + + $list = $formatter->format($data); + + $fs = new Filesystem(); + $path = $destination . '/api-docs.json'; + + $string = sprintf('Dump resource list to %s: ', $path); + try { + $fs->dumpFile($path, json_encode($list)); + } catch (IOException $e) { + $output->writeln($string . ' NOT OK'); + } + $output->writeln($string . 'OK'); + } + + protected function dumpApiDeclaration($destination, array $data, $resource, OutputInterface $output) + { + $container = $this->getContainer(); + $formatter = $container->get('nelmio_api_doc.formatter.swagger_formatter'); + + $list = $formatter->format($data, '/' . $resource); + + $fs = new Filesystem(); + $path = sprintf($destination . '/%s.json', $resource); + + $string = sprintf('Dump API declaration to %s: ', $path); + try { + $fs->dumpFile($path, json_encode($list)); + } catch (IOException $e) { + $output->writeln($string . ' NOT OK'); + } + $output->writeln($string . 'OK'); + } +} \ No newline at end of file diff --git a/Controller/ApiDocController.php b/Controller/ApiDocController.php index 8ea71aa..1d033b3 100644 --- a/Controller/ApiDocController.php +++ b/Controller/ApiDocController.php @@ -12,6 +12,8 @@ namespace Nelmio\ApiDocBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class ApiDocController extends Controller @@ -23,4 +25,12 @@ class ApiDocController extends Controller return new Response($htmlContent, 200, array('Content-Type' => 'text/html')); } + + public function swaggerAction(Request $request, $resource = null) + { + $docs = $this->get('nelmio_api_doc.extractor.api_doc_extractor')->all(); + $formatter = $this->get('nelmio_api_doc.formatter.swagger_formatter'); + $spec = $formatter->format($docs, $resource ? '/' . $resource : null); + return new JsonResponse($spec); + } } diff --git a/DataTypes.php b/DataTypes.php index 1df2190..60a6a4b 100644 --- a/DataTypes.php +++ b/DataTypes.php @@ -43,7 +43,7 @@ class DataTypes /** * Returns true if the supplied `actualType` value is considered a primitive type. Returns false, otherwise. * - * @param string $type + * @param string $type * @return bool */ public static function isPrimitive($type) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index bd5c8fc..bd412ed 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -99,6 +99,25 @@ class Configuration implements ConfigurationInterface ->end() ->end() ->end() + ->arrayNode('swagger') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('api_base_path')->defaultValue('/api')->end() + ->scalarNode('swagger_version')->defaultValue('1.2')->end() + ->scalarNode('api_version')->defaultValue('0.1')->end() + ->arrayNode('info') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('title')->defaultValue('Symfony2')->end() + ->scalarNode('description')->defaultValue('My awesome Symfony2 app!')->end() + ->scalarNode('TermsOfServiceUrl')->defaultValue(null)->end() + ->scalarNode('contact')->defaultValue(null)->end() + ->scalarNode('license')->defaultValue(null)->end() + ->scalarNode('licenseUrl')->defaultValue(null)->end() + ->end() + ->end() + ->end() + ->end() ->end(); return $treeBuilder; diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index f5b3c78..dabe480 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -57,6 +57,12 @@ class NelmioApiDocExtension extends Extension if (!interface_exists('\Symfony\Component\Validator\MetadataFactoryInterface')) { $container->setParameter('nelmio_api_doc.parser.validation_parser.class', 'Nelmio\ApiDocBundle\Parser\ValidationParserLegacy'); } + + $container->setParameter('nelmio_api_doc.swagger.base_path', $config['swagger']['api_base_path']); + $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']); + } /** diff --git a/DependencyInjection/SwaggerConfigCompilerPass.php b/DependencyInjection/SwaggerConfigCompilerPass.php new file mode 100644 index 0000000..e79e6bf --- /dev/null +++ b/DependencyInjection/SwaggerConfigCompilerPass.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Nelmio\ApiDocBundle\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + + +/** + * Compiler pass that configures the SwaggerFormatter instance. + * + * @author Bez Hermoso + */ +class SwaggerConfigCompilerPass implements CompilerPassInterface +{ + + /** + * You can modify the container here before it is dumped to PHP code. + * + * @param ContainerBuilder $container + * + * @api + */ + public function process(ContainerBuilder $container) + { + $formatter = $container->getDefinition('nelmio_api_doc.formatter.swagger_formatter'); + + $formatter->addMethodCall('setBasePath', array($container->getParameter('nelmio_api_doc.swagger.base_path'))); + $formatter->addMethodCall('setApiVersion', array($container->getParameter('nelmio_api_doc.swagger.api_version'))); + $formatter->addMethodCall('setSwaggerVersion', array($container->getParameter('nelmio_api_doc.swagger.swagger_version'))); + $formatter->addMethodCall('setInfo', array($container->getParameter('nelmio_api_doc.swagger.info'))); + + } +} \ No newline at end of file diff --git a/Extractor/ApiDocExtractor.php b/Extractor/ApiDocExtractor.php index 03f272f..8249ab0 100644 --- a/Extractor/ApiDocExtractor.php +++ b/Extractor/ApiDocExtractor.php @@ -317,6 +317,36 @@ class ApiDocExtractor $response = $this->generateHumanReadableTypes($response); $annotation->setResponse($response); + $annotation->setResponseForStatusCode($response, $normalizedOutput, 200); + } + + if (count($annotation->getResponseMap()) > 0) { + + foreach ($annotation->getResponseMap() as $code => $modelName) { + + if ('200' === (string) $code && isset($modelName['type']) && isset($modelName['model'])) { + /* + * Model was already parsed as the default `output` for this ApiDoc. + */ + continue; + } + + $normalizedModel = $this->normalizeClassParameter($modelName); + + $parameters = array(); + foreach ($this->getParsers($normalizedModel) as $parser) { + if ($parser->supports($normalizedModel)) { + $parameters = $this->mergeParameters($parameters, $parser->parse($normalizedModel)); + } + } + + $parameters = $this->clearClasses($parameters); + $parameters = $this->generateHumanReadableTypes($parameters); + + $annotation->setResponseForStatusCode($parameters, $normalizedModel, $code); + + } + } return $annotation; @@ -455,16 +485,20 @@ class ApiDocExtractor /** * Creates a human-readable version of the `actualType`. `subType` is taken into account. * - * @param string $actualType - * @param string $subType + * @param string $actualType + * @param string $subType * @return string */ protected function generateHumanReadableType($actualType, $subType) { if ($actualType == DataTypes::MODEL) { - $parts = explode('\\', $subType); - return sprintf('object (%s)', end($parts)); + if (class_exists($subType)) { + $parts = explode('\\', $subType); + return sprintf('object (%s)', end($parts)); + } + + return sprintf('object (%s)', $subType); } if ($actualType == DataTypes::COLLECTION) { @@ -475,7 +509,6 @@ class ApiDocExtractor if (class_exists($subType)) { $parts = explode('\\', $subType); - return sprintf('array of objects (%s)', end($parts)); } diff --git a/Formatter/SwaggerFormatter.php b/Formatter/SwaggerFormatter.php new file mode 100644 index 0000000..65248eb --- /dev/null +++ b/Formatter/SwaggerFormatter.php @@ -0,0 +1,595 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Nelmio\ApiDocBundle\Formatter; + + +use Nelmio\ApiDocBundle\Annotation\ApiDoc; +use Nelmio\ApiDocBundle\DataTypes; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Router; +use Symfony\Component\Routing\RouterInterface; + +/** + * Produces Swagger-compliant resource lists and API declarations as defined here: + * https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md + * + * This formatter produces an array. Therefore output still needs to be `json_encode`d before passing on as HTTP response. + * + * @author Bezalel Hermoso + */ +class SwaggerFormatter implements FormatterInterface +{ + protected $basePath; + + protected $apiVersion; + + protected $swaggerVersion; + + protected $info = array(); + + 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', + ); + + /** + * Format a collection of documentation data. + * + * If resource is provided, an API declaration for that resource is produced. Otherwise, a resource listing is returned. + * + * @param array|ApiDoc[] $collection + * @param null|string $resource + * @return string|array + */ + public function format(array $collection, $resource = null) + { + if ($resource === null) { + return $this->produceResourceListing($collection); + } else { + return $this->produceApiDeclaration($collection, $resource); + } + } + + /** + * Formats the collection into Swagger-compliant output. + * + * @param array $collection + * @return array + */ + public function produceResourceListing(array $collection) + { + $resourceList = array( + 'swaggerVersion' => (string) $this->swaggerVersion, + 'apis' => array(), + 'apiVersion' => (string) $this->apiVersion, + 'info' => $this->getInfo(), + 'authorizations' => $this->getAuthorizations(), + ); + + $apis = &$resourceList['apis']; + + foreach ($collection as $item) { + + /** @var $apiDoc ApiDoc */ + $apiDoc = $item['annotation']; + $resource = $item['resource']; + + if (!$apiDoc->isResource()) { + continue; + } + + $subPath = $this->stripBasePath($resource); + $normalizedName = $this->normalizeResourcePath($subPath); + + $apis[] = array( + 'path' => '/' . $normalizedName, + 'description' => $apiDoc->getResourceDescription(), + ); + + } + + return $resourceList; + } + + protected function getAuthorizations() + { + return array(); + } + + /** + * @return array + */ + protected function getInfo() + { + return $this->info; + } + + /** + * Format documentation data for one route. + * + * @param ApiDoc $annotation + * return string|array + */ + public function formatOne(ApiDoc $annotation) + { + // TODO: Implement formatOne() method. + } + + /** + * Formats collection to produce a Swagger-compliant API declaration for the given resource. + * + * @param array $collection + * @param string $resource + * @return array + */ + private function produceApiDeclaration(array $collection, $resource) + { + + $apiDeclaration = array( + 'swaggerVersion' => (string) $this->swaggerVersion, + 'apiVersion' => (string) $this->apiVersion, + 'basePath' => $this->basePath, + 'resourcePath' => $resource, + 'apis' => array(), + 'models' => array(), + 'produces' => array(), + 'consumes' => array(), + 'authorizations' => array(), + ); + + $main = null; + + $apiBag = array(); + + $models = array(); + + + foreach ($collection as $item) { + + /** @var $apiDoc ApiDoc */ + $apiDoc = $item['annotation']; + $itemResource = $this->stripBasePath($item['resource']); + + $route = $apiDoc->getRoute(); + + $itemResource = $this->normalizeResourcePath($itemResource); + + if ('/' . $itemResource !== $resource) { + continue; + } + + $compiled = $route->compile(); + + $path = $this->stripBasePath($route->getPath()); + + if (!isset($apiBag[$path])) { + $apiBag[$path] = array(); + } + + $parameters = array(); + $responseMessages = array(); + + foreach ($compiled->getPathVariables() as $paramValue) { + $parameter = array( + 'paramType' => 'path', + 'name' => $paramValue, + 'type' => 'string', + 'required' => true, + ); + + if ($paramValue === '_format' && false != ($req = $route->getRequirement('_format'))) { + $parameter['enum'] = explode('|', $req); + } + + $parameters[] = $parameter; + } + + if (isset($data['filters'])) { + $parameters = array_merge($parameters, $this->deriveQueryParameters($data['filters'])); + } + + $data = $apiDoc->toArray(); + + if (isset($data['parameters'])) { + $parameters = array_merge($parameters, $this->deriveParameters($data['parameters'], $models)); + } + + $responseMap = $apiDoc->getParsedResponseMap(); + + $statusMessages = isset($data['statusCodes']) ? $data['statusCodes'] : array(); + + foreach ($responseMap as $statusCode => $prop) { + + if (isset($statusMessages[$statusCode])) { + $message = is_array($statusMessages[$statusCode]) ? implode('; ', $statusMessages[$statusCode]) : $statusCode[$statusCode]; + } else { + $message = sprintf('See standard HTTP status code reason for %s', $statusCode); + } + + $responseModel = array( + 'code' => $statusCode, + 'message' => $message, + 'responseModel' => $this->registerModel($prop['type']['class'], $prop['model'], '', $models), + ); + $responseMessages[$statusCode] = $responseModel; + } + + $unmappedMessages = array_diff(array_keys($statusMessages), array_keys($responseMessages)); + + foreach ($unmappedMessages as $code) { + $responseMessages[$code] = array( + 'code' => $code, + 'message' => is_array($statusMessages[$code]) ? implode('; ', $statusMessages[$code]) : $statusMessages[$code], + ); + } + + foreach ($apiDoc->getRoute()->getMethods() as $method) { + $operation = array( + 'method' => $method, + 'summary' => $apiDoc->getDescription(), + 'nickname' => $this->generateNickname($method, $itemResource), + 'parameters' => $parameters, + 'responseMessages' => array_values($responseMessages), + ); + + $apiBag[$path][] = $operation; + } + } + + $apiDeclaration['resourcePath'] = $resource; + + foreach ($apiBag as $path => $operations) { + $apiDeclaration['apis'][] = array( + 'path' => $path, + 'operations' => $operations, + ); + } + + $apiDeclaration['models'] = $models; + + return $apiDeclaration; + } + + /** + * Slugify a URL path. Trims out path parameters wrapped in curly brackets. + * + * @param $path + * @return string + */ + protected function normalizeResourcePath($path) + { + $path = preg_replace('/({.*?})/', '', $path); + $path = trim(preg_replace('/[^0-9a-zA-Z]/', '-', $path), '-'); + $path = preg_replace('/-+/', '-', $path); + return $path; + } + + /** + * @param $path + */ + public function setBasePath($path) + { + $this->basePath = $path; + } + + /** + * Formats query parameters to Swagger-compliant form. + * + * @param array $input + * @return array + */ + protected function deriveQueryParameters(array $input) + { + $parameters = array(); + + foreach ($input as $name => $prop) { + $parameters[] = array( + 'paramType' => 'query', + 'name' => $name, + 'type' => isset($this->typeMap[$prop['dataType']]) ? $this->typeMap[$prop['dataType']] : 'string', + ); + } + + return $parameters; + + } + + /** + * Builds a Swagger-compliant parameter list from the provided parameter array. Models are built when necessary. + * + * @param array $input + * @param array $models + * @return array + */ + protected function deriveParameters(array $input, array &$models) + { + + $parameters = array(); + + foreach ($input as $name => $prop) { + + $type = null; + $format = null; + $ref = null; + $enum = null; + $items = null; + + if (isset ($this->typeMap[$prop['actualType']])) { + $type = $this->typeMap[$prop['actualType']]; + } else { + switch ($prop['actualType']) { + case DataTypes::ENUM: + $type = 'string'; + $enum = array_keys(json_decode($prop['format'], true)); + break; + + case DataTypes::MODEL: + $ref = + $this->registerModel( + $prop['subType'], + isset($prop['children']) ? $prop['children'] : null, + $prop['description'] ?: $prop['dataType'], + $models); + break; + } + } + + if (isset($this->formatMap[$prop['actualType']])) { + $format = $this->formatMap[$prop['actualType']]; + } + + if (null === $type && null === $ref) { + /* `type` or `$ref` is required. Continue to next of none of these was determined. */ + continue; + } + + $parameter = array( + 'paramType' => 'form', + 'name' => $name, + ); + + if (null !== $type) { + $parameter['type'] = $type; + } + + if (null !== $ref) { + $parameter['$ref'] = $ref; + $parameter['type'] = $ref; + } + + if (null !== $format) { + $parameter['format'] = $format; + } + + if (is_array($enum) && count($enum) > 0) { + $parameter['enum'] = $enum; + } + + $parameters[] = $parameter; + } + + return $parameters; + } + + /** + * 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 string $description + * @param $models + * @return mixed + */ + public function registerModel($className, array $parameters = null, $description = '', &$models) + { + 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; + } + + /** + * @param mixed $swaggerVersion + */ + public function setSwaggerVersion($swaggerVersion) + { + $this->swaggerVersion = $swaggerVersion; + } + + /** + * @param mixed $apiVersion + */ + public function setApiVersion($apiVersion) + { + $this->apiVersion = $apiVersion; + } + + /** + * @param mixed $info + */ + public function setInfo($info) + { + $this->info = $info; + } + + /** + * Strips the base path from a URL path. + * + * @param $path + * @return mixed + */ + protected function stripBasePath($path) + { + $pattern = sprintf('#%s#', preg_quote($this->basePath)); + $subPath = preg_replace($pattern, '', $path); + return $subPath; + } + + /** + * Generate nicknames based on support HTTP methods and the resource name. + * + * @param $method + * @param $resource + * @return string + */ + protected function generateNickname($method, $resource) + { + $resource = preg_replace('#/^#', '', $resource); + $resource = $this->normalizeResourcePath($resource); + return sprintf('%s_%s', strtolower($method), $resource); + } +} \ No newline at end of file diff --git a/NelmioApiDocBundle.php b/NelmioApiDocBundle.php index a1851e7..7f26e16 100644 --- a/NelmioApiDocBundle.php +++ b/NelmioApiDocBundle.php @@ -2,6 +2,7 @@ namespace Nelmio\ApiDocBundle; +use Nelmio\ApiDocBundle\DependencyInjection\SwaggerConfigCompilerPass; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Nelmio\ApiDocBundle\DependencyInjection\LoadExtractorParsersPass; @@ -17,5 +18,6 @@ class NelmioApiDocBundle extends Bundle $container->addCompilerPass(new LoadExtractorParsersPass()); $container->addCompilerPass(new RegisterExtractorParsersPass()); $container->addCompilerPass(new ExtractorHandlerCompilerPass()); + $container->addCompilerPass(new SwaggerConfigCompilerPass()); } } diff --git a/Resources/config/formatters.xml b/Resources/config/formatters.xml index 5cd711f..ed5ddb7 100644 --- a/Resources/config/formatters.xml +++ b/Resources/config/formatters.xml @@ -8,6 +8,7 @@ Nelmio\ApiDocBundle\Formatter\MarkdownFormatter Nelmio\ApiDocBundle\Formatter\SimpleFormatter Nelmio\ApiDocBundle\Formatter\HtmlFormatter + Nelmio\ApiDocBundle\Formatter\SwaggerFormatter null @@ -56,6 +57,7 @@ %nelmio_api_doc.sandbox.authentication% + diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index 4591631..d0bd03f 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -2,4 +2,4 @@ nelmio_api_doc_index: pattern: / defaults: { _controller: NelmioApiDocBundle:ApiDoc:index } requirements: - _method: GET + _method: GET \ No newline at end of file diff --git a/Resources/config/swagger_routing.yml b/Resources/config/swagger_routing.yml new file mode 100644 index 0000000..c36f6d3 --- /dev/null +++ b/Resources/config/swagger_routing.yml @@ -0,0 +1,11 @@ +nelmio_api_doc_swagger_resource_list: + pattern: / + defaults: { _controller: NelmioApiDocBundle:ApiDoc:swagger } + requirements: + _method: GET + +nelmio_api_doc_swagger_api_declaration: + pattern: /{resource} + defaults: { _controller: NelmioApiDocBundle:ApiDoc:swagger } + requirements: + _method: GET \ No newline at end of file diff --git a/Tests/Annotation/ApiDocTest.php b/Tests/Annotation/ApiDocTest.php index d425042..a3dac24 100644 --- a/Tests/Annotation/ApiDocTest.php +++ b/Tests/Annotation/ApiDocTest.php @@ -319,4 +319,41 @@ class ApiDocTest extends TestCase $this->assertTrue(is_array($array['tags']), 'Tags should be in array'); $this->assertEquals($data['tags'], $array['tags']); } + + public function testAlignmentOfOutputAndResponseModels() + { + $data = array( + 'output' => 'FooBar', + 'responseMap' => array( + 400 => 'Foo\\ValidationErrorCollection', + ), + ); + + $apiDoc = new ApiDoc($data); + + $map = $apiDoc->getResponseMap(); + + $this->assertCount(2, $map); + $this->assertArrayHasKey(200, $map); + $this->assertArrayHasKey(400, $map); + $this->assertEquals($data['output'], $map[200]); + } + + public function testAlignmentOfOutputAndResponseModels2() + { + $data = array( + 'responseMap' => array( + 200 => 'FooBar', + 400 => 'Foo\\ValidationErrorCollection', + ), + ); + + $apiDoc = new ApiDoc($data); + $map = $apiDoc->getResponseMap(); + + $this->assertCount(2, $map); + $this->assertArrayHasKey(200, $map); + $this->assertArrayHasKey(400, $map); + $this->assertEquals($apiDoc->getOutput(), $map[200]); + } } diff --git a/Tests/Extractor/ApiDocExtractorTest.php b/Tests/Extractor/ApiDocExtractorTest.php index 7207c14..abd969f 100644 --- a/Tests/Extractor/ApiDocExtractorTest.php +++ b/Tests/Extractor/ApiDocExtractorTest.php @@ -15,7 +15,7 @@ use Nelmio\ApiDocBundle\Tests\WebTestCase; class ApiDocExtractorTest extends WebTestCase { - const ROUTES_QUANTITY = 25; + const ROUTES_QUANTITY = 31; public function testAll() { @@ -38,39 +38,39 @@ class ApiDocExtractorTest extends WebTestCase $this->assertNotNull($d['resource']); } - $a1 = $data[0]['annotation']; + $a1 = $data[7]['annotation']; $array1 = $a1->toArray(); $this->assertTrue($a1->isResource()); $this->assertEquals('index action', $a1->getDescription()); $this->assertTrue(is_array($array1['filters'])); $this->assertNull($a1->getInput()); - $a1 = $data[1]['annotation']; + $a1 = $data[7]['annotation']; $array1 = $a1->toArray(); $this->assertTrue($a1->isResource()); $this->assertEquals('index action', $a1->getDescription()); $this->assertTrue(is_array($array1['filters'])); $this->assertNull($a1->getInput()); - $a2 = $data[2]['annotation']; + $a2 = $data[8]['annotation']; $array2 = $a2->toArray(); $this->assertFalse($a2->isResource()); $this->assertEquals('create test', $a2->getDescription()); $this->assertFalse(isset($array2['filters'])); $this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput()); - $a2 = $data[3]['annotation']; + $a2 = $data[9]['annotation']; $array2 = $a2->toArray(); $this->assertFalse($a2->isResource()); $this->assertEquals('create test', $a2->getDescription()); $this->assertFalse(isset($array2['filters'])); $this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput()); - $a4 = $data[5]['annotation']; + $a4 = $data[11]['annotation']; $this->assertTrue($a4->isResource()); $this->assertEquals('TestResource', $a4->getResource()); - $a3 = $data['14']['annotation']; + $a3 = $data[20]['annotation']; $this->assertTrue($a3->getHttps()); } @@ -224,6 +224,7 @@ class ApiDocExtractorTest extends WebTestCase $this->assertNotNull($annotation); $output = $annotation->getOutput(); + $parsers = $output['parsers']; $this->assertEquals( "Nelmio\\ApiDocBundle\\Parser\\JmsMetadataParser", diff --git a/Tests/Fixtures/Controller/ResourceController.php b/Tests/Fixtures/Controller/ResourceController.php new file mode 100644 index 0000000..e5d77a4 --- /dev/null +++ b/Tests/Fixtures/Controller/ResourceController.php @@ -0,0 +1,85 @@ + + */ +class ResourceController +{ + /** + * @ApiDoc( + * resource=true, + * resourceDescription="Operations on resource.", + * description="List resources.", + * statusCodes={200 = "Returned on success.", 404 = "Returned if resource cannot be found."} + * ) + */ + public function listResourcesAction() + { + + } + + /** + * @ApiDoc(description="Retrieve a resource by ID.") + */ + public function getResourceAction() + { + + } + + /** + * @ApiDoc(description="Delete a resource by ID.") + */ + public function deleteResourceAction() + { + + } + + /** + * @ApiDoc( + * description="Create a new resource.", + * input={"class" = "Nelmio\ApiDocBundle\Tests\Fixtures\Form\SimpleType", "name" = ""}, + * output="Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested" + * ) + */ + public function createResourceAction() + { + + } + + /** + * @ApiDoc(resource=true, description="List another resource.", resourceDescription="Operations on another resource.") + */ + public function listAnotherResourcesAction() + { + + } + + /** + * @ApiDoc(description="Retrieve another resource by ID.") + */ + public function getAnotherResourceAction() + { + + } + + /** + * @ApiDoc(description="Update a resource bu ID.") + */ + public function updateAnotherResourceAction() + { + + } +} \ No newline at end of file diff --git a/Tests/Fixtures/Form/SimpleType.php b/Tests/Fixtures/Form/SimpleType.php index e79e176..bb7886a 100644 --- a/Tests/Fixtures/Form/SimpleType.php +++ b/Tests/Fixtures/Form/SimpleType.php @@ -1,6 +1,7 @@ get('nelmio_api_doc.formatter.markdown_formatter')->format($data); $expected = <<=0.2 + +circular[until]: + + * type: string + * versions: <=0.3 + +circular[since_and_until]: + + * type: string + * versions: >=0.4,<=0.5 + +parent: + + * type: object (JmsTest) + +parent[foo]: + + * type: string + +parent[bar]: + + * type: DateTime + +parent[number]: + + * type: double + +parent[arr]: + + * type: array + +parent[nested]: + + * type: object (JmsNested) + +parent[nested_array][]: + + * type: array of objects (JmsNested) + +since: + + * type: string + * versions: >=0.2 + +until: + + * type: string + * versions: <=0.3 + +since_and_until: + + * type: string + * versions: >=0.4,<=0.5 + + +### `GET` /api/resources/{id}.{_format} ### + +_Retrieve a resource by ID._ + +#### Requirements #### + +**_format** + + - Requirement: json|xml|html +**id** + + + +### `DELETE` /api/resources/{id}.{_format} ### + +_Delete a resource by ID._ + +#### Requirements #### + +**_format** + + - Requirement: json|xml|html +**id** + + + ## /tests ## ### `GET` /tests.{_format} ### @@ -431,7 +666,7 @@ nested_array[]: **id** - - Requirement: \\d+ + - Requirement: \d+ ### `GET` /z-action-with-deprecated-indicator ### @@ -459,7 +694,7 @@ param1: page: - * Requirement: \\d+ + * Requirement: \d+ * Description: Page of the overview. * Default: 1 @@ -527,14 +762,6 @@ related: * type: object (Test) -related[a]: - - * type: string - -related[b]: - - * type: DateTime - ### `ANY` /z-return-selected-parsers-input ### @@ -592,14 +819,6 @@ number: related: * type: object (Test) - -related[a]: - - * type: string - -related[b]: - - * type: DateTime MARKDOWN; $this->assertEquals($expected, $result); diff --git a/Tests/Formatter/SimpleFormatterTest.php b/Tests/Formatter/SimpleFormatterTest.php index 6c85ff7..f174015 100644 --- a/Tests/Formatter/SimpleFormatterTest.php +++ b/Tests/Formatter/SimpleFormatterTest.php @@ -198,7 +198,6 @@ class SimpleFormatterTest extends WebTestCase 'dataType' => 'string', 'actualType' => DataTypes::STRING, 'subType' => null, - 'default' => null, 'default' => "DefaultTest", 'required' => true, 'description' => '', @@ -622,8 +621,6 @@ And, it supports multilines until the first \'@\' char.', ), ), 'https' => false, - 'description' => 'This method is useful to test if the getDocComment works.', - 'documentation' => "This method is useful to test if the getDocComment works.\nAnd, it supports multilines until the first '@' char.", 'authentication' => false, 'authenticationRoles' => array(), 'deprecated' => false, @@ -1069,39 +1066,21 @@ With multiple lines.', 'subType' => null, 'default' => null, 'required' => null, - 'readonly' => null + 'readonly' => null, ) ) ), 'related' => array( 'dataType' => 'object (Test)', + 'actualType' => DataTypes::MODEL, + 'subType' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test', + 'default' => null, 'readonly' => false, 'required' => false, 'description' => '', 'sinceVersion' => null, 'untilVersion' => null, - 'actualType' => DataTypes::MODEL, - 'subType' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test', - 'default' => null, - 'children' => array( - 'a' => array( - 'dataType' => 'string', - 'format' => '{length: min: foo}, {not blank}', - 'required' => true, - 'readonly' => null, - 'actualType' => DataTypes::STRING, - 'subType' => null, - 'default' => 'nelmio', - ), - 'b' => array( - 'dataType' => 'DateTime', - 'required' => null, - 'readonly' => null, - 'actualType' => DataTypes::DATETIME, - 'subType' => null, - 'default' => null, - ) - ) + 'children' => array(), ) ), 'authenticationRoles' => array(), @@ -1147,7 +1126,6 @@ With multiple lines.', 'dataType' => 'string', 'actualType' => DataTypes::STRING, 'subType' => null, - 'default' => null, 'default' => "DefaultTest", 'required' => true, 'description' => '', @@ -1211,33 +1189,15 @@ With multiple lines.', ), 'related' => array( 'dataType' => 'object (Test)', + 'subType' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test', + 'actualType' => DataTypes::MODEL, + 'default' => null, 'readonly' => false, 'required' => false, 'description' => '', 'sinceVersion' => null, 'untilVersion' => null, - 'actualType' => DataTypes::MODEL, - 'subType' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test', - 'default' => null, - 'children' => array( - 'a' => array( - 'dataType' => 'string', - 'format' => '{length: min: foo}, {not blank}', - 'required' => true, - 'readonly' => null, - 'actualType' => DataTypes::STRING, - 'subType' => null, - 'default' => 'nelmio', - ), - 'b' => array( - 'dataType' => 'DateTime', - 'required' => null, - 'readonly' => null, - 'actualType' => DataTypes::DATETIME, - 'subType' => null, - 'default' => null, - ) - ) + 'children' => array(), ) ), 'authenticationRoles' => array(), @@ -1245,7 +1205,6 @@ With multiple lines.', ), '/tests2' => array( - 0 => array( 'method' => 'POST', 'uri' => '/tests2.{_format}', @@ -1267,7 +1226,6 @@ With multiple lines.', ), '/tests2' => array( - 0 => array( 'method' => 'POST', 'uri' => '/tests2.{_format}', @@ -1299,6 +1257,573 @@ With multiple lines.', 'deprecated' => false, ), ), + '/api/other-resources' => + array( + array( + 'method' => 'GET', + 'uri' => '/api/other-resources.{_format}', + 'description' => 'List another resource.', + 'requirements' => + array( + '_format' => + array( + 'requirement' => 'json|xml|html', + 'dataType' => '', + 'description' => '', + ), + ), + 'resourceDescription' => 'Operations on another resource.', + 'https' => false, + 'authentication' => false, + 'authenticationRoles' => + array(), + 'deprecated' => false, + ), + array( + 'method' => 'PUT|PATCH', + 'uri' => '/api/other-resources/{id}.{_format}', + 'description' => 'Update a resource bu ID.', + 'requirements' => + array( + '_format' => + array( + 'requirement' => 'json|xml|html', + 'dataType' => '', + 'description' => '', + ), + 'id' => + array( + 'requirement' => '', + 'dataType' => '', + 'description' => '', + ), + ), + 'https' => false, + 'authentication' => false, + 'authenticationRoles' => + array(), + 'deprecated' => false, + ), + ), + '/api/resources' => + array( + array( + 'method' => 'GET', + 'uri' => '/api/resources.{_format}', + 'description' => 'List resources.', + 'requirements' => + array( + '_format' => + array( + 'requirement' => 'json|xml|html', + 'dataType' => '', + 'description' => '', + ), + ), + 'statusCodes' => + array( + 200 => + array( + 'Returned on success.', + ), + 404 => + array( + 'Returned if resource cannot be found.', + ), + ), + 'resourceDescription' => 'Operations on resource.', + 'https' => false, + 'authentication' => false, + 'authenticationRoles' => + array(), + 'deprecated' => false, + ), + array( + 'method' => 'POST', + 'uri' => '/api/resources.{_format}', + 'description' => 'Create a new resource.', + 'parameters' => + array( + 'a' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'required' => true, + 'description' => 'Something that describes A.', + 'readonly' => false, + 'default' => null, + ), + 'b' => + array( + 'dataType' => 'float', + 'actualType' => 'float', + 'subType' => NULL, + 'required' => true, + 'description' => '', + 'readonly' => false, + 'default' => null, + ), + 'c' => + array( + 'dataType' => 'choice', + 'actualType' => 'choice', + 'subType' => NULL, + 'required' => true, + 'description' => '', + 'readonly' => false, + 'format' => '{"x":"X","y":"Y","z":"Z"}', + 'default' => null, + ), + 'd' => + array( + 'dataType' => 'datetime', + 'actualType' => 'datetime', + 'subType' => NULL, + 'required' => true, + 'description' => '', + 'readonly' => false, + 'default' => null, + ), + 'e' => + array( + 'dataType' => 'date', + 'actualType' => 'date', + 'subType' => NULL, + 'required' => true, + 'description' => '', + 'readonly' => false, + 'default' => null, + ), + 'g' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'required' => true, + 'description' => '', + 'readonly' => false, + 'default' => null, + ), + ), + 'requirements' => + array( + '_format' => + array( + 'requirement' => 'json|xml|html', + 'dataType' => '', + 'description' => '', + ), + ), + 'response' => + array( + 'foo' => + array( + 'dataType' => 'DateTime', + 'actualType' => 'datetime', + 'default' => null, + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => true, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + ), + 'bar' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'default' => 'baz', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + ), + 'baz' => + array( + 'dataType' => 'array of integers', + 'actualType' => 'collection', + 'subType' => 'integer', + 'required' => false, + 'description' => 'Epic description. + +With multiple lines.', + 'readonly' => false, + 'sinceVersion' => NULL, + 'default' => null, + 'untilVersion' => NULL, + ), + 'circular' => + array( + 'dataType' => 'object (JmsNested)', + 'actualType' => 'model', + 'default' => null, + 'subType' => 'Nelmio\\ApiDocBundle\\Tests\\Fixtures\\Model\\JmsNested', + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'children' => + array( + 'foo' => + array( + 'dataType' => 'DateTime', + 'actualType' => 'datetime', + 'default' => null, + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => true, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + ), + 'bar' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'default' => 'baz', + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + ), + 'baz' => + array( + 'dataType' => 'array of integers', + 'actualType' => 'collection', + 'subType' => 'integer', + 'required' => false, + 'description' => 'Epic description. + +With multiple lines.', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'default' => null, + ), + 'circular' => + array( + 'dataType' => 'object (JmsNested)', + 'actualType' => 'model', + 'default' => null, + 'subType' => 'Nelmio\\ApiDocBundle\\Tests\\Fixtures\\Model\\JmsNested', + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + ), + 'parent' => + array( + 'dataType' => 'object (JmsTest)', + 'actualType' => 'model', + 'default' => null, + 'subType' => 'Nelmio\\ApiDocBundle\\Tests\\Fixtures\\Model\\JmsTest', + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'children' => + array( + 'foo' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'default' => null, + ), + 'bar' => + array( + 'dataType' => 'DateTime', + 'actualType' => 'datetime', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => true, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'default' => null, + ), + 'number' => + array( + 'dataType' => 'double', + 'actualType' => 'float', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'default' => null, + ), + 'arr' => + array( + 'dataType' => 'array', + 'actualType' => 'collection', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'default' => null, + ), + 'nested' => + array( + 'dataType' => 'object (JmsNested)', + 'actualType' => 'model', + 'default' => null, + 'subType' => 'Nelmio\\ApiDocBundle\\Tests\\Fixtures\\Model\\JmsNested', + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + ), + 'nested_array' => + array( + 'dataType' => 'array of objects (JmsNested)', + 'actualType' => 'collection', + 'subType' => 'Nelmio\\ApiDocBundle\\Tests\\Fixtures\\Model\\JmsNested', + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'default' => null, + ), + ), + ), + 'since' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => '0.2', + 'untilVersion' => NULL, + 'default' => null, + ), + 'until' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => '0.3', + 'default' => null, + ), + 'since_and_until' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => '0.4', + 'untilVersion' => '0.5', + 'default' => null, + ), + ), + ), + 'parent' => + array( + 'dataType' => 'object (JmsTest)', + 'actualType' => 'model', + 'default' => null, + 'subType' => 'Nelmio\\ApiDocBundle\\Tests\\Fixtures\\Model\\JmsTest', + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'children' => + array( + 'foo' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'default' => null, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + ), + 'bar' => + array( + 'dataType' => 'DateTime', + 'actualType' => 'datetime', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => true, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'default' => null, + ), + 'number' => + array( + 'dataType' => 'double', + 'actualType' => 'float', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'default' => null, + ), + 'arr' => + array( + 'dataType' => 'array', + 'actualType' => 'collection', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'default' => null, + ), + 'nested' => + array( + 'dataType' => 'object (JmsNested)', + 'actualType' => 'model', + 'default' => null, + 'subType' => 'Nelmio\\ApiDocBundle\\Tests\\Fixtures\\Model\\JmsNested', + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + ), + 'nested_array' => + array( + 'dataType' => 'array of objects (JmsNested)', + 'actualType' => 'collection', + 'subType' => 'Nelmio\\ApiDocBundle\\Tests\\Fixtures\\Model\\JmsNested', + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => NULL, + 'default' => null, + ), + ), + ), + 'since' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => '0.2', + 'untilVersion' => NULL, + 'default' => null, + ), + 'until' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => NULL, + 'untilVersion' => '0.3', + 'default' => null, + ), + 'since_and_until' => + array( + 'dataType' => 'string', + 'actualType' => 'string', + 'subType' => NULL, + 'required' => false, + 'description' => '', + 'readonly' => false, + 'sinceVersion' => '0.4', + 'untilVersion' => '0.5', + 'default' => null, + ), + ), + 'https' => false, + 'authentication' => false, + 'authenticationRoles' => + array(), + 'deprecated' => false, + ), + array( + 'method' => 'GET', + 'uri' => '/api/resources/{id}.{_format}', + 'description' => 'Retrieve a resource by ID.', + 'requirements' => + array( + '_format' => + array( + 'requirement' => 'json|xml|html', + 'dataType' => '', + 'description' => '', + ), + 'id' => + array( + 'requirement' => '', + 'dataType' => '', + 'description' => '', + ), + ), + 'https' => false, + 'authentication' => false, + 'authenticationRoles' => + array(), + 'deprecated' => false, + ), + array( + 'method' => 'DELETE', + 'uri' => '/api/resources/{id}.{_format}', + 'description' => 'Delete a resource by ID.', + 'requirements' => + array( + '_format' => + array( + 'requirement' => 'json|xml|html', + 'dataType' => '', + 'description' => '', + ), + 'id' => + array( + 'requirement' => '', + 'dataType' => '', + 'description' => '', + ), + ), + 'https' => false, + 'authentication' => false, + 'authenticationRoles' => + array(), + 'deprecated' => false, + ), + ), ); $this->assertEquals($expected, $result); diff --git a/Tests/Formatter/SwaggerFormatterTest.php b/Tests/Formatter/SwaggerFormatterTest.php new file mode 100644 index 0000000..7a03171 --- /dev/null +++ b/Tests/Formatter/SwaggerFormatterTest.php @@ -0,0 +1,726 @@ + + */ +class SwaggerFormatterTest extends WebTestCase +{ + /** + * @var ApiDocExtractor + */ + protected $extractor; + + /** + * @var SwaggerFormatter + */ + protected $formatter; + + protected function setUp() + { + parent::setUp(); + + $container = $this->getContainer(); + $this->extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor'); + $this->formatter = $container->get('nelmio_api_doc.formatter.swagger_formatter'); + } + + + public function testResourceListing() + { + + set_error_handler(array($this, 'handleDeprecation')); + $data = $this->extractor->all(); + restore_error_handler(); + + /** @var $formatter SwaggerFormatter */ + + $actual = $this->formatter->format($data, null); + + + $expected = array( + 'swaggerVersion' => '1.2', + 'apiVersion' => '3.14', + 'info' => + array( + 'title' => 'Nelmio Swagger', + 'description' => 'Testing Swagger integration.', + 'TermsOfServiceUrl' => 'https://github.com', + 'contact' => 'user@domain.tld', + 'license' => 'MIT', + 'licenseUrl' => 'http://opensource.org/licenses/MIT', + ), + 'authorizations' => + array(), + 'apis' => + array( + array( + 'path' => '/other-resources', + 'description' => 'Operations on another resource.', + ), + array( + 'path' => '/resources', + 'description' => 'Operations on resource.', + ), + array( + 'path' => '/tests', + 'description' => NULL, + ), + array( + 'path' => '/tests', + 'description' => NULL, + ), + array( + 'path' => '/tests2', + 'description' => NULL, + ), + array( + 'path' => '/TestResource', + 'description' => NULL, + ), + ), + ); + + $this->assertEquals($expected, $actual); + + + } + + /** + * @dataProvider dataTestApiDeclaration + */ + public function testApiDeclaration($resource, $expected) + { + set_error_handler(array($this, 'handleDeprecation')); + $data = $this->extractor->all(); + restore_error_handler(); + + $actual = $this->formatter->format($data, $resource); + + $this->assertEquals($expected, $actual); + + } + + public function dataTestApiDeclaration() + { + return array( + array( + '/resources', + array( + 'swaggerVersion' => '1.2', + 'apiVersion' => '3.14', + 'basePath' => '/api', + 'resourcePath' => '/resources', + 'apis' => + array( + + array( + 'path' => '/resources.{_format}', + 'operations' => + array( + array( + 'method' => 'GET', + 'summary' => 'List resources.', + 'nickname' => 'get_resources', + 'parameters' => + array( + + array( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + 'enum' => + array( + 'json', + 'xml', + 'html', + ), + ), + ), + 'responseMessages' => + array( + + array( + 'code' => 200, + 'message' => 'Returned on success.', + ), + + array( + 'code' => 404, + 'message' => 'Returned if resource cannot be found.', + ), + ), + ), + + array( + 'method' => 'POST', + 'summary' => 'Create a new resource.', + 'nickname' => 'post_resources', + 'parameters' => + array( + + array( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + 'enum' => + array( + 'json', + 'xml', + 'html', + ), + ), + + array( + 'paramType' => 'form', + 'name' => 'a', + 'type' => 'string', + ), + + array( + 'paramType' => 'form', + 'name' => 'b', + 'type' => 'number', + 'format' => 'float', + ), + + array( + 'paramType' => 'form', + 'name' => 'c', + 'type' => 'string', + 'enum' => + array( + 'x', + 'y', + 'z', + ), + ), + + array( + 'paramType' => 'form', + 'name' => 'd', + 'type' => 'string', + 'format' => 'date-time', + ), + + array( + 'paramType' => 'form', + 'name' => 'e', + 'type' => 'string', + 'format' => 'date', + ), + + array( + 'paramType' => 'form', + 'name' => 'g', + 'type' => 'string', + ), + ), + 'responseMessages' => + array( + + array( + 'code' => 200, + 'message' => 'See standard HTTP status code reason for 200', + 'responseModel' => 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsNested', + ), + ), + ), + ), + ), + + array( + 'path' => '/resources/{id}.{_format}', + 'operations' => + array( + + array( + 'method' => 'GET', + 'summary' => 'Retrieve a resource by ID.', + 'nickname' => 'get_resources', + 'parameters' => + array( + + array( + 'paramType' => 'path', + 'name' => 'id', + 'type' => 'string', + 'required' => true, + ), + + array( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + 'enum' => + array( + 'json', + 'xml', + 'html', + ), + ), + ), + 'responseMessages' => + array(), + ), + + array( + 'method' => 'DELETE', + 'summary' => 'Delete a resource by ID.', + 'nickname' => 'delete_resources', + 'parameters' => + array( + + array( + 'paramType' => 'path', + 'name' => 'id', + 'type' => 'string', + 'required' => true, + ), + + array( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + 'enum' => + array( + 'json', + 'xml', + 'html', + ), + ), + ), + 'responseMessages' => + array(), + ), + ), + ), + ), + 'models' => + array( + 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsTest' => + array( + 'id' => 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsTest', + 'description' => 'object (JmsTest)', + 'properties' => + array( + 'foo' => + array( + 'type' => 'string', + 'description' => 'string', + ), + 'bar' => + array( + 'type' => 'string', + 'description' => 'DateTime', + 'format' => 'date-time', + ), + 'number' => + array( + 'type' => 'number', + 'description' => 'double', + 'format' => 'float', + ), + 'arr' => + array( + 'type' => 'array', + 'description' => 'array', + 'items' => array( + 'type' => 'string', + ) + ), + 'nested' => + array( + '$ref' => 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsNested', + ), + 'nested_array' => + array( + 'type' => 'array', + 'description' => 'array of objects (JmsNested)', + 'items' => array( + '$ref' => 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsNested', + ) + ), + ), + 'required' => + array(), + ), + 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsNested' => + array( + 'id' => 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsNested', + 'description' => '', + 'properties' => + array( + 'foo' => + array( + 'type' => 'string', + 'description' => 'DateTime', + 'format' => 'date-time', + ), + 'bar' => + array( + 'type' => 'string', + 'description' => 'string', + ), + 'baz' => + array( + 'type' => 'array', + 'description' => 'Epic description. + +With multiple lines.', + 'items' => array( + 'type' => 'string', + ) + ), + 'circular' => + array( + '$ref' => 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsNested', + ), + 'parent' => + array( + '$ref' => 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsTest', + ), + 'since' => + array( + 'type' => 'string', + 'description' => 'string', + ), + 'until' => + array( + 'type' => 'string', + 'description' => 'string', + ), + 'since_and_until' => + array( + 'type' => 'string', + 'description' => 'string', + ), + ), + 'required' => + array(), + ), + ), + 'produces' => + array(), + 'consumes' => + array(), + 'authorizations' => + array(), + ), + ), + array( + '/other-resources', + array( + 'swaggerVersion' => '1.2', + 'apiVersion' => '3.14', + 'basePath' => '/api', + 'resourcePath' => '/other-resources', + 'apis' => + array( + + array( + 'path' => '/other-resources.{_format}', + 'operations' => + array( + + array( + 'method' => 'GET', + 'summary' => 'List another resource.', + 'nickname' => 'get_other-resources', + 'parameters' => + array( + + array( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + 'enum' => + array( + 'json', + 'xml', + 'html', + ), + ), + ), + 'responseMessages' => + array(), + ), + ), + ), + + array( + 'path' => '/other-resources/{id}.{_format}', + 'operations' => + array( + + array( + 'method' => 'PUT', + 'summary' => 'Update a resource bu ID.', + 'nickname' => 'put_other-resources', + 'parameters' => + array( + + array( + 'paramType' => 'path', + 'name' => 'id', + 'type' => 'string', + 'required' => true, + ), + + array( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + 'enum' => + array( + 'json', + 'xml', + 'html', + ), + ), + ), + 'responseMessages' => + array(), + ), + + array( + 'method' => 'PATCH', + 'summary' => 'Update a resource bu ID.', + 'nickname' => 'patch_other-resources', + 'parameters' => + array( + + array( + 'paramType' => 'path', + 'name' => 'id', + 'type' => 'string', + 'required' => true, + ), + + array( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + 'enum' => + array( + 'json', + 'xml', + 'html', + ), + ), + ), + 'responseMessages' => + array(), + ), + ), + ), + ), + 'models' => + array(), + 'produces' => + array(), + 'consumes' => + array(), + 'authorizations' => + array(), + ), + ), + array( + '/tests', + array ( + 'swaggerVersion' => '1.2', + 'apiVersion' => '3.14', + 'basePath' => '/api', + 'resourcePath' => '/tests', + 'apis' => + array ( + + array ( + 'path' => '/tests.{_format}', + 'operations' => + array ( + + array ( + 'method' => 'GET', + 'summary' => 'index action', + 'nickname' => 'get_tests', + 'parameters' => + array ( + + array ( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + ), + ), + 'responseMessages' => + array ( + ), + ), + + array ( + 'method' => 'GET', + 'summary' => 'index action', + 'nickname' => 'get_tests', + 'parameters' => + array ( + + array ( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + ), + + array ( + 'paramType' => 'query', + 'name' => 'a', + 'type' => 'integer', + ), + + array ( + 'paramType' => 'query', + 'name' => 'b', + 'type' => 'string', + ), + ), + 'responseMessages' => + array ( + ), + ), + + array ( + 'method' => 'POST', + 'summary' => 'create test', + 'nickname' => 'post_tests', + 'parameters' => + array ( + + array ( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + ), + + array ( + 'paramType' => 'query', + 'name' => 'a', + 'type' => 'integer', + ), + + array ( + 'paramType' => 'query', + 'name' => 'b', + 'type' => 'string', + ), + + array ( + 'paramType' => 'form', + 'name' => 'a', + 'type' => 'string', + ), + + array ( + 'paramType' => 'form', + 'name' => 'b', + 'type' => 'string', + ), + + array ( + 'paramType' => 'form', + 'name' => 'c', + 'type' => 'boolean', + ), + + array ( + 'paramType' => 'form', + 'name' => 'd', + 'type' => 'string', + ), + ), + 'responseMessages' => + array ( + ), + ), + + array ( + 'method' => 'POST', + 'summary' => 'create test', + 'nickname' => 'post_tests', + 'parameters' => + array ( + + array ( + 'paramType' => 'path', + 'name' => '_format', + 'type' => 'string', + 'required' => true, + ), + + array ( + 'paramType' => 'form', + 'name' => 'a', + 'type' => 'string', + ), + + array ( + 'paramType' => 'form', + 'name' => 'b', + 'type' => 'string', + ), + + array ( + 'paramType' => 'form', + 'name' => 'c', + 'type' => 'boolean', + ), + + array ( + 'paramType' => 'form', + 'name' => 'd', + 'type' => 'string', + ), + ), + 'responseMessages' => + array ( + ), + ), + ), + ), + ), + 'models' => + array ( + ), + 'produces' => + array ( + ), + 'consumes' => + array ( + ), + 'authorizations' => + array ( + ), + ), + ), + ); + } +} \ No newline at end of file From cfe6cbc1346d950d7661031bb81b2d88751798c2 Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Wed, 25 Jun 2014 11:16:47 -0700 Subject: [PATCH 02/11] Create swagger-support.md --- Resources/doc/swagger-support.md | 99 ++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 Resources/doc/swagger-support.md diff --git a/Resources/doc/swagger-support.md b/Resources/doc/swagger-support.md new file mode 100644 index 0000000..027c3f9 --- /dev/null +++ b/Resources/doc/swagger-support.md @@ -0,0 +1,99 @@ +NelmioApiDocBundle +=================== + +##Swagger support## + +It is now possible to make your application produce Swagger-compliant JSON output based on `@ApiDoc` annotations, which can be used for consumption by [swagger-ui](https://github.com/wordnik/swagger-ui). + +###Annotations### + +A couple of properties has been added to `@ApiDoc`: + +To define a __resource description__: + +```php + Date: Wed, 25 Jun 2014 14:20:30 -0700 Subject: [PATCH 03/11] Added configuration reference. --- Resources/doc/swagger-support.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Resources/doc/swagger-support.md b/Resources/doc/swagger-support.md index 027c3f9..493cd64 100644 --- a/Resources/doc/swagger-support.md +++ b/Resources/doc/swagger-support.md @@ -97,3 +97,20 @@ Dump a specific resource API declaration only: php app/console api:swagger:dump --resource=users ``` The above command will dump the `/users` API declaration in an `users.json` file. + +##Configuration reference + +```yml +nelmio_api_doc: + swagger: + api_base_path: /api + swagger_version: 1.2 + api_version: 0.1 + info: + title: Symfony2 + description: My awesome Symfony2 app! + TermsOfServiceUrl: ~ + contact: ~ + license: ~ + licenseUrl: ~ +``` From af5cb3dd76c46740a69b4135f6a7b6b1932e0326 Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Wed, 25 Jun 2014 14:23:00 -0700 Subject: [PATCH 04/11] Removed ambiguity --- Resources/doc/swagger-support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/doc/swagger-support.md b/Resources/doc/swagger-support.md index 493cd64..f45d0dd 100644 --- a/Resources/doc/swagger-support.md +++ b/Resources/doc/swagger-support.md @@ -83,7 +83,7 @@ The routes registered with the method above will read your `@ApiDoc` annotation php app/console api:swagger:dump --all app/Resources/swagger-docs ``` -The above command will dump JSON files under the `app/Resources/swagger-docs` directory (relative to your project root, which is the default destination if the argument is not provided), and you can now process or server the files however you want. +The above command will dump JSON files under the `app/Resources/swagger-docs` directory (relative to your project root), and you can now process or server the files however you want. (If the destination defaults to the project root if not specified.) ####Selective dumps From ee0496af6590ee1a98671d1ae6170ea8b0e27a5d Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Wed, 25 Jun 2014 14:23:19 -0700 Subject: [PATCH 05/11] Update swagger-support.md --- Formatter/SwaggerFormatter.php | 4 +++- Resources/doc/swagger-support.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Formatter/SwaggerFormatter.php b/Formatter/SwaggerFormatter.php index 65248eb..532249a 100644 --- a/Formatter/SwaggerFormatter.php +++ b/Formatter/SwaggerFormatter.php @@ -342,7 +342,9 @@ class SwaggerFormatter implements FormatterInterface switch ($prop['actualType']) { case DataTypes::ENUM: $type = 'string'; - $enum = array_keys(json_decode($prop['format'], true)); + if (isset($prop['format'])) { + $enum = array_keys(json_decode($prop['format'], true)); + } break; case DataTypes::MODEL: diff --git a/Resources/doc/swagger-support.md b/Resources/doc/swagger-support.md index f45d0dd..b2b50fe 100644 --- a/Resources/doc/swagger-support.md +++ b/Resources/doc/swagger-support.md @@ -83,7 +83,7 @@ The routes registered with the method above will read your `@ApiDoc` annotation php app/console api:swagger:dump --all app/Resources/swagger-docs ``` -The above command will dump JSON files under the `app/Resources/swagger-docs` directory (relative to your project root), and you can now process or server the files however you want. (If the destination defaults to the project root if not specified.) +The above command will dump JSON files under the `app/Resources/swagger-docs` directory (relative to your project root), and you can now process or server the files however you want. The destination defaults to the project root if not specified. ####Selective dumps From bb723bdb40c0d56a824d6ab65b118eb281aeca27 Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Sat, 28 Jun 2014 00:20:12 +0000 Subject: [PATCH 06/11] Added new tests for Swagger doc controllers. Also some CS fixes. --- Controller/ApiDocController.php | 7 +- .../SwaggerConfigCompilerPass.php | 7 ++ Extractor/ApiDocExtractor.php | 5 +- Formatter/RequestAwareSwaggerFormatter.php | 49 ++++++++++++++ Formatter/SwaggerFormatter.php | 10 ++- Resources/config/formatters.xml | 4 ++ Tests/Controller/ApiDocControllerTest.php | 66 +++++++++++++++++++ .../Controller/ResourceController.php | 20 +++--- Tests/Fixtures/app/config/routing.yml | 4 ++ 9 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 Formatter/RequestAwareSwaggerFormatter.php create mode 100644 Tests/Controller/ApiDocControllerTest.php diff --git a/Controller/ApiDocController.php b/Controller/ApiDocController.php index 1d033b3..7972914 100644 --- a/Controller/ApiDocController.php +++ b/Controller/ApiDocController.php @@ -29,8 +29,13 @@ class ApiDocController extends Controller public function swaggerAction(Request $request, $resource = null) { $docs = $this->get('nelmio_api_doc.extractor.api_doc_extractor')->all(); - $formatter = $this->get('nelmio_api_doc.formatter.swagger_formatter'); + $formatter = $this->get('nelmio_api_doc.formatter.request_aware_swagger_formatter'); $spec = $formatter->format($docs, $resource ? '/' . $resource : null); + + if (count($spec['apis']) === 0) { + throw $this->createNotFoundException(sprintf('Cannot find resource "%s"', $resource)); + } + return new JsonResponse($spec); } } diff --git a/DependencyInjection/SwaggerConfigCompilerPass.php b/DependencyInjection/SwaggerConfigCompilerPass.php index e79e6bf..16d81a2 100644 --- a/DependencyInjection/SwaggerConfigCompilerPass.php +++ b/DependencyInjection/SwaggerConfigCompilerPass.php @@ -39,5 +39,12 @@ class SwaggerConfigCompilerPass implements CompilerPassInterface $formatter->addMethodCall('setSwaggerVersion', array($container->getParameter('nelmio_api_doc.swagger.swagger_version'))); $formatter->addMethodCall('setInfo', array($container->getParameter('nelmio_api_doc.swagger.info'))); + $formatter = $container->getDefinition('nelmio_api_doc.formatter.request_aware_swagger_formatter'); + + $formatter->addMethodCall('setBasePath', array($container->getParameter('nelmio_api_doc.swagger.base_path'))); + $formatter->addMethodCall('setApiVersion', array($container->getParameter('nelmio_api_doc.swagger.api_version'))); + $formatter->addMethodCall('setSwaggerVersion', array($container->getParameter('nelmio_api_doc.swagger.swagger_version'))); + $formatter->addMethodCall('setInfo', array($container->getParameter('nelmio_api_doc.swagger.info'))); + } } \ No newline at end of file diff --git a/Extractor/ApiDocExtractor.php b/Extractor/ApiDocExtractor.php index 8249ab0..239167b 100644 --- a/Extractor/ApiDocExtractor.php +++ b/Extractor/ApiDocExtractor.php @@ -485,8 +485,8 @@ class ApiDocExtractor /** * Creates a human-readable version of the `actualType`. `subType` is taken into account. * - * @param string $actualType - * @param string $subType + * @param string $actualType + * @param string $subType * @return string */ protected function generateHumanReadableType($actualType, $subType) @@ -509,6 +509,7 @@ class ApiDocExtractor if (class_exists($subType)) { $parts = explode('\\', $subType); + return sprintf('array of objects (%s)', end($parts)); } diff --git a/Formatter/RequestAwareSwaggerFormatter.php b/Formatter/RequestAwareSwaggerFormatter.php new file mode 100644 index 0000000..0454699 --- /dev/null +++ b/Formatter/RequestAwareSwaggerFormatter.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Nelmio\ApiDocBundle\Formatter; + + +use Symfony\Component\HttpFoundation\Request; + +/** + * Extends SwaggerFormatter which takes into account the request's base URL when generating the documents for direct swagger-ui consumption. + * + * @author Bezalel Hermoso + */ +class RequestAwareSwaggerFormatter extends SwaggerFormatter +{ + /** + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $request; + + + /** + * @param Request $request + */ + public function __construct(Request $request) + { + $this->request = $request; + } + + /** + * @param array $collection + * @param string $resource + * @return array + */ + protected function produceApiDeclaration(array $collection, $resource) + { + $data = parent::produceApiDeclaration($collection, $resource); + $data['basePath'] = $this->request->getBaseUrl() . $data['basePath']; + return $data; + } +} \ No newline at end of file diff --git a/Formatter/SwaggerFormatter.php b/Formatter/SwaggerFormatter.php index 532249a..9610141 100644 --- a/Formatter/SwaggerFormatter.php +++ b/Formatter/SwaggerFormatter.php @@ -14,6 +14,9 @@ namespace Nelmio\ApiDocBundle\Formatter; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\DataTypes; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Router; use Symfony\Component\Routing\RouterInterface; @@ -54,6 +57,11 @@ class SwaggerFormatter implements FormatterInterface DataTypes::DATETIME => 'date-time', ); + /** + * @var Request + */ + protected $request; + /** * Format a collection of documentation data. * @@ -144,7 +152,7 @@ class SwaggerFormatter implements FormatterInterface * @param string $resource * @return array */ - private function produceApiDeclaration(array $collection, $resource) + protected function produceApiDeclaration(array $collection, $resource) { $apiDeclaration = array( diff --git a/Resources/config/formatters.xml b/Resources/config/formatters.xml index ed5ddb7..607be2f 100644 --- a/Resources/config/formatters.xml +++ b/Resources/config/formatters.xml @@ -9,6 +9,7 @@ Nelmio\ApiDocBundle\Formatter\SimpleFormatter Nelmio\ApiDocBundle\Formatter\HtmlFormatter Nelmio\ApiDocBundle\Formatter\SwaggerFormatter + Nelmio\ApiDocBundle\Formatter\RequestAwareSwaggerFormatter null @@ -58,6 +59,9 @@ + + + diff --git a/Tests/Controller/ApiDocControllerTest.php b/Tests/Controller/ApiDocControllerTest.php new file mode 100644 index 0000000..e3c955f --- /dev/null +++ b/Tests/Controller/ApiDocControllerTest.php @@ -0,0 +1,66 @@ + + */ +class ApiDocControllerTest extends WebTestCase +{ + public function testSwaggerDocResourceListRoute() + { + $client = static::createClient(); + $client->request('GET', '/api-docs/'); + + $response = $client->getResponse(); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/json', $response->headers->get('Content-type')); + + } + + public function dataTestApiDeclarations() + { + return array( + array('resources'), + array('tests'), + array('tests2'), + array('TestResource'), + ); + } + + /** + * @dataProvider dataTestApiDeclarations + */ + public function testApiDeclarationRoutes($resource) + { + $client = static::createClient(); + $client->request('GET', '/api-docs/' . $resource); + + $response = $client->getResponse(); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('application/json', $response->headers->get('Content-type')); + } + + public function testNonExistingApiDeclaration() + { + $client = static::createClient(); + $client->request('GET', '/api-docs/santa'); + + $response = $client->getResponse(); + $this->assertEquals(404, $response->getStatusCode()); + + } +} diff --git a/Tests/Fixtures/Controller/ResourceController.php b/Tests/Fixtures/Controller/ResourceController.php index e5d77a4..502f9c2 100644 --- a/Tests/Fixtures/Controller/ResourceController.php +++ b/Tests/Fixtures/Controller/ResourceController.php @@ -1,21 +1,17 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ namespace Nelmio\ApiDocBundle\Tests\Fixtures\Controller; use Nelmio\ApiDocBundle\Annotation\ApiDoc; - -/** - * Class ResourceController - * - * @package Nelmio\ApiDocBundle\Tests\Fixtures\Controller - * @author Bez Hermoso - */ class ResourceController { /** diff --git a/Tests/Fixtures/app/config/routing.yml b/Tests/Fixtures/app/config/routing.yml index 45b90b6..0ac72be 100644 --- a/Tests/Fixtures/app/config/routing.yml +++ b/Tests/Fixtures/app/config/routing.yml @@ -203,3 +203,7 @@ test_route_update_another_resource: requirements: _method: PUT|PATCH _format: json|xml|html + +swagger_doc: + resource: @NelmioApiDocBundle/Resources/config/swagger_routing.yml + prefix: /api-docs \ No newline at end of file From 9d3c0a8c293f0e2488e1289c791b57f1171a1bdd Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Tue, 17 Jun 2014 17:05:00 -0700 Subject: [PATCH 07/11] Swagger support: Unified data types [actualType and subType] Updated tests. JMS parsing fixes; updated {Validator,FormType}Parser, FOSRestHandler, and AbstractFormatter, and updated DataTypes enum. Modified dataType checking. Updated tests. Updated DataTypes enum. Quick fix and added doc comments. CS fixes. Refactored FormTypeParser to produce nested parameters. Updated tests accordingly. Logical and CS fixes. Sub-forms and more tests. Logical and CS fixes. Swagger support: created formatter. Configuration and resourcePath logic update. ApiDoc annotation update. Updated formatter and added tests. Parameter formatting. Added tests for SwaggerFormatter. Added option in annotation, and the corresponding logic for parsing the supplied values and processing them in the formatter. Routing update. Updated tests. Removed unused dependency and updated doc comments. Renamed 'responseModels' to 'responseMap' Update the resource filtering and formatting of response messages. Updated check for 200 response model. Ignore data_class and always use form-type to avoid conflicts. Fix: add 'type' even if '' is specified. Refactored responseMap; added parsedResponseMap. Added tests and updated some. Fix: add 'type' even if '' is specified. Initial commit of command. Finished logic for dumping files. Updated doc comment; added license and added more meaningful class comment. Array of models support. --- Controller/ApiDocController.php | 8 +++- DependencyInjection/NelmioApiDocExtension.php | 2 +- .../SwaggerConfigCompilerPass.php | 13 +++--- Extractor/ApiDocExtractor.php | 5 +-- Formatter/RequestAwareSwaggerFormatter.php | 43 +++++++++++++++---- Formatter/SwaggerFormatter.php | 11 +---- Resources/config/formatters.xml | 7 +-- 7 files changed, 53 insertions(+), 36 deletions(-) diff --git a/Controller/ApiDocController.php b/Controller/ApiDocController.php index 7972914..33c75f8 100644 --- a/Controller/ApiDocController.php +++ b/Controller/ApiDocController.php @@ -11,7 +11,9 @@ namespace Nelmio\ApiDocBundle\Controller; +use Nelmio\ApiDocBundle\Formatter\RequestAwareSwaggerFormatter; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -28,11 +30,13 @@ class ApiDocController extends Controller public function swaggerAction(Request $request, $resource = null) { + $docs = $this->get('nelmio_api_doc.extractor.api_doc_extractor')->all(); - $formatter = $this->get('nelmio_api_doc.formatter.request_aware_swagger_formatter'); + $formatter = new RequestAwareSwaggerFormatter($request, $this->get('nelmio_api_doc.formatter.swagger_formatter')); + $spec = $formatter->format($docs, $resource ? '/' . $resource : null); - if (count($spec['apis']) === 0) { + if ($resource !== null && count($spec['apis']) === 0) { throw $this->createNotFoundException(sprintf('Cannot find resource "%s"', $resource)); } diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index dabe480..7734b98 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -11,11 +11,11 @@ namespace Nelmio\ApiDocBundle\DependencyInjection; +use Symfony\Component\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\Definition\Processor; -use Symfony\Component\Config\FileLocator; class NelmioApiDocExtension extends Extension { diff --git a/DependencyInjection/SwaggerConfigCompilerPass.php b/DependencyInjection/SwaggerConfigCompilerPass.php index 16d81a2..3e9d488 100644 --- a/DependencyInjection/SwaggerConfigCompilerPass.php +++ b/DependencyInjection/SwaggerConfigCompilerPass.php @@ -11,8 +11,14 @@ namespace Nelmio\ApiDocBundle\DependencyInjection; +use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Parameter; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Filesystem; /** @@ -39,12 +45,5 @@ class SwaggerConfigCompilerPass implements CompilerPassInterface $formatter->addMethodCall('setSwaggerVersion', array($container->getParameter('nelmio_api_doc.swagger.swagger_version'))); $formatter->addMethodCall('setInfo', array($container->getParameter('nelmio_api_doc.swagger.info'))); - $formatter = $container->getDefinition('nelmio_api_doc.formatter.request_aware_swagger_formatter'); - - $formatter->addMethodCall('setBasePath', array($container->getParameter('nelmio_api_doc.swagger.base_path'))); - $formatter->addMethodCall('setApiVersion', array($container->getParameter('nelmio_api_doc.swagger.api_version'))); - $formatter->addMethodCall('setSwaggerVersion', array($container->getParameter('nelmio_api_doc.swagger.swagger_version'))); - $formatter->addMethodCall('setInfo', array($container->getParameter('nelmio_api_doc.swagger.info'))); - } } \ No newline at end of file diff --git a/Extractor/ApiDocExtractor.php b/Extractor/ApiDocExtractor.php index 239167b..8249ab0 100644 --- a/Extractor/ApiDocExtractor.php +++ b/Extractor/ApiDocExtractor.php @@ -485,8 +485,8 @@ class ApiDocExtractor /** * Creates a human-readable version of the `actualType`. `subType` is taken into account. * - * @param string $actualType - * @param string $subType + * @param string $actualType + * @param string $subType * @return string */ protected function generateHumanReadableType($actualType, $subType) @@ -509,7 +509,6 @@ class ApiDocExtractor if (class_exists($subType)) { $parts = explode('\\', $subType); - return sprintf('array of objects (%s)', end($parts)); } diff --git a/Formatter/RequestAwareSwaggerFormatter.php b/Formatter/RequestAwareSwaggerFormatter.php index 0454699..049e9d2 100644 --- a/Formatter/RequestAwareSwaggerFormatter.php +++ b/Formatter/RequestAwareSwaggerFormatter.php @@ -12,6 +12,7 @@ namespace Nelmio\ApiDocBundle\Formatter; +use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Symfony\Component\HttpFoundation\Request; /** @@ -19,31 +20,55 @@ use Symfony\Component\HttpFoundation\Request; * * @author Bezalel Hermoso */ -class RequestAwareSwaggerFormatter extends SwaggerFormatter +class RequestAwareSwaggerFormatter implements FormatterInterface { /** * @var \Symfony\Component\HttpFoundation\Request */ protected $request; + /** + * @var SwaggerFormatter + */ + protected $formatter; /** * @param Request $request + * @param SwaggerFormatter $formatter */ - public function __construct(Request $request) + public function __construct(Request $request, SwaggerFormatter $formatter) { $this->request = $request; + $this->formatter = $formatter; } /** + * Format a collection of documentation data. + * * @param array $collection - * @param string $resource - * @return array + * @param null $resource + * @internal param $array [ApiDoc] $collection + * @return string|array */ - protected function produceApiDeclaration(array $collection, $resource) + public function format(array $collection, $resource = null) { - $data = parent::produceApiDeclaration($collection, $resource); - $data['basePath'] = $this->request->getBaseUrl() . $data['basePath']; - return $data; + $result = $this->formatter->format($collection, $resource); + + if ($resource !== null) { + $result['basePath'] = $this->request->getBaseUrl() . $result['basePath']; + } + + return $result; } -} \ No newline at end of file + + /** + * Format documentation data for one route. + * + * @param ApiDoc $annotation + * return string|array + */ + public function formatOne(ApiDoc $annotation) + { + return $this->formatter->formatOne($annotation); + } +} \ No newline at end of file diff --git a/Formatter/SwaggerFormatter.php b/Formatter/SwaggerFormatter.php index 9610141..078f098 100644 --- a/Formatter/SwaggerFormatter.php +++ b/Formatter/SwaggerFormatter.php @@ -14,9 +14,6 @@ namespace Nelmio\ApiDocBundle\Formatter; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\DataTypes; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Router; use Symfony\Component\Routing\RouterInterface; @@ -57,11 +54,6 @@ class SwaggerFormatter implements FormatterInterface DataTypes::DATETIME => 'date-time', ); - /** - * @var Request - */ - protected $request; - /** * Format a collection of documentation data. * @@ -139,10 +131,11 @@ class SwaggerFormatter implements FormatterInterface * * @param ApiDoc $annotation * return string|array + * @throws \BadMethodCallException */ public function formatOne(ApiDoc $annotation) { - // TODO: Implement formatOne() method. + throw new \BadMethodCallException(sprintf('%s does not support formatting a single ApiDoc only.', __CLASS__)); } /** diff --git a/Resources/config/formatters.xml b/Resources/config/formatters.xml index 607be2f..93b9dd1 100644 --- a/Resources/config/formatters.xml +++ b/Resources/config/formatters.xml @@ -9,7 +9,6 @@ Nelmio\ApiDocBundle\Formatter\SimpleFormatter Nelmio\ApiDocBundle\Formatter\HtmlFormatter Nelmio\ApiDocBundle\Formatter\SwaggerFormatter - Nelmio\ApiDocBundle\Formatter\RequestAwareSwaggerFormatter null @@ -58,10 +57,8 @@ %nelmio_api_doc.sandbox.authentication% - - - - + From abaeb374e86c062617f3b53d00d29a41c593a758 Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Thu, 24 Jul 2014 12:18:33 -0700 Subject: [PATCH 08/11] Added 'type' to API item if applicable. --- Formatter/SwaggerFormatter.php | 6 ++++++ Tests/Formatter/SwaggerFormatterTest.php | 1 + 2 files changed, 7 insertions(+) diff --git a/Formatter/SwaggerFormatter.php b/Formatter/SwaggerFormatter.php index 078f098..3ab0cef 100644 --- a/Formatter/SwaggerFormatter.php +++ b/Formatter/SwaggerFormatter.php @@ -246,6 +246,8 @@ class SwaggerFormatter implements FormatterInterface ); } + $type = isset($responseMessages[200]['responseModel']) ? $responseMessages[200]['responseModel'] : null; + foreach ($apiDoc->getRoute()->getMethods() as $method) { $operation = array( 'method' => $method, @@ -255,6 +257,10 @@ class SwaggerFormatter implements FormatterInterface 'responseMessages' => array_values($responseMessages), ); + if ($type !== null) { + $operation['type'] = $type; + } + $apiBag[$path][] = $operation; } } diff --git a/Tests/Formatter/SwaggerFormatterTest.php b/Tests/Formatter/SwaggerFormatterTest.php index 7a03171..d8f543f 100644 --- a/Tests/Formatter/SwaggerFormatterTest.php +++ b/Tests/Formatter/SwaggerFormatterTest.php @@ -237,6 +237,7 @@ class SwaggerFormatterTest extends WebTestCase 'responseModel' => 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsNested', ), ), + 'type' => 'Nelmio.ApiDocBundle.Tests.Fixtures.Model.JmsNested', ), ), ), From 9824a6ba3cd2703c53d83f88d21455000e008590 Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Fri, 25 Jul 2014 17:31:24 -0700 Subject: [PATCH 09/11] Added default value handling. --- Formatter/SwaggerFormatter.php | 7 ++++++- Tests/Formatter/SwaggerFormatterTest.php | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Formatter/SwaggerFormatter.php b/Formatter/SwaggerFormatter.php index 3ab0cef..6b9cb53 100644 --- a/Formatter/SwaggerFormatter.php +++ b/Formatter/SwaggerFormatter.php @@ -360,7 +360,8 @@ class SwaggerFormatter implements FormatterInterface $prop['subType'], isset($prop['children']) ? $prop['children'] : null, $prop['description'] ?: $prop['dataType'], - $models); + $models + ); break; } } @@ -396,6 +397,10 @@ class SwaggerFormatter implements FormatterInterface $parameter['enum'] = $enum; } + if ($prop['default'] !== null) { + $parameter['defaultValue'] = $prop['default']; + } + $parameters[] = $parameter; } diff --git a/Tests/Formatter/SwaggerFormatterTest.php b/Tests/Formatter/SwaggerFormatterTest.php index d8f543f..488849b 100644 --- a/Tests/Formatter/SwaggerFormatterTest.php +++ b/Tests/Formatter/SwaggerFormatterTest.php @@ -230,7 +230,6 @@ class SwaggerFormatterTest extends WebTestCase ), 'responseMessages' => array( - array( 'code' => 200, 'message' => 'See standard HTTP status code reason for 200', @@ -650,12 +649,14 @@ With multiple lines.', 'paramType' => 'form', 'name' => 'c', 'type' => 'boolean', + 'defaultValue' => false, ), array ( 'paramType' => 'form', 'name' => 'd', 'type' => 'string', + 'defaultValue' => 'DefaultTest', ), ), 'responseMessages' => @@ -693,12 +694,14 @@ With multiple lines.', 'paramType' => 'form', 'name' => 'c', 'type' => 'boolean', + 'defaultValue' => false, ), array ( 'paramType' => 'form', 'name' => 'd', 'type' => 'string', + 'defaultValue' => 'DefaultTest', ), ), 'responseMessages' => From a8221d45154f9281e56ac95f275c5d02da4cbee5 Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Mon, 28 Jul 2014 12:18:55 -0700 Subject: [PATCH 10/11] Post-parser support for response map models. --- Extractor/ApiDocExtractor.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Extractor/ApiDocExtractor.php b/Extractor/ApiDocExtractor.php index 8249ab0..133a2d5 100644 --- a/Extractor/ApiDocExtractor.php +++ b/Extractor/ApiDocExtractor.php @@ -306,8 +306,8 @@ class ApiDocExtractor } } - foreach($supportedParsers as $parser) { - if($parser instanceof PostParserInterface) { + foreach ($supportedParsers as $parser) { + if ($parser instanceof PostParserInterface) { $mp = $parser->postParse($normalizedOutput, $response); $response = $this->mergeParameters($response, $mp); } @@ -334,12 +334,21 @@ class ApiDocExtractor $normalizedModel = $this->normalizeClassParameter($modelName); $parameters = array(); + $supportedParsers = array(); foreach ($this->getParsers($normalizedModel) as $parser) { if ($parser->supports($normalizedModel)) { + $supportedParsers[] = $parser; $parameters = $this->mergeParameters($parameters, $parser->parse($normalizedModel)); } } + foreach ($supportedParsers as $parser) { + if ($parser instanceof PostParserInterface) { + $mp = $parser->postParse($normalizedModel, $parameters); + $parameters = $this->mergeParameters($parameters, $mp); + } + } + $parameters = $this->clearClasses($parameters); $parameters = $this->generateHumanReadableTypes($parameters); From 07c6557fc5f2069dcf5d5ee72aaa323657e64069 Mon Sep 17 00:00:00 2001 From: Bez Hermoso Date: Mon, 28 Jul 2014 14:43:16 -0700 Subject: [PATCH 11/11] Test fixes. --- Tests/Formatter/MarkdownFormatterTest.php | 16 +++++++++ Tests/Formatter/SimpleFormatterTest.php | 40 +++++++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/Tests/Formatter/MarkdownFormatterTest.php b/Tests/Formatter/MarkdownFormatterTest.php index f8cf2b4..b49d6a3 100644 --- a/Tests/Formatter/MarkdownFormatterTest.php +++ b/Tests/Formatter/MarkdownFormatterTest.php @@ -762,6 +762,14 @@ related: * type: object (Test) +related[a]: + + * type: string + +related[b]: + + * type: DateTime + ### `ANY` /z-return-selected-parsers-input ### @@ -819,6 +827,14 @@ number: related: * type: object (Test) + +related[a]: + + * type: string + +related[b]: + + * type: DateTime MARKDOWN; $this->assertEquals($expected, $result); diff --git a/Tests/Formatter/SimpleFormatterTest.php b/Tests/Formatter/SimpleFormatterTest.php index f174015..31abd0b 100644 --- a/Tests/Formatter/SimpleFormatterTest.php +++ b/Tests/Formatter/SimpleFormatterTest.php @@ -1080,7 +1080,25 @@ With multiple lines.', 'description' => '', 'sinceVersion' => null, 'untilVersion' => null, - 'children' => array(), + 'children' => array( + 'a' => array( + 'dataType' => 'string', + 'actualType' => DataTypes::STRING, + 'subType' => null, + 'default' => 'nelmio', + 'format' => '{length: min: foo}, {not blank}', + 'required' => true, + 'readonly' => null + ), + 'b' => array( + 'dataType' => 'DateTime', + 'actualType' => DataTypes::DATETIME, + 'subType' => null, + 'default' => null, + 'required' => null, + 'readonly' => null + ) + ), ) ), 'authenticationRoles' => array(), @@ -1197,7 +1215,25 @@ With multiple lines.', 'description' => '', 'sinceVersion' => null, 'untilVersion' => null, - 'children' => array(), + 'children' => array( + 'a' => array( + 'dataType' => 'string', + 'actualType' => DataTypes::STRING, + 'subType' => null, + 'default' => 'nelmio', + 'format' => '{length: min: foo}, {not blank}', + 'required' => true, + 'readonly' => null + ), + 'b' => array( + 'dataType' => 'DateTime', + 'actualType' => DataTypes::DATETIME, + 'subType' => null, + 'default' => null, + 'required' => null, + 'readonly' => null + ) + ), ) ), 'authenticationRoles' => array(),