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.
This commit is contained in:
Bez Hermoso 2014-06-17 17:05:00 -07:00
parent c03d35bee4
commit 6f85aed33c
22 changed files with 2654 additions and 83 deletions

View File

@ -135,6 +135,21 @@ class ApiDoc
*/ */
private $statusCodes = array(); private $statusCodes = array();
/**
* @var string|null
*/
private $resourceDescription = null;
/**
* @var array
*/
private $responseMap = array();
/**
* @var array
*/
private $parsedResponseMap = array();
/** /**
* @var array * @var array
*/ */
@ -241,6 +256,17 @@ class ApiDoc
if (isset($data['https'])) { if (isset($data['https'])) {
$this->https = $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; $data['tags'] = $tags;
} }
if ($resourceDescription = $this->resourceDescription) {
$data['resourceDescription'] = $resourceDescription;
}
$data['https'] = $this->https; $data['https'] = $this->https;
$data['authentication'] = $this->authentication; $data['authentication'] = $this->authentication;
$data['authenticationRoles'] = $this->authenticationRoles; $data['authenticationRoles'] = $this->authenticationRoles;
@ -613,4 +643,45 @@ class ApiDoc
return $data; 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;
}
}
} }

View File

@ -0,0 +1,132 @@
<?php
/*
* This file is part of the NelmioApiDocBundle.
*
* (c) Nelmio <hello@nelm.io>
*
* 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 <bez@activelamp.com>
*/
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('<comment>Dump resource list to %s: </comment>', $path);
try {
$fs->dumpFile($path, json_encode($list));
} catch (IOException $e) {
$output->writeln($string . ' <error>NOT OK</error>');
}
$output->writeln($string . '<info>OK</info>');
}
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('<comment>Dump API declaration to %s: </comment>', $path);
try {
$fs->dumpFile($path, json_encode($list));
} catch (IOException $e) {
$output->writeln($string . ' <error>NOT OK</error>');
}
$output->writeln($string . '<info>OK</info>');
}
}

View File

@ -12,6 +12,8 @@
namespace Nelmio\ApiDocBundle\Controller; namespace Nelmio\ApiDocBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
class ApiDocController extends Controller class ApiDocController extends Controller
@ -23,4 +25,12 @@ class ApiDocController extends Controller
return new Response($htmlContent, 200, array('Content-Type' => 'text/html')); 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);
}
} }

View File

@ -99,6 +99,25 @@ class Configuration implements ConfigurationInterface
->end() ->end()
->end() ->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(); ->end();
return $treeBuilder; return $treeBuilder;

View File

@ -57,6 +57,12 @@ class NelmioApiDocExtension extends Extension
if (!interface_exists('\Symfony\Component\Validator\MetadataFactoryInterface')) { 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.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']);
} }
/** /**

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <bez@activelamp.com>
*/
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')));
}
}

View File

@ -317,6 +317,36 @@ class ApiDocExtractor
$response = $this->generateHumanReadableTypes($response); $response = $this->generateHumanReadableTypes($response);
$annotation->setResponse($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; return $annotation;
@ -462,11 +492,15 @@ class ApiDocExtractor
protected function generateHumanReadableType($actualType, $subType) protected function generateHumanReadableType($actualType, $subType)
{ {
if ($actualType == DataTypes::MODEL) { if ($actualType == DataTypes::MODEL) {
$parts = explode('\\', $subType);
if (class_exists($subType)) {
$parts = explode('\\', $subType);
return sprintf('object (%s)', end($parts)); return sprintf('object (%s)', end($parts));
} }
return sprintf('object (%s)', $subType);
}
if ($actualType == DataTypes::COLLECTION) { if ($actualType == DataTypes::COLLECTION) {
if (DataTypes::isPrimitive($subType)) { if (DataTypes::isPrimitive($subType)) {
@ -475,7 +509,6 @@ class ApiDocExtractor
if (class_exists($subType)) { if (class_exists($subType)) {
$parts = explode('\\', $subType); $parts = explode('\\', $subType);
return sprintf('array of objects (%s)', end($parts)); return sprintf('array of objects (%s)', end($parts));
} }

View File

@ -0,0 +1,595 @@
<?php
/*
* This file is part of the NelmioApiDocBundle.
*
* (c) Nelmio <hello@nelm.io>
*
* 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 <bezalelhermoso@gmail.com>
*/
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);
}
}

View File

@ -2,6 +2,7 @@
namespace Nelmio\ApiDocBundle; namespace Nelmio\ApiDocBundle;
use Nelmio\ApiDocBundle\DependencyInjection\SwaggerConfigCompilerPass;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Nelmio\ApiDocBundle\DependencyInjection\LoadExtractorParsersPass; use Nelmio\ApiDocBundle\DependencyInjection\LoadExtractorParsersPass;
@ -17,5 +18,6 @@ class NelmioApiDocBundle extends Bundle
$container->addCompilerPass(new LoadExtractorParsersPass()); $container->addCompilerPass(new LoadExtractorParsersPass());
$container->addCompilerPass(new RegisterExtractorParsersPass()); $container->addCompilerPass(new RegisterExtractorParsersPass());
$container->addCompilerPass(new ExtractorHandlerCompilerPass()); $container->addCompilerPass(new ExtractorHandlerCompilerPass());
$container->addCompilerPass(new SwaggerConfigCompilerPass());
} }
} }

View File

@ -8,6 +8,7 @@
<parameter key="nelmio_api_doc.formatter.markdown_formatter.class">Nelmio\ApiDocBundle\Formatter\MarkdownFormatter</parameter> <parameter key="nelmio_api_doc.formatter.markdown_formatter.class">Nelmio\ApiDocBundle\Formatter\MarkdownFormatter</parameter>
<parameter key="nelmio_api_doc.formatter.simple_formatter.class">Nelmio\ApiDocBundle\Formatter\SimpleFormatter</parameter> <parameter key="nelmio_api_doc.formatter.simple_formatter.class">Nelmio\ApiDocBundle\Formatter\SimpleFormatter</parameter>
<parameter key="nelmio_api_doc.formatter.html_formatter.class">Nelmio\ApiDocBundle\Formatter\HtmlFormatter</parameter> <parameter key="nelmio_api_doc.formatter.html_formatter.class">Nelmio\ApiDocBundle\Formatter\HtmlFormatter</parameter>
<parameter key="nelmio_api_doc.formatter.swagger_formatter.class">Nelmio\ApiDocBundle\Formatter\SwaggerFormatter</parameter>
<parameter key="nelmio_api_doc.sandbox.authentication">null</parameter> <parameter key="nelmio_api_doc.sandbox.authentication">null</parameter>
</parameters> </parameters>
@ -56,6 +57,7 @@
<argument>%nelmio_api_doc.sandbox.authentication%</argument> <argument>%nelmio_api_doc.sandbox.authentication%</argument>
</call> </call>
</service> </service>
<service id="nelmio_api_doc.formatter.swagger_formatter" class="%nelmio_api_doc.formatter.swagger_formatter.class%" />
</services> </services>
</container> </container>

View File

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

View File

@ -319,4 +319,41 @@ class ApiDocTest extends TestCase
$this->assertTrue(is_array($array['tags']), 'Tags should be in array'); $this->assertTrue(is_array($array['tags']), 'Tags should be in array');
$this->assertEquals($data['tags'], $array['tags']); $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]);
}
} }

View File

@ -15,7 +15,7 @@ use Nelmio\ApiDocBundle\Tests\WebTestCase;
class ApiDocExtractorTest extends WebTestCase class ApiDocExtractorTest extends WebTestCase
{ {
const ROUTES_QUANTITY = 25; const ROUTES_QUANTITY = 31;
public function testAll() public function testAll()
{ {
@ -38,39 +38,39 @@ class ApiDocExtractorTest extends WebTestCase
$this->assertNotNull($d['resource']); $this->assertNotNull($d['resource']);
} }
$a1 = $data[0]['annotation']; $a1 = $data[7]['annotation'];
$array1 = $a1->toArray(); $array1 = $a1->toArray();
$this->assertTrue($a1->isResource()); $this->assertTrue($a1->isResource());
$this->assertEquals('index action', $a1->getDescription()); $this->assertEquals('index action', $a1->getDescription());
$this->assertTrue(is_array($array1['filters'])); $this->assertTrue(is_array($array1['filters']));
$this->assertNull($a1->getInput()); $this->assertNull($a1->getInput());
$a1 = $data[1]['annotation']; $a1 = $data[7]['annotation'];
$array1 = $a1->toArray(); $array1 = $a1->toArray();
$this->assertTrue($a1->isResource()); $this->assertTrue($a1->isResource());
$this->assertEquals('index action', $a1->getDescription()); $this->assertEquals('index action', $a1->getDescription());
$this->assertTrue(is_array($array1['filters'])); $this->assertTrue(is_array($array1['filters']));
$this->assertNull($a1->getInput()); $this->assertNull($a1->getInput());
$a2 = $data[2]['annotation']; $a2 = $data[8]['annotation'];
$array2 = $a2->toArray(); $array2 = $a2->toArray();
$this->assertFalse($a2->isResource()); $this->assertFalse($a2->isResource());
$this->assertEquals('create test', $a2->getDescription()); $this->assertEquals('create test', $a2->getDescription());
$this->assertFalse(isset($array2['filters'])); $this->assertFalse(isset($array2['filters']));
$this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput()); $this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput());
$a2 = $data[3]['annotation']; $a2 = $data[9]['annotation'];
$array2 = $a2->toArray(); $array2 = $a2->toArray();
$this->assertFalse($a2->isResource()); $this->assertFalse($a2->isResource());
$this->assertEquals('create test', $a2->getDescription()); $this->assertEquals('create test', $a2->getDescription());
$this->assertFalse(isset($array2['filters'])); $this->assertFalse(isset($array2['filters']));
$this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput()); $this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput());
$a4 = $data[5]['annotation']; $a4 = $data[11]['annotation'];
$this->assertTrue($a4->isResource()); $this->assertTrue($a4->isResource());
$this->assertEquals('TestResource', $a4->getResource()); $this->assertEquals('TestResource', $a4->getResource());
$a3 = $data['14']['annotation']; $a3 = $data[20]['annotation'];
$this->assertTrue($a3->getHttps()); $this->assertTrue($a3->getHttps());
} }
@ -224,6 +224,7 @@ class ApiDocExtractorTest extends WebTestCase
$this->assertNotNull($annotation); $this->assertNotNull($annotation);
$output = $annotation->getOutput(); $output = $annotation->getOutput();
$parsers = $output['parsers']; $parsers = $output['parsers'];
$this->assertEquals( $this->assertEquals(
"Nelmio\\ApiDocBundle\\Parser\\JmsMetadataParser", "Nelmio\\ApiDocBundle\\Parser\\JmsMetadataParser",

View File

@ -0,0 +1,85 @@
<?php
/**
* Created by PhpStorm.
* User: bezalelhermoso
* Date: 6/20/14
* Time: 3:21 PM
*/
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Controller;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
/**
* Class ResourceController
*
* @package Nelmio\ApiDocBundle\Tests\Fixtures\Controller
* @author Bez Hermoso <bez@activelamp.com>
*/
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()
{
}
}

View File

@ -1,6 +1,7 @@
<?php <?php
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form; namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;

View File

@ -54,3 +54,14 @@ jms_serializer:
nelmio_api_doc: nelmio_api_doc:
exclude_sections: ["private", "exclusive"] exclude_sections: ["private", "exclusive"]
swagger:
api_base_path: /api
swagger_version: 1.2
api_version: 3.14
info:
title: Nelmio Swagger
description: Testing Swagger integration.
TermsOfServiceUrl: https://github.com
contact: user@domain.tld
license: MIT
licenseUrl: http://opensource.org/licenses/MIT

View File

@ -161,3 +161,45 @@ test_route_22:
defaults: { _controller: NelmioApiDocTestBundle:Test:zActionWithNullableRequestParam } defaults: { _controller: NelmioApiDocTestBundle:Test:zActionWithNullableRequestParam }
requirements: requirements:
_method: POST _method: POST
test_route_list_resource:
pattern: /api/resources.{_format}
defaults: { _controller: NelmioApiDocTestBundle:Resource:listResources, _format: json }
requirements:
_method: GET
_format: json|xml|html
test_route_get_resource:
pattern: /api/resources/{id}.{_format}
defaults: { _controller: NelmioApiDocTestBundle:Resource:getResource, _format: json }
requirements:
_method: GET
_format: json|xml|html
test_route_delete_resource:
pattern: /api/resources/{id}.{_format}
defaults: { _controller: NelmioApiDocTestBundle:Resource:deleteResource, _format: json }
requirements:
_method: DELETE
_format: json|xml|html
test_route_create_resource:
pattern: /api/resources.{_format}
defaults: { _controller: NelmioApiDocTestBundle:Resource:createResource, _format: json }
requirements:
_method: POST
_format: json|xml|html
test_route_list_another_resource:
pattern: /api/other-resources.{_format}
defaults: { _controller: NelmioApiDocTestBundle:Resource:listAnotherResources, _format: json }
requirements:
_method: GET
_format: json|xml|html
test_route_update_another_resource:
pattern: /api/other-resources/{id}.{_format}
defaults: { _controller: NelmioApiDocTestBundle:Resource:updateAnotherResource, _format: json }
requirements:
_method: PUT|PATCH
_format: json|xml|html

View File

@ -26,6 +26,241 @@ class MarkdownFormatterTest extends WebTestCase
$result = $container->get('nelmio_api_doc.formatter.markdown_formatter')->format($data); $result = $container->get('nelmio_api_doc.formatter.markdown_formatter')->format($data);
$expected = <<<MARKDOWN $expected = <<<MARKDOWN
## /api/other-resources ##
### `GET` /api/other-resources.{_format} ###
_List another resource._
#### Requirements ####
**_format**
- Requirement: json|xml|html
### `PUT|PATCH` /api/other-resources/{id}.{_format} ###
_Update a resource bu ID._
#### Requirements ####
**_format**
- Requirement: json|xml|html
**id**
## /api/resources ##
### `GET` /api/resources.{_format} ###
_List resources._
#### Requirements ####
**_format**
- Requirement: json|xml|html
### `POST` /api/resources.{_format} ###
_Create a new resource._
#### Requirements ####
**_format**
- Requirement: json|xml|html
#### Parameters ####
a:
* type: string
* required: true
* description: Something that describes A.
b:
* type: float
* required: true
c:
* type: choice
* required: true
d:
* type: datetime
* required: true
e:
* type: date
* required: true
g:
* type: string
* required: true
#### Response ####
foo:
* type: DateTime
bar:
* type: string
baz[]:
* type: array of integers
* description: Epic description.
With multiple lines.
circular:
* type: object (JmsNested)
circular[foo]:
* type: DateTime
circular[bar]:
* type: string
circular[baz][]:
* type: array of integers
* description: Epic description.
With multiple lines.
circular[circular]:
* type: object (JmsNested)
circular[parent]:
* type: object (JmsTest)
circular[parent][foo]:
* type: string
circular[parent][bar]:
* type: DateTime
circular[parent][number]:
* type: double
circular[parent][arr]:
* type: array
circular[parent][nested]:
* type: object (JmsNested)
circular[parent][nested_array][]:
* type: array of objects (JmsNested)
circular[since]:
* type: string
* versions: >=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 ## ## /tests ##
### `GET` /tests.{_format} ### ### `GET` /tests.{_format} ###
@ -431,7 +666,7 @@ nested_array[]:
**id** **id**
- Requirement: \\d+ - Requirement: \d+
### `GET` /z-action-with-deprecated-indicator ### ### `GET` /z-action-with-deprecated-indicator ###
@ -459,7 +694,7 @@ param1:
page: page:
* Requirement: \\d+ * Requirement: \d+
* Description: Page of the overview. * Description: Page of the overview.
* Default: 1 * Default: 1
@ -527,14 +762,6 @@ related:
* type: object (Test) * type: object (Test)
related[a]:
* type: string
related[b]:
* type: DateTime
### `ANY` /z-return-selected-parsers-input ### ### `ANY` /z-return-selected-parsers-input ###
@ -592,14 +819,6 @@ number:
related: related:
* type: object (Test) * type: object (Test)
related[a]:
* type: string
related[b]:
* type: DateTime
MARKDOWN; MARKDOWN;
$this->assertEquals($expected, $result); $this->assertEquals($expected, $result);

View File

@ -198,7 +198,6 @@ class SimpleFormatterTest extends WebTestCase
'dataType' => 'string', 'dataType' => 'string',
'actualType' => DataTypes::STRING, 'actualType' => DataTypes::STRING,
'subType' => null, 'subType' => null,
'default' => null,
'default' => "DefaultTest", 'default' => "DefaultTest",
'required' => true, 'required' => true,
'description' => '', 'description' => '',
@ -622,8 +621,6 @@ And, it supports multilines until the first \'@\' char.',
), ),
), ),
'https' => false, '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, 'authentication' => false,
'authenticationRoles' => array(), 'authenticationRoles' => array(),
'deprecated' => false, 'deprecated' => false,
@ -1069,39 +1066,21 @@ With multiple lines.',
'subType' => null, 'subType' => null,
'default' => null, 'default' => null,
'required' => null, 'required' => null,
'readonly' => null 'readonly' => null,
) )
) )
), ),
'related' => array( 'related' => array(
'dataType' => 'object (Test)', 'dataType' => 'object (Test)',
'actualType' => DataTypes::MODEL,
'subType' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test',
'default' => null,
'readonly' => false, 'readonly' => false,
'required' => false, 'required' => false,
'description' => '', 'description' => '',
'sinceVersion' => null, 'sinceVersion' => null,
'untilVersion' => null, 'untilVersion' => null,
'actualType' => DataTypes::MODEL, 'children' => array(),
'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,
)
)
) )
), ),
'authenticationRoles' => array(), 'authenticationRoles' => array(),
@ -1147,7 +1126,6 @@ With multiple lines.',
'dataType' => 'string', 'dataType' => 'string',
'actualType' => DataTypes::STRING, 'actualType' => DataTypes::STRING,
'subType' => null, 'subType' => null,
'default' => null,
'default' => "DefaultTest", 'default' => "DefaultTest",
'required' => true, 'required' => true,
'description' => '', 'description' => '',
@ -1211,33 +1189,15 @@ With multiple lines.',
), ),
'related' => array( 'related' => array(
'dataType' => 'object (Test)', 'dataType' => 'object (Test)',
'subType' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test',
'actualType' => DataTypes::MODEL,
'default' => null,
'readonly' => false, 'readonly' => false,
'required' => false, 'required' => false,
'description' => '', 'description' => '',
'sinceVersion' => null, 'sinceVersion' => null,
'untilVersion' => null, 'untilVersion' => null,
'actualType' => DataTypes::MODEL, 'children' => array(),
'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,
)
)
) )
), ),
'authenticationRoles' => array(), 'authenticationRoles' => array(),
@ -1245,7 +1205,6 @@ With multiple lines.',
), ),
'/tests2' => '/tests2' =>
array( array(
0 =>
array( array(
'method' => 'POST', 'method' => 'POST',
'uri' => '/tests2.{_format}', 'uri' => '/tests2.{_format}',
@ -1267,7 +1226,6 @@ With multiple lines.',
), ),
'/tests2' => '/tests2' =>
array( array(
0 =>
array( array(
'method' => 'POST', 'method' => 'POST',
'uri' => '/tests2.{_format}', 'uri' => '/tests2.{_format}',
@ -1299,6 +1257,573 @@ With multiple lines.',
'deprecated' => false, '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); $this->assertEquals($expected, $result);

View File

@ -0,0 +1,726 @@
<?php
namespace Nelmio\ApiDocBundle\Tests\Formatter;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Formatter\SwaggerFormatter;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
/**
* Class SwaggerFormatterTest
*
* @package Nelmio\ApiDocBundle\Tests\Formatter
* @author Bez Hermoso <bez@activelamp.com>
*/
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 (
),
),
),
);
}
}