mirror of
https://github.com/retailcrm/NelmioApiDocBundle.git
synced 2025-02-02 15:51:48 +03:00
OpenApi 3 Support (#1623)
* Initial pass for OA3 upgrade * Fix Util Tests * Fix first batch of Unit Tests. Up to Model * Another batch of fixed tests * Update annotations * Convert Model & Property Describers * Update tests, Fix RouteDescribers, FIx additional bugs * Another batch of updates * Another batch of fixed Functional Tests * Fix FunctionalTest tests * Fix Bazinga Tests * FIx FOS Rest * Fix JMS TEsts & describers * Fix all Tests * Fix few stuff from own CR * CS Fixes * CS Fixes 2 * CS Fixes 3 * CS Fixes 4 * Remove collection bug * Updates after first CRs * CS * Drop support for SF3 * Update the docs * Add an upgrade guide * misc doc fixes * Configurable media types * Code Style Fixes * Don't use ::$ref for @Response and @RequestBody * Fix upgrading guide * Fix OA case Co-authored-by: Filip Benčo <filip.benco@websupport.sk> Co-authored-by: Guilhem Niot <guilhem.niot@gmail.com> Co-authored-by: Mantis Development <mantis@users.noreply.github.com>
This commit is contained in:
parent
2a78b42a94
commit
78664ef9ec
@ -18,8 +18,6 @@ matrix:
|
||||
include:
|
||||
- php: 7.1
|
||||
env: COMPOSER_FLAGS="--prefer-lowest"
|
||||
- php: 7.2
|
||||
env: SYMFONY_VERSION=^3.4
|
||||
- php: 7.3
|
||||
env: SYMFONY_VERSION=^4.0
|
||||
- php: 7.3
|
||||
|
@ -11,7 +11,8 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Annotation;
|
||||
|
||||
use Swagger\Annotations\AbstractAnnotation;
|
||||
use OpenApi\Annotations\AbstractAnnotation;
|
||||
use OpenApi\Annotations\Parameter;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
@ -28,8 +29,7 @@ final class Model extends AbstractAnnotation
|
||||
public static $_required = ['type'];
|
||||
|
||||
public static $_parents = [
|
||||
'Swagger\Annotations\Parameter',
|
||||
'Swagger\Annotations\Response',
|
||||
Parameter::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Annotation;
|
||||
|
||||
use Swagger\Annotations\Operation as BaseOperation;
|
||||
use OpenApi\Annotations\Operation as BaseOperation;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Annotation;
|
||||
|
||||
use Swagger\Annotations\AbstractAnnotation;
|
||||
use OpenApi\Annotations\AbstractAnnotation;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
|
@ -11,23 +11,31 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle;
|
||||
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\Describer\DescriberInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
final class ApiDocGenerator
|
||||
{
|
||||
private $swagger;
|
||||
/** @var OpenApi */
|
||||
private $openApi;
|
||||
|
||||
/** @var iterable|DescriberInterface[] */
|
||||
private $describers;
|
||||
|
||||
/** @var iterable|ModelDescriberInterface[] */
|
||||
private $modelDescribers;
|
||||
|
||||
/** @var CacheItemPoolInterface|null */
|
||||
private $cacheItemPool;
|
||||
|
||||
/** @var string|null */
|
||||
private $cacheItemId;
|
||||
|
||||
/** @var string[] */
|
||||
private $alternativeNames = [];
|
||||
|
||||
/**
|
||||
@ -47,34 +55,34 @@ final class ApiDocGenerator
|
||||
$this->alternativeNames = $alternativeNames;
|
||||
}
|
||||
|
||||
public function generate(): Swagger
|
||||
public function generate(): OpenApi
|
||||
{
|
||||
if (null !== $this->swagger) {
|
||||
return $this->swagger;
|
||||
if (null !== $this->openApi) {
|
||||
return $this->openApi;
|
||||
}
|
||||
|
||||
if ($this->cacheItemPool) {
|
||||
$item = $this->cacheItemPool->getItem($this->cacheItemId ?? 'swagger_doc');
|
||||
$item = $this->cacheItemPool->getItem($this->cacheItemId ?? 'openapi_doc');
|
||||
if ($item->isHit()) {
|
||||
return $this->swagger = $item->get();
|
||||
return $this->openApi = $item->get();
|
||||
}
|
||||
}
|
||||
|
||||
$this->swagger = new Swagger();
|
||||
$modelRegistry = new ModelRegistry($this->modelDescribers, $this->swagger, $this->alternativeNames);
|
||||
$this->openApi = new OpenApi([]);
|
||||
$modelRegistry = new ModelRegistry($this->modelDescribers, $this->openApi, $this->alternativeNames);
|
||||
foreach ($this->describers as $describer) {
|
||||
if ($describer instanceof ModelRegistryAwareInterface) {
|
||||
$describer->setModelRegistry($modelRegistry);
|
||||
}
|
||||
|
||||
$describer->describe($this->swagger);
|
||||
$describer->describe($this->openApi);
|
||||
}
|
||||
$modelRegistry->registerDefinitions();
|
||||
$modelRegistry->registerSchemas();
|
||||
|
||||
if (isset($item)) {
|
||||
$this->cacheItemPool->save($item->set($this->swagger));
|
||||
$this->cacheItemPool->save($item->set($this->openApi));
|
||||
}
|
||||
|
||||
return $this->swagger;
|
||||
return $this->openApi;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
3.3.0 (unreleased)
|
||||
------------------
|
||||
4.0.0
|
||||
-----
|
||||
* Added support of OpenAPI 3.0. The internals were completely reworked and this version introduces BC breaks.
|
||||
|
||||
3.3.0
|
||||
-----
|
||||
|
||||
* Usage of Google Fonts was removed. System fonts `serif` / `sans` will be used instead.
|
||||
This can lead to a different look on different operating systems.
|
||||
|
@ -12,6 +12,8 @@
|
||||
namespace Nelmio\ApiDocBundle\Controller;
|
||||
|
||||
use Nelmio\ApiDocBundle\ApiDocGenerator;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use OpenApi\Annotations\Server;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
@ -47,14 +49,11 @@ final class DocumentationController
|
||||
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area));
|
||||
}
|
||||
|
||||
$spec = $this->generatorLocator->get($area)->generate()->toArray();
|
||||
/** @var OpenApi $spec */
|
||||
$spec = $this->generatorLocator->get($area)->generate();
|
||||
|
||||
if ('' !== $request->getBaseUrl()) {
|
||||
$spec['basePath'] = $request->getBaseUrl();
|
||||
}
|
||||
|
||||
if (empty($spec['host'])) {
|
||||
$spec['host'] = $request->getHost();
|
||||
$spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
|
||||
}
|
||||
|
||||
return new JsonResponse($spec);
|
||||
|
@ -12,6 +12,8 @@
|
||||
namespace Nelmio\ApiDocBundle\Controller;
|
||||
|
||||
use Nelmio\ApiDocBundle\ApiDocGenerator;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use OpenApi\Annotations\Server;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@ -60,13 +62,18 @@ final class SwaggerUiController
|
||||
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.%s', $area, $advice));
|
||||
}
|
||||
|
||||
$spec = $this->generatorLocator->get($area)->generate()->toArray();
|
||||
/** @var OpenApi $spec */
|
||||
$spec = $this->generatorLocator->get($area)->generate();
|
||||
|
||||
if ('' !== $request->getBaseUrl()) {
|
||||
$spec['basePath'] = $request->getBaseUrl();
|
||||
$spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->twig->render('@NelmioApiDoc/SwaggerUi/index.html.twig', ['swagger_data' => ['spec' => $spec]]),
|
||||
$this->twig->render(
|
||||
'@NelmioApiDoc/SwaggerUi/index.html.twig',
|
||||
['swagger_data' => ['spec' => json_decode($spec->toJson(), true)]]
|
||||
),
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/html']
|
||||
);
|
||||
|
@ -53,6 +53,11 @@ final class Configuration implements ConfigurationInterface
|
||||
->example(['info' => ['title' => 'My App']])
|
||||
->prototype('variable')->end()
|
||||
->end()
|
||||
->arrayNode('media_types')
|
||||
->info('List of enabled Media Types')
|
||||
->defaultValue(['json'])
|
||||
->prototype('scalar')->end()
|
||||
->end()
|
||||
->arrayNode('areas')
|
||||
->info('Filter the routes that are documented')
|
||||
->defaultValue(
|
||||
|
@ -15,8 +15,8 @@ use FOS\RestBundle\Controller\Annotations\ParamInterface;
|
||||
use JMS\Serializer\Visitor\SerializationVisitorInterface;
|
||||
use Nelmio\ApiDocBundle\ApiDocGenerator;
|
||||
use Nelmio\ApiDocBundle\Describer\ExternalDocDescriber;
|
||||
use Nelmio\ApiDocBundle\Describer\OpenApiPhpDescriber;
|
||||
use Nelmio\ApiDocBundle\Describer\RouteDescriber;
|
||||
use Nelmio\ApiDocBundle\Describer\SwaggerPhpDescriber;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\BazingaHateoasModelDescriber;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber;
|
||||
use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder;
|
||||
@ -82,13 +82,14 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
|
||||
])
|
||||
->addTag(sprintf('nelmio_api_doc.describer.%s', $area), ['priority' => -400]);
|
||||
|
||||
$container->register(sprintf('nelmio_api_doc.describers.swagger_php.%s', $area), SwaggerPhpDescriber::class)
|
||||
$container->register(sprintf('nelmio_api_doc.describers.openapi_php.%s', $area), OpenApiPhpDescriber::class)
|
||||
->setPublic(false)
|
||||
->setArguments([
|
||||
new Reference(sprintf('nelmio_api_doc.routes.%s', $area)),
|
||||
new Reference('nelmio_api_doc.controller_reflector'),
|
||||
new Reference('annotation_reader'),
|
||||
new Reference('logger'),
|
||||
$config['media_types'],
|
||||
])
|
||||
->addTag(sprintf('nelmio_api_doc.describer.%s', $area), ['priority' => -200]);
|
||||
|
||||
@ -135,11 +136,16 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
|
||||
array_map(function ($area) { return new Reference(sprintf('nelmio_api_doc.generator.%s', $area)); }, array_keys($config['areas']))
|
||||
));
|
||||
|
||||
$container->getDefinition('nelmio_api_doc.model_describers.object')
|
||||
->setArgument(3, $config['media_types']);
|
||||
|
||||
// Import services needed for each library
|
||||
$loader->load('php_doc.xml');
|
||||
|
||||
if (interface_exists(ParamInterface::class)) {
|
||||
$loader->load('fos_rest.xml');
|
||||
$container->getDefinition('nelmio_api_doc.route_describers.fos_rest')
|
||||
->setArgument(1, $config['media_types']);
|
||||
}
|
||||
|
||||
// ApiPlatform support
|
||||
@ -159,8 +165,9 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
|
||||
->setPublic(false)
|
||||
->setArguments([
|
||||
new Reference('jms_serializer.metadata_factory'),
|
||||
$jmsNamingStrategy,
|
||||
new Reference('annotation_reader'),
|
||||
$config['media_types'],
|
||||
$jmsNamingStrategy,
|
||||
])
|
||||
->addTag('nelmio_api_doc.model_describer', ['priority' => 50]);
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Nelmio\ApiDocBundle\Describer;
|
||||
|
||||
use ApiPlatform\Core\Documentation\Documentation;
|
||||
use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
final class ApiPlatformDescriber extends ExternalDocDescriber
|
||||
@ -23,7 +24,12 @@ final class ApiPlatformDescriber extends ExternalDocDescriber
|
||||
}
|
||||
|
||||
parent::__construct(function () use ($documentation, $normalizer) {
|
||||
$documentation = (array) $normalizer->normalize($documentation);
|
||||
$documentation = (array) $normalizer->normalize(
|
||||
$documentation,
|
||||
null,
|
||||
[DocumentationNormalizer::SPEC_VERSION => 3]
|
||||
);
|
||||
|
||||
unset($documentation['basePath']);
|
||||
|
||||
return $documentation;
|
||||
|
@ -11,7 +11,8 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Describer;
|
||||
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
* Makes the swagger documentation valid even if there are missing fields.
|
||||
@ -20,27 +21,28 @@ use EXSyst\Component\Swagger\Swagger;
|
||||
*/
|
||||
final class DefaultDescriber implements DescriberInterface
|
||||
{
|
||||
public function describe(Swagger $api)
|
||||
public function describe(OA\OpenApi $api)
|
||||
{
|
||||
// Info
|
||||
$info = $api->getInfo();
|
||||
if (null === $info->getTitle()) {
|
||||
$info->setTitle('');
|
||||
/** @var OA\Info $info */
|
||||
$info = Util::getChild($api, OA\Info::class);
|
||||
if (OA\UNDEFINED === $info->title) {
|
||||
$info->title = '';
|
||||
}
|
||||
if (null === $info->getVersion()) {
|
||||
$info->setVersion('0.0.0');
|
||||
if (OA\UNDEFINED === $info->version) {
|
||||
$info->version = '0.0.0';
|
||||
}
|
||||
|
||||
// Paths
|
||||
$paths = $api->getPaths();
|
||||
foreach ($paths as $uri => $path) {
|
||||
foreach ($path->getMethods() as $method) {
|
||||
$operation = $path->getOperation($method);
|
||||
|
||||
// Default Response
|
||||
if (0 === iterator_count($operation->getResponses())) {
|
||||
$defaultResponse = $operation->getResponses()->get('default');
|
||||
$defaultResponse->setDescription('');
|
||||
$paths = OA\UNDEFINED === $api->paths ? [] : $api->paths;
|
||||
foreach ($paths as $path) {
|
||||
foreach (Util::OPERATIONS as $method) {
|
||||
/** @var OA\Operation $operation */
|
||||
$operation = $path->{$method};
|
||||
if (OA\UNDEFINED !== $operation && null !== $operation && empty($operation->responses ?? [])) {
|
||||
/** @var OA\Response $response */
|
||||
$response = Util::getIndexedCollectionItem($operation, OA\Response::class, 'default');
|
||||
$response->description = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,9 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Describer;
|
||||
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
|
||||
interface DescriberInterface
|
||||
{
|
||||
public function describe(Swagger $api);
|
||||
public function describe(OpenApi $api);
|
||||
}
|
||||
|
@ -11,7 +11,8 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Describer;
|
||||
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
class ExternalDocDescriber implements DescriberInterface
|
||||
{
|
||||
@ -29,10 +30,13 @@ class ExternalDocDescriber implements DescriberInterface
|
||||
$this->overwrite = $overwrite;
|
||||
}
|
||||
|
||||
public function describe(Swagger $api)
|
||||
public function describe(OA\OpenApi $api)
|
||||
{
|
||||
$externalDoc = $this->getExternalDoc();
|
||||
$api->merge($externalDoc, $this->overwrite);
|
||||
|
||||
if (!empty($externalDoc)) {
|
||||
Util::merge($api, $externalDoc, $this->overwrite);
|
||||
}
|
||||
}
|
||||
|
||||
private function getExternalDoc()
|
||||
|
212
Describer/OpenApiPhpDescriber.php
Normal file
212
Describer/OpenApiPhpDescriber.php
Normal file
@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Describer;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use Nelmio\ApiDocBundle\Annotation\Operation;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\AddDefaults;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use Nelmio\ApiDocBundle\Util\ControllerReflector;
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
// Help opcache.preload discover Swagger\Annotations\Swagger
|
||||
class_exists(OA\OpenApi::class);
|
||||
|
||||
final class OpenApiPhpDescriber implements ModelRegistryAwareInterface
|
||||
{
|
||||
use ModelRegistryAwareTrait;
|
||||
|
||||
private $routeCollection;
|
||||
private $controllerReflector;
|
||||
private $annotationReader;
|
||||
private $logger;
|
||||
private $mediaTypes;
|
||||
private $overwrite;
|
||||
|
||||
public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, Reader $annotationReader, LoggerInterface $logger, array $mediaTypes, bool $overwrite = false)
|
||||
{
|
||||
$this->routeCollection = $routeCollection;
|
||||
$this->controllerReflector = $controllerReflector;
|
||||
$this->annotationReader = $annotationReader;
|
||||
$this->logger = $logger;
|
||||
$this->mediaTypes = $mediaTypes;
|
||||
$this->overwrite = $overwrite;
|
||||
}
|
||||
|
||||
public function describe(OA\OpenApi $api)
|
||||
{
|
||||
$analysis = $this->getAnnotations($api);
|
||||
$analysis->process($this->getProcessors());
|
||||
$analysis->validate();
|
||||
}
|
||||
|
||||
private function getProcessors(): array
|
||||
{
|
||||
$processors = [
|
||||
new AddDefaults(),
|
||||
new ModelRegister($this->modelRegistry, $this->mediaTypes),
|
||||
];
|
||||
|
||||
return array_merge($processors, Analysis::processors());
|
||||
}
|
||||
|
||||
private function getAnnotations(OA\OpenApi $api): Analysis
|
||||
{
|
||||
$analysis = new Analysis();
|
||||
$analysis->openapi = $api;
|
||||
|
||||
$classAnnotations = [];
|
||||
|
||||
/** @var \ReflectionMethod $method */
|
||||
foreach ($this->getMethodsToParse() as $method => list($path, $httpMethods)) {
|
||||
$declaringClass = $method->getDeclaringClass();
|
||||
if (!array_key_exists($declaringClass->getName(), $classAnnotations)) {
|
||||
$classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) {
|
||||
return $v instanceof OA\AbstractAnnotation;
|
||||
});
|
||||
$classAnnotations[$declaringClass->getName()] = $classAnnotations;
|
||||
}
|
||||
|
||||
$annotations = array_filter($this->annotationReader->getMethodAnnotations($method), function ($v) {
|
||||
return $v instanceof OA\AbstractAnnotation;
|
||||
});
|
||||
|
||||
if (0 === count($annotations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = Util::getPath($api, $path);
|
||||
$path->_context->namespace = $method->getNamespaceName();
|
||||
$path->_context->class = $declaringClass->getShortName();
|
||||
$path->_context->method = $method->name;
|
||||
$path->_context->filename = $method->getFileName();
|
||||
|
||||
$nestedContext = Util::createContext(['nested' => $path], $path->_context);
|
||||
$implicitAnnotations = [];
|
||||
$mergeProperties = new \stdClass();
|
||||
|
||||
foreach (array_merge($annotations, $classAnnotations[$declaringClass->getName()]) as $annotation) {
|
||||
$annotation->_context = $nestedContext;
|
||||
|
||||
if ($annotation instanceof Operation) {
|
||||
foreach ($httpMethods as $httpMethod) {
|
||||
$operation = Util::getOperation($path, $httpMethod);
|
||||
$operation->mergeProperties($annotation);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($annotation instanceof OA\Operation) {
|
||||
$operation = Util::getOperation($path, $annotation->method);
|
||||
$operation->mergeProperties($annotation);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($annotation instanceof Security) {
|
||||
$annotation->validate();
|
||||
$mergeProperties->security[] = [$annotation->name => []];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($annotation instanceof OA\Tag) {
|
||||
$annotation->validate();
|
||||
$mergeProperties->tags[] = $annotation->name;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!$annotation instanceof OA\Response &&
|
||||
!$annotation instanceof OA\RequestBody &&
|
||||
!$annotation instanceof OA\Parameter &&
|
||||
!$annotation instanceof OA\ExternalDocumentation
|
||||
) {
|
||||
throw new \LogicException(sprintf('Using the annotation "%s" as a root annotation in "%s::%s()" is not allowed.', get_class($annotation), $method->getDeclaringClass()->name, $method->name));
|
||||
}
|
||||
|
||||
foreach ($annotation->_unmerged as $unmergedAnnotation) {
|
||||
if (!$unmergedAnnotation instanceof OA\JsonContent && !$unmergedAnnotation instanceof OA\XmlContent) {
|
||||
continue;
|
||||
}
|
||||
$unmergedAnnotation->_context->nested = $annotation;
|
||||
}
|
||||
|
||||
$implicitAnnotations[] = $annotation;
|
||||
}
|
||||
|
||||
if (empty($implicitAnnotations) && empty(get_object_vars($mergeProperties))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Registers new annotations
|
||||
$analysis->addAnnotations($implicitAnnotations, null);
|
||||
|
||||
foreach ($httpMethods as $httpMethod) {
|
||||
$operation = Util::getOperation($path, $httpMethod);
|
||||
$operation->merge($implicitAnnotations);
|
||||
$operation->mergeProperties($mergeProperties);
|
||||
}
|
||||
}
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
private function getMethodsToParse(): \Generator
|
||||
{
|
||||
foreach ($this->routeCollection->all() as $route) {
|
||||
if (!$route->hasDefault('_controller')) {
|
||||
continue;
|
||||
}
|
||||
$controller = $route->getDefault('_controller');
|
||||
$reflectedMethod = $this->controllerReflector->getReflectionMethod($controller);
|
||||
if (null === $reflectedMethod) {
|
||||
continue;
|
||||
}
|
||||
$path = $this->normalizePath($route->getPath());
|
||||
$supportedHttpMethods = $this->getSupportedHttpMethods($route);
|
||||
if (empty($supportedHttpMethods)) {
|
||||
$this->logger->warning('None of the HTTP methods specified for path {path} are supported by swagger-ui, skipping this path', [
|
||||
'path' => $path,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
yield $reflectedMethod => [$path, $supportedHttpMethods];
|
||||
}
|
||||
}
|
||||
|
||||
private function getSupportedHttpMethods(Route $route): array
|
||||
{
|
||||
$allMethods = Util::OPERATIONS;
|
||||
$methods = array_map('strtolower', $route->getMethods());
|
||||
|
||||
return array_intersect($methods ?: $allMethods, $allMethods);
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
if ('.{_format}' === substr($path, -10)) {
|
||||
$path = substr($path, 0, -10);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
@ -11,10 +11,9 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Describer;
|
||||
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\RouteDescriber\RouteDescriberInterface;
|
||||
use Nelmio\ApiDocBundle\Util\ControllerReflector;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
final class RouteDescriber implements DescriberInterface, ModelRegistryAwareInterface
|
||||
@ -39,7 +38,7 @@ final class RouteDescriber implements DescriberInterface, ModelRegistryAwareInte
|
||||
$this->routeDescribers = $routeDescribers;
|
||||
}
|
||||
|
||||
public function describe(Swagger $api)
|
||||
public function describe(OA\OpenApi $api)
|
||||
{
|
||||
if (0 === count($this->routeDescribers)) {
|
||||
return;
|
||||
|
@ -1,276 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Describer;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\Annotation\Operation;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security;
|
||||
use Nelmio\ApiDocBundle\SwaggerPhp\AddDefaults;
|
||||
use Nelmio\ApiDocBundle\SwaggerPhp\ModelRegister;
|
||||
use Nelmio\ApiDocBundle\Util\ControllerReflector;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Swagger\Analysis;
|
||||
use Swagger\Annotations\AbstractAnnotation;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Swagger\Context;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
// Help opcache.preload discover Swagger\Annotations\Swagger
|
||||
class_exists(SWG\Swagger::class);
|
||||
|
||||
final class SwaggerPhpDescriber implements ModelRegistryAwareInterface
|
||||
{
|
||||
use ModelRegistryAwareTrait;
|
||||
|
||||
private $routeCollection;
|
||||
private $controllerReflector;
|
||||
private $annotationReader;
|
||||
private $logger;
|
||||
private $overwrite;
|
||||
|
||||
public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, Reader $annotationReader, LoggerInterface $logger, bool $overwrite = false)
|
||||
{
|
||||
$this->routeCollection = $routeCollection;
|
||||
$this->controllerReflector = $controllerReflector;
|
||||
$this->annotationReader = $annotationReader;
|
||||
$this->logger = $logger;
|
||||
$this->overwrite = $overwrite;
|
||||
}
|
||||
|
||||
public function describe(Swagger $api)
|
||||
{
|
||||
$analysis = $this->getAnnotations($api);
|
||||
|
||||
$analysis->process($this->getProcessors());
|
||||
$analysis->validate();
|
||||
|
||||
$api->merge(json_decode(json_encode($analysis->swagger), true), $this->overwrite);
|
||||
}
|
||||
|
||||
private function getProcessors(): array
|
||||
{
|
||||
$processors = [
|
||||
new AddDefaults(),
|
||||
new ModelRegister($this->modelRegistry),
|
||||
];
|
||||
|
||||
return array_merge($processors, Analysis::processors());
|
||||
}
|
||||
|
||||
private function getAnnotations(Swagger $api): Analysis
|
||||
{
|
||||
$analysis = new Analysis();
|
||||
$analysis->addAnnotation(new class($api) extends SWG\Swagger {
|
||||
private $api;
|
||||
|
||||
public function __construct(Swagger $api)
|
||||
{
|
||||
$this->api = $api;
|
||||
parent::__construct([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Support definitions from the config and reference to models.
|
||||
*/
|
||||
public function ref($ref)
|
||||
{
|
||||
if (0 === strpos($ref, '#/definitions/') && $this->api->getDefinitions()->has(substr($ref, 14))) {
|
||||
return;
|
||||
}
|
||||
if (0 === strpos($ref, '#/parameters/') && isset($this->api->getParameters()[substr($ref, 13)])) {
|
||||
return;
|
||||
}
|
||||
if (0 === strpos($ref, '#/responses/') && $this->api->getResponses()->has(substr($ref, 12))) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent::ref($ref);
|
||||
}
|
||||
}, null);
|
||||
|
||||
$operationAnnotations = [
|
||||
'get' => SWG\Get::class,
|
||||
'post' => SWG\Post::class,
|
||||
'put' => SWG\Put::class,
|
||||
'patch' => SWG\Patch::class,
|
||||
'delete' => SWG\Delete::class,
|
||||
'options' => SWG\Options::class,
|
||||
'head' => SWG\Head::class,
|
||||
];
|
||||
|
||||
$classAnnotations = [];
|
||||
|
||||
foreach ($this->getMethodsToParse() as $method => list($path, $httpMethods)) {
|
||||
$declaringClass = $method->getDeclaringClass();
|
||||
if (!array_key_exists($declaringClass->getName(), $classAnnotations)) {
|
||||
$classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) {
|
||||
return $v instanceof SWG\AbstractAnnotation;
|
||||
});
|
||||
$classAnnotations[$declaringClass->getName()] = $classAnnotations;
|
||||
}
|
||||
|
||||
$annotations = array_filter($this->annotationReader->getMethodAnnotations($method), function ($v) {
|
||||
return $v instanceof SWG\AbstractAnnotation;
|
||||
});
|
||||
|
||||
if (0 === count($annotations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context = new Context([
|
||||
'namespace' => $method->getNamespaceName(),
|
||||
'class' => $declaringClass->getShortName(),
|
||||
'method' => $method->name,
|
||||
'filename' => $method->getFileName(),
|
||||
]);
|
||||
$nestedContext = clone $context;
|
||||
$nestedContext->nested = true;
|
||||
$implicitAnnotations = [];
|
||||
$operations = [];
|
||||
$tags = [];
|
||||
$security = [];
|
||||
foreach (array_merge($annotations, $classAnnotations[$declaringClass->getName()]) as $annotation) {
|
||||
$annotation->_context = $context;
|
||||
$this->updateNestedAnnotations($annotation, $nestedContext);
|
||||
|
||||
if ($annotation instanceof Operation) {
|
||||
foreach ($httpMethods as $httpMethod) {
|
||||
$annotationClass = $operationAnnotations[$httpMethod];
|
||||
$operation = new $annotationClass(['_context' => $context]);
|
||||
$operation->path = $path;
|
||||
$operation->mergeProperties($annotation);
|
||||
|
||||
$operations[$httpMethod] = $operation;
|
||||
$analysis->addAnnotation($operation, null);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($annotation instanceof SWG\Operation) {
|
||||
if (null === $annotation->path) {
|
||||
$annotation = clone $annotation;
|
||||
$annotation->path = $path;
|
||||
}
|
||||
|
||||
$operations[$annotation->method] = $annotation;
|
||||
$analysis->addAnnotation($annotation, null);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($annotation instanceof Security) {
|
||||
$annotation->validate();
|
||||
$security[] = [$annotation->name => []];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($annotation instanceof SWG\Tag) {
|
||||
$annotation->validate();
|
||||
$tags[] = $annotation->name;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$annotation instanceof SWG\Response && !$annotation instanceof SWG\Parameter && !$annotation instanceof SWG\ExternalDocumentation) {
|
||||
throw new \LogicException(sprintf('Using the annotation "%s" as a root annotation in "%s::%s()" is not allowed. It should probably be nested in a `@SWG\Response` or `@SWG\Parameter` annotation.', get_class($annotation), $method->getDeclaringClass()->name, $method->name));
|
||||
}
|
||||
|
||||
$implicitAnnotations[] = $annotation;
|
||||
}
|
||||
|
||||
if (0 === count($implicitAnnotations) && 0 === count($tags) && 0 === count($security)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Registers new annotations
|
||||
$analysis->addAnnotations($implicitAnnotations, null);
|
||||
|
||||
foreach ($httpMethods as $httpMethod) {
|
||||
$annotationClass = $operationAnnotations[$httpMethod];
|
||||
$constructorArg = [
|
||||
'_context' => $context,
|
||||
'path' => $path,
|
||||
'value' => $implicitAnnotations,
|
||||
];
|
||||
|
||||
if (0 !== count($tags)) {
|
||||
$constructorArg['tags'] = $tags;
|
||||
}
|
||||
if (0 !== count($security)) {
|
||||
$constructorArg['security'] = $security;
|
||||
}
|
||||
|
||||
$operation = new $annotationClass($constructorArg);
|
||||
if (isset($operations[$httpMethod])) {
|
||||
$operations[$httpMethod]->mergeProperties($operation);
|
||||
} else {
|
||||
$analysis->addAnnotation($operation, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
private function getMethodsToParse(): \Generator
|
||||
{
|
||||
foreach ($this->routeCollection->all() as $route) {
|
||||
if (!$route->hasDefault('_controller')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$controller = $route->getDefault('_controller');
|
||||
if ($method = $this->controllerReflector->getReflectionMethod($controller)) {
|
||||
$path = $this->normalizePath($route->getPath());
|
||||
$httpMethods = $route->getMethods() ?: Swagger::$METHODS;
|
||||
$httpMethods = array_map('strtolower', $httpMethods);
|
||||
$supportedHttpMethods = array_intersect($httpMethods, Swagger::$METHODS);
|
||||
|
||||
if (empty($supportedHttpMethods)) {
|
||||
$this->logger->warning('None of the HTTP methods specified for path {path} are supported by swagger-ui, skipping this path', [
|
||||
'path' => $path,
|
||||
'methods' => $httpMethods,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $method => [$path, $supportedHttpMethods];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
if ('.{_format}' === substr($path, -10)) {
|
||||
$path = substr($path, 0, -10);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function updateNestedAnnotations($value, Context $context)
|
||||
{
|
||||
if ($value instanceof AbstractAnnotation) {
|
||||
$value->_context = $context;
|
||||
} elseif (!is_array($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($value as $v) {
|
||||
$this->updateNestedAnnotations($v, $context);
|
||||
}
|
||||
}
|
||||
}
|
@ -11,10 +11,10 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Model;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
final class ModelRegistry
|
||||
@ -36,7 +36,7 @@ final class ModelRegistry
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function __construct($modelDescribers, Swagger $api, array $alternativeNames = [])
|
||||
public function __construct($modelDescribers, OA\OpenApi $api, array $alternativeNames = [])
|
||||
{
|
||||
$this->modelDescribers = $modelDescribers;
|
||||
$this->api = $api;
|
||||
@ -45,7 +45,7 @@ final class ModelRegistry
|
||||
foreach (array_reverse($alternativeNames) as $alternativeName => $criteria) {
|
||||
$this->alternativeNames[] = $model = new Model(new Type('object', false, $criteria['type']), $criteria['groups']);
|
||||
$this->names[$model->getHash()] = $alternativeName;
|
||||
$this->api->getDefinitions()->get($alternativeName);
|
||||
Util::getSchema($this->api, $alternativeName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,15 +61,15 @@ final class ModelRegistry
|
||||
}
|
||||
|
||||
// Reserve the name
|
||||
$this->api->getDefinitions()->get($this->names[$hash]);
|
||||
Util::getSchema($this->api, $this->names[$hash]);
|
||||
|
||||
return '#/definitions/'.$this->names[$hash];
|
||||
return OA\Components::SCHEMA_REF.$this->names[$hash];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function registerDefinitions()
|
||||
public function registerSchemas(): void
|
||||
{
|
||||
while (count($this->unregistered)) {
|
||||
$tmp = [];
|
||||
@ -85,7 +85,7 @@ final class ModelRegistry
|
||||
$modelDescriber->setModelRegistry($this);
|
||||
}
|
||||
if ($modelDescriber->supports($model)) {
|
||||
$schema = new Schema();
|
||||
$schema = Util::getSchema($this->api, $name);
|
||||
$modelDescriber->describe($model, $schema);
|
||||
|
||||
break;
|
||||
@ -95,8 +95,6 @@ final class ModelRegistry
|
||||
if (null === $schema) {
|
||||
throw new \LogicException(sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $this->typeToString($model->getType())));
|
||||
}
|
||||
|
||||
$this->api->getDefinitions()->set($name, $schema);
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,18 +103,19 @@ final class ModelRegistry
|
||||
$this->register($model);
|
||||
}
|
||||
$this->alternativeNames = [];
|
||||
$this->registerDefinitions();
|
||||
$this->registerSchemas();
|
||||
}
|
||||
}
|
||||
|
||||
private function generateModelName(Model $model): string
|
||||
{
|
||||
$definitions = $this->api->getDefinitions();
|
||||
|
||||
$name = $base = $this->getTypeShortName($model->getType());
|
||||
|
||||
$names = array_column(
|
||||
$this->api->components instanceof OA\Components && is_array($this->api->components->schemas) ? $this->api->components->schemas : [],
|
||||
'schema'
|
||||
);
|
||||
$i = 1;
|
||||
while ($definitions->has($name)) {
|
||||
while (\in_array($name, $names, true)) {
|
||||
++$i;
|
||||
$name = $base.$i;
|
||||
}
|
||||
|
@ -12,8 +12,8 @@
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -24,34 +24,34 @@ class AnnotationsReader
|
||||
private $modelRegistry;
|
||||
|
||||
private $phpDocReader;
|
||||
private $swgAnnotationsReader;
|
||||
private $openApiAnnotationsReader;
|
||||
private $symfonyConstraintAnnotationReader;
|
||||
|
||||
public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry)
|
||||
public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes)
|
||||
{
|
||||
$this->annotationsReader = $annotationsReader;
|
||||
$this->modelRegistry = $modelRegistry;
|
||||
|
||||
$this->phpDocReader = new PropertyPhpDocReader();
|
||||
$this->swgAnnotationsReader = new SwgAnnotationsReader($annotationsReader, $modelRegistry);
|
||||
$this->openApiAnnotationsReader = new OpenApiAnnotationsReader($annotationsReader, $modelRegistry, $mediaTypes);
|
||||
$this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($annotationsReader);
|
||||
}
|
||||
|
||||
public function updateDefinition(\ReflectionClass $reflectionClass, Schema $schema)
|
||||
public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): void
|
||||
{
|
||||
$this->swgAnnotationsReader->updateDefinition($reflectionClass, $schema);
|
||||
$this->openApiAnnotationsReader->updateSchema($reflectionClass, $schema);
|
||||
$this->symfonyConstraintAnnotationReader->setSchema($schema);
|
||||
}
|
||||
|
||||
public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string
|
||||
{
|
||||
return $this->swgAnnotationsReader->getPropertyName($reflectionProperty, $default);
|
||||
return $this->openApiAnnotationsReader->getPropertyName($reflectionProperty, $default);
|
||||
}
|
||||
|
||||
public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property, array $serializationGroups = null)
|
||||
public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property, array $serializationGroups = null): void
|
||||
{
|
||||
$this->phpDocReader->updateProperty($reflectionProperty, $property);
|
||||
$this->swgAnnotationsReader->updateProperty($reflectionProperty, $property, $serializationGroups);
|
||||
$this->openApiAnnotationsReader->updateProperty($reflectionProperty, $property, $serializationGroups);
|
||||
$this->symfonyConstraintAnnotationReader->updateProperty($reflectionProperty, $property);
|
||||
}
|
||||
}
|
||||
|
87
ModelDescriber/Annotations/OpenApiAnnotationsReader.php
Normal file
87
ModelDescriber/Annotations/OpenApiAnnotationsReader.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Context;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class OpenApiAnnotationsReader
|
||||
{
|
||||
private $annotationsReader;
|
||||
private $modelRegister;
|
||||
|
||||
public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes)
|
||||
{
|
||||
$this->annotationsReader = $annotationsReader;
|
||||
$this->modelRegister = new ModelRegister($modelRegistry, $mediaTypes);
|
||||
}
|
||||
|
||||
public function updateSchema(\ReflectionClass $reflectionClass, OA\Schema $schema): void
|
||||
{
|
||||
/** @var OA\Schema $oaSchema */
|
||||
if (!$oaSchema = $this->annotationsReader->getClassAnnotation($reflectionClass, OA\Schema::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read @Model annotations
|
||||
$this->modelRegister->__invoke(new Analysis([$oaSchema]));
|
||||
|
||||
if (!$oaSchema->validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema->mergeProperties($oaSchema);
|
||||
}
|
||||
|
||||
public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string
|
||||
{
|
||||
/** @var OA\Property $oaProperty */
|
||||
if (!$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, OA\Property::class)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return OA\UNDEFINED !== $oaProperty->property ? $oaProperty->property : $default;
|
||||
}
|
||||
|
||||
public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property, array $serializationGroups = null): void
|
||||
{
|
||||
/** @var OA\Property $oaProperty */
|
||||
if (!$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, OA\Property::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$declaringClass = $reflectionProperty->getDeclaringClass();
|
||||
$context = new Context([
|
||||
'namespace' => $declaringClass->getNamespaceName(),
|
||||
'class' => $declaringClass->getShortName(),
|
||||
'property' => $reflectionProperty->name,
|
||||
'filename' => $declaringClass->getFileName(),
|
||||
]);
|
||||
$oaProperty->_context = $context;
|
||||
|
||||
// Read @Model annotations
|
||||
$this->modelRegister->__invoke(new Analysis([$oaProperty]), $serializationGroups);
|
||||
|
||||
if (!$oaProperty->validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$property->mergeProperties($oaProperty);
|
||||
}
|
||||
}
|
@ -11,8 +11,7 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations as OA;
|
||||
use phpDocumentor\Reflection\DocBlock\Tags\Var_;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
|
||||
@ -33,7 +32,7 @@ class PropertyPhpDocReader
|
||||
/**
|
||||
* Update the Swagger information with information from the DocBlock comment.
|
||||
*/
|
||||
public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property)
|
||||
public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property): void
|
||||
{
|
||||
try {
|
||||
$docBlock = $this->docBlockFactory->create($reflectionProperty);
|
||||
@ -54,11 +53,11 @@ class PropertyPhpDocReader
|
||||
}
|
||||
}
|
||||
}
|
||||
if (null === $property->getTitle() && $title) {
|
||||
$property->setTitle($title);
|
||||
if (OA\UNDEFINED === $property->title && $title) {
|
||||
$property->title = $title;
|
||||
}
|
||||
if (null === $property->getDescription() && $docBlock->getDescription() && $docBlock->getDescription()->render()) {
|
||||
$property->setDescription($docBlock->getDescription()->render());
|
||||
if (OA\UNDEFINED === $property->description && $docBlock->getDescription() && $docBlock->getDescription()->render()) {
|
||||
$property->description = $docBlock->getDescription()->render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,89 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use Nelmio\ApiDocBundle\SwaggerPhp\ModelRegister;
|
||||
use Swagger\Analysis;
|
||||
use Swagger\Annotations\Definition as SwgDefinition;
|
||||
use Swagger\Annotations\Property as SwgProperty;
|
||||
use Swagger\Context;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class SwgAnnotationsReader
|
||||
{
|
||||
private $annotationsReader;
|
||||
private $modelRegister;
|
||||
|
||||
public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry)
|
||||
{
|
||||
$this->annotationsReader = $annotationsReader;
|
||||
$this->modelRegister = new ModelRegister($modelRegistry);
|
||||
}
|
||||
|
||||
public function updateDefinition(\ReflectionClass $reflectionClass, Schema $schema)
|
||||
{
|
||||
/** @var SwgDefinition $swgDefinition */
|
||||
if (!$swgDefinition = $this->annotationsReader->getClassAnnotation($reflectionClass, SwgDefinition::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read @Model annotations
|
||||
$this->modelRegister->__invoke(new Analysis([$swgDefinition]));
|
||||
|
||||
if (!$swgDefinition->validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema->merge(json_decode(json_encode($swgDefinition)));
|
||||
}
|
||||
|
||||
public function getPropertyName(\ReflectionProperty $reflectionProperty, string $default): string
|
||||
{
|
||||
/** @var SwgProperty $swgProperty */
|
||||
if (!$swgProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, SwgProperty::class)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $swgProperty->property ?? $default;
|
||||
}
|
||||
|
||||
public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property, array $serializationGroups = null)
|
||||
{
|
||||
if (!$swgProperty = $this->annotationsReader->getPropertyAnnotation($reflectionProperty, SwgProperty::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$declaringClass = $reflectionProperty->getDeclaringClass();
|
||||
$context = new Context([
|
||||
'namespace' => $declaringClass->getNamespaceName(),
|
||||
'class' => $declaringClass->getShortName(),
|
||||
'property' => $reflectionProperty->name,
|
||||
'filename' => $declaringClass->getFileName(),
|
||||
]);
|
||||
$swgProperty->_context = $context;
|
||||
|
||||
// Read @Model annotations
|
||||
$this->modelRegister->__invoke(new Analysis([$swgProperty]), $serializationGroups);
|
||||
|
||||
if (!$swgProperty->validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$property->merge(json_decode(json_encode($swgProperty)));
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
@ -26,7 +26,7 @@ class SymfonyConstraintAnnotationReader
|
||||
private $annotationsReader;
|
||||
|
||||
/**
|
||||
* @var Schema
|
||||
* @var OA\Schema
|
||||
*/
|
||||
private $schema;
|
||||
|
||||
@ -38,7 +38,7 @@ class SymfonyConstraintAnnotationReader
|
||||
/**
|
||||
* Update the given property and schema with defined Symfony constraints.
|
||||
*/
|
||||
public function updateProperty(\ReflectionProperty $reflectionProperty, Schema $property)
|
||||
public function updateProperty(\ReflectionProperty $reflectionProperty, OA\Property $property): void
|
||||
{
|
||||
$annotations = $this->annotationsReader->getPropertyAnnotations($reflectionProperty);
|
||||
|
||||
@ -54,35 +54,35 @@ class SymfonyConstraintAnnotationReader
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingRequiredFields = $this->schema->getRequired() ?? [];
|
||||
$existingRequiredFields = OA\UNDEFINED !== $this->schema->required ? $this->schema->required : [];
|
||||
$existingRequiredFields[] = $propertyName;
|
||||
|
||||
$this->schema->setRequired(array_values(array_unique($existingRequiredFields)));
|
||||
$this->schema->required = array_values(array_unique($existingRequiredFields));
|
||||
} elseif ($annotation instanceof Assert\Length) {
|
||||
$property->setMinLength($annotation->min);
|
||||
$property->setMaxLength($annotation->max);
|
||||
$property->minLength = $annotation->min;
|
||||
$property->maxLength = $annotation->max;
|
||||
} elseif ($annotation instanceof Assert\Regex) {
|
||||
$this->appendPattern($property, $annotation->getHtmlPattern());
|
||||
} elseif ($annotation instanceof Assert\Count) {
|
||||
$property->setMinItems($annotation->min);
|
||||
$property->setMaxItems($annotation->max);
|
||||
$property->minItems = $annotation->min;
|
||||
$property->maxItems = $annotation->max;
|
||||
} elseif ($annotation instanceof Assert\Choice) {
|
||||
$values = $annotation->callback ? call_user_func(is_array($annotation->callback) ? $annotation->callback : [$reflectionProperty->class, $annotation->callback]) : $annotation->choices;
|
||||
$property->setEnum(array_values($values));
|
||||
$property->enum = array_values($values);
|
||||
} elseif ($annotation instanceof Assert\Expression) {
|
||||
$this->appendPattern($property, $annotation->message);
|
||||
} elseif ($annotation instanceof Assert\Range) {
|
||||
$property->setMinimum($annotation->min);
|
||||
$property->setMaximum($annotation->max);
|
||||
$property->minimum = $annotation->min;
|
||||
$property->maximum = $annotation->max;
|
||||
} elseif ($annotation instanceof Assert\LessThan) {
|
||||
$property->setExclusiveMaximum($annotation->value);
|
||||
$property->exclusiveMaximum= $annotation->value;
|
||||
} elseif ($annotation instanceof Assert\LessThanOrEqual) {
|
||||
$property->setMaximum($annotation->value);
|
||||
$property->maximum = $annotation->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setSchema($schema)
|
||||
public function setSchema($schema): void
|
||||
{
|
||||
$this->schema = $schema;
|
||||
}
|
||||
@ -90,15 +90,14 @@ class SymfonyConstraintAnnotationReader
|
||||
/**
|
||||
* Get assigned property name for property schema.
|
||||
*/
|
||||
private function getSchemaPropertyName(Schema $property)
|
||||
private function getSchemaPropertyName(OA\Schema $property): ?string
|
||||
{
|
||||
if (null === $this->schema) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->schema->getProperties() as $name => $schemaProperty) {
|
||||
foreach ($this->schema->properties as $schemaProperty) {
|
||||
if ($schemaProperty === $property) {
|
||||
return $name;
|
||||
return OA\UNDEFINED !== $schemaProperty->property ? $schemaProperty->property : null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,16 +107,15 @@ class SymfonyConstraintAnnotationReader
|
||||
/**
|
||||
* Append the pattern from the constraint to the existing pattern.
|
||||
*/
|
||||
private function appendPattern(Schema $property, $newPattern)
|
||||
private function appendPattern(OA\Schema $property, $newPattern): void
|
||||
{
|
||||
if (null === $newPattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $property->getPattern()) {
|
||||
$property->setPattern(sprintf('%s, %s', $property->getPattern(), $newPattern));
|
||||
if (OA\UNDEFINED !== $property->pattern) {
|
||||
$property->pattern = sprintf('%s, %s', $property->pattern, $newPattern);
|
||||
} else {
|
||||
$property->setPattern($newPattern);
|
||||
$property->pattern = $newPattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Hateoas\Configuration\Metadata\ClassMetadata;
|
||||
use Hateoas\Configuration\Relation;
|
||||
use Hateoas\Serializer\Metadata\RelationPropertyMetadata;
|
||||
@ -20,6 +19,8 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
|
||||
{
|
||||
@ -43,7 +44,7 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function describe(Model $model, Schema $schema)
|
||||
public function describe(Model $model, OA\Schema $schema): void
|
||||
{
|
||||
$this->JMSModelDescriber->describe($model, $schema);
|
||||
|
||||
@ -55,9 +56,10 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi
|
||||
return;
|
||||
}
|
||||
|
||||
$schema->setType('object');
|
||||
$schema->type = 'object';
|
||||
$context = $this->JMSModelDescriber->getSerializationContext($model);
|
||||
|
||||
/** @var Relation $relation */
|
||||
foreach ($metadata->getRelations() as $relation) {
|
||||
if (!$relation->getEmbedded() && !$relation->getHref()) {
|
||||
continue;
|
||||
@ -71,26 +73,19 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi
|
||||
$context->pushPropertyMetadata($item);
|
||||
|
||||
$embedded = $relation->getEmbedded();
|
||||
$relationSchema = $schema->getProperties()->get($embedded ? '_embedded' : '_links');
|
||||
|
||||
$properties = $relationSchema->getProperties();
|
||||
$relationSchema->setReadOnly(true);
|
||||
|
||||
$name = $relation->getName();
|
||||
$property = $properties->get($name);
|
||||
$relationSchema = Util::getProperty($schema, $relation->getEmbedded() ? '_embedded' : '_links');
|
||||
$relationSchema->readOnly = true;
|
||||
|
||||
$property = Util::getProperty($relationSchema, $relation->getName());
|
||||
if ($embedded && method_exists($embedded, 'getType') && $embedded->getType()) {
|
||||
$this->JMSModelDescriber->describeItem($embedded->getType(), $property, $context);
|
||||
} else {
|
||||
$property->setType('object');
|
||||
$property->type = 'object';
|
||||
}
|
||||
if ($relation->getHref()) {
|
||||
$subProperties = $property->getProperties();
|
||||
|
||||
$hrefProp = $subProperties->get('href');
|
||||
$hrefProp->setType('string');
|
||||
|
||||
$this->setAttributeProperties($relation, $subProperties);
|
||||
$hrefProp = Util::getProperty($property, 'href');
|
||||
$hrefProp->type = 'string';
|
||||
$this->setAttributeProperties($relation, $property);
|
||||
}
|
||||
|
||||
$context->popPropertyMetadata();
|
||||
@ -119,30 +114,30 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi
|
||||
return $this->JMSModelDescriber->supports($model) || null !== $this->getHateoasMetadata($model);
|
||||
}
|
||||
|
||||
private function setAttributeProperties(Relation $relation, $subProperties)
|
||||
private function setAttributeProperties(Relation $relation, OA\Property $subProperty): void
|
||||
{
|
||||
foreach ($relation->getAttributes() as $attribute => $value) {
|
||||
$subSubProp = $subProperties->get($attribute);
|
||||
$subSubProp = Util::getProperty($subProperty, $attribute);
|
||||
switch (gettype($value)) {
|
||||
case 'integer':
|
||||
$subSubProp->setType('integer');
|
||||
$subSubProp->setDefault($value);
|
||||
$subSubProp->type = 'integer';
|
||||
$subSubProp->default = $value;
|
||||
|
||||
break;
|
||||
case 'double':
|
||||
case 'float':
|
||||
$subSubProp->setType('number');
|
||||
$subSubProp->setDefault($value);
|
||||
$subSubProp->type = 'number';
|
||||
$subSubProp->default = $value;
|
||||
|
||||
break;
|
||||
case 'boolean':
|
||||
$subSubProp->setType('boolean');
|
||||
$subSubProp->setDefault($value);
|
||||
$subSubProp->type = 'boolean';
|
||||
$subSubProp->default = $value;
|
||||
|
||||
break;
|
||||
case 'string':
|
||||
$subSubProp->setType('string');
|
||||
$subSubProp->setDefault($value);
|
||||
$subSubProp->type = 'string';
|
||||
$subSubProp->default = $value;
|
||||
|
||||
break;
|
||||
}
|
||||
|
@ -11,13 +11,13 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class FallbackObjectModelDescriber implements ModelDescriberInterface
|
||||
{
|
||||
public function describe(Model $model, Schema $schema)
|
||||
public function describe(Model $model, OA\Schema $schema)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -11,13 +11,14 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormConfigBuilderInterface;
|
||||
use Symfony\Component\Form\FormConfigInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
@ -38,7 +39,7 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
||||
$this->formFactory = $formFactory;
|
||||
}
|
||||
|
||||
public function describe(Model $model, Schema $schema)
|
||||
public function describe(Model $model, OA\Schema $schema)
|
||||
{
|
||||
if (method_exists(AbstractType::class, 'setDefaultOptions')) {
|
||||
throw new \LogicException('symfony/form < 3.0 is not supported, please upgrade to an higher version to use a form as a model.');
|
||||
@ -47,7 +48,7 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
||||
throw new \LogicException('You need to enable forms in your application to use a form as a model.');
|
||||
}
|
||||
|
||||
$schema->setType('object');
|
||||
$schema->type = 'object';
|
||||
|
||||
$class = $model->getType()->getClassName();
|
||||
|
||||
@ -60,26 +61,24 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
||||
return is_a($model->getType()->getClassName(), FormTypeInterface::class, true);
|
||||
}
|
||||
|
||||
private function parseForm(Schema $schema, FormInterface $form)
|
||||
private function parseForm(OA\Schema $schema, FormInterface $form)
|
||||
{
|
||||
$properties = $schema->getProperties();
|
||||
|
||||
foreach ($form as $name => $child) {
|
||||
$config = $child->getConfig();
|
||||
$property = $properties->get($name);
|
||||
$property = Util::getProperty($schema, $name);
|
||||
|
||||
if ($config->getRequired()) {
|
||||
$required = $schema->getRequired() ?? [];
|
||||
$required = OA\UNDEFINED !== $schema->required ? $schema->required : [];
|
||||
$required[] = $name;
|
||||
|
||||
$schema->setRequired($required);
|
||||
$schema->required = $required;
|
||||
}
|
||||
|
||||
if ($config->hasOption('documentation')) {
|
||||
$property->merge($config->getOption('documentation'));
|
||||
$property->mergeProperties($config->getOption('documentation'));
|
||||
}
|
||||
|
||||
if (null !== $property->getType()) {
|
||||
if (OA\UNDEFINED !== $property->type) {
|
||||
continue; // Type manually defined
|
||||
}
|
||||
|
||||
@ -90,12 +89,9 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
||||
/**
|
||||
* Finds and sets the schema type on $property based on $config info.
|
||||
*
|
||||
* Returns true if a native Swagger type was found, false otherwise
|
||||
*
|
||||
* @param FormConfigBuilderInterface $config
|
||||
* @param $property
|
||||
* Returns true if a native OpenAPi type was found, false otherwise
|
||||
*/
|
||||
private function findFormType(FormConfigBuilderInterface $config, $property)
|
||||
private function findFormType(FormConfigInterface $config, OA\Schema $property)
|
||||
{
|
||||
$type = $config->getType();
|
||||
|
||||
@ -106,7 +102,7 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
||||
null,
|
||||
$config->getOptions()
|
||||
);
|
||||
$property->setRef($this->modelRegistry->register($model));
|
||||
$property->ref = $this->modelRegistry->register($model);
|
||||
|
||||
return;
|
||||
}
|
||||
@ -115,42 +111,42 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
||||
$blockPrefix = $builtinFormType->getBlockPrefix();
|
||||
|
||||
if ('text' === $blockPrefix) {
|
||||
$property->setType('string');
|
||||
$property->type = 'string';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('number' === $blockPrefix) {
|
||||
$property->setType('number');
|
||||
$property->type = 'number';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('integer' === $blockPrefix) {
|
||||
$property->setType('integer');
|
||||
$property->type = 'integer';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('date' === $blockPrefix) {
|
||||
$property->setType('string');
|
||||
$property->setFormat('date');
|
||||
$property->type = 'string';
|
||||
$property->format = 'date';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('datetime' === $blockPrefix) {
|
||||
$property->setType('string');
|
||||
$property->setFormat('date-time');
|
||||
$property->type = 'string';
|
||||
$property->format = 'date-time';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('choice' === $blockPrefix) {
|
||||
if ($config->getOption('multiple')) {
|
||||
$property->setType('array');
|
||||
$property->type = 'array';
|
||||
} else {
|
||||
$property->setType('string');
|
||||
$property->type = 'string';
|
||||
}
|
||||
if (($choices = $config->getOption('choices')) && is_array($choices) && count($choices)) {
|
||||
$enums = array_values($choices);
|
||||
@ -163,9 +159,10 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
||||
}
|
||||
|
||||
if ($config->getOption('multiple')) {
|
||||
$property->getItems()->setType($type)->setEnum($enums);
|
||||
$property->items = Util::createChild($property, OA\Items::class, ['type' => $type, 'enum' => $enums]);
|
||||
} else {
|
||||
$property->setType($type)->setEnum($enums);
|
||||
$property->type = $type;
|
||||
$property->enum = $enums;
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,28 +170,28 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
||||
}
|
||||
|
||||
if ('checkbox' === $blockPrefix) {
|
||||
$property->setType('boolean');
|
||||
$property->type= 'boolean';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('password' === $blockPrefix) {
|
||||
$property->setType('string');
|
||||
$property->setFormat('password');
|
||||
$property->type = 'string';
|
||||
$property->format = 'password';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('repeated' === $blockPrefix) {
|
||||
$property->setType('object');
|
||||
$property->setRequired([$config->getOption('first_name'), $config->getOption('second_name')]);
|
||||
$property->type = 'object';
|
||||
$property->required = [$config->getOption('first_name'), $config->getOption('second_name')];
|
||||
$subType = $config->getOption('type');
|
||||
|
||||
foreach (['first', 'second'] as $subField) {
|
||||
$subName = $config->getOption($subField.'_name');
|
||||
$subForm = $this->formFactory->create($subType, null, array_merge($config->getOption('options'), $config->getOption($subField.'_options')));
|
||||
|
||||
$this->findFormType($subForm->getConfig(), $property->getProperties()->get($subName));
|
||||
$this->findFormType($subForm->getConfig(), Util::getProperty($property, $subName));
|
||||
}
|
||||
|
||||
break;
|
||||
@ -205,10 +202,10 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
||||
$subOptions = $config->getOption('entry_options');
|
||||
$subForm = $this->formFactory->create($subType, null, $subOptions);
|
||||
|
||||
$property->setType('array');
|
||||
$itemsProp = $property->getItems();
|
||||
$property->type = 'array';
|
||||
$property->items = Util::createChild($property, OA\Items::class);
|
||||
|
||||
$this->findFormType($subForm->getConfig(), $itemsProp);
|
||||
$this->findFormType($subForm->getConfig(), $property->items);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -218,12 +215,12 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry
|
||||
$entityClass = $config->getOption('class');
|
||||
|
||||
if ($config->getOption('multiple')) {
|
||||
$property->setFormat(sprintf('[%s id]', $entityClass));
|
||||
$property->setType('array');
|
||||
$property->getItems()->setType('string');
|
||||
$property->format = sprintf('[%s id]', $entityClass);
|
||||
$property->type = 'array';
|
||||
$property->items = Util::createChild($property, OA\Items::class, ['type' => 'string']);
|
||||
} else {
|
||||
$property->setType('string');
|
||||
$property->setFormat(sprintf('%s id', $entityClass));
|
||||
$property->type = 'string';
|
||||
$property->format = sprintf('%s id', $entityClass);
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -12,7 +12,6 @@
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use JMS\Serializer\Context;
|
||||
use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
|
||||
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
|
||||
@ -22,6 +21,8 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
/**
|
||||
@ -41,6 +42,8 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
|
||||
|
||||
private $metadataStacks = [];
|
||||
|
||||
private $mediaTypes;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
@ -48,18 +51,20 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
|
||||
|
||||
public function __construct(
|
||||
MetadataFactoryInterface $factory,
|
||||
PropertyNamingStrategyInterface $namingStrategy = null,
|
||||
Reader $reader
|
||||
Reader $reader,
|
||||
array $mediaTypes,
|
||||
?PropertyNamingStrategyInterface $namingStrategy = null
|
||||
) {
|
||||
$this->factory = $factory;
|
||||
$this->namingStrategy = $namingStrategy;
|
||||
$this->doctrineReader = $reader;
|
||||
$this->mediaTypes = $mediaTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function describe(Model $model, Schema $schema)
|
||||
public function describe(Model $model, OA\Schema $schema)
|
||||
{
|
||||
$className = $model->getType()->getClassName();
|
||||
$metadata = $this->factory->getMetadataForClass($className);
|
||||
@ -67,12 +72,11 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
|
||||
throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className));
|
||||
}
|
||||
|
||||
$schema->setType('object');
|
||||
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry);
|
||||
$schema->type = 'object';
|
||||
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
|
||||
$annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
|
||||
|
||||
$isJmsV1 = null !== $this->namingStrategy;
|
||||
$properties = $schema->getProperties();
|
||||
|
||||
$context = $this->getSerializationContext($model);
|
||||
$context->pushClassMetadata($metadata);
|
||||
@ -106,25 +110,26 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
|
||||
continue;
|
||||
}
|
||||
|
||||
$property = $properties->get($annotationsReader->getPropertyName($reflection, $name));
|
||||
$property = Util::getProperty($schema, $annotationsReader->getPropertyName($reflection, $name));
|
||||
$annotationsReader->updateProperty($reflection, $property, $groups);
|
||||
} catch (\ReflectionException $e) {
|
||||
$property = $properties->get($name);
|
||||
$property = Util::getProperty($schema, $name);
|
||||
}
|
||||
|
||||
if (null !== $property->getType() || null !== $property->getRef()) {
|
||||
if (OA\UNDEFINED !== $property->type || OA\UNDEFINED !== $property->ref) {
|
||||
$context->popPropertyMetadata();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (null === $item->type) {
|
||||
$properties->remove($name);
|
||||
$key = Util::searchIndexedCollectionItem($schema->properties, 'property', $name);
|
||||
unset($schema->properties[$key]);
|
||||
$context->popPropertyMetadata();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->describeItem($item->type, $property, $context, $item);
|
||||
$this->describeItem($item->type, $property, $context);
|
||||
$context->popPropertyMetadata();
|
||||
}
|
||||
$context->popClassMetadata();
|
||||
@ -196,48 +201,51 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function describeItem(array $type, $property, Context $context)
|
||||
public function describeItem(array $type, OA\Schema $property, Context $context)
|
||||
{
|
||||
$nestedTypeInfo = $this->getNestedTypeInArray($type);
|
||||
if (null !== $nestedTypeInfo) {
|
||||
list($nestedType, $isHash) = $nestedTypeInfo;
|
||||
if ($isHash) {
|
||||
$property->setType('object');
|
||||
// in the case of a virtual property, set it as free object type
|
||||
$property->merge(['additionalProperties' => []]);
|
||||
$property->type = 'object';
|
||||
$property->additionalProperties = Util::createChild($property, OA\Property::class);
|
||||
|
||||
// this is a free form object (as nested array)
|
||||
if ('array' === $nestedType['name'] && !isset($nestedType['params'][0])) {
|
||||
// in the case of a virtual property, set it as free object type
|
||||
$property->additionalProperties = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->describeItem($nestedType, $property->getAdditionalProperties(), $context);
|
||||
$this->describeItem($nestedType, $property->additionalProperties, $context);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$property->setType('array');
|
||||
$this->describeItem($nestedType, $property->getItems(), $context);
|
||||
$property->type = 'array';
|
||||
$property->items = Util::createChild($property, OA\Items::class);
|
||||
$this->describeItem($nestedType, $property->items, $context);
|
||||
} elseif ('array' === $type['name']) {
|
||||
$property->setType('object');
|
||||
$property->merge(['additionalProperties' => []]);
|
||||
$property->type = 'object';
|
||||
$property->additionalProperties = true;
|
||||
} elseif ('string' === $type['name']) {
|
||||
$property->setType('string');
|
||||
$property->type = 'string';
|
||||
} elseif (in_array($type['name'], ['bool', 'boolean'], true)) {
|
||||
$property->setType('boolean');
|
||||
$property->type = 'boolean';
|
||||
} elseif (in_array($type['name'], ['int', 'integer'], true)) {
|
||||
$property->setType('integer');
|
||||
$property->type = 'integer';
|
||||
} elseif (in_array($type['name'], ['double', 'float'], true)) {
|
||||
$property->setType('number');
|
||||
$property->setFormat($type['name']);
|
||||
$property->type = 'number';
|
||||
$property->format = $type['name'];
|
||||
} elseif (is_subclass_of($type['name'], \DateTimeInterface::class)) {
|
||||
$property->setType('string');
|
||||
$property->setFormat('date-time');
|
||||
$property->type = 'string';
|
||||
$property->format = 'date-time';
|
||||
} else {
|
||||
$groups = $this->computeGroups($context, $type);
|
||||
|
||||
$model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type['name']), $groups);
|
||||
$property->setRef($this->modelRegistry->register($model));
|
||||
$property->ref = $this->modelRegistry->register($model);
|
||||
|
||||
$this->contexts[$model->getHash()] = $context;
|
||||
$this->metadataStacks[$model->getHash()] = clone $context->getMetadataStack();
|
||||
|
@ -11,8 +11,8 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations\Schema;
|
||||
|
||||
interface ModelDescriberInterface
|
||||
{
|
||||
|
@ -12,12 +12,13 @@
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
@ -31,31 +32,36 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
|
||||
private $doctrineReader;
|
||||
/** @var PropertyDescriberInterface[] */
|
||||
private $propertyDescribers;
|
||||
/** @var string[] */
|
||||
private $mediaTypes;
|
||||
|
||||
private $swaggerDefinitionAnnotationReader;
|
||||
|
||||
public function __construct(
|
||||
PropertyInfoExtractorInterface $propertyInfo,
|
||||
Reader $reader,
|
||||
$propertyDescribers
|
||||
$propertyDescribers,
|
||||
array $mediaTypes
|
||||
) {
|
||||
$this->propertyInfo = $propertyInfo;
|
||||
$this->doctrineReader = $reader;
|
||||
$this->propertyDescribers = $propertyDescribers;
|
||||
$this->mediaTypes = $mediaTypes;
|
||||
}
|
||||
|
||||
public function describe(Model $model, Schema $schema)
|
||||
public function describe(Model $model, OA\Schema $schema)
|
||||
{
|
||||
$schema->setType('object');
|
||||
$properties = $schema->getProperties();
|
||||
$schema->type = 'object';
|
||||
|
||||
$class = $model->getType()->getClassName();
|
||||
$schema->_context->class = $class;
|
||||
|
||||
$context = [];
|
||||
if (null !== $model->getGroups()) {
|
||||
$context = ['serializer_groups' => array_filter($model->getGroups(), 'is_string')];
|
||||
}
|
||||
|
||||
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry);
|
||||
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
|
||||
$annotationsReader->updateDefinition(new \ReflectionClass($class), $schema);
|
||||
|
||||
$propertyInfoProperties = $this->propertyInfo->getProperties($class, $context);
|
||||
@ -64,10 +70,10 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
|
||||
}
|
||||
|
||||
foreach ($propertyInfoProperties as $propertyName) {
|
||||
// read property options from Swagger Property annotation if it exists
|
||||
// read property options from OpenApi Property annotation if it exists
|
||||
if (property_exists($class, $propertyName)) {
|
||||
$reflectionProperty = new \ReflectionProperty($class, $propertyName);
|
||||
$property = $properties->get($annotationsReader->getPropertyName($reflectionProperty, $propertyName));
|
||||
$property = Util::getProperty($schema, $annotationsReader->getPropertyName($reflectionProperty, $propertyName));
|
||||
|
||||
$groups = $model->getGroups();
|
||||
if (isset($groups[$propertyName]) && is_array($groups[$propertyName])) {
|
||||
@ -76,11 +82,11 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
|
||||
|
||||
$annotationsReader->updateProperty($reflectionProperty, $property, $groups);
|
||||
} else {
|
||||
$property = $properties->get($propertyName);
|
||||
$property = Util::getProperty($schema, $propertyName);
|
||||
}
|
||||
|
||||
// If type manually defined
|
||||
if (null !== $property->getType() || null !== $property->getRef()) {
|
||||
if (OA\UNDEFINED !== $property->type || OA\UNDEFINED !== $property->ref) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -97,7 +103,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
|
||||
}
|
||||
}
|
||||
|
||||
private function describeProperty(Type $type, Model $model, Schema $property, string $propertyName)
|
||||
private function describeProperty(Type $type, Model $model, OA\Schema $property, string $propertyName)
|
||||
{
|
||||
foreach ($this->propertyDescribers as $propertyDescriber) {
|
||||
if ($propertyDescriber instanceof ModelRegistryAwareInterface) {
|
||||
@ -110,7 +116,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
|
||||
}
|
||||
}
|
||||
|
||||
throw new \Exception(sprintf('Type "%s" is not supported in %s::$%s. You may use the `@SWG\Property(type="")` annotation to specify it manually.', $type->getBuiltinType(), $model->getType()->getClassName(), $propertyName));
|
||||
throw new \Exception(sprintf('Type "%s" is not supported in %s::$%s. You may use the `@OA\Property(type="")` annotation to specify it manually.', $type->getBuiltinType(), $model->getType()->getClassName(), $propertyName));
|
||||
}
|
||||
|
||||
public function supports(Model $model): bool
|
||||
|
39
OpenApiPhp/AddDefaults.php
Normal file
39
OpenApiPhp/AddDefaults.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\OpenApiPhp;
|
||||
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Context;
|
||||
|
||||
/**
|
||||
* Add defaults to fix default warnings.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class AddDefaults
|
||||
{
|
||||
public function __invoke(Analysis $analysis)
|
||||
{
|
||||
if ($analysis->getAnnotationsOfType(OA\Info::class)) {
|
||||
return;
|
||||
}
|
||||
if (($annotations = $analysis->getAnnotationsOfType(OA\OpenApi::class)) && OA\UNDEFINED !== $annotations[0]->info) {
|
||||
return;
|
||||
}
|
||||
if (OA\UNDEFINED !== $analysis->openapi->info) {
|
||||
return;
|
||||
}
|
||||
|
||||
$analysis->addAnnotation(new OA\Info(['title' => '', 'version' => '0.0.0', '_context' => new Context(['generated' => true])]), null);
|
||||
}
|
||||
}
|
171
OpenApiPhp/ModelRegister.php
Normal file
171
OpenApiPhp/ModelRegister.php
Normal file
@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\OpenApiPhp;
|
||||
|
||||
use Nelmio\ApiDocBundle\Annotation\Model as ModelAnnotation;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
/**
|
||||
* Resolves the path in SwaggerPhp annotation when needed.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ModelRegister
|
||||
{
|
||||
/** @var ModelRegistry */
|
||||
private $modelRegistry;
|
||||
|
||||
/** @var string[] */
|
||||
private $mediaTypes;
|
||||
|
||||
public function __construct(ModelRegistry $modelRegistry, array $mediaTypes)
|
||||
{
|
||||
$this->modelRegistry = $modelRegistry;
|
||||
$this->mediaTypes = $mediaTypes;
|
||||
}
|
||||
|
||||
public function __invoke(Analysis $analysis, array $parentGroups = null)
|
||||
{
|
||||
foreach ($analysis->annotations as $annotation) {
|
||||
// @Model using the ref field
|
||||
if ($annotation instanceof OA\Schema && $annotation->ref instanceof ModelAnnotation) {
|
||||
$model = $annotation->ref;
|
||||
|
||||
$annotation->ref = $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options));
|
||||
|
||||
// It is no longer an unmerged annotation
|
||||
$this->detach($model, $annotation, $analysis);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Misusage of ::$ref
|
||||
if (($annotation instanceof OA\Response || $annotation instanceof OA\RequestBody) && $annotation->ref instanceof ModelAnnotation) {
|
||||
throw new \InvalidArgumentException(sprintf('Using @Model inside @%s::$ref is not allowed. You should use ::$ref with @Property, @Parameter, @Schema, @Items but within @Response or @RequestBody you should put @Model directly at the root of the annotation : `@Response(..., @Model(...))`.', get_class($annotation)));
|
||||
}
|
||||
|
||||
// Implicit usages
|
||||
|
||||
// We don't use $ref for @Responses, @RequestBody and @Parameter to respect semantics
|
||||
// We don't replace these objects with the @Model found (we inject it in a subfield) whereas we do for @Schemas
|
||||
|
||||
$model = $this->getModel($annotation); // We check whether there is a @Model annotation nested
|
||||
if (null === $model) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($annotation instanceof OA\Response || $annotation instanceof OA\RequestBody) {
|
||||
$properties = [
|
||||
'_context' => Util::createContext(['nested' => $annotation], $annotation->_context),
|
||||
'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)),
|
||||
];
|
||||
|
||||
foreach ($this->mediaTypes as $mediaType) {
|
||||
$this->createContentForMediaType($mediaType, $properties, $annotation, $analysis);
|
||||
}
|
||||
$this->detach($model, $annotation, $analysis);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$annotation instanceof OA\Parameter) {
|
||||
throw new \InvalidArgumentException(sprintf("@Model annotation can't be nested with an annotation of type @%s.", get_class($annotation)));
|
||||
}
|
||||
|
||||
if ($annotation->schema instanceof OA\Schema && 'array' === $annotation->schema->type) {
|
||||
$annotationClass = OA\Items::class;
|
||||
} else {
|
||||
$annotationClass = OA\Schema::class;
|
||||
}
|
||||
|
||||
if (!is_string($model->type)) {
|
||||
// Ignore invalid annotations, they are validated later
|
||||
continue;
|
||||
}
|
||||
|
||||
$annotation->merge([new $annotationClass([
|
||||
'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)),
|
||||
])]);
|
||||
|
||||
// It is no longer an unmerged annotation
|
||||
$this->detach($model, $annotation, $analysis);
|
||||
}
|
||||
}
|
||||
|
||||
private function getGroups(ModelAnnotation $model, array $parentGroups = null)
|
||||
{
|
||||
if (null === $model->groups) {
|
||||
return $parentGroups;
|
||||
}
|
||||
|
||||
return array_merge($parentGroups ?? [], $model->groups);
|
||||
}
|
||||
|
||||
private function detach(ModelAnnotation $model, OA\AbstractAnnotation $annotation, Analysis $analysis)
|
||||
{
|
||||
foreach ($annotation->_unmerged as $key => $unmerged) {
|
||||
if ($unmerged === $model) {
|
||||
unset($annotation->_unmerged[$key]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
$analysis->annotations->detach($model);
|
||||
}
|
||||
|
||||
private function createType(string $type): Type
|
||||
{
|
||||
if ('[]' === substr($type, -2)) {
|
||||
return new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, $this->createType(substr($type, 0, -2)));
|
||||
}
|
||||
|
||||
return new Type(Type::BUILTIN_TYPE_OBJECT, false, $type);
|
||||
}
|
||||
|
||||
private function getModel(OA\AbstractAnnotation $annotation): ?ModelAnnotation
|
||||
{
|
||||
foreach ($annotation->_unmerged as $unmerged) {
|
||||
if ($unmerged instanceof ModelAnnotation) {
|
||||
return $unmerged;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function createContentForMediaType(
|
||||
string $type,
|
||||
array $properties,
|
||||
OA\AbstractAnnotation $annotation,
|
||||
Analysis $analysis
|
||||
) {
|
||||
switch ($type) {
|
||||
case 'json':
|
||||
$modelAnnotation = new OA\JsonContent($properties);
|
||||
|
||||
break;
|
||||
case 'xml':
|
||||
$modelAnnotation = new OA\XmlContent($properties);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException(sprintf("@Model annotation is not compatible with the media type '%s'. It must be one of 'json' or 'xml'.", $this->mediaType));
|
||||
}
|
||||
|
||||
$annotation->merge([$modelAnnotation]);
|
||||
$analysis->addAnnotation($modelAnnotation, null);
|
||||
}
|
||||
}
|
519
OpenApiPhp/Util.php
Normal file
519
OpenApiPhp/Util.php
Normal file
@ -0,0 +1,519 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\OpenApiPhp;
|
||||
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Context;
|
||||
use const OpenApi\UNDEFINED;
|
||||
|
||||
/**
|
||||
* Class Util.
|
||||
*
|
||||
* This class acts as compatibility layer between NelmioApiDocBundle and swagger-php.
|
||||
*
|
||||
* It was written to replace the GuilhemN/swagger layer as a lower effort to maintain alternative.
|
||||
*
|
||||
* The main purpose of this class is to search for and create child Annotations
|
||||
* of swagger Annotation classes with the following convenience methods
|
||||
* to get or create the respective Annotation instances if not found
|
||||
*
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getPath()
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getSchema()
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getProperty()
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getOperation()
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getOperationParameter()
|
||||
*
|
||||
* which in turn get or create the Annotation instances through the following more general methods
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getChild()
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getCollectionItem()
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::getIndexedCollectionItem()
|
||||
*
|
||||
* which then searches for an existing Annotation through
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::searchCollectionItem()
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::searchIndexedCollectionItem()
|
||||
*
|
||||
* and if not found the Annotation creates it through
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::createCollectionItem()
|
||||
* @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::createContext()
|
||||
*
|
||||
* The merge method @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::merge() has the main purpose to be able
|
||||
* to merge properties from an deeply nested array of Annotation properties in the structure of a
|
||||
* generated swagger json decoded array.
|
||||
*/
|
||||
final class Util
|
||||
{
|
||||
/**
|
||||
* All http method verbs as known by swagger.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public const OPERATIONS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];
|
||||
|
||||
/**
|
||||
* Return an existing PathItem object from $api->paths[] having its member path set to $path.
|
||||
* Create, add to $api->paths[] and return this new PathItem object and set the property if none found.
|
||||
*
|
||||
* @see OA\OpenApi::$paths
|
||||
* @see OA\PathItem::path
|
||||
*
|
||||
* @param OA\OpenApi $api
|
||||
* @param string $path
|
||||
*
|
||||
* @return OA\PathItem
|
||||
*/
|
||||
public static function getPath(OA\OpenApi $api, $path): OA\PathItem
|
||||
{
|
||||
return self::getIndexedCollectionItem($api, OA\PathItem::class, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an existing Schema object from $api->components->schemas[] having its member schema set to $schema.
|
||||
* Create, add to $api->components->schemas[] and return this new Schema object and set the property if none found.
|
||||
*
|
||||
* @param OA\OpenApi $api
|
||||
* @param string $schema
|
||||
*
|
||||
* @return OA\Schema
|
||||
*
|
||||
* @see OA\Schema::$schema
|
||||
* @see OA\Components::$schemas
|
||||
*/
|
||||
public static function getSchema(OA\OpenApi $api, $schema): OA\Schema
|
||||
{
|
||||
if (!$api->components instanceof OA\Components) {
|
||||
$api->components = new OA\Components([]);
|
||||
}
|
||||
|
||||
return self::getIndexedCollectionItem($api->components, OA\Schema::class, $schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an existing Property object from $schema->properties[]
|
||||
* having its member property set to $property.
|
||||
*
|
||||
* Create, add to $schema->properties[] and return this new Property object
|
||||
* and set the property if none found.
|
||||
*
|
||||
* @see OA\Schema::$properties
|
||||
* @see OA\Property::$property
|
||||
*
|
||||
* @param OA\Schema $schema
|
||||
* @param string $property
|
||||
*
|
||||
* @return OA\Property
|
||||
*/
|
||||
public static function getProperty(OA\Schema $schema, $property): OA\Property
|
||||
{
|
||||
return self::getIndexedCollectionItem($schema, OA\Property::class, $property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an existing Operation from $path->{$method}
|
||||
* or create, set $path->{$method} and return this new Operation object.
|
||||
*
|
||||
* @see OA\PathItem::$get
|
||||
* @see OA\PathItem::$post
|
||||
* @see OA\PathItem::$put
|
||||
* @see OA\PathItem::$patch
|
||||
* @see OA\PathItem::$delete
|
||||
* @see OA\PathItem::$options
|
||||
* @see OA\PathItem::$head
|
||||
*
|
||||
* @param OA\PathItem $path
|
||||
* @param string $method
|
||||
*
|
||||
* @return OA\Operation
|
||||
*/
|
||||
public static function getOperation(OA\PathItem $path, $method): OA\Operation
|
||||
{
|
||||
$class = array_keys($path::$_nested, \strtolower($method), true)[0];
|
||||
|
||||
return self::getChild($path, $class, ['path' => $path->path]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an existing Parameter object from $operation->parameters[]
|
||||
* having its members name set to $name and in set to $in.
|
||||
*
|
||||
* Create, add to $operation->parameters[] and return
|
||||
* this new Parameter object and set its members if none found.
|
||||
*
|
||||
* @see OA\Operation::$parameters
|
||||
* @see OA\Parameter::$name
|
||||
* @see OA\Parameter::$in
|
||||
*
|
||||
* @param OA\Operation $operation
|
||||
* @param string $name
|
||||
* @param string $in
|
||||
*
|
||||
* @return OA\Parameter
|
||||
*/
|
||||
public static function getOperationParameter(OA\Operation $operation, $name, $in): OA\Parameter
|
||||
{
|
||||
return self::getCollectionItem($operation, OA\Parameter::class, ['name' => $name, 'in' => $in]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an existing nested Annotation from $parent->{$property} if exists.
|
||||
* Create, add to $parent->{$property} and set its members to $properties otherwise.
|
||||
*
|
||||
* $property is determined from $parent::$_nested[$class]
|
||||
* it is expected to be a string nested property.
|
||||
*
|
||||
* @see OA\AbstractAnnotation::$_nested
|
||||
*
|
||||
* @param OA\AbstractAnnotation $parent
|
||||
* @param $class
|
||||
* @param array $properties
|
||||
*
|
||||
* @return OA\AbstractAnnotation
|
||||
*/
|
||||
public static function getChild(OA\AbstractAnnotation $parent, $class, array $properties = []): OA\AbstractAnnotation
|
||||
{
|
||||
$nested = $parent::$_nested;
|
||||
$property = $nested[$class];
|
||||
|
||||
if (null === $parent->{$property} || UNDEFINED === $parent->{$property}) {
|
||||
$parent->{$property} = self::createChild($parent, $class, $properties);
|
||||
}
|
||||
|
||||
return $parent->{$property};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an existing nested Annotation from $parent->{$collection}[]
|
||||
* having all $properties set to the respective values.
|
||||
*
|
||||
* Create, add to $parent->{$collection}[] and set its members
|
||||
* to $properties otherwise.
|
||||
*
|
||||
* $collection is determined from $parent::$_nested[$class]
|
||||
* it is expected to be a single value array nested Annotation.
|
||||
*
|
||||
* @see OA\AbstractAnnotation::$_nested
|
||||
*
|
||||
* @param OA\AbstractAnnotation $parent
|
||||
* @param string $class
|
||||
* @param array $properties
|
||||
*
|
||||
* @return OA\AbstractAnnotation
|
||||
*/
|
||||
public static function getCollectionItem(OA\AbstractAnnotation $parent, $class, array $properties = []): OA\AbstractAnnotation
|
||||
{
|
||||
$key = null;
|
||||
$nested = $parent::$_nested;
|
||||
$collection = $nested[$class][0];
|
||||
|
||||
if (!empty($properties)) {
|
||||
$key = self::searchCollectionItem(
|
||||
$parent->{$collection} && UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [],
|
||||
$properties
|
||||
);
|
||||
}
|
||||
if (null === $key) {
|
||||
$key = self::createCollectionItem($parent, $collection, $class, $properties);
|
||||
}
|
||||
|
||||
return $parent->{$collection}[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an existing nested Annotation from $parent->{$collection}[]
|
||||
* having its mapped $property set to $value.
|
||||
*
|
||||
* Create, add to $parent->{$collection}[] and set its member $property to $value otherwise.
|
||||
*
|
||||
* $collection is determined from $parent::$_nested[$class]
|
||||
* it is expected to be a double value array nested Annotation
|
||||
* with the second value being the mapping index $property.
|
||||
*
|
||||
* @see OA\AbstractAnnotation::$_nested
|
||||
*
|
||||
* @param OA\AbstractAnnotation $parent
|
||||
* @param string $class
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return OA\AbstractAnnotation
|
||||
*/
|
||||
public static function getIndexedCollectionItem(OA\AbstractAnnotation $parent, $class, $value): OA\AbstractAnnotation
|
||||
{
|
||||
$nested = $parent::$_nested;
|
||||
[$collection, $property] = $nested[$class];
|
||||
|
||||
$key = self::searchIndexedCollectionItem(
|
||||
$parent->{$collection} && UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [],
|
||||
$property,
|
||||
$value
|
||||
);
|
||||
|
||||
if (false === $key) {
|
||||
$key = self::createCollectionItem($parent, $collection, $class, [$property => $value]);
|
||||
}
|
||||
|
||||
return $parent->{$collection}[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an Annotation within $collection that has all members set
|
||||
* to the respective values in the associative array $properties.
|
||||
*
|
||||
* @param array $collection
|
||||
* @param array $properties
|
||||
*
|
||||
* @return int|string|null
|
||||
*/
|
||||
public static function searchCollectionItem(array $collection, array $properties)
|
||||
{
|
||||
foreach ($collection ?: [] as $i => $child) {
|
||||
foreach ($properties as $k => $prop) {
|
||||
if ($child->{$k} !== $prop) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
return $i;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an Annotation within the $collection that has its member $index set to $value.
|
||||
*
|
||||
* @param array $collection
|
||||
* @param string $member
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return false|int|string
|
||||
*/
|
||||
public static function searchIndexedCollectionItem(array $collection, $member, $value)
|
||||
{
|
||||
return array_search($value, array_column($collection, $member), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Object of $class with members $properties within $parent->{$collection}[]
|
||||
* and return the created index.
|
||||
*
|
||||
* @param OA\AbstractAnnotation $parent
|
||||
* @param string $collection
|
||||
* @param string $class
|
||||
* @param array $properties
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function createCollectionItem(OA\AbstractAnnotation $parent, $collection, $class, array $properties = []): int
|
||||
{
|
||||
if (UNDEFINED === $parent->{$collection}) {
|
||||
$parent->{$collection} = [];
|
||||
}
|
||||
|
||||
$key = \count($parent->{$collection} ?: []);
|
||||
$parent->{$collection}[$key] = self::createChild($parent, $class, $properties);
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Object of $class with members $properties and set the context parent to be $parent.
|
||||
*
|
||||
*
|
||||
* @param OA\AbstractAnnotation $parent
|
||||
* @param string $class
|
||||
* @param array $properties
|
||||
*
|
||||
* @throws \InvalidArgumentException at an attempt to pass in properties that are found in $parent::$_nested
|
||||
*
|
||||
* @return OA\AbstractAnnotation
|
||||
*/
|
||||
public static function createChild(OA\AbstractAnnotation $parent, $class, array $properties = []): OA\AbstractAnnotation
|
||||
{
|
||||
$nesting = self::getNestingIndexes($class);
|
||||
|
||||
if (!empty(array_intersect(array_keys($properties), $nesting))) {
|
||||
throw new \InvalidArgumentException('Nesting Annotations is not supported.');
|
||||
}
|
||||
|
||||
return new $class(
|
||||
array_merge($properties, ['_context' => self::createContext(['nested' => $parent], $parent->_context)])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Context with members $properties and parent context $parent.
|
||||
*
|
||||
* @see Context
|
||||
*
|
||||
* @param array $properties
|
||||
* @param Context|null $parent
|
||||
*
|
||||
* @return Context
|
||||
*/
|
||||
public static function createContext(array $properties = [], Context $parent = null): Context
|
||||
{
|
||||
return new Context($properties, $parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge $from into $annotation. $overwrite is only used for leaf scalar values.
|
||||
*
|
||||
* The main purpose is to create a Swagger Object from array config values
|
||||
* in the structure of a json serialized Swagger object.
|
||||
*
|
||||
* @param OA\AbstractAnnotation $annotation
|
||||
* @param array|\ArrayObject|OA\AbstractAnnotation $from
|
||||
* @param bool $overwrite
|
||||
*/
|
||||
public static function merge(OA\AbstractAnnotation $annotation, $from, bool $overwrite = false)
|
||||
{
|
||||
if (\is_array($from)) {
|
||||
self::mergeFromArray($annotation, $from, $overwrite);
|
||||
} elseif (\is_a($from, OA\AbstractAnnotation::class)) {
|
||||
/* @var OA\AbstractAnnotation $from */
|
||||
self::mergeFromArray($annotation, json_decode(json_encode($from), true), $overwrite);
|
||||
} elseif (\is_a($from, \ArrayObject::class)) {
|
||||
/* @var \ArrayObject $from */
|
||||
self::mergeFromArray($annotation, $from->getArrayCopy(), $overwrite);
|
||||
}
|
||||
}
|
||||
|
||||
private static function mergeFromArray(OA\AbstractAnnotation $annotation, array $properties, bool $overwrite)
|
||||
{
|
||||
$done = [];
|
||||
|
||||
foreach ($annotation::$_nested as $className => $propertyName) {
|
||||
if (\is_string($propertyName)) {
|
||||
if (array_key_exists($propertyName, $properties)) {
|
||||
self::mergeChild($annotation, $className, $properties[$propertyName], $overwrite);
|
||||
$done[] = $propertyName;
|
||||
}
|
||||
} elseif (\array_key_exists($propertyName[0], $properties)) {
|
||||
$collection = $propertyName[0];
|
||||
$property = $propertyName[1] ?? null;
|
||||
self::mergeCollection($annotation, $className, $collection, $property, $properties[$collection], $overwrite);
|
||||
$done[] = $collection;
|
||||
}
|
||||
}
|
||||
|
||||
$defaults = \get_class_vars(\get_class($annotation));
|
||||
|
||||
foreach ($annotation::$_types as $propertyName => $type) {
|
||||
if (array_key_exists($propertyName, $properties)) {
|
||||
self::mergeTyped($annotation, $propertyName, $type, $properties, $defaults, $overwrite);
|
||||
$done[] = $propertyName;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($properties as $propertyName => $value) {
|
||||
if ('$ref' === $propertyName) {
|
||||
$propertyName = 'ref';
|
||||
}
|
||||
if (!\in_array($propertyName, $done, true)) {
|
||||
self::mergeProperty($annotation, $propertyName, $value, $defaults[$propertyName], $overwrite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function mergeChild(OA\AbstractAnnotation $annotation, $className, $value, bool $overwrite)
|
||||
{
|
||||
self::merge(self::getChild($annotation, $className), $value, $overwrite);
|
||||
}
|
||||
|
||||
private static function mergeCollection(OA\AbstractAnnotation $annotation, $className, $collection, $property, $items, bool $overwrite)
|
||||
{
|
||||
if (null !== $property) {
|
||||
foreach ($items as $prop => $value) {
|
||||
$child = self::getIndexedCollectionItem($annotation, $className, (string) $prop);
|
||||
self::merge($child, $value);
|
||||
}
|
||||
} else {
|
||||
$nesting = self::getNestingIndexes($className);
|
||||
foreach ($items as $props) {
|
||||
$create = [];
|
||||
$merge = [];
|
||||
foreach ($props as $k => $v) {
|
||||
if (\in_array($k, $nesting, true)) {
|
||||
$merge[$k] = $v;
|
||||
} else {
|
||||
$create[$k] = $v;
|
||||
}
|
||||
}
|
||||
self::merge(self::getCollectionItem($annotation, $className, $create), $merge, $overwrite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function mergeTyped(OA\AbstractAnnotation $annotation, $propertyName, $type, array $properties, array $defaults, bool $overwrite)
|
||||
{
|
||||
if (\is_string($type) && 0 === strpos($type, '[')) {
|
||||
/* type is declared as array in @see OA\AbstractAnnotation::$_types */
|
||||
$annotation->{$propertyName} = array_unique(array_merge(
|
||||
$annotation->{$propertyName} && UNDEFINED !== $annotation->{$propertyName} ? $annotation->{$propertyName} : [],
|
||||
$properties[$propertyName]
|
||||
));
|
||||
} else {
|
||||
self::mergeProperty($annotation, $propertyName, $properties[$propertyName], $defaults[$propertyName], $overwrite);
|
||||
}
|
||||
}
|
||||
|
||||
private static function mergeProperty(OA\AbstractAnnotation $annotation, $propertyName, $value, $default, bool $overwrite)
|
||||
{
|
||||
if (true === $overwrite || $default === $annotation->{$propertyName}) {
|
||||
$annotation->{$propertyName} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
private static function getNestingIndexes($class): array
|
||||
{
|
||||
return array_values(array_map(
|
||||
function ($value) {
|
||||
return \is_array($value) ? $value[0] : $value;
|
||||
},
|
||||
self::getNesting($class) ?? []
|
||||
));
|
||||
}
|
||||
|
||||
private static function getNesting($class)
|
||||
{
|
||||
switch ($class) {
|
||||
case OA\OpenApi::class:
|
||||
return OA\OpenApi::$_nested;
|
||||
case OA\Info::class:
|
||||
return OA\Info::$_nested;
|
||||
case OA\PathItem::class:
|
||||
return OA\PathItem::$_nested;
|
||||
case OA\Get::class:
|
||||
case OA\Post::class:
|
||||
case OA\Put::class:
|
||||
case OA\Delete::class:
|
||||
case OA\Patch::class:
|
||||
case OA\Head::class:
|
||||
case OA\Options::class:
|
||||
return OA\Operation::$_nested;
|
||||
case OA\Parameter::class:
|
||||
return OA\Parameter::$_nested;
|
||||
case OA\Items::class:
|
||||
return OA\Items::$_nested;
|
||||
case OA\Property::class:
|
||||
case OA\Schema::class:
|
||||
return OA\Schema::$_nested;
|
||||
case OA\Tag::class:
|
||||
return OA\Tag::$_nested;
|
||||
case OA\Response::class:
|
||||
return OA\Response::$_nested;
|
||||
case OA\Header::class:
|
||||
return OA\Header::$_nested;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -11,9 +11,10 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\PropertyDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface
|
||||
@ -28,15 +29,15 @@ class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistr
|
||||
$this->propertyDescribers = $propertyDescribers;
|
||||
}
|
||||
|
||||
public function describe(Type $type, Schema $property, array $groups = null)
|
||||
public function describe(Type $type, OA\Schema $property, array $groups = null)
|
||||
{
|
||||
$type = $type->getCollectionValueType();
|
||||
if (null === $type) {
|
||||
throw new \LogicException(sprintf('Property "%s" is an array, but its items type isn\'t specified. You can specify that by using the type `string[]` for instance or `@SWG\Property(type="array", @SWG\Items(type="string"))`.', $property->getTitle()));
|
||||
}
|
||||
|
||||
$property->setType('array');
|
||||
$property = $property->getItems();
|
||||
$property->type = 'array';
|
||||
$property = Util::getChild($property, OA\Items::class);
|
||||
|
||||
foreach ($this->propertyDescribers as $propertyDescriber) {
|
||||
if ($propertyDescriber instanceof ModelRegistryAwareInterface) {
|
||||
|
@ -11,14 +11,14 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\PropertyDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class BooleanPropertyDescriber implements PropertyDescriberInterface
|
||||
{
|
||||
public function describe(Type $type, Schema $property, array $groups = null)
|
||||
public function describe(Type $type, OA\Schema $property, array $groups = null)
|
||||
{
|
||||
$property->setType('boolean');
|
||||
$property->type = 'boolean';
|
||||
}
|
||||
|
||||
public function supports(Type $type): bool
|
||||
|
@ -11,15 +11,15 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\PropertyDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class DateTimePropertyDescriber implements PropertyDescriberInterface
|
||||
{
|
||||
public function describe(Type $type, Schema $property, array $groups = null)
|
||||
public function describe(Type $type, OA\Schema $property, array $groups = null)
|
||||
{
|
||||
$property->setType('string');
|
||||
$property->setFormat('date-time');
|
||||
$property->type = 'string';
|
||||
$property->format = 'date-time';
|
||||
}
|
||||
|
||||
public function supports(Type $type): bool
|
||||
|
@ -11,15 +11,15 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\PropertyDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class FloatPropertyDescriber implements PropertyDescriberInterface
|
||||
{
|
||||
public function describe(Type $type, Schema $property, array $groups = null)
|
||||
public function describe(Type $type, OA\Schema $property, array $groups = null)
|
||||
{
|
||||
$property->setType('number');
|
||||
$property->setFormat('float');
|
||||
$property->type = 'number';
|
||||
$property->format = 'float';
|
||||
}
|
||||
|
||||
public function supports(Type $type): bool
|
||||
|
@ -11,14 +11,14 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\PropertyDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class IntegerPropertyDescriber implements PropertyDescriberInterface
|
||||
{
|
||||
public function describe(Type $type, Schema $property, array $groups = null)
|
||||
public function describe(Type $type, OA\Schema $property, array $groups = null)
|
||||
{
|
||||
$property->setType('integer');
|
||||
$property->type = 'integer';
|
||||
}
|
||||
|
||||
public function supports(Type $type): bool
|
||||
|
@ -11,23 +11,21 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\PropertyDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class ObjectPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface
|
||||
{
|
||||
use ModelRegistryAwareTrait;
|
||||
|
||||
public function describe(Type $type, Schema $property, array $groups = null)
|
||||
public function describe(Type $type, OA\Schema $property, array $groups = null)
|
||||
{
|
||||
$type = new Type($type->getBuiltinType(), false, $type->getClassName(), $type->isCollection(), $type->getCollectionKeyType(), $type->getCollectionValueType()); // ignore nullable field
|
||||
|
||||
$property->setRef(
|
||||
$this->modelRegistry->register(new Model($type, $groups))
|
||||
);
|
||||
$property->ref = $this->modelRegistry->register(new Model($type, $groups));
|
||||
}
|
||||
|
||||
public function supports(Type $type): bool
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\PropertyDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use OpenApi\Annotations\Schema;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
interface PropertyDescriberInterface
|
||||
|
@ -11,14 +11,14 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\PropertyDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class StringPropertyDescriber implements PropertyDescriberInterface
|
||||
{
|
||||
public function describe(Type $type, Schema $property, array $groups = null)
|
||||
public function describe(Type $type, OA\Schema $property, array $groups = null)
|
||||
{
|
||||
$property->setType('string');
|
||||
$property->type = 'string';
|
||||
}
|
||||
|
||||
public function supports(Type $type): bool
|
||||
|
@ -10,6 +10,12 @@ Version](https://poser.pugx.org/nelmio/api-doc-bundle/v/stable)](https://packagi
|
||||
The **NelmioApiDocBundle** bundle allows you to generate a decent documentation
|
||||
for your APIs.
|
||||
|
||||
## Migrate from 3.x to 4.0
|
||||
|
||||
[To migrate from 3.x to 4.0, follow our guide.](https://github.com/nelmio/NelmioApiDocBundle/blob/master/UPGRADE-4.0.md)
|
||||
|
||||
Version 4.0 brings OpenAPI 3.0 support. If you want to stick to Swagger 2.0, you should use the version 3 of this bundle.
|
||||
|
||||
## Migrate from 2.x to 3.0
|
||||
|
||||
[To migrate from 2.x to 3.0, follow our guide.](https://github.com/nelmio/NelmioApiDocBundle/blob/master/UPGRADE-3.0.md)
|
||||
|
@ -6,6 +6,7 @@
|
||||
<services>
|
||||
<service id="nelmio_api_doc.route_describers.fos_rest" class="Nelmio\ApiDocBundle\RouteDescriber\FosRestDescriber" public="false">
|
||||
<argument type="service" id="annotation_reader" />
|
||||
<argument />
|
||||
|
||||
<tag name="nelmio_api_doc.route_describer" priority="-250" />
|
||||
</service>
|
||||
|
@ -42,6 +42,7 @@
|
||||
<argument type="service" id="property_info" />
|
||||
<argument type="service" id="annotation_reader" />
|
||||
<argument type="tagged" tag="nelmio_api_doc.object_model.property_describer" />
|
||||
<argument />
|
||||
|
||||
<tag name="nelmio_api_doc.model_describer" />
|
||||
</service>
|
||||
|
@ -43,7 +43,7 @@ In this case the class ``App\Entity\User`` will be aliased into:
|
||||
class HomeController
|
||||
{
|
||||
/**
|
||||
* @SWG\Response(response=200, @SWG\Schema(ref="#/definitions/MyModel"))
|
||||
* @OA\Response(response=200, @OA\JsonContent(@OA\Schema(ref="#/components/schemas/MyModel")))
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
|
@ -6,39 +6,40 @@ Sharing parameter configuration
|
||||
|
||||
Q: I use the same value in multiple endpoints. How can I avoid duplicating the descriptions?
|
||||
|
||||
A: You can configure ``definitions`` in the nelmio_api_doc configuration and then reference them:
|
||||
A: You can configure ``schemas`` in the nelmio_api_doc configuration and then reference them:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# config/nelmio_api_doc.yml
|
||||
nelmio_api_doc:
|
||||
documentation:
|
||||
definitions:
|
||||
NelmioImageList:
|
||||
description: "Response for some queries"
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
example: 42
|
||||
items:
|
||||
type: array
|
||||
components:
|
||||
schemas:
|
||||
NelmioImageList:
|
||||
description: "Response for some queries"
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
example: 42
|
||||
items:
|
||||
$ref: "#/definitions/ImageMetadata"
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ImageMetadata"
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
// src/App/Controller/NelmioController.php
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="List of image definitions",
|
||||
* @SWG\Schema(
|
||||
* @OA\JsonContent(@OA\Schema(
|
||||
* type="object",
|
||||
* title="ListOperationsResponse",
|
||||
* additionalProperties={"$ref": "#/definitions/NelmioImageList"}
|
||||
* )
|
||||
* additionalProperties={"$ref": "#/components/schemas/NelmioImageList"}
|
||||
* ))
|
||||
*/
|
||||
|
||||
Optional Path Parameters
|
||||
@ -55,7 +56,7 @@ optional? The controller might look like this::
|
||||
* name="get_user_metadata"
|
||||
* )
|
||||
*
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Json object with all user meta data or a json string with the value of the requested field"
|
||||
* )
|
||||
@ -76,14 +77,13 @@ separate actions in your controller. For example::
|
||||
* name="get_user_metadata"
|
||||
* )
|
||||
*
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Json hashmap with all user meta data",
|
||||
* @SWG\Schema(
|
||||
* @OA\JsonContent(@OA\Schema(
|
||||
* type="object",
|
||||
* example={"foo": "bar", "hello": "world"}
|
||||
* )
|
||||
*
|
||||
* ))
|
||||
* )
|
||||
*/
|
||||
public function cgetAction(string $user)
|
||||
@ -99,12 +99,12 @@ separate actions in your controller. For example::
|
||||
* name="get_user_metadata_single"
|
||||
* )
|
||||
*
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="A json string with the value of the requested field",
|
||||
* @SWG\Schema(
|
||||
* @OA\JsonContent(@OA\Schema(
|
||||
* type="string"
|
||||
* )
|
||||
* ))
|
||||
* )
|
||||
*/
|
||||
public function getAction(string $user, string $metaName = null)
|
||||
@ -184,14 +184,14 @@ Endpoints grouping
|
||||
|
||||
Q: Areas feature doesn't fit my needs. So how can I group similar endpoints of one or more controllers in a separate section in the documentation?
|
||||
|
||||
A: Use ``@SWG\Tag`` annotation.
|
||||
A: Use ``@OA\Tag`` annotation.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
/**
|
||||
* Class BookmarkController
|
||||
*
|
||||
* @SWG\Tag(name="Bookmarks")
|
||||
* @OA\Tag(name="Bookmarks")
|
||||
*/
|
||||
class BookmarkController extends AbstractFOSRestController implements ContextPresetInterface
|
||||
{
|
||||
|
@ -99,28 +99,31 @@ Using the bundle
|
||||
----------------
|
||||
|
||||
You can configure global information in the bundle configuration ``documentation.info`` section (take a look at
|
||||
`the OpenAPI 2.0 specification (formerly Swagger)`_ to know the available fields):
|
||||
`the OpenAPI 3.0 specification (formerly Swagger)`_ to know the available fields):
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
nelmio_api_doc:
|
||||
documentation:
|
||||
host: api.example.com
|
||||
schemes: [http, https]
|
||||
servers:
|
||||
- url: http://api.example.com/unsafe
|
||||
description: API over HTTP
|
||||
- url: https://api.example.com/secured
|
||||
description: API over HTTPS
|
||||
info:
|
||||
title: My App
|
||||
description: This is an awesome app!
|
||||
version: 1.0.0
|
||||
securityDefinitions:
|
||||
Bearer:
|
||||
type: apiKey
|
||||
description: 'Value: Bearer {jwt}'
|
||||
name: Authorization
|
||||
in: header
|
||||
components:
|
||||
securitySchemes:
|
||||
Bearer:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
security:
|
||||
- Bearer: []
|
||||
|
||||
.. _`the OpenAPI 2.0 specification (formerly Swagger)`: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md
|
||||
.. _`the OpenAPI 3.0 specification (formerly Swagger)`: https://swagger.io/docs/specification
|
||||
|
||||
.. note::
|
||||
|
||||
@ -135,7 +138,7 @@ To document your routes, you can use the SwaggerPHP annotations and the
|
||||
use AppBundle\Entity\Reward;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Annotation\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class UserController
|
||||
@ -146,21 +149,23 @@ To document your routes, you can use the SwaggerPHP annotations and the
|
||||
* This call takes into account all confirmed awards, but not pending or refused awards.
|
||||
*
|
||||
* @Route("/api/{user}/rewards", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Returns the rewards of an user",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(ref=@Model(type=Reward::class, groups={"full"}))
|
||||
* @OA\JsonContent(
|
||||
* @OA\Schema(
|
||||
* type="array",
|
||||
* @OA\Items(ref=@Model(type=Reward::class, groups={"full"}))
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* @OA\Parameter(
|
||||
* name="order",
|
||||
* in="query",
|
||||
* type="string",
|
||||
* description="The field used to order rewards"
|
||||
* )
|
||||
* @SWG\Tag(name="rewards")
|
||||
* @OA\Tag(name="rewards")
|
||||
* @Security(name="Bearer")
|
||||
*/
|
||||
public function fetchUserRewardsAction(User $user)
|
||||
@ -186,7 +191,7 @@ This annotation has two options:
|
||||
* ``type`` to specify your model's type::
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* @Model(type=User::class)
|
||||
* )
|
||||
@ -195,7 +200,7 @@ This annotation has two options:
|
||||
* ``groups`` to specify the serialization groups used to (de)serialize your model::
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* @Model(type=User::class, groups={"non_sensitive_data"})
|
||||
* )
|
||||
@ -203,23 +208,25 @@ This annotation has two options:
|
||||
|
||||
.. tip::
|
||||
|
||||
When used at the root of ``@SWG\Response`` and ``@SWG\Parameter``, ``@Model`` is automatically nested
|
||||
in a ``@SWG\Schema``.
|
||||
When used at the root of ``@OA\Response`` and ``@OA\Parameter``, ``@Model`` is automatically nested
|
||||
in a ``@OA\Schema``.
|
||||
|
||||
To use ``@Model`` directly within a ``@SWG\Schema``, ``@SWG\Items`` or ``@SWG\Property``, you have to use the ``$ref`` field::
|
||||
The media type defaults to ``application/json``.
|
||||
|
||||
To use ``@Model`` directly within a ``@OA\Schema``, ``@OA\Items`` or ``@OA\Property``, you have to use the ``$ref`` field::
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* @SWG\Schema(ref=@Model(type=User::class))
|
||||
* @OA\Response(
|
||||
* @OA\JsonContent(ref=@Model(type=User::class))
|
||||
* )
|
||||
*
|
||||
* or
|
||||
*
|
||||
* @SWG\Response(
|
||||
* @SWG\Schema(type="object",
|
||||
* @SWG\Property(property="foo", ref=@Model(type=FooClass::class))
|
||||
* @OA\Response(@OA\XmlContent(
|
||||
* @OA\Schema(type="object",
|
||||
* @OA\Property(property="foo", ref=@Model(type=FooClass::class))
|
||||
* )
|
||||
* )
|
||||
* ))
|
||||
*/
|
||||
|
||||
Symfony Form types
|
||||
@ -234,9 +241,9 @@ You can customize the documentation of a form field using the ``documentation``
|
||||
],
|
||||
]);
|
||||
|
||||
See the `OpenAPI 2.0 specification`__ to see all the available fields of the ``documentation`` option.
|
||||
See the `OpenAPI 3.0 specification`__ to see all the available fields of the ``documentation`` option.
|
||||
|
||||
__ https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
|
||||
__ https://swagger.io/specification/
|
||||
|
||||
|
||||
General PHP objects
|
||||
@ -264,33 +271,33 @@ General PHP objects
|
||||
When using the JMS serializer combined with `willdurand/Hateoas`_ (and the `BazingaHateoasBundle`_),
|
||||
HATEOAS metadata are automatically extracted
|
||||
|
||||
If you want to customize the documentation of an object's property, you can use ``@SWG\Property``::
|
||||
If you want to customize the documentation of an object's property, you can use ``@OA\Property``::
|
||||
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
class User
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
* @SWG\Property(description="The unique identifier of the user.")
|
||||
* @OA\Property(description="The unique identifier of the user.")
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @SWG\Property(type="string", maxLength=255)
|
||||
* @OA\Property(type="string", maxLength=255)
|
||||
*/
|
||||
public $username;
|
||||
|
||||
/**
|
||||
* @SWG\Property(ref=@Model(type=User::class))
|
||||
* @OA\Property(ref=@Model(type=User::class))
|
||||
*/
|
||||
public $friend;
|
||||
}
|
||||
|
||||
See the `OpenAPI 2.0 specification`__ to see all the available fields of ``@SWG\Property``.
|
||||
See the `OpenAPI 3.0 specification`__ to see all the available fields of ``@OA\Property``.
|
||||
|
||||
__ https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
|
||||
__ https://swagger.io/specification/
|
||||
|
||||
Learn more
|
||||
----------
|
||||
|
@ -12,9 +12,10 @@
|
||||
namespace Nelmio\ApiDocBundle\RouteDescriber;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use FOS\RestBundle\Controller\Annotations\QueryParam;
|
||||
use FOS\RestBundle\Controller\Annotations\RequestParam;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints\Regex;
|
||||
@ -23,17 +24,22 @@ final class FosRestDescriber implements RouteDescriberInterface
|
||||
{
|
||||
use RouteDescriberTrait;
|
||||
|
||||
/** @var Reader */
|
||||
private $annotationReader;
|
||||
|
||||
public function __construct(Reader $annotationReader)
|
||||
/** @var string[] */
|
||||
private $mediaTypes;
|
||||
|
||||
public function __construct(Reader $annotationReader, array $mediaTypes)
|
||||
{
|
||||
$this->annotationReader = $annotationReader;
|
||||
$this->mediaTypes = $mediaTypes;
|
||||
}
|
||||
|
||||
public function describe(Swagger $api, Route $route, \ReflectionMethod $reflectionMethod)
|
||||
public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod)
|
||||
{
|
||||
$annotations = $this->annotationReader->getMethodAnnotations($reflectionMethod);
|
||||
$annotations = array_filter($annotations, function ($value) {
|
||||
$annotations = array_filter($annotations, static function ($value) {
|
||||
return $value instanceof RequestParam || $value instanceof QueryParam;
|
||||
});
|
||||
|
||||
@ -43,48 +49,32 @@ final class FosRestDescriber implements RouteDescriberInterface
|
||||
|
||||
if ($annotation instanceof QueryParam) {
|
||||
$name = $parameterName.($annotation->map ? '[]' : '');
|
||||
$parameter = $operation->getParameters()->get($name, 'query');
|
||||
$parameter->setAllowEmptyValue($annotation->nullable && $annotation->allowBlank);
|
||||
$parameter = Util::getOperationParameter($operation, $name, 'query');
|
||||
$parameter->allowEmptyValue = $annotation->nullable && $annotation->allowBlank;
|
||||
|
||||
$parameter->setRequired(!$annotation->nullable && $annotation->strict);
|
||||
} else {
|
||||
$body = $operation->getParameters()->get('body', 'body')->getSchema();
|
||||
$body->setType('object');
|
||||
$parameter = $body->getProperties()->get($parameterName);
|
||||
$parameter->required = !$annotation->nullable && $annotation->strict;
|
||||
|
||||
if (!$annotation->nullable && $annotation->strict) {
|
||||
$requiredParameters = $body->getRequired();
|
||||
$requiredParameters[] = $parameterName;
|
||||
|
||||
$body->setRequired(array_values(array_unique($requiredParameters)));
|
||||
if (OA\UNDEFINED === $parameter->description) {
|
||||
$parameter->description = $annotation->description;
|
||||
}
|
||||
}
|
||||
|
||||
$parameter->setDefault($annotation->getDefault());
|
||||
if (null !== $parameter->getType()) {
|
||||
continue;
|
||||
}
|
||||
$schema = Util::getChild($parameter, OA\Schema::class);
|
||||
$this->describeCommonSchemaFromAnnotation($schema, $annotation);
|
||||
} else {
|
||||
/** @var OA\RequestBody $requestBody */
|
||||
$requestBody = Util::getChild($operation, OA\RequestBody::class);
|
||||
foreach ($this->mediaTypes as $mediaType) {
|
||||
$contentSchema = $this->getContentSchemaForType($requestBody, $mediaType);
|
||||
$schema = Util::getProperty($contentSchema, $parameterName);
|
||||
|
||||
if (null === $parameter->getDescription()) {
|
||||
$parameter->setDescription($annotation->description);
|
||||
}
|
||||
if (!$annotation->nullable && $annotation->strict) {
|
||||
$requiredParameters = is_array($contentSchema->required) ? $contentSchema->required : [];
|
||||
$requiredParameters[] = $parameterName;
|
||||
|
||||
if ($annotation->map) {
|
||||
$parameter->setType('array');
|
||||
$parameter->setCollectionFormat('multi');
|
||||
$parameter = $parameter->getItems();
|
||||
}
|
||||
|
||||
$parameter->setType('string');
|
||||
|
||||
$pattern = $this->getPattern($annotation->requirements);
|
||||
if (null !== $pattern) {
|
||||
$parameter->setPattern($pattern);
|
||||
}
|
||||
|
||||
$format = $this->getFormat($annotation->requirements);
|
||||
if (null !== $format) {
|
||||
$parameter->setFormat($format);
|
||||
$contentSchema->required = array_values(array_unique($requiredParameters));
|
||||
}
|
||||
$this->describeCommonSchemaFromAnnotation($schema, $annotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -117,4 +107,64 @@ final class FosRestDescriber implements RouteDescriberInterface
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema
|
||||
{
|
||||
$requestBody->content = OA\UNDEFINED !== $requestBody->content ? $requestBody->content : [];
|
||||
switch ($type) {
|
||||
case 'json':
|
||||
$contentType = 'application\json';
|
||||
|
||||
break;
|
||||
case 'xml':
|
||||
$contentType = 'application\xml';
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException('Unsupported media type');
|
||||
}
|
||||
if (!isset($requestBody->content[$contentType])) {
|
||||
$requestBody->content[$contentType] = new OA\MediaType(
|
||||
[
|
||||
'mediaType' => $contentType,
|
||||
]
|
||||
);
|
||||
/** @var OA\Schema $schema */
|
||||
$schema = Util::getChild(
|
||||
$requestBody->content[$contentType],
|
||||
OA\Schema::class
|
||||
);
|
||||
$schema->type = 'object';
|
||||
}
|
||||
|
||||
return Util::getChild(
|
||||
$requestBody->content[$contentType],
|
||||
OA\Schema::class
|
||||
);
|
||||
}
|
||||
|
||||
private function describeCommonSchemaFromAnnotation(OA\Schema $schema, $annotation)
|
||||
{
|
||||
$schema->default = $annotation->getDefault();
|
||||
|
||||
if (OA\UNDEFINED === $schema->type) {
|
||||
$schema->type = $annotation->map ? 'array' : 'string';
|
||||
}
|
||||
|
||||
if ($annotation->map) {
|
||||
$schema->type = 'array';
|
||||
$schema->collectionFormat = 'multi';
|
||||
$schema->items = Util::getChild($schema, OA\Items::class);
|
||||
}
|
||||
|
||||
$pattern = $this->getPattern($annotation->requirements);
|
||||
if (null !== $pattern) {
|
||||
$schema->pattern = $pattern;
|
||||
}
|
||||
|
||||
$format = $this->getFormat($annotation->requirements);
|
||||
if (null !== $format) {
|
||||
$schema->format = $format;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\RouteDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use OpenApi\Annotations as OA;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use phpDocumentor\Reflection\DocBlockFactoryInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
@ -30,7 +30,7 @@ final class PhpDocDescriber implements RouteDescriberInterface
|
||||
$this->docBlockFactory = $docBlockFactory;
|
||||
}
|
||||
|
||||
public function describe(Swagger $api, Route $route, \ReflectionMethod $reflectionMethod)
|
||||
public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod)
|
||||
{
|
||||
$classDocBlock = null;
|
||||
$docBlock = null;
|
||||
@ -47,19 +47,19 @@ final class PhpDocDescriber implements RouteDescriberInterface
|
||||
|
||||
foreach ($this->getOperations($api, $route) as $operation) {
|
||||
if (null !== $docBlock) {
|
||||
if (null === $operation->getSummary() && '' !== $docBlock->getSummary()) {
|
||||
$operation->setSummary($docBlock->getSummary());
|
||||
if (OA\UNDEFINED === $operation->summary && '' !== $docBlock->getSummary()) {
|
||||
$operation->summary = $docBlock->getSummary();
|
||||
}
|
||||
if (null === $operation->getDescription() && '' !== (string) $docBlock->getDescription()) {
|
||||
$operation->setDescription((string) $docBlock->getDescription());
|
||||
if (OA\UNDEFINED === $operation->description && '' !== (string) $docBlock->getDescription()) {
|
||||
$operation->description = (string) $docBlock->getDescription();
|
||||
}
|
||||
if ($docBlock->hasTag('deprecated')) {
|
||||
$operation->setDeprecated(true);
|
||||
$operation->deprecated = true;
|
||||
}
|
||||
}
|
||||
if (null !== $classDocBlock) {
|
||||
if ($classDocBlock->hasTag('deprecated')) {
|
||||
$operation->setDeprecated(true);
|
||||
$operation->deprecated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,10 +11,10 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\RouteDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
interface RouteDescriberInterface
|
||||
{
|
||||
public function describe(Swagger $api, Route $route, \ReflectionMethod $reflectionMethod);
|
||||
public function describe(OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod);
|
||||
}
|
||||
|
@ -11,8 +11,9 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\RouteDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Operation;
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
@ -23,20 +24,20 @@ trait RouteDescriberTrait
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return Operation[]
|
||||
* @return OA\Operation[]
|
||||
*/
|
||||
private function getOperations(Swagger $api, Route $route): array
|
||||
private function getOperations(OpenApi $api, Route $route): array
|
||||
{
|
||||
$operations = [];
|
||||
$path = $api->getPaths()->get($this->normalizePath($route->getPath()));
|
||||
$methods = $route->getMethods() ?: Swagger::$METHODS;
|
||||
$path = Util::getPath($api, $this->normalizePath($route->getPath()));
|
||||
$methods = $route->getMethods() ?: Util::OPERATIONS;
|
||||
foreach ($methods as $method) {
|
||||
$method = strtolower($method);
|
||||
if (!in_array($method, Swagger::$METHODS)) {
|
||||
if (!in_array($method, Util::OPERATIONS)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$operations[] = $path->getOperation($method);
|
||||
$operations[] = Util::getOperation($path, $method);
|
||||
}
|
||||
|
||||
return $operations;
|
||||
|
@ -11,10 +11,9 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\RouteDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Operation;
|
||||
use EXSyst\Component\Swagger\Parameter;
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use LogicException;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
@ -24,10 +23,10 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
|
||||
{
|
||||
use RouteDescriberTrait;
|
||||
|
||||
public function describe(Swagger $api, Route $route, \ReflectionMethod $reflectionMethod)
|
||||
public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod)
|
||||
{
|
||||
foreach ($this->getOperations($api, $route) as $operation) {
|
||||
$operation->merge(['schemes' => $route->getSchemes()]);
|
||||
Util::merge($operation, ['security' => $route->getSchemes()]);
|
||||
|
||||
$requirements = $route->getRequirements();
|
||||
$compiledRoute = $route->compile();
|
||||
@ -40,24 +39,27 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
|
||||
}
|
||||
|
||||
$paramId = $pathVariable.'/path';
|
||||
/** @var OA\Parameter $parameter */
|
||||
$parameter = $existingParams[$paramId] ?? null;
|
||||
if (null !== $parameter) {
|
||||
if (!$parameter->getRequired()) {
|
||||
if (!$parameter->required || OA\UNDEFINED === $parameter->required) {
|
||||
throw new LogicException(\sprintf('Global parameter "%s" is used as part of route "%s" and must be set as "required"', $pathVariable, $route->getPath()));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$parameter = $operation->getParameters()->get($pathVariable, 'path');
|
||||
$parameter->setRequired(true);
|
||||
$parameter = Util::getOperationParameter($operation, $pathVariable, 'path');
|
||||
$parameter->required = true;
|
||||
|
||||
if (null === $parameter->getType()) {
|
||||
$parameter->setType('string');
|
||||
$parameter->schema = Util::getChild($parameter, OA\Schema::class);
|
||||
|
||||
if (OA\UNDEFINED === $parameter->schema->type) {
|
||||
$parameter->schema->type = 'string';
|
||||
}
|
||||
|
||||
if (isset($requirements[$pathVariable]) && null === $parameter->getPattern()) {
|
||||
$parameter->setPattern($requirements[$pathVariable]);
|
||||
if (isset($requirements[$pathVariable]) && OA\UNDEFINED === $parameter->schema->pattern) {
|
||||
$parameter->schema->pattern = $requirements[$pathVariable];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,22 +68,24 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
|
||||
/**
|
||||
* The '$ref' parameters need special handling, since their objects are missing 'name' and 'in'.
|
||||
*
|
||||
* @return Parameter[] existing $ref parameters
|
||||
* @return OA\Parameter[] existing $ref parameters
|
||||
*/
|
||||
private function getRefParams(Swagger $api, Operation $operation): array
|
||||
private function getRefParams(OA\OpenApi $api, OA\Operation $operation): array
|
||||
{
|
||||
/** @var Parameter[] $globalParams */
|
||||
$globalParams = $api->getParameters();
|
||||
/** @var OA\Parameter[] $globalParams */
|
||||
$globalParams = OA\UNDEFINED !== $api->components->parameters ? $api->components->parameters : [];
|
||||
$existingParams = [];
|
||||
|
||||
foreach ($operation->getParameters() as $id => $parameter) {
|
||||
$ref = $parameter->getRef();
|
||||
if (null === $ref) {
|
||||
$operationParameters = OA\UNDEFINED !== $operation->parameters ? $operation->parameters : [];
|
||||
/** @var OA\Parameter $parameter */
|
||||
foreach ($operationParameters as $id => $parameter) {
|
||||
$ref = $parameter->ref;
|
||||
if (OA\UNDEFINED === $ref) {
|
||||
// we only concern ourselves with '$ref' parameters, so continue the loop
|
||||
continue;
|
||||
}
|
||||
|
||||
$ref = \mb_substr($ref, 13); // trim the '#/parameters/' part of ref
|
||||
$ref = \mb_substr($ref, 24); // trim the '#/components/parameters/' part of ref
|
||||
if (!isset($globalParams[$ref])) {
|
||||
// this shouldn't happen during proper configs, but in case of bad config, just ignore it here
|
||||
continue;
|
||||
@ -90,7 +94,7 @@ final class RouteMetadataDescriber implements RouteDescriberInterface
|
||||
$refParameter = $globalParams[$ref];
|
||||
|
||||
// param ids are in form {name}/{in}
|
||||
$existingParams[\sprintf('%s/%s', $refParameter->getName(), $refParameter->getIn())] = $refParameter;
|
||||
$existingParams[\sprintf('%s/%s', $refParameter->name, $refParameter->in)] = $refParameter;
|
||||
}
|
||||
|
||||
return $existingParams;
|
||||
|
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\SwaggerPhp;
|
||||
|
||||
use Swagger\Analysis;
|
||||
use Swagger\Annotations\Info;
|
||||
use Swagger\Annotations\Swagger;
|
||||
use Swagger\Context;
|
||||
|
||||
/**
|
||||
* Add defaults to fix default warnings.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class AddDefaults
|
||||
{
|
||||
public function __invoke(Analysis $analysis)
|
||||
{
|
||||
if ($analysis->getAnnotationsOfType(Info::class)) {
|
||||
return;
|
||||
}
|
||||
if (($annotations = $analysis->getAnnotationsOfType(Swagger::class)) && null !== $annotations[0]->info) {
|
||||
return;
|
||||
}
|
||||
|
||||
$analysis->addAnnotation(new Info(['title' => '', 'version' => '0.0.0', '_context' => new Context(['generated' => true])]), null);
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\SwaggerPhp;
|
||||
|
||||
use Nelmio\ApiDocBundle\Annotation\Model as ModelAnnotation;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use Swagger\Analysis;
|
||||
use Swagger\Annotations\AbstractAnnotation;
|
||||
use Swagger\Annotations\Items;
|
||||
use Swagger\Annotations\Parameter;
|
||||
use Swagger\Annotations\Response;
|
||||
use Swagger\Annotations\Schema;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
/**
|
||||
* Resolves the path in SwaggerPhp annotation when needed.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ModelRegister
|
||||
{
|
||||
private $modelRegistry;
|
||||
|
||||
public function __construct(ModelRegistry $modelRegistry)
|
||||
{
|
||||
$this->modelRegistry = $modelRegistry;
|
||||
}
|
||||
|
||||
public function __invoke(Analysis $analysis, array $parentGroups = null)
|
||||
{
|
||||
$modelsRegistered = [];
|
||||
foreach ($analysis->annotations as $annotation) {
|
||||
// @Model using the ref field
|
||||
if ($annotation instanceof Schema && $annotation->ref instanceof ModelAnnotation) {
|
||||
$model = $annotation->ref;
|
||||
|
||||
$annotation->ref = $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options));
|
||||
|
||||
// It is no longer an unmerged annotation
|
||||
$this->detach($model, $annotation, $analysis);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Implicit usages
|
||||
if ($annotation instanceof Response) {
|
||||
$annotationClass = Schema::class;
|
||||
} elseif ($annotation instanceof Parameter) {
|
||||
if ('array' === $annotation->type) {
|
||||
$annotationClass = Items::class;
|
||||
} else {
|
||||
$annotationClass = Schema::class;
|
||||
}
|
||||
} elseif ($annotation instanceof Schema) {
|
||||
$annotationClass = Items::class;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$model = null;
|
||||
foreach ($annotation->_unmerged as $unmerged) {
|
||||
if ($unmerged instanceof ModelAnnotation) {
|
||||
$model = $unmerged;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $model || !$model instanceof ModelAnnotation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_string($model->type)) {
|
||||
// Ignore invalid annotations, they are validated later
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($annotation instanceof Schema) {
|
||||
@trigger_error(sprintf('Using `@Model` implicitly in a `@SWG\Schema`, `@SWG\Items` or `@SWG\Property` annotation in %s is deprecated since version 3.2 and won\'t be supported in 4.0. Use `ref=@Model()` instead.', $annotation->_context->getDebugLocation()), E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
$annotation->merge([new $annotationClass([
|
||||
'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)),
|
||||
])]);
|
||||
|
||||
// It is no longer an unmerged annotation
|
||||
$this->detach($model, $annotation, $analysis);
|
||||
}
|
||||
}
|
||||
|
||||
private function getGroups(ModelAnnotation $model, array $parentGroups = null)
|
||||
{
|
||||
if (null === $model->groups) {
|
||||
return $parentGroups;
|
||||
}
|
||||
|
||||
return array_merge($parentGroups ?? [], $model->groups);
|
||||
}
|
||||
|
||||
private function detach(ModelAnnotation $model, AbstractAnnotation $annotation, Analysis $analysis)
|
||||
{
|
||||
foreach ($annotation->_unmerged as $key => $unmerged) {
|
||||
if ($unmerged === $model) {
|
||||
unset($annotation->_unmerged[$key]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
$analysis->annotations->detach($model);
|
||||
}
|
||||
|
||||
private function createType(string $type): Type
|
||||
{
|
||||
if ('[]' === substr($type, -2)) {
|
||||
return new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, $this->createType(substr($type, 0, -2)));
|
||||
}
|
||||
|
||||
return new Type(Type::BUILTIN_TYPE_OBJECT, false, $type);
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ class ApiDocGeneratorTest extends TestCase
|
||||
$adapter = new ArrayAdapter();
|
||||
$generator = new ApiDocGenerator([new DefaultDescriber()], [], $adapter);
|
||||
|
||||
$this->assertEquals($generator->generate(), $adapter->getItem('swagger_doc')->get());
|
||||
$this->assertEquals($generator->generate(), $adapter->getItem('openapi_doc')->get());
|
||||
}
|
||||
|
||||
public function testCacheWithCustomId()
|
||||
|
@ -11,8 +11,8 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Describer;
|
||||
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\Describer\DescriberInterface;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
abstract class AbstractDescriberTest extends TestCase
|
||||
@ -20,9 +20,9 @@ abstract class AbstractDescriberTest extends TestCase
|
||||
/** @var DescriberInterface */
|
||||
protected $describer;
|
||||
|
||||
protected function getSwaggerDoc(): Swagger
|
||||
protected function getOpenApiDoc(): OpenApi
|
||||
{
|
||||
$api = new Swagger();
|
||||
$api = new OpenApi([]);
|
||||
$this->describer->describe($api);
|
||||
|
||||
return $api;
|
||||
|
@ -13,8 +13,8 @@ namespace Nelmio\ApiDocBundle\Tests\Describer;
|
||||
|
||||
use ApiPlatform\Core\Documentation\Documentation;
|
||||
use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection;
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\Describer\ApiPlatformDescriber;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class ApiPlatformDescriberTest extends AbstractDescriberTest
|
||||
@ -30,8 +30,8 @@ class ApiPlatformDescriberTest extends AbstractDescriberTest
|
||||
->with($this->documentation)
|
||||
->willReturn(['info' => ['title' => 'My Test App']]);
|
||||
|
||||
$expectedApi = new Swagger(['info' => ['title' => 'My Test App']]);
|
||||
$this->assertEquals($expectedApi->toArray(), $this->getSwaggerDoc()->toArray());
|
||||
$expectedApi = new OpenApi(['info' => ['title' => 'My Test App']]);
|
||||
$this->assertEquals($expectedApi->toJson(), $this->getOpenApiDoc()->toJson());
|
||||
}
|
||||
|
||||
public function testDescribeRemovesBasePathAfterNormalization()
|
||||
@ -41,8 +41,8 @@ class ApiPlatformDescriberTest extends AbstractDescriberTest
|
||||
->with($this->documentation)
|
||||
->willReturn(['info' => ['title' => 'My Test App'], 'basePath' => '/foo']);
|
||||
|
||||
$expectedApi = new Swagger(['info' => ['title' => 'My Test App']]);
|
||||
$this->assertEquals($expectedApi->toArray(), $this->getSwaggerDoc()->toArray());
|
||||
$expectedApi = new OpenApi(['info' => ['title' => 'My Test App']]);
|
||||
$this->assertEquals($expectedApi->toJson(), $this->getOpenApiDoc()->toJson());
|
||||
}
|
||||
|
||||
protected function setUp()
|
||||
|
@ -11,10 +11,10 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Describer;
|
||||
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\Describer\RouteDescriber;
|
||||
use Nelmio\ApiDocBundle\RouteDescriber\RouteDescriberInterface;
|
||||
use Nelmio\ApiDocBundle\Util\ControllerReflector;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
use Symfony\Component\Routing\Route;
|
||||
@ -32,7 +32,7 @@ class RouteDescriberTest extends AbstractDescriberTest
|
||||
$this->routeDescriber->expects($this->never())
|
||||
->method('describe');
|
||||
|
||||
$this->assertEquals((new Swagger())->toArray(), $this->getSwaggerDoc()->toArray());
|
||||
$this->assertEquals((new OpenApi([]))->toJson(), $this->getOpenApiDoc()->toJson());
|
||||
}
|
||||
|
||||
protected function setUp()
|
||||
|
@ -72,7 +72,8 @@ class BazingaFunctionalTest extends WebTestCase
|
||||
],
|
||||
],
|
||||
],
|
||||
], $this->getModel('BazingaUser')->toArray());
|
||||
'schema' => 'BazingaUser',
|
||||
], json_decode($this->getModel('BazingaUser')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testWithGroup()
|
||||
@ -89,7 +90,8 @@ class BazingaFunctionalTest extends WebTestCase
|
||||
],
|
||||
],
|
||||
],
|
||||
], $this->getModel('BazingaUser_grouped')->toArray());
|
||||
'schema' => 'BazingaUser_grouped',
|
||||
], json_decode($this->getModel('BazingaUser_grouped')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testWithType()
|
||||
@ -107,7 +109,7 @@ class BazingaFunctionalTest extends WebTestCase
|
||||
'properties' => [
|
||||
'typed_bazinga_users' => [
|
||||
'items' => [
|
||||
'$ref' => '#/definitions/BazingaUser',
|
||||
'$ref' => '#/components/schemas/BazingaUser',
|
||||
],
|
||||
'type' => 'array',
|
||||
],
|
||||
@ -117,7 +119,8 @@ class BazingaFunctionalTest extends WebTestCase
|
||||
],
|
||||
],
|
||||
],
|
||||
], $this->getModel('BazingaUserTyped')->toArray());
|
||||
'schema' => 'BazingaUserTyped',
|
||||
], json_decode($this->getModel('BazingaUserTyped')->toJson(), true));
|
||||
}
|
||||
|
||||
protected static function createKernel(array $options = [])
|
||||
|
@ -20,7 +20,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints;
|
||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
|
||||
use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType;
|
||||
use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@ -29,12 +29,12 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
class ApiController
|
||||
{
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response="200",
|
||||
* description="Success",
|
||||
* @SWG\Schema(ref=@Model(type=Article::class, groups={"light"}))
|
||||
* @OA\Response(
|
||||
* response="200",
|
||||
* description="Success",
|
||||
* @Model(type=Article::class, groups={"light"}))
|
||||
* )
|
||||
* @SWG\Parameter(ref="#/parameters/test")
|
||||
* @OA\Parameter(ref="#/components/parameters/test")
|
||||
* @Route("/article/{id}", methods={"GET"})
|
||||
*/
|
||||
public function fetchArticleAction()
|
||||
@ -47,7 +47,7 @@ class ApiController
|
||||
* @Route("/swagger", methods={"GET", "LINK"})
|
||||
* @Route("/swagger2", methods={"GET"})
|
||||
* @Operation(
|
||||
* @SWG\Response(response="201", description="An example resource")
|
||||
* @OA\Response(response="201", description="An example resource")
|
||||
* )
|
||||
*/
|
||||
public function swaggerAction()
|
||||
@ -56,21 +56,19 @@ class ApiController
|
||||
|
||||
/**
|
||||
* @Route("/swagger/implicit", methods={"GET", "POST"})
|
||||
* @SWG\Response(
|
||||
* response="201",
|
||||
* description="Operation automatically detected",
|
||||
* @Model(type=User::class)
|
||||
* @OA\Response(
|
||||
* response="201",
|
||||
* description="Operation automatically detected",
|
||||
* @Model(type=User::class)
|
||||
* ),
|
||||
* @OA\RequestBody(
|
||||
* description="This is a request body",
|
||||
* @OA\JsonContent(
|
||||
* type="array",
|
||||
* @OA\Items(ref=@Model(type=User::class))
|
||||
* )
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="foo",
|
||||
* in="body",
|
||||
* description="This is a parameter",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(ref=@Model(type=User::class))
|
||||
* )
|
||||
* )
|
||||
* @SWG\Tag(name="implicit")
|
||||
* @OA\Tag(name="implicit")
|
||||
*/
|
||||
public function implicitSwaggerAction()
|
||||
{
|
||||
@ -78,16 +76,14 @@ class ApiController
|
||||
|
||||
/**
|
||||
* @Route("/test/users/{user}", methods={"POST"}, schemes={"https"}, requirements={"user"="/foo/"})
|
||||
* @SWG\Response(
|
||||
* response="201",
|
||||
* description="Operation automatically detected",
|
||||
* @Model(type=User::class)
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="foo",
|
||||
* in="body",
|
||||
* description="This is a parameter",
|
||||
* @SWG\Schema(ref=@Model(type=UserType::class, options={"bar": "baz"}))
|
||||
* @OA\Response(
|
||||
* response="201",
|
||||
* description="Operation automatically detected",
|
||||
* @Model(type=User::class)
|
||||
* ),
|
||||
* @OA\RequestBody(
|
||||
* description="This is a request body",
|
||||
* @Model(type=UserType::class, options={"bar": "baz"}))
|
||||
* )
|
||||
*/
|
||||
public function submitUserTypeAction()
|
||||
@ -96,9 +92,7 @@ class ApiController
|
||||
|
||||
/**
|
||||
* @Route("/test/{user}", methods={"GET"}, schemes={"https"}, requirements={"user"="/foo/"})
|
||||
* @Operation(
|
||||
* @SWG\Response(response=200, description="sucessful")
|
||||
* )
|
||||
* @OA\Response(response=200, description="sucessful")
|
||||
*/
|
||||
public function userAction()
|
||||
{
|
||||
@ -127,9 +121,9 @@ class ApiController
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Get(
|
||||
* @OA\Get(
|
||||
* path="/filtered",
|
||||
* @SWG\Response(response="201", description="")
|
||||
* @OA\Response(response="201", description="")
|
||||
* )
|
||||
*/
|
||||
public function filteredAction()
|
||||
@ -138,13 +132,11 @@ class ApiController
|
||||
|
||||
/**
|
||||
* @Route("/form", methods={"POST"})
|
||||
* @SWG\Parameter(
|
||||
* name="form",
|
||||
* in="body",
|
||||
* description="Request content",
|
||||
* @SWG\Schema(ref=@Model(type=DummyType::class))
|
||||
* @OA\RequestBody(
|
||||
* description="Request content",
|
||||
* @Model(type=DummyType::class))
|
||||
* )
|
||||
* @SWG\Response(response="201", description="")
|
||||
* @OA\Response(response="201", description="")
|
||||
*/
|
||||
public function formAction()
|
||||
{
|
||||
@ -152,7 +144,7 @@ class ApiController
|
||||
|
||||
/**
|
||||
* @Route("/security")
|
||||
* @SWG\Response(response="201", description="")
|
||||
* @OA\Response(response="201", description="")
|
||||
* @Security(name="api_key")
|
||||
* @Security(name="basic")
|
||||
*/
|
||||
@ -162,10 +154,10 @@ class ApiController
|
||||
|
||||
/**
|
||||
* @Route("/swagger/symfonyConstraints", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* response="201",
|
||||
* description="Used for symfony constraints test",
|
||||
* @SWG\Schema(ref=@Model(type=SymfonyConstraints::class))
|
||||
* @OA\Response(
|
||||
* response="201",
|
||||
* description="Used for symfony constraints test",
|
||||
* @Model(type=SymfonyConstraints::class)
|
||||
* )
|
||||
*/
|
||||
public function symfonyConstraintsAction()
|
||||
@ -173,15 +165,15 @@ class ApiController
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response="200",
|
||||
* description="Success",
|
||||
* @SWG\Schema(ref="#/definitions/Test")
|
||||
* )
|
||||
* @SWG\Response(
|
||||
* ref="#/components/schemas/Test"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="201",
|
||||
* ref="#/responses/201"
|
||||
* )
|
||||
* ref="#/components/responses/201"
|
||||
* )
|
||||
* @Route("/configReference", methods={"GET"})
|
||||
*/
|
||||
public function configReferenceAction()
|
||||
@ -190,10 +182,10 @@ class ApiController
|
||||
|
||||
/**
|
||||
* @Route("/multi-annotations", methods={"GET", "POST"})
|
||||
* @SWG\Get(description="This is the get operation")
|
||||
* @SWG\Post(description="This is post")
|
||||
* @OA\Get(description="This is the get operation")
|
||||
* @OA\Post(description="This is post")
|
||||
*
|
||||
* @SWG\Response(response=200, description="Worked well!", @Model(type=DummyType::class))
|
||||
* @OA\Response(response=200, description="Worked well!", @Model(type=DummyType::class))
|
||||
*/
|
||||
public function operationsWithOtherAnnotations()
|
||||
{
|
||||
|
@ -13,7 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
|
||||
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@ -23,7 +23,7 @@ class BazingaController
|
||||
{
|
||||
/**
|
||||
* @Route("/api/bazinga", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=BazingaUser::class)
|
||||
@ -35,7 +35,7 @@ class BazingaController
|
||||
|
||||
/**
|
||||
* @Route("/api/bazinga_foo", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=BazingaUser::class, groups={"foo"})
|
||||
|
@ -13,7 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
|
||||
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Nelmio\ApiDocBundle\Tests\Functional\EntityExcluded\BazingaUserTyped;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@ -23,7 +23,7 @@ class BazingaTypedController
|
||||
{
|
||||
/**
|
||||
* @Route("/api/bazinga_typed", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=BazingaUserTyped::class)
|
||||
|
@ -12,7 +12,7 @@
|
||||
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
|
||||
|
||||
use Nelmio\ApiDocBundle\Annotation\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@ -23,7 +23,7 @@ class ClassApiController
|
||||
{
|
||||
/**
|
||||
* @Route("/security/class")
|
||||
* @SWG\Response(response="201", description="")
|
||||
* @OA\Response(response="201", description="")
|
||||
*/
|
||||
public function securityAction()
|
||||
{
|
||||
|
@ -21,7 +21,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatRoomUser;
|
||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSChatUser;
|
||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture;
|
||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\VirtualProperty;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@ -31,7 +31,7 @@ class JMSController
|
||||
{
|
||||
/**
|
||||
* @Route("/api/jms", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=JMSUser::class)
|
||||
@ -43,7 +43,7 @@ class JMSController
|
||||
|
||||
/**
|
||||
* @Route("/api/yaml", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=VirtualProperty::class)
|
||||
@ -55,7 +55,7 @@ class JMSController
|
||||
|
||||
/**
|
||||
* @Route("/api/jms_complex", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=JMSComplex::class, groups={"list", "details", "User" : {"list"}})
|
||||
@ -67,7 +67,7 @@ class JMSController
|
||||
|
||||
/**
|
||||
* @Route("/api/jms_complex_dual", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=JMSDualComplex::class, groups={"Default", "complex" : {"User" : {"details"}}})
|
||||
@ -79,7 +79,7 @@ class JMSController
|
||||
|
||||
/**
|
||||
* @Route("/api/jms_naming_strategy", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=JMSNamingStrategyConstraints::class, groups={"Default"})
|
||||
@ -91,7 +91,7 @@ class JMSController
|
||||
|
||||
/**
|
||||
* @Route("/api/jms_chat", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=JMSChat::class, groups={"Default", "members" : {"mini"}})
|
||||
@ -103,7 +103,7 @@ class JMSController
|
||||
|
||||
/**
|
||||
* @Route("/api/jms_picture", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=JMSPicture::class, groups={"mini"})
|
||||
@ -115,7 +115,7 @@ class JMSController
|
||||
|
||||
/**
|
||||
* @Route("/api/jms_mini_user", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=JMSChatUser::class, groups={"mini"})
|
||||
@ -127,7 +127,7 @@ class JMSController
|
||||
|
||||
/**
|
||||
* @Route("/api/jms_mini_user_nested", methods={"GET"})
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Success",
|
||||
* @Model(type=JMSChatRoomUser::class, groups={"mini", "friend": {"living":{"Default"}}})
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
|
||||
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@ -20,7 +20,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
class TestController
|
||||
{
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* @OA\Response(
|
||||
* response="200",
|
||||
* description="Test"
|
||||
* )
|
||||
@ -31,8 +31,8 @@ class TestController
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Parameter(ref="#/parameters/test"),
|
||||
* @SWG\Response(
|
||||
* @OA\Parameter(ref="#/components/parameters/test"),
|
||||
* @OA\Response(
|
||||
* response="200",
|
||||
* description="Test Ref"
|
||||
* )
|
||||
|
@ -13,13 +13,13 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
|
||||
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
* @SWG\Definition(
|
||||
* @OA\Schema(
|
||||
* required={"id", "user"},
|
||||
* @SWG\Property(property="virtual", ref=@Model(type=JMSUser::class))
|
||||
* @OA\Property(property="virtual", ref=@Model(type=JMSUser::class))
|
||||
* )
|
||||
*/
|
||||
class JMSComplex
|
||||
@ -32,7 +32,7 @@ class JMSComplex
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @SWG\Property(ref=@Model(type=JMSUser::class))
|
||||
* @OA\Property(ref=@Model(type=JMSUser::class))
|
||||
* @Serializer\Expose
|
||||
* @Serializer\Groups({"details"})
|
||||
* @Serializer\SerializedName("user")
|
||||
|
@ -13,7 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
|
||||
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
class JMSDualComplex
|
||||
{
|
||||
@ -23,12 +23,12 @@ class JMSDualComplex
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @SWG\Property(ref=@Model(type=JMSComplex::class))
|
||||
* @OA\Property(ref=@Model(type=JMSComplex::class))
|
||||
*/
|
||||
private $complex;
|
||||
|
||||
/**
|
||||
* @SWG\Property(ref=@Model(type=JMSUser::class))
|
||||
* @OA\Property(ref=@Model(type=JMSUser::class))
|
||||
*/
|
||||
private $user;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
|
||||
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
* User.
|
||||
@ -26,7 +26,7 @@ class JMSUser
|
||||
* @Serializer\Expose
|
||||
* @Serializer\Groups({"list"})
|
||||
*
|
||||
* @SWG\Property(description = "User id", readOnly = true, title = "userid", example=1, default = null)
|
||||
* @OA\Property(description = "User id", readOnly = true, title = "userid", example=1, default = null)
|
||||
*/
|
||||
private $id;
|
||||
|
||||
@ -35,14 +35,14 @@ class JMSUser
|
||||
* @Serializer\Expose
|
||||
* @Serializer\SerializedName("daysOnline")
|
||||
*
|
||||
* @SWG\Property(default = 0, minimum = 1, maximum = 300)
|
||||
* @OA\Property(default = 0, minimum = 1, maximum = 300)
|
||||
*/
|
||||
private $daysOnline;
|
||||
|
||||
/**
|
||||
* @Serializer\Type("string")
|
||||
* @Serializer\Expose
|
||||
* @SWG\Property(readOnly = false)
|
||||
* @OA\Property(readOnly = false)
|
||||
* @Serializer\Groups({"details"})
|
||||
*/
|
||||
private $email;
|
||||
@ -52,7 +52,7 @@ class JMSUser
|
||||
* @Serializer\Accessor(getter="getRoles", setter="setRoles")
|
||||
* @Serializer\Expose
|
||||
*
|
||||
* @SWG\Property(default = {"user"}, description = "Roles list", example="[""ADMIN"",""SUPERUSER""]", title="roles")
|
||||
* @OA\Property(default = {"user"}, description = "Roles list", example="[""ADMIN"",""SUPERUSER""]", title="roles")
|
||||
*/
|
||||
private $roles;
|
||||
|
||||
@ -62,7 +62,7 @@ class JMSUser
|
||||
private $password;
|
||||
|
||||
/**
|
||||
* @SWG\Property(property="last_update", type="date")
|
||||
* @OA\Property(property="last_update", type="date")
|
||||
* @Serializer\Expose
|
||||
*/
|
||||
private $updatedAt;
|
||||
@ -103,7 +103,7 @@ class JMSUser
|
||||
* @Serializer\Expose
|
||||
* @Serializer\SerializedName("friendsNumber")
|
||||
*
|
||||
* @SWG\Property(type = "string", minLength = 1, maxLength = 100)
|
||||
* @OA\Property(type = "string", minLength = 1, maxLength = 100)
|
||||
*/
|
||||
private $friendsNumber;
|
||||
|
||||
@ -122,7 +122,7 @@ class JMSUser
|
||||
* @Serializer\Type("string")
|
||||
* @Serializer\Expose
|
||||
*
|
||||
* @SWG\Property(enum = {"disabled", "enabled"})
|
||||
* @OA\Property(enum = {"disabled", "enabled"})
|
||||
*/
|
||||
private $status;
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
|
||||
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
* @author Guilhem N. <egetick@gmail.com>
|
||||
@ -21,19 +21,19 @@ class User
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @SWG\Property(description = "User id", readOnly = true, title = "userid", example=1, default = null)
|
||||
* @OA\Property(description = "User id", readOnly = true, title = "userid", example=1, default = null)
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @SWG\Property(type="string", readOnly = false)
|
||||
* @OA\Property(type="string", readOnly = false)
|
||||
*/
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*
|
||||
* @SWG\Property(
|
||||
* @OA\Property(
|
||||
* description = "User roles",
|
||||
* title = "roles",
|
||||
* example="[""ADMIN"",""SUPERUSER""]",
|
||||
@ -45,19 +45,19 @@ class User
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @SWG\Property(type = "string")
|
||||
* @OA\Property(type = "string")
|
||||
*/
|
||||
private $friendsNumber;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
* @SWG\Property(default = 0.0)
|
||||
* @OA\Property(default = 0.0)
|
||||
*/
|
||||
private $money;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @SWG\Property(property="creationDate")
|
||||
* @OA\Property(property="creationDate")
|
||||
*/
|
||||
private $createdAt;
|
||||
|
||||
@ -74,7 +74,7 @@ class User
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @SWG\Property(enum = {"disabled", "enabled"})
|
||||
* @OA\Property(enum = {"disabled", "enabled"})
|
||||
*/
|
||||
private $status;
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Nelmio\ApiDocBundle\Tests\Functional;
|
||||
|
||||
use FOS\RestBundle\FOSRestBundle;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
class FOSRestTest extends WebTestCase
|
||||
{
|
||||
@ -30,30 +31,28 @@ class FOSRestTest extends WebTestCase
|
||||
|
||||
$operation = $this->getOperation('/api/fosrest', 'post');
|
||||
|
||||
$parameters = $operation->getParameters();
|
||||
$this->assertTrue($parameters->has('foo', 'query'));
|
||||
$this->assertTrue($parameters->has('body', 'body'));
|
||||
$body = $parameters->get('body', 'body')->getSchema()->getProperties();
|
||||
$this->assertHasParameter('foo', 'query', $operation);
|
||||
$this->assertInstanceOf(OA\RequestBody::class, $operation->requestBody);
|
||||
|
||||
$this->assertTrue($body->has('bar'));
|
||||
$this->assertTrue($body->has('baz'));
|
||||
$bodySchema = $operation->requestBody->content['application\json']->schema;
|
||||
|
||||
$fooParameter = $parameters->get('foo', 'query');
|
||||
$this->assertNotNull($fooParameter->getPattern());
|
||||
$this->assertEquals('\d+', $fooParameter->getPattern());
|
||||
$this->assertNull($fooParameter->getFormat());
|
||||
$this->assertHasProperty('bar', $bodySchema);
|
||||
$this->assertHasProperty('baz', $bodySchema);
|
||||
|
||||
$barParameter = $body->get('bar');
|
||||
$this->assertNotNull($barParameter->getPattern());
|
||||
$this->assertEquals('\d+', $barParameter->getPattern());
|
||||
$this->assertNull($barParameter->getFormat());
|
||||
$fooParameter = $this->getParameter($operation, 'foo', 'query');
|
||||
$this->assertInstanceOf(OA\Schema::class, $fooParameter->schema);
|
||||
$this->assertEquals('\d+', $fooParameter->schema->pattern);
|
||||
$this->assertEquals(OA\UNDEFINED, $fooParameter->schema->format);
|
||||
|
||||
$bazParameter = $body->get('baz');
|
||||
$this->assertNotNull($bazParameter->getFormat());
|
||||
$this->assertEquals('IsTrue', $bazParameter->getFormat());
|
||||
$this->assertNull($bazParameter->getPattern());
|
||||
$barProperty = $this->getProperty($bodySchema, 'bar');
|
||||
$this->assertEquals('\d+', $barProperty->pattern);
|
||||
$this->assertEquals(OA\UNDEFINED, $barProperty->format);
|
||||
|
||||
$bazProperty = $this->getProperty($bodySchema, 'baz');
|
||||
$this->assertEquals(OA\UNDEFINED, $bazProperty->pattern);
|
||||
$this->assertEquals('IsTrue', $bazProperty->format);
|
||||
|
||||
// The _format path attribute should be removed
|
||||
$this->assertFalse($parameters->has('_format', 'path'));
|
||||
$this->assertNotHasParameter('_format', 'path', $operation);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,8 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Functional;
|
||||
|
||||
use EXSyst\Component\Swagger\Tag;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
class FunctionalTest extends WebTestCase
|
||||
{
|
||||
@ -24,39 +25,39 @@ class FunctionalTest extends WebTestCase
|
||||
|
||||
public function testConfiguredDocumentation()
|
||||
{
|
||||
$this->assertEquals('My Default App', $this->getSwaggerDefinition()->getInfo()->getTitle());
|
||||
$this->assertEquals('My Test App', $this->getSwaggerDefinition('test')->getInfo()->getTitle());
|
||||
$this->assertEquals('My Default App', $this->getOpenApiDefinition()->info->title);
|
||||
$this->assertEquals('My Test App', $this->getOpenApiDefinition('test')->info->title);
|
||||
}
|
||||
|
||||
public function testUndocumentedAction()
|
||||
{
|
||||
$paths = $this->getSwaggerDefinition()->getPaths();
|
||||
$this->assertFalse($paths->has('/undocumented'));
|
||||
$this->assertFalse($paths->has('/api/admin'));
|
||||
$api = $this->getOpenApiDefinition();
|
||||
|
||||
$this->assertNotHasPath('/undocumented', $api);
|
||||
$this->assertNotHasPath('/api/admin', $api);
|
||||
}
|
||||
|
||||
public function testFetchArticleAction()
|
||||
{
|
||||
$operation = $this->getOperation('/api/article/{id}', 'get');
|
||||
|
||||
$responses = $operation->getResponses();
|
||||
$this->assertTrue($responses->has('200'));
|
||||
$this->assertEquals('#/definitions/Article', $responses->get('200')->getSchema()->getRef());
|
||||
$this->assertHasResponse('200', $operation);
|
||||
$response = $this->getOperationResponse($operation, '200');
|
||||
$this->assertEquals('#/components/schemas/Article', $response->content['application/json']->schema->ref);
|
||||
|
||||
// Ensure that groups are supported
|
||||
$modelProperties = $this->getModel('Article')->getProperties();
|
||||
$this->assertCount(1, $modelProperties);
|
||||
$this->assertTrue($modelProperties->has('author'));
|
||||
$this->assertSame('#/definitions/User2', $modelProperties->get('author')->getRef());
|
||||
|
||||
$this->assertFalse($modelProperties->has('content'));
|
||||
$articleModel = $this->getModel('Article');
|
||||
$this->assertCount(1, $articleModel->properties);
|
||||
$this->assertHasProperty('author', $articleModel);
|
||||
$this->assertSame('#/components/schemas/User2', Util::getProperty($articleModel, 'author')->ref);
|
||||
$this->assertNotHasProperty('author', Util::getProperty($articleModel, 'author'));
|
||||
}
|
||||
|
||||
public function testFilteredAction()
|
||||
{
|
||||
$paths = $this->getSwaggerDefinition()->getPaths();
|
||||
$openApi = $this->getOpenApiDefinition();
|
||||
|
||||
$this->assertFalse($paths->has('/filtered'));
|
||||
$this->assertNotHasPath('/filtered', $openApi);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,13 +65,13 @@ class FunctionalTest extends WebTestCase
|
||||
*
|
||||
* @dataProvider swaggerActionPathsProvider
|
||||
*/
|
||||
public function testSwaggerAction($path)
|
||||
public function testSwaggerAction(string $path)
|
||||
{
|
||||
$operation = $this->getOperation($path, 'get');
|
||||
|
||||
$responses = $operation->getResponses();
|
||||
$this->assertTrue($responses->has('201'));
|
||||
$this->assertEquals('An example resource', $responses->get('201')->getDescription());
|
||||
$this->assertHasResponse('201', $operation);
|
||||
$response = $this->getOperationResponse($operation, '201');
|
||||
$this->assertEquals('An example resource', $response->description);
|
||||
}
|
||||
|
||||
public function swaggerActionPathsProvider()
|
||||
@ -81,24 +82,22 @@ class FunctionalTest extends WebTestCase
|
||||
/**
|
||||
* @dataProvider implicitSwaggerActionMethodsProvider
|
||||
*/
|
||||
public function testImplicitSwaggerAction($method)
|
||||
public function testImplicitSwaggerAction(string $method)
|
||||
{
|
||||
$operation = $this->getOperation('/api/swagger/implicit', $method);
|
||||
|
||||
$this->assertEquals([new Tag('implicit')], $operation->getTags());
|
||||
$this->assertEquals(['implicit'], $operation->tags);
|
||||
|
||||
$responses = $operation->getResponses();
|
||||
$this->assertTrue($responses->has('201'));
|
||||
$response = $responses->get('201');
|
||||
$this->assertEquals('Operation automatically detected', $response->getDescription());
|
||||
$this->assertEquals('#/definitions/User', $response->getSchema()->getRef());
|
||||
$this->assertHasResponse('201', $operation);
|
||||
$response = $this->getOperationResponse($operation, '201');
|
||||
$this->assertEquals('Operation automatically detected', $response->description);
|
||||
$this->assertEquals('#/components/schemas/User', $response->content['application/json']->schema->ref);
|
||||
|
||||
$parameters = $operation->getParameters();
|
||||
$this->assertTrue($parameters->has('foo', 'body'));
|
||||
$parameter = $parameters->get('foo', 'body');
|
||||
|
||||
$this->assertEquals('This is a parameter', $parameter->getDescription());
|
||||
$this->assertEquals('#/definitions/User', $parameter->getSchema()->getItems()->getRef());
|
||||
$this->assertInstanceOf(OA\RequestBody::class, $operation->requestBody);
|
||||
$requestBody = $operation->requestBody;
|
||||
$this->assertEquals('This is a request body', $requestBody->description);
|
||||
$this->assertEquals('array', $requestBody->content['application/json']->schema->type);
|
||||
$this->assertEquals('#/components/schemas/User', $requestBody->content['application/json']->schema->items->ref);
|
||||
}
|
||||
|
||||
public function implicitSwaggerActionMethodsProvider()
|
||||
@ -110,29 +109,27 @@ class FunctionalTest extends WebTestCase
|
||||
{
|
||||
$operation = $this->getOperation('/api/test/{user}', 'get');
|
||||
|
||||
$this->assertEquals(['https'], $operation->getSchemes());
|
||||
$this->assertEmpty($operation->getSummary());
|
||||
$this->assertEmpty($operation->getDescription());
|
||||
$this->assertNull($operation->getDeprecated());
|
||||
$this->assertTrue($operation->getResponses()->has(200));
|
||||
$this->assertEquals(['https'], $operation->security);
|
||||
$this->assertEquals(OA\UNDEFINED, $operation->summary);
|
||||
$this->assertEquals(OA\UNDEFINED, $operation->description);
|
||||
$this->assertEquals(OA\UNDEFINED, $operation->deprecated);
|
||||
$this->assertHasResponse(200, $operation);
|
||||
|
||||
$parameters = $operation->getParameters();
|
||||
$this->assertTrue($parameters->has('user', 'path'));
|
||||
|
||||
$parameter = $parameters->get('user', 'path');
|
||||
$this->assertTrue($parameter->getRequired());
|
||||
$this->assertEquals('string', $parameter->getType());
|
||||
$this->assertEquals('/foo/', $parameter->getPattern());
|
||||
$this->assertEmpty($parameter->getFormat());
|
||||
$this->assertHasParameter('user', 'path', $operation);
|
||||
$parameter = Util::getOperationParameter($operation, 'user', 'path');
|
||||
$this->assertTrue($parameter->required);
|
||||
$this->assertEquals('string', $parameter->schema->type);
|
||||
$this->assertEquals('/foo/', $parameter->schema->pattern);
|
||||
$this->assertEquals(OA\UNDEFINED, $parameter->schema->format);
|
||||
}
|
||||
|
||||
public function testDeprecatedAction()
|
||||
{
|
||||
$operation = $this->getOperation('/api/deprecated', 'get');
|
||||
|
||||
$this->assertEquals('This action is deprecated.', $operation->getSummary());
|
||||
$this->assertEquals('Please do not use this action.', $operation->getDescription());
|
||||
$this->assertTrue($operation->getDeprecated());
|
||||
$this->assertEquals('This action is deprecated.', $operation->summary);
|
||||
$this->assertEquals('Please do not use this action.', $operation->description);
|
||||
$this->assertTrue($operation->deprecated);
|
||||
}
|
||||
|
||||
public function testApiPlatform()
|
||||
@ -160,6 +157,7 @@ class FunctionalTest extends WebTestCase
|
||||
'readOnly' => true,
|
||||
'title' => 'userid',
|
||||
'example' => 1,
|
||||
'default' => null,
|
||||
],
|
||||
'email' => [
|
||||
'type' => 'string',
|
||||
@ -182,15 +180,15 @@ class FunctionalTest extends WebTestCase
|
||||
],
|
||||
'users' => [
|
||||
'items' => [
|
||||
'$ref' => '#/definitions/User',
|
||||
'$ref' => '#/components/schemas/User',
|
||||
],
|
||||
'type' => 'array',
|
||||
],
|
||||
'friend' => [
|
||||
'$ref' => '#/definitions/User',
|
||||
'$ref' => '#/components/schemas/User',
|
||||
],
|
||||
'dummy' => [
|
||||
'$ref' => '#/definitions/Dummy2',
|
||||
'$ref' => '#/components/schemas/Dummy2',
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'string',
|
||||
@ -201,8 +199,9 @@ class FunctionalTest extends WebTestCase
|
||||
'format' => 'date-time',
|
||||
],
|
||||
],
|
||||
'schema' => 'User',
|
||||
],
|
||||
$this->getModel('User')->toArray()
|
||||
json_decode($this->getModel('User')->toJson(), true)
|
||||
);
|
||||
}
|
||||
|
||||
@ -215,13 +214,13 @@ class FunctionalTest extends WebTestCase
|
||||
'items' => ['type' => 'string'],
|
||||
'type' => 'array',
|
||||
],
|
||||
'dummy' => ['$ref' => '#/definitions/DummyType'],
|
||||
'dummy' => ['$ref' => '#/components/schemas/DummyType'],
|
||||
'dummies' => [
|
||||
'items' => ['$ref' => '#/definitions/DummyType'],
|
||||
'items' => ['$ref' => '#/components/schemas/DummyType'],
|
||||
'type' => 'array',
|
||||
],
|
||||
'empty_dummies' => [
|
||||
'items' => ['$ref' => '#/definitions/DummyEmptyType'],
|
||||
'items' => ['$ref' => '#/components/schemas/DummyEmptyType'],
|
||||
'type' => 'array',
|
||||
],
|
||||
'quz' => [
|
||||
@ -254,7 +253,8 @@ class FunctionalTest extends WebTestCase
|
||||
],
|
||||
],
|
||||
'required' => ['dummy', 'dummies', 'entity', 'entities', 'document', 'documents', 'extended_builtin'],
|
||||
], $this->getModel('UserType')->toArray());
|
||||
'schema' => 'UserType',
|
||||
], json_decode($this->getModel('UserType')->toJson(), true));
|
||||
|
||||
$this->assertEquals([
|
||||
'type' => 'object',
|
||||
@ -299,7 +299,8 @@ class FunctionalTest extends WebTestCase
|
||||
],
|
||||
],
|
||||
'required' => ['foo', 'foz', 'password'],
|
||||
], $this->getModel('DummyType')->toArray());
|
||||
'schema' => 'DummyType',
|
||||
], json_decode($this->getModel('DummyType')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testSecurityAction()
|
||||
@ -310,7 +311,7 @@ class FunctionalTest extends WebTestCase
|
||||
['api_key' => []],
|
||||
['basic' => []],
|
||||
];
|
||||
$this->assertEquals($expected, $operation->getSecurity());
|
||||
$this->assertEquals($expected, $operation->security);
|
||||
}
|
||||
|
||||
public function testClassSecurityAction()
|
||||
@ -320,7 +321,7 @@ class FunctionalTest extends WebTestCase
|
||||
$expected = [
|
||||
['basic' => []],
|
||||
];
|
||||
$this->assertEquals($expected, $operation->getSecurity());
|
||||
$this->assertEquals($expected, $operation->security);
|
||||
}
|
||||
|
||||
public function testSymfonyConstraintDocumentation()
|
||||
@ -382,29 +383,30 @@ class FunctionalTest extends WebTestCase
|
||||
],
|
||||
],
|
||||
'type' => 'object',
|
||||
], $this->getModel('SymfonyConstraints')->toArray());
|
||||
'schema' => 'SymfonyConstraints',
|
||||
], json_decode($this->getModel('SymfonyConstraints')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testConfigReference()
|
||||
{
|
||||
$operation = $this->getOperation('/api/configReference', 'get');
|
||||
$this->assertEquals('#/definitions/Test', $operation->getResponses()->get('200')->getSchema()->getRef());
|
||||
$this->assertEquals('#/responses/201', $operation->getResponses()->get('201')->getRef());
|
||||
$this->assertEquals('#/components/schemas/Test', $this->getOperationResponse($operation, '200')->ref);
|
||||
$this->assertEquals('#/components/responses/201', $this->getOperationResponse($operation, '201')->ref);
|
||||
}
|
||||
|
||||
public function testOperationsWithOtherAnnotationsAction()
|
||||
{
|
||||
$getOperation = $this->getOperation('/api/multi-annotations', 'get');
|
||||
$this->assertSame('This is the get operation', $getOperation->getDescription());
|
||||
$this->assertSame('Worked well!', $getOperation->getResponses()->get(200)->getDescription());
|
||||
$this->assertSame('This is the get operation', $getOperation->description);
|
||||
$this->assertSame('Worked well!', $this->getOperationResponse($getOperation, 200)->description);
|
||||
|
||||
$postOperation = $this->getOperation('/api/multi-annotations', 'post');
|
||||
$this->assertSame('This is post', $postOperation->getDescription());
|
||||
$this->assertSame('Worked well!', $postOperation->getResponses()->get(200)->getDescription());
|
||||
$this->assertSame('This is post', $postOperation->description);
|
||||
$this->assertSame('Worked well!', $this->getOperationResponse($postOperation, 200)->description);
|
||||
}
|
||||
|
||||
public function testNoDuplicatedParameters()
|
||||
{
|
||||
$this->assertFalse($this->getOperation('/api/article/{id}', 'get')->getParameters()->has('id', 'path'));
|
||||
$this->assertNotHasParameter('name', 'path', $this->getOperation('/api/article/{id}', 'get'));
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,8 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'type' => 'integer',
|
||||
],
|
||||
],
|
||||
], $this->getModel('JMSPicture')->toArray());
|
||||
'schema' => 'JMSPicture',
|
||||
], json_decode($this->getModel('JMSPicture')->toJson(), true));
|
||||
|
||||
$this->assertEquals([
|
||||
'type' => 'object',
|
||||
@ -38,7 +39,8 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'type' => 'integer',
|
||||
],
|
||||
],
|
||||
], $this->getModel('JMSPicture_mini')->toArray());
|
||||
'schema' => 'JMSPicture_mini',
|
||||
], json_decode($this->getModel('JMSPicture_mini')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testModeChatDocumentation()
|
||||
@ -51,21 +53,23 @@ class JMSFunctionalTest extends WebTestCase
|
||||
],
|
||||
'members' => [
|
||||
'items' => [
|
||||
'$ref' => '#/definitions/JMSChatUser',
|
||||
'$ref' => '#/components/schemas/JMSChatUser',
|
||||
],
|
||||
'type' => 'array',
|
||||
],
|
||||
],
|
||||
], $this->getModel('JMSChat')->toArray());
|
||||
'schema' => 'JMSChat',
|
||||
], json_decode($this->getModel('JMSChat')->toJson(), true));
|
||||
|
||||
$this->assertEquals([
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'picture' => [
|
||||
'$ref' => '#/definitions/JMSPicture',
|
||||
'$ref' => '#/components/schemas/JMSPicture',
|
||||
],
|
||||
],
|
||||
], $this->getModel('JMSChatUser')->toArray());
|
||||
'schema' => 'JMSChatUser',
|
||||
], json_decode($this->getModel('JMSChatUser')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testModelDocumentation()
|
||||
@ -79,6 +83,7 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'readOnly' => true,
|
||||
'title' => 'userid',
|
||||
'example' => 1,
|
||||
'default' => null,
|
||||
],
|
||||
'daysOnline' => [
|
||||
'type' => 'integer',
|
||||
@ -106,13 +111,13 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'friends' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'$ref' => '#/definitions/User',
|
||||
'$ref' => '#/components/schemas/User',
|
||||
],
|
||||
],
|
||||
'indexed_friends' => [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => [
|
||||
'$ref' => '#/definitions/User',
|
||||
'$ref' => '#/components/schemas/User',
|
||||
],
|
||||
],
|
||||
'favorite_dates' => [
|
||||
@ -127,7 +132,7 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'format' => 'date-time',
|
||||
],
|
||||
'best_friend' => [
|
||||
'$ref' => '#/definitions/User',
|
||||
'$ref' => '#/components/schemas/User',
|
||||
],
|
||||
'status' => [
|
||||
'type' => 'string',
|
||||
@ -136,10 +141,12 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'enum' => ['disabled', 'enabled'],
|
||||
],
|
||||
'virtual_type1' => [
|
||||
'$ref' => '#/definitions/VirtualTypeClassDoesNotExistsHandlerDefined',
|
||||
'title' => 'JMS custom types handled via Custom Type Handlers.',
|
||||
'$ref' => '#/components/schemas/VirtualTypeClassDoesNotExistsHandlerDefined',
|
||||
],
|
||||
'virtual_type2' => [
|
||||
'$ref' => '#/definitions/VirtualTypeClassDoesNotExistsHandlerNotDefined',
|
||||
'title' => 'JMS custom types handled via Custom Type Handlers.',
|
||||
'$ref' => '#/components/schemas/VirtualTypeClassDoesNotExistsHandlerNotDefined',
|
||||
],
|
||||
'last_update' => [
|
||||
'type' => 'date',
|
||||
@ -199,10 +206,12 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'type' => 'integer',
|
||||
],
|
||||
],
|
||||
], $this->getModel('JMSUser')->toArray());
|
||||
'schema' => 'JMSUser',
|
||||
], json_decode($this->getModel('JMSUser')->toJson(), true));
|
||||
|
||||
$this->assertEquals([
|
||||
], $this->getModel('VirtualTypeClassDoesNotExistsHandlerNotDefined')->toArray());
|
||||
'schema' => 'VirtualTypeClassDoesNotExistsHandlerNotDefined',
|
||||
], json_decode($this->getModel('VirtualTypeClassDoesNotExistsHandlerNotDefined')->toJson(), true));
|
||||
|
||||
$this->assertEquals([
|
||||
'type' => 'object',
|
||||
@ -211,7 +220,8 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
], $this->getModel('VirtualTypeClassDoesNotExistsHandlerDefined')->toArray());
|
||||
'schema' => 'VirtualTypeClassDoesNotExistsHandlerDefined',
|
||||
], json_decode($this->getModel('VirtualTypeClassDoesNotExistsHandlerDefined')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testModelComplexDualDocumentation()
|
||||
@ -223,13 +233,14 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'type' => 'integer',
|
||||
],
|
||||
'complex' => [
|
||||
'$ref' => '#/definitions/JMSComplex2',
|
||||
'$ref' => '#/components/schemas/JMSComplex2',
|
||||
],
|
||||
'user' => [
|
||||
'$ref' => '#/definitions/JMSUser',
|
||||
'$ref' => '#/components/schemas/JMSUser',
|
||||
],
|
||||
],
|
||||
], $this->getModel('JMSDualComplex')->toArray());
|
||||
'schema' => 'JMSDualComplex',
|
||||
], json_decode($this->getModel('JMSDualComplex')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testNestedGroups()
|
||||
@ -237,10 +248,11 @@ class JMSFunctionalTest extends WebTestCase
|
||||
$this->assertEquals([
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'living' => ['$ref' => '#/definitions/JMSChatLivingRoom'],
|
||||
'dining' => ['$ref' => '#/definitions/JMSChatRoom'],
|
||||
'living' => ['$ref' => '#/components/schemas/JMSChatLivingRoom'],
|
||||
'dining' => ['$ref' => '#/components/schemas/JMSChatRoom'],
|
||||
],
|
||||
], $this->getModel('JMSChatFriend')->toArray());
|
||||
'schema' => 'JMSChatFriend',
|
||||
], json_decode($this->getModel('JMSChatFriend')->toJson(), true));
|
||||
|
||||
$this->assertEquals([
|
||||
'type' => 'object',
|
||||
@ -248,7 +260,8 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'id1' => ['type' => 'integer'],
|
||||
'id3' => ['type' => 'integer'],
|
||||
],
|
||||
], $this->getModel('JMSChatRoom')->toArray());
|
||||
'schema' => 'JMSChatRoom',
|
||||
], json_decode($this->getModel('JMSChatRoom')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testModelComplexDocumentation()
|
||||
@ -257,15 +270,16 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => ['type' => 'integer'],
|
||||
'user' => ['$ref' => '#/definitions/JMSUser'],
|
||||
'user' => ['$ref' => '#/components/schemas/JMSUser'],
|
||||
'name' => ['type' => 'string'],
|
||||
'virtual' => ['$ref' => '#/definitions/JMSUser'],
|
||||
'virtual' => ['$ref' => '#/components/schemas/JMSUser'],
|
||||
],
|
||||
'required' => [
|
||||
'id',
|
||||
'user',
|
||||
],
|
||||
], $this->getModel('JMSComplex')->toArray());
|
||||
'schema' => 'JMSComplex',
|
||||
], json_decode($this->getModel('JMSComplex')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testYamlConfig()
|
||||
@ -280,7 +294,8 @@ class JMSFunctionalTest extends WebTestCase
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
], $this->getModel('VirtualProperty')->toArray());
|
||||
'schema' => 'VirtualProperty',
|
||||
], json_decode($this->getModel('VirtualProperty')->toJson(), true));
|
||||
}
|
||||
|
||||
public function testNamingStrategyWithConstraints()
|
||||
@ -295,7 +310,8 @@ class JMSFunctionalTest extends WebTestCase
|
||||
],
|
||||
],
|
||||
'required' => ['beautifulName'],
|
||||
], $this->getModel('JMSNamingStrategyConstraints')->toArray());
|
||||
'schema' => 'JMSNamingStrategyConstraints',
|
||||
], json_decode($this->getModel('JMSNamingStrategyConstraints')->toJson(), true));
|
||||
}
|
||||
|
||||
protected static function createKernel(array $options = [])
|
||||
|
@ -11,17 +11,19 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber;
|
||||
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class VirtualTypeClassDoesNotExistsHandlerDefinedDescriber implements ModelDescriberInterface
|
||||
{
|
||||
public function describe(Model $model, Schema $schema)
|
||||
public function describe(Model $model, OA\Schema $schema)
|
||||
{
|
||||
$schema->setType('object');
|
||||
$schema->getProperties()->get('custom_prop')->setType('string');
|
||||
$schema->type = 'object';
|
||||
$property = Util::getProperty($schema, 'custom_prop');
|
||||
$property->type = 'string';
|
||||
}
|
||||
|
||||
public function supports(Model $model): bool
|
||||
|
@ -35,8 +35,7 @@ class SwaggerUiTest extends WebTestCase
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-Type'));
|
||||
|
||||
$expected = $this->getSwaggerDefinition()->toArray();
|
||||
$expected['basePath'] = '/app_dev.php';
|
||||
$expected = json_decode($this->getOpenApiDefinition()->toJson(), true);
|
||||
|
||||
$this->assertEquals($expected, json_decode($crawler->filterXPath('//script[@id="swagger-data"]')->text(), true)['spec']);
|
||||
}
|
||||
@ -49,28 +48,10 @@ class SwaggerUiTest extends WebTestCase
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-Type'));
|
||||
|
||||
$expected = $this->getSwaggerDefinition()->toArray();
|
||||
$expected['basePath'] = '/app_dev.php';
|
||||
$expected['info']['title'] = 'My Test App';
|
||||
$expected['paths'] = [
|
||||
'/api/dummies' => $expected['paths']['/api/dummies'],
|
||||
'/api/foo' => $expected['paths']['/api/foo'],
|
||||
'/api/dummies/{id}' => $expected['paths']['/api/dummies/{id}'],
|
||||
'/test/test/' => ['get' => [
|
||||
'responses' => ['200' => ['description' => 'Test']],
|
||||
]],
|
||||
'/test/test/{id}' => ['get' => [
|
||||
'responses' => ['200' => ['description' => 'Test Ref']],
|
||||
'parameters' => [['$ref' => '#/parameters/test']],
|
||||
]],
|
||||
$expected = json_decode($this->getOpenApiDefinition('test')->toJson(), true);
|
||||
$expected['servers'] = [
|
||||
['url' => 'http://api.example.com/app_dev.php'],
|
||||
];
|
||||
$expected['definitions'] = [
|
||||
'Dummy' => $expected['definitions']['Dummy'],
|
||||
'Test' => ['type' => 'string'],
|
||||
'JMSPicture_mini' => ['type' => 'object'],
|
||||
'BazingaUser_grouped' => ['type' => 'object'],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, json_decode($crawler->filterXPath('//script[@id="swagger-data"]')->text(), true)['spec']);
|
||||
}
|
||||
|
||||
@ -82,9 +63,10 @@ class SwaggerUiTest extends WebTestCase
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('application/json', $response->headers->get('Content-Type'));
|
||||
|
||||
$expected = $this->getSwaggerDefinition()->toArray();
|
||||
$expected['basePath'] = '/app_dev.php';
|
||||
$expected['host'] = 'api.example.com';
|
||||
$expected = json_decode($this->getOpenApiDefinition()->toJson(), true);
|
||||
$expected['servers'] = [
|
||||
['url' => 'http://api.example.com/app_dev.php'],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, json_decode($response->getContent(), true));
|
||||
}
|
||||
|
@ -160,21 +160,23 @@ class TestKernel extends Kernel
|
||||
'info' => [
|
||||
'title' => 'My Default App',
|
||||
],
|
||||
'definitions' => [
|
||||
'Test' => [
|
||||
'type' => 'string',
|
||||
'components' => [
|
||||
'schemas' => [
|
||||
'Test' => [
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
'parameters' => [
|
||||
'test' => [
|
||||
'name' => 'id',
|
||||
'in' => 'path',
|
||||
'required' => true,
|
||||
'parameters' => [
|
||||
'test' => [
|
||||
'name' => 'id',
|
||||
'in' => 'path',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'responses' => [
|
||||
'201' => [
|
||||
'description' => 'Awesome description',
|
||||
'responses' => [
|
||||
'201' => [
|
||||
'description' => 'Awesome description',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
@ -11,8 +11,7 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Functional;
|
||||
|
||||
use EXSyst\Component\Swagger\Operation;
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
|
||||
|
||||
class WebTestCase extends BaseWebTestCase
|
||||
@ -22,29 +21,139 @@ class WebTestCase extends BaseWebTestCase
|
||||
return new TestKernel();
|
||||
}
|
||||
|
||||
protected function getSwaggerDefinition($area = 'default')
|
||||
protected function getOpenApiDefinition($area = 'default'): OA\OpenApi
|
||||
{
|
||||
return static::$kernel->getContainer()->get(sprintf('nelmio_api_doc.generator.%s', $area))->generate();
|
||||
}
|
||||
|
||||
protected function getModel($name): Schema
|
||||
protected function getModel($name): OA\Schema
|
||||
{
|
||||
$definitions = $this->getSwaggerDefinition()->getDefinitions();
|
||||
$this->assertTrue($definitions->has($name));
|
||||
$api = $this->getOpenApiDefinition();
|
||||
$key = array_search($name, array_column($api->components->schemas, 'schema'));
|
||||
static::assertNotFalse($key, sprintf('Model "%s" does not exist.', $name));
|
||||
|
||||
return $definitions->get($name);
|
||||
return $api->components->schemas[$key];
|
||||
}
|
||||
|
||||
protected function getOperation($path, $method): Operation
|
||||
protected function getOperation($path, $method): OA\Operation
|
||||
{
|
||||
$api = $this->getSwaggerDefinition();
|
||||
$paths = $api->getPaths();
|
||||
$path = $this->getPath($path);
|
||||
|
||||
$this->assertTrue($paths->has($path), sprintf('Path "%s" does not exist.', $path));
|
||||
$action = $paths->get($path);
|
||||
$this->assertInstanceOf(
|
||||
OA\Operation::class,
|
||||
$path->{$method},
|
||||
sprintf('Operation "%s" for path "%s" does not exist', $method, $path->path)
|
||||
);
|
||||
|
||||
$this->assertTrue($action->hasOperation($method), sprintf('Operation "%s" for path "%s" does not exist', $path, $method));
|
||||
return $path->{$method};
|
||||
}
|
||||
|
||||
return $action->getOperation($method);
|
||||
protected function getOperationResponse(OA\Operation $operation, $response): OA\Response
|
||||
{
|
||||
$this->assertHasResponse($response, $operation);
|
||||
$key = array_search($response, array_column($operation->responses, 'response'));
|
||||
|
||||
return $operation->responses[$key];
|
||||
}
|
||||
|
||||
protected function getProperty(OA\Schema $annotation, $property): OA\Property
|
||||
{
|
||||
$this->assertHasProperty($property, $annotation);
|
||||
$key = array_search($property, array_column($annotation->properties, 'property'));
|
||||
|
||||
return $annotation->properties[$key];
|
||||
}
|
||||
|
||||
protected function getParameter(OA\AbstractAnnotation $annotation, $name, $in): OA\Parameter
|
||||
{
|
||||
/* @var OA\Operation|OA\OpenApi $annotation */
|
||||
$this->assertHasParameter($name, $in, $annotation);
|
||||
$parameters = array_filter($annotation->parameters ?: [], function (OA\Parameter $parameter) use ($name, $in) {
|
||||
return $parameter->name === $name && $parameter->in === $in;
|
||||
});
|
||||
|
||||
return array_values($parameters)[0];
|
||||
}
|
||||
|
||||
protected function getPath($path): OA\PathItem
|
||||
{
|
||||
$api = $this->getOpenApiDefinition();
|
||||
self::assertHasPath($path, $api);
|
||||
|
||||
return $api->paths[array_search($path, array_column($api->paths, 'path'))];
|
||||
}
|
||||
|
||||
public function assertHasPath($path, OA\OpenApi $api)
|
||||
{
|
||||
$paths = array_column(OA\UNDEFINED !== $api->paths ? $api->paths : [], 'path');
|
||||
static::assertContains(
|
||||
$path,
|
||||
$paths,
|
||||
sprintf('Failed asserting that path "%s" does exist.', $path)
|
||||
);
|
||||
}
|
||||
|
||||
public function assertNotHasPath($path, OA\OpenApi $api)
|
||||
{
|
||||
$paths = array_column(OA\UNDEFINED !== $api->paths ? $api->paths : [], 'path');
|
||||
static::assertNotContains(
|
||||
$path,
|
||||
$paths,
|
||||
sprintf('Failed asserting that path "%s" does not exist.', $path)
|
||||
);
|
||||
}
|
||||
|
||||
public function assertHasResponse($responseCode, OA\Operation $operation)
|
||||
{
|
||||
$responses = array_column(OA\UNDEFINED !== $operation->responses ? $operation->responses : [], 'response');
|
||||
static::assertContains(
|
||||
$responseCode,
|
||||
$responses,
|
||||
sprintf('Failed asserting that response "%s" does exist.', $responseCode)
|
||||
);
|
||||
}
|
||||
|
||||
public function assertHasParameter($name, $in, OA\AbstractAnnotation $annotation)
|
||||
{
|
||||
/* @var OA\Operation|OA\OpenApi $annotation */
|
||||
$parameters = array_column(OA\UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], 'name', 'in');
|
||||
static::assertContains(
|
||||
$name,
|
||||
$parameters[$in] ?? [],
|
||||
sprintf('Failed asserting that parameter "%s" in "%s" does exist.', $name, $in)
|
||||
);
|
||||
}
|
||||
|
||||
public function assertNotHasParameter($name, $in, OA\AbstractAnnotation $annotation)
|
||||
{
|
||||
/* @var OA\Operation|OA\OpenApi $annotation */
|
||||
$parameters = array_column(OA\UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], 'name', 'in');
|
||||
static::assertNotContains(
|
||||
$name,
|
||||
$parameters[$in] ?? [],
|
||||
sprintf('Failed asserting that parameter "%s" in "%s" does not exist.', $name, $in)
|
||||
);
|
||||
}
|
||||
|
||||
public function assertHasProperty($property, OA\AbstractAnnotation $annotation)
|
||||
{
|
||||
/* @var OA\Schema|OA\Property|OA\Items $annotation */
|
||||
$properties = array_column(OA\UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property');
|
||||
static::assertContains(
|
||||
$property,
|
||||
$properties,
|
||||
sprintf('Failed asserting that property "%s" does exist.', $property)
|
||||
);
|
||||
}
|
||||
|
||||
public function assertNotHasProperty($property, OA\AbstractAnnotation $annotation)
|
||||
{
|
||||
/* @var OA\Schema|OA\Property|OA\Items $annotation */
|
||||
$properties = array_column(OA\UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property');
|
||||
static::assertNotContains(
|
||||
$property,
|
||||
$properties,
|
||||
sprintf('Failed asserting that property "%s" does not exist.', $property)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,9 @@
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Model;
|
||||
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use OpenApi\Annotations as OA;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
@ -27,10 +27,10 @@ class ModelRegistryTest extends TestCase
|
||||
'groups' => ['group1'],
|
||||
],
|
||||
];
|
||||
$registry = new ModelRegistry([], new Swagger(), $alternativeNames);
|
||||
$registry = new ModelRegistry([], new OA\OpenApi([]), $alternativeNames);
|
||||
$type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true);
|
||||
|
||||
$this->assertEquals('#/definitions/array', $registry->register(new Model($type, ['group1'])));
|
||||
$this->assertEquals('#/components/schemas/array', $registry->register(new Model($type, ['group1'])));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -40,7 +40,7 @@ class ModelRegistryTest extends TestCase
|
||||
*/
|
||||
public function testNameAliasingForObjects(string $expected, $groups, array $alternativeNames)
|
||||
{
|
||||
$registry = new ModelRegistry([], new Swagger(), $alternativeNames);
|
||||
$registry = new ModelRegistry([], new OA\OpenApi([]), $alternativeNames);
|
||||
$type = new Type(Type::BUILTIN_TYPE_OBJECT, false, self::class);
|
||||
|
||||
$this->assertEquals($expected, $registry->register(new Model($type, $groups)));
|
||||
@ -50,7 +50,7 @@ class ModelRegistryTest extends TestCase
|
||||
{
|
||||
return [
|
||||
[
|
||||
'#/definitions/ModelRegistryTest',
|
||||
'#/components/schemas/ModelRegistryTest',
|
||||
null,
|
||||
[
|
||||
'Foo1' => [
|
||||
@ -60,7 +60,7 @@ class ModelRegistryTest extends TestCase
|
||||
],
|
||||
],
|
||||
[
|
||||
'#/definitions/Foo1',
|
||||
'#/components/schemas/Foo1',
|
||||
['group1'],
|
||||
[
|
||||
'Foo1' => [
|
||||
@ -70,7 +70,7 @@ class ModelRegistryTest extends TestCase
|
||||
],
|
||||
],
|
||||
[
|
||||
'#/definitions/Foo1',
|
||||
'#/components/schemas/Foo1',
|
||||
['group1', 'group2'],
|
||||
[
|
||||
'Foo1' => [
|
||||
@ -80,7 +80,7 @@ class ModelRegistryTest extends TestCase
|
||||
],
|
||||
],
|
||||
[
|
||||
'#/definitions/ModelRegistryTest',
|
||||
'#/components/schemas/ModelRegistryTest',
|
||||
null,
|
||||
[
|
||||
'Foo1' => [
|
||||
@ -90,7 +90,7 @@ class ModelRegistryTest extends TestCase
|
||||
],
|
||||
],
|
||||
[
|
||||
'#/definitions/Foo1',
|
||||
'#/components/schemas/Foo1',
|
||||
[],
|
||||
[
|
||||
'Foo1' => [
|
||||
@ -110,9 +110,9 @@ class ModelRegistryTest extends TestCase
|
||||
$this->expectException('\LogicException');
|
||||
$this->expectExceptionMessage(sprintf('Schema of type "%s" can\'t be generated, no describer supports it.', $stringType));
|
||||
|
||||
$registry = new ModelRegistry([], new Swagger());
|
||||
$registry = new ModelRegistry([], new OA\OpenApi([]));
|
||||
$registry->register(new Model($type));
|
||||
$registry->registerDefinitions();
|
||||
$registry->registerSchemas();
|
||||
}
|
||||
|
||||
public function unsupportedTypesProvider()
|
||||
|
@ -12,8 +12,8 @@
|
||||
namespace Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations;
|
||||
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\SymfonyConstraintAnnotationReader;
|
||||
use OpenApi\Annotations as OA;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@ -33,18 +33,18 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
|
||||
private $property2;
|
||||
};
|
||||
|
||||
$schema = new Schema();
|
||||
$schema->getProperties()->set('property1', new Schema());
|
||||
$schema->getProperties()->set('property2', new Schema());
|
||||
$schema = new OA\Schema([]);
|
||||
$schema->merge([new OA\Property(['property' => 'property1'])]);
|
||||
$schema->merge([new OA\Property(['property' => 'property2'])]);
|
||||
|
||||
$symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader());
|
||||
$symfonyConstraintAnnotationReader->setSchema($schema);
|
||||
|
||||
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->getProperties()->get('property1'));
|
||||
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property2'), $schema->getProperties()->get('property2'));
|
||||
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
|
||||
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property2'), $schema->properties[1]);
|
||||
|
||||
// expect required to be numeric array with sequential keys (not [0 => ..., 2 => ...])
|
||||
$this->assertEquals($schema->getRequired(), ['property1', 'property2']);
|
||||
$this->assertEquals($schema->required, ['property1', 'property2']);
|
||||
}
|
||||
|
||||
public function testAssertChoiceResultsInNumericArray()
|
||||
@ -62,15 +62,15 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
|
||||
private $property1;
|
||||
};
|
||||
|
||||
$schema = new Schema();
|
||||
$schema->getProperties()->set('property1', new Schema());
|
||||
$schema = new OA\Schema([]);
|
||||
$schema->merge([new OA\Property(['property' => 'property1'])]);
|
||||
|
||||
$symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader());
|
||||
$symfonyConstraintAnnotationReader->setSchema($schema);
|
||||
|
||||
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->getProperties()->get('property1'));
|
||||
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
|
||||
|
||||
// expect enum to be numeric array with sequential keys (not [1 => "active", 2 => "active"])
|
||||
$this->assertEquals($schema->getProperties()->get('property1')->getEnum(), ['active', 'blocked']);
|
||||
$this->assertEquals($schema->properties[0]->enum, ['active', 'blocked']);
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\Swagger;
|
||||
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use EXSyst\Component\Swagger\Schema;
|
||||
use EXSyst\Component\Swagger\Swagger;
|
||||
use Nelmio\ApiDocBundle\Annotation\Model as ModelAnnotation;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
|
||||
use Nelmio\ApiDocBundle\SwaggerPhp\ModelRegister;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Swagger\Analysis;
|
||||
use Swagger\Annotations as SWG;
|
||||
|
||||
class ModelRegisterTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @group legacy
|
||||
* @expectedDeprecation Using `@Model` implicitly in a `@SWG\Schema`, `@SWG\Items` or `@SWG\Property` annotation in %s. Use `ref=@Model()` instead.
|
||||
*/
|
||||
public function testDeprecatedImplicitUseOfModel()
|
||||
{
|
||||
$api = new Swagger();
|
||||
$registry = new ModelRegistry([new NullModelDescriber()], $api);
|
||||
$modelRegister = new ModelRegister($registry);
|
||||
|
||||
$annotationsReader = new AnnotationReader();
|
||||
|
||||
$modelRegister->__invoke(new Analysis([$annotation = $annotationsReader->getPropertyAnnotation(
|
||||
new \ReflectionProperty(Foo::class, 'bar'),
|
||||
SWG\Property::class
|
||||
)]));
|
||||
|
||||
$this->assertEquals(['items' => ['$ref' => '#/definitions/Foo']], json_decode(json_encode($annotation), true));
|
||||
}
|
||||
}
|
||||
|
||||
class Foo
|
||||
{
|
||||
/**
|
||||
* @SWG\Property(@ModelAnnotation(type=Foo::class))
|
||||
*/
|
||||
private $bar;
|
||||
}
|
||||
|
||||
class NullModelDescriber implements ModelDescriberInterface
|
||||
{
|
||||
public function describe(Model $model, Schema $schema)
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Model $model): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
842
Tests/SwaggerPhp/UtilTest.php
Normal file
842
Tests/SwaggerPhp/UtilTest.php
Normal file
@ -0,0 +1,842 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the NelmioApiDocBundle package.
|
||||
*
|
||||
* (c) Nelmio
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Nelmio\ApiDocBundle\Tests\SwaggerPhp;
|
||||
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Context;
|
||||
use const OpenApi\UNDEFINED;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Class UtilTest.
|
||||
*
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getOperation
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getOperationParameter
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getChild
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getCollectionItem
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getIndexedCollectionItem
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::searchCollectionItem
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::searchIndexedCollectionItem
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::createCollectionItem
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::createChild
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::createContext
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::merge
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeFromArray
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeChild
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeCollection
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeTyped
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::mergeProperty
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getNestingIndexes
|
||||
* @covers \Nelmio\ApiDocBundle\OpenApiPhp\Util::getNesting
|
||||
*/
|
||||
class UtilTest extends TestCase
|
||||
{
|
||||
public $rootContext;
|
||||
|
||||
/** @var OA\OpenApi */
|
||||
public $rootAnnotation;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->rootContext = new Context(['isTestingRoot' => true]);
|
||||
$this->rootAnnotation = new OA\OpenApi(['_context' => $this->rootContext]);
|
||||
}
|
||||
|
||||
public function testCreateContextSetsParentContext()
|
||||
{
|
||||
$context = Util::createContext([], $this->rootContext);
|
||||
|
||||
$this->assertSame($this->rootContext, $context->getRootContext());
|
||||
}
|
||||
|
||||
public function testCreateContextWithProperties()
|
||||
{
|
||||
$context = Util::createContext(['testing' => 'trait']);
|
||||
|
||||
$this->assertTrue($context->is('testing'));
|
||||
$this->assertSame('trait', $context->testing);
|
||||
}
|
||||
|
||||
public function testCreateChild()
|
||||
{
|
||||
$info = Util::createChild($this->rootAnnotation, OA\Info::class);
|
||||
|
||||
$this->assertInstanceOf(OA\Info::class, $info);
|
||||
}
|
||||
|
||||
public function testCreateChildHasContext()
|
||||
{
|
||||
$info = Util::createChild($this->rootAnnotation, OA\Info::class);
|
||||
|
||||
$this->assertInstanceOf(Context::class, $info->_context);
|
||||
}
|
||||
|
||||
public function testCreateChildHasNestedContext()
|
||||
{
|
||||
$path = Util::createChild($this->rootAnnotation, OA\PathItem::class);
|
||||
$this->assertIsNested($this->rootAnnotation, $path);
|
||||
|
||||
$parameter = Util::createChild($path, OA\Parameter::class);
|
||||
$this->assertIsNested($path, $parameter);
|
||||
|
||||
$schema = Util::createChild($parameter, OA\Schema::class);
|
||||
$this->assertIsNested($parameter, $schema);
|
||||
|
||||
$this->assertIsConnectedToRootContext($schema);
|
||||
}
|
||||
|
||||
public function testCreateChildWithEmptyProperties()
|
||||
{
|
||||
$properties = [];
|
||||
/** @var OA\Info $info */
|
||||
$info = Util::createChild($this->rootAnnotation, OA\Info::class, $properties);
|
||||
|
||||
$properties = array_filter(get_object_vars($info), function ($key) {
|
||||
return 0 !== strpos($key, '_');
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
$this->assertEquals([UNDEFINED], array_unique(array_values($properties)));
|
||||
|
||||
$this->assertIsNested($this->rootAnnotation, $info);
|
||||
$this->assertIsConnectedToRootContext($info);
|
||||
}
|
||||
|
||||
public function testCreateChildWithProperties()
|
||||
{
|
||||
$properties = ['title' => 'testing', 'version' => '999', 'x' => new \stdClass()];
|
||||
/** @var OA\Info $info */
|
||||
$info = Util::createChild($this->rootAnnotation, OA\Info::class, $properties);
|
||||
|
||||
$this->assertSame($info->title, $properties['title']);
|
||||
$this->assertSame($info->version, $properties['version']);
|
||||
$this->assertSame($info->x, $properties['x']);
|
||||
|
||||
$this->assertIsNested($this->rootAnnotation, $info);
|
||||
$this->assertIsConnectedToRootContext($info);
|
||||
}
|
||||
|
||||
public function testCreateCollectionItemAddsCreatedItemToCollection()
|
||||
{
|
||||
$collection = 'paths';
|
||||
$class = OA\PathItem::class;
|
||||
|
||||
$p1 = Util::createCollectionItem($this->rootAnnotation, $collection, $class);
|
||||
$this->assertSame(0, $p1);
|
||||
$this->assertCount(1, $this->rootAnnotation->{$collection});
|
||||
$this->assertInstanceOf($class, $this->rootAnnotation->{$collection}[$p1]);
|
||||
$this->assertIsNested($this->rootAnnotation, $this->rootAnnotation->{$collection}[$p1]);
|
||||
$this->assertIsConnectedToRootContext($this->rootAnnotation->{$collection}[$p1]);
|
||||
|
||||
$p2 = Util::createCollectionItem($this->rootAnnotation, $collection, $class);
|
||||
$this->assertSame(1, $p2);
|
||||
$this->assertCount(2, $this->rootAnnotation->{$collection});
|
||||
$this->assertInstanceOf($class, $this->rootAnnotation->{$collection}[$p2]);
|
||||
$this->assertIsNested($this->rootAnnotation, $this->rootAnnotation->{$collection}[$p2]);
|
||||
$this->assertIsConnectedToRootContext($this->rootAnnotation->{$collection}[$p2]);
|
||||
|
||||
$this->rootAnnotation->components = Util::createChild($this->rootAnnotation, OA\Components::class);
|
||||
|
||||
$collection = 'schemas';
|
||||
$class = OA\Schema::class;
|
||||
|
||||
$d1 = Util::createCollectionItem($this->rootAnnotation->components, $collection, $class);
|
||||
$this->assertSame(0, $d1);
|
||||
$this->assertCount(1, $this->rootAnnotation->components->{$collection});
|
||||
$this->assertInstanceOf($class, $this->rootAnnotation->components->{$collection}[$d1]);
|
||||
$this->assertIsNested($this->rootAnnotation->components, $this->rootAnnotation->components->{$collection}[$d1]);
|
||||
$this->assertIsConnectedToRootContext($this->rootAnnotation->components->{$collection}[$d1]);
|
||||
}
|
||||
|
||||
public function testCreateCollectionItemDoesNotAddToUnknownProperty()
|
||||
{
|
||||
$collection = 'foobars';
|
||||
$class = OA\Info::class;
|
||||
|
||||
$expectedRegex = "/Property \"{$collection}\" doesn't exist .*/";
|
||||
set_error_handler(function ($_, $err) { echo $err; });
|
||||
$this->expectOutputRegex($expectedRegex);
|
||||
Util::createCollectionItem($this->rootAnnotation, $collection, $class);
|
||||
$this->expectOutputRegex($expectedRegex);
|
||||
$this->assertNull($this->rootAnnotation->{$collection});
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
public function testSearchCollectionItem()
|
||||
{
|
||||
$item1 = new \stdClass();
|
||||
$item1->prop1 = 'item 1 prop 1';
|
||||
$item1->prop2 = 'item 1 prop 2';
|
||||
$item1->prop3 = 'item 1 prop 3';
|
||||
|
||||
$item2 = new \stdClass();
|
||||
$item2->prop1 = 'item 2 prop 1';
|
||||
$item2->prop2 = 'item 2 prop 2';
|
||||
$item2->prop3 = 'item 2 prop 3';
|
||||
|
||||
$collection = [
|
||||
$item1,
|
||||
$item2,
|
||||
];
|
||||
|
||||
$this->assertSame(0, Util::searchCollectionItem($collection, get_object_vars($item1)));
|
||||
$this->assertSame(1, Util::searchCollectionItem($collection, get_object_vars($item2)));
|
||||
|
||||
$this->assertNull(Util::searchCollectionItem(
|
||||
$collection,
|
||||
array_merge(get_object_vars($item2), ['prop3' => 'foobar'])
|
||||
));
|
||||
|
||||
$search = ['baz' => 'foobar'];
|
||||
$this->expectOutputString('Undefined property: stdClass::$baz');
|
||||
|
||||
try {
|
||||
Util::searchCollectionItem($collection, array_merge(get_object_vars($item2), $search));
|
||||
} catch (\Exception $e) {
|
||||
echo $e->getMessage();
|
||||
}
|
||||
|
||||
// no exception on empty collection
|
||||
$this->assertNull(Util::searchCollectionItem([], get_object_vars($item2)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideIndexedCollectionData
|
||||
*/
|
||||
public function testSearchIndexedCollectionItem($setup, $asserts)
|
||||
{
|
||||
foreach ($asserts as $collection => $items) {
|
||||
foreach ($items as $assert) {
|
||||
$setupCollection = empty($assert['components']) ?
|
||||
($setup[$collection] ?? []) :
|
||||
(OA\UNDEFINED !== $setup['components']->{$collection} ? $setup['components']->{$collection} : []);
|
||||
|
||||
// get the indexing correct within haystack preparation
|
||||
$properties = array_fill(0, \count($setupCollection), null);
|
||||
|
||||
// prepare the haystack array
|
||||
foreach ($items as $assertItem) {
|
||||
// e.g. $properties[1] = new OA\PathItem(['path' => 'path 1'])
|
||||
$properties[$assertItem['index']] = new $assertItem['class']([
|
||||
$assertItem['key'] => $assertItem['value'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->assertSame(
|
||||
$assert['index'],
|
||||
Util::searchIndexedCollectionItem($properties, $assert['key'], $assert['value']),
|
||||
sprintf('Failed to get the correct index for %s', print_r($assert, true))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideIndexedCollectionData
|
||||
*/
|
||||
public function testGetIndexedCollectionItem($setup, $asserts)
|
||||
{
|
||||
$parent = new $setup['class'](array_merge(
|
||||
$this->getSetupPropertiesWithoutClass($setup),
|
||||
['_context' => $this->rootContext]
|
||||
));
|
||||
|
||||
foreach ($asserts as $collection => $items) {
|
||||
foreach ($items as $assert) {
|
||||
$itemParent = empty($assert['components']) ? $parent : $parent->components;
|
||||
|
||||
$child = Util::getIndexedCollectionItem(
|
||||
$itemParent,
|
||||
$assert['class'],
|
||||
$assert['value']
|
||||
);
|
||||
|
||||
$this->assertInstanceOf($assert['class'], $child);
|
||||
$this->assertSame($child->{$assert['key']}, $assert['value']);
|
||||
$this->assertSame(
|
||||
$itemParent->{$collection}[$assert['index']],
|
||||
$child
|
||||
);
|
||||
|
||||
$setupHaystack = empty($assert['components']) ?
|
||||
$setup[$collection] ?? [] :
|
||||
$setup['components']->{$collection} ?? [];
|
||||
|
||||
// the children created within provider are not connected
|
||||
if (!\in_array($child, $setupHaystack, true)) {
|
||||
$this->assertIsNested($itemParent, $child);
|
||||
$this->assertIsConnectedToRootContext($child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function provideIndexedCollectionData(): array
|
||||
{
|
||||
return [[
|
||||
'setup' => [
|
||||
'class' => OA\OpenApi::class,
|
||||
'paths' => [
|
||||
new OA\PathItem(['path' => 'path 0']),
|
||||
],
|
||||
'components' => new OA\Components([
|
||||
'parameters' => [
|
||||
new OA\Parameter(['parameter' => 'parameter 0']),
|
||||
new OA\Parameter(['parameter' => 'parameter 1']),
|
||||
],
|
||||
]),
|
||||
],
|
||||
'assert' => [
|
||||
// one fixed within setup and one dynamically created
|
||||
'paths' => [
|
||||
[
|
||||
'index' => 0,
|
||||
'class' => OA\PathItem::class,
|
||||
'key' => 'path',
|
||||
'value' => 'path 0',
|
||||
],
|
||||
[
|
||||
'index' => 1,
|
||||
'class' => OA\PathItem::class,
|
||||
'key' => 'path',
|
||||
'value' => 'path 1',
|
||||
],
|
||||
],
|
||||
// search indexes out of order followed by dynamically created
|
||||
'parameters' => [
|
||||
[
|
||||
'index' => 1,
|
||||
'class' => OA\Parameter::class,
|
||||
'key' => 'parameter',
|
||||
'value' => 'parameter 1',
|
||||
'components' => true,
|
||||
],
|
||||
[
|
||||
'index' => 0,
|
||||
'class' => OA\Parameter::class,
|
||||
'key' => 'parameter',
|
||||
'value' => 'parameter 0',
|
||||
'components' => true,
|
||||
],
|
||||
[
|
||||
'index' => 2,
|
||||
'class' => OA\Parameter::class,
|
||||
'key' => 'parameter',
|
||||
'value' => 'parameter 2',
|
||||
'components' => true,
|
||||
],
|
||||
],
|
||||
// two dynamically created
|
||||
'responses' => [
|
||||
[
|
||||
'index' => 0,
|
||||
'class' => OA\Response::class,
|
||||
'key' => 'response',
|
||||
'value' => 'response 0',
|
||||
'components' => true,
|
||||
],
|
||||
[
|
||||
'index' => 1,
|
||||
'class' => OA\Response::class,
|
||||
'key' => 'response',
|
||||
'value' => 'response 1',
|
||||
'components' => true,
|
||||
],
|
||||
],
|
||||
// for sake of completeness
|
||||
'securitySchemes' => [
|
||||
[
|
||||
'index' => 0,
|
||||
'class' => OA\SecurityScheme::class,
|
||||
'key' => 'securityScheme',
|
||||
'value' => 'securityScheme 0',
|
||||
'components' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideChildData
|
||||
*/
|
||||
public function testGetChild($setup, $asserts)
|
||||
{
|
||||
$parent = new $setup['class'](array_merge(
|
||||
$this->getSetupPropertiesWithoutClass($setup),
|
||||
['_context' => $this->rootContext]
|
||||
));
|
||||
|
||||
foreach ($asserts as $key => $assert) {
|
||||
if (array_key_exists('exceptionMessage', $assert)) {
|
||||
$this->expectExceptionMessage($assert['exceptionMessage']);
|
||||
}
|
||||
$child = Util::getChild($parent, $assert['class'], $assert['props']);
|
||||
|
||||
$this->assertInstanceOf($assert['class'], $child);
|
||||
$this->assertSame($child, $parent->{$key});
|
||||
|
||||
if (\array_key_exists($key, $setup)) {
|
||||
$this->assertSame($setup[$key], $parent->{$key});
|
||||
}
|
||||
|
||||
$this->assertEquals($assert['props'], $this->getNonDefaultProperties($child));
|
||||
}
|
||||
}
|
||||
|
||||
public function provideChildData(): array
|
||||
{
|
||||
return [[
|
||||
'setup' => [
|
||||
'class' => OA\PathItem::class,
|
||||
'get' => new OA\Get([]),
|
||||
],
|
||||
'assert' => [
|
||||
// fixed within setup
|
||||
'get' => [
|
||||
'class' => OA\Get::class,
|
||||
'props' => [],
|
||||
],
|
||||
// create new without props
|
||||
'put' => [
|
||||
'class' => OA\Put::class,
|
||||
'props' => [],
|
||||
],
|
||||
// create new with multiple props
|
||||
'delete' => [
|
||||
'class' => OA\Delete::class,
|
||||
'props' => [
|
||||
'summary' => 'testing delete',
|
||||
'deprecated' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
], [
|
||||
'setup' => [
|
||||
'class' => OA\Parameter::class,
|
||||
],
|
||||
'assert' => [
|
||||
// create new with multiple props
|
||||
'schema' => [
|
||||
'class' => OA\Schema::class,
|
||||
'props' => [
|
||||
'ref' => '#/testing/schema',
|
||||
'minProperties' => 0,
|
||||
'enum' => [null, 'check', 999, false],
|
||||
],
|
||||
],
|
||||
],
|
||||
], [
|
||||
'setup' => [
|
||||
'class' => OA\Parameter::class,
|
||||
],
|
||||
'assert' => [
|
||||
// externalDocs triggers invalid argument exception
|
||||
'schema' => [
|
||||
'class' => OA\Schema::class,
|
||||
'props' => [
|
||||
'externalDocs' => [],
|
||||
],
|
||||
'exceptionMessage' => 'Nesting Annotations is not supported.',
|
||||
],
|
||||
],
|
||||
]];
|
||||
}
|
||||
|
||||
public function testGetOperationParameterReturnsExisting()
|
||||
{
|
||||
$name = 'operation name';
|
||||
$in = 'operation in';
|
||||
|
||||
$parameter = new OA\Parameter(['name' => $name, 'in' => $in]);
|
||||
$operation = new OA\Get(['parameters' => [
|
||||
new OA\Parameter([]),
|
||||
new OA\Parameter(['name' => 'foo']),
|
||||
new OA\Parameter(['in' => 'bar']),
|
||||
new OA\Parameter(['name' => $name, 'in' => 'bar']),
|
||||
new OA\Parameter(['name' => 'foo', 'in' => $in]),
|
||||
$parameter,
|
||||
]]);
|
||||
|
||||
$actual = Util::getOperationParameter($operation, $name, $in);
|
||||
$this->assertSame($parameter, $actual);
|
||||
}
|
||||
|
||||
public function testGetOperationParameterCreatesWithNameAndIn()
|
||||
{
|
||||
$name = 'operation name';
|
||||
$in = 'operation in';
|
||||
|
||||
$operation = new OA\Get(['parameters' => [
|
||||
new OA\Parameter([]),
|
||||
new OA\Parameter(['name' => 'foo']),
|
||||
new OA\Parameter(['in' => 'bar']),
|
||||
new OA\Parameter(['name' => $name, 'in' => 'bar']),
|
||||
new OA\Parameter(['name' => 'foo', 'in' => $in]),
|
||||
]]);
|
||||
|
||||
$actual = Util::getOperationParameter($operation, $name, $in);
|
||||
$this->assertInstanceOf(OA\Parameter::class, $actual);
|
||||
$this->assertSame($name, $actual->name);
|
||||
$this->assertSame($in, $actual->in);
|
||||
}
|
||||
|
||||
public function testGetOperationReturnsExisting()
|
||||
{
|
||||
$get = new OA\Get([]);
|
||||
$path = new OA\PathItem(['get' => $get]);
|
||||
|
||||
$this->assertSame($get, Util::getOperation($path, 'get'));
|
||||
}
|
||||
|
||||
public function testGetOperationCreatesWithPath()
|
||||
{
|
||||
$pathStr = '/testing/get/path';
|
||||
$path = new OA\PathItem(['path' => $pathStr]);
|
||||
|
||||
$get = Util::getOperation($path, 'get');
|
||||
$this->assertInstanceOf(OA\Get::class, $get);
|
||||
$this->assertSame($pathStr, $get->path);
|
||||
}
|
||||
|
||||
public function testMergeWithEmptyArray()
|
||||
{
|
||||
$api = new OA\OpenApi([]);
|
||||
$expected = json_encode($api);
|
||||
|
||||
Util::merge($api, [], false);
|
||||
$actual = json_encode($api);
|
||||
|
||||
$this->assertSame($expected, $actual);
|
||||
|
||||
Util::merge($api, [], true);
|
||||
$actual = json_encode($api);
|
||||
|
||||
$this->assertSame($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMergeData
|
||||
*/
|
||||
public function testMerge($setup, $merge, $assert)
|
||||
{
|
||||
$api = new OA\OpenApi($setup);
|
||||
|
||||
Util::merge($api, $merge, false);
|
||||
$this->assertTrue($api->validate());
|
||||
$actual = json_decode(json_encode($api), true);
|
||||
|
||||
$this->assertEquals($assert, $actual);
|
||||
}
|
||||
|
||||
public function provideMergeData(): array
|
||||
{
|
||||
$no = 'do not overwrite';
|
||||
$yes = 'do overwrite';
|
||||
|
||||
$requiredInfo = ['title' => '', 'version' => ''];
|
||||
|
||||
$setupDefaults = [
|
||||
'info' => new OA\Info($requiredInfo),
|
||||
'paths' => [],
|
||||
];
|
||||
$assertDefaults = [
|
||||
'info' => $requiredInfo,
|
||||
'openapi' => '3.0.0',
|
||||
'paths' => [],
|
||||
];
|
||||
|
||||
return [[
|
||||
// simple child merge
|
||||
'setup' => [
|
||||
'info' => new OA\Info(['version' => $no]),
|
||||
'paths' => [],
|
||||
],
|
||||
'merge' => [
|
||||
'info' => ['title' => $yes, 'version' => $yes],
|
||||
],
|
||||
'assert' => [
|
||||
'info' => ['title' => $yes, 'version' => $no],
|
||||
] + $assertDefaults,
|
||||
], [
|
||||
// indexed collection merge
|
||||
'setup' => [
|
||||
'components' => new OA\Components([
|
||||
'schemas' => [
|
||||
new OA\Schema(['schema' => $no, 'title' => $no]),
|
||||
],
|
||||
]),
|
||||
] + $setupDefaults,
|
||||
'merge' => [
|
||||
'components' => [
|
||||
'schemas' => [
|
||||
$no => ['title' => $yes, 'description' => $yes],
|
||||
],
|
||||
],
|
||||
],
|
||||
'assert' => [
|
||||
'components' => [
|
||||
'schemas' => [
|
||||
$no => ['title' => $no, 'description' => $yes],
|
||||
],
|
||||
],
|
||||
] + $assertDefaults,
|
||||
], [
|
||||
// collection merge
|
||||
'setup' => [
|
||||
'tags' => [new OA\Tag(['name' => $no])],
|
||||
] + $setupDefaults,
|
||||
'merge' => [
|
||||
'tags' => [
|
||||
// this is actually appending right now, no clue if this is wanted,
|
||||
// but the complete NelmioApiDocBundle test suite is not upset by this fact
|
||||
['name' => $yes],
|
||||
// this should not append since a tag with exactly the same properties
|
||||
// is already present
|
||||
['name' => $no],
|
||||
// this does, but should not append since the name already exists, and the
|
||||
// docs in Tag state that the tag names must be unique, but it is complicated
|
||||
// and $api->validate() does not complain either
|
||||
['name' => $no, 'description' => $yes],
|
||||
],
|
||||
],
|
||||
'assert' => [
|
||||
'tags' => [
|
||||
['name' => $no],
|
||||
['name' => $yes],
|
||||
['name' => $no, 'description' => $yes],
|
||||
],
|
||||
] + $assertDefaults,
|
||||
],
|
||||
[
|
||||
// heavy nested merge array
|
||||
'setup' => $setupDefaults,
|
||||
'merge' => $merge = [
|
||||
'servers' => [
|
||||
['url' => 'http'],
|
||||
['url' => 'https'],
|
||||
],
|
||||
'paths' => [
|
||||
'/path/to/resource' => [
|
||||
'get' => [
|
||||
'responses' => [
|
||||
'200' => [
|
||||
'$ref' => '#/components/responses/default',
|
||||
],
|
||||
],
|
||||
'requestBody' => [
|
||||
'description' => 'request foo',
|
||||
'content' => [
|
||||
'foo-request' => [
|
||||
'schema' => [
|
||||
'type' => 'object',
|
||||
'required' => ['baz', 'bar'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'tags' => [
|
||||
['name' => 'baz'],
|
||||
['name' => 'foo'],
|
||||
['name' => 'baz'],
|
||||
['name' => 'foo'],
|
||||
['name' => 'foo'],
|
||||
],
|
||||
'components' => [
|
||||
'responses' => [
|
||||
'default' => [
|
||||
'description' => 'default response',
|
||||
'headers' => [
|
||||
'foo-header' => [
|
||||
'schema' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['foo', 'bar', 'baz'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'assert' => array_merge(
|
||||
$assertDefaults,
|
||||
$merge,
|
||||
['tags' => \array_slice($merge['tags'], 0, 2, true)]
|
||||
),
|
||||
], [
|
||||
// heavy nested merge array object
|
||||
'setup' => $setupDefaults,
|
||||
'merge' => new \ArrayObject([
|
||||
'servers' => [
|
||||
['url' => 'http'],
|
||||
['url' => 'https'],
|
||||
],
|
||||
'paths' => [
|
||||
'/path/to/resource' => [
|
||||
'get' => new \ArrayObject([
|
||||
'responses' => [
|
||||
'200' => [
|
||||
'$ref' => '#/components/responses/default',
|
||||
],
|
||||
],
|
||||
'requestBody' => new \ArrayObject([
|
||||
'description' => 'request foo',
|
||||
'content' => [
|
||||
'foo-request' => [
|
||||
'schema' => [
|
||||
'required' => ['baz', 'bar'],
|
||||
'type' => 'object',
|
||||
],
|
||||
],
|
||||
],
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
'tags' => new \ArrayObject([
|
||||
['name' => 'baz'],
|
||||
['name' => 'foo'],
|
||||
new \ArrayObject(['name' => 'baz']),
|
||||
['name' => 'foo'],
|
||||
['name' => 'foo'],
|
||||
]),
|
||||
'components' => new \ArrayObject([
|
||||
'responses' => [
|
||||
'default' => [
|
||||
'description' => 'default response',
|
||||
'headers' => new \ArrayObject([
|
||||
'foo-header' => new \ArrayObject([
|
||||
'schema' => new \ArrayObject([
|
||||
'type' => 'array',
|
||||
'items' => new \ArrayObject([
|
||||
'type' => 'string',
|
||||
'enum' => ['foo', 'bar', 'baz'],
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
]),
|
||||
]),
|
||||
'assert' => array_merge(
|
||||
$assertDefaults,
|
||||
$merge,
|
||||
['tags' => \array_slice($merge['tags'], 0, 2, true)]
|
||||
),
|
||||
], [
|
||||
// heavy nested merge swagger instance
|
||||
'setup' => $setupDefaults,
|
||||
'merge' => new OA\OpenApi([
|
||||
'servers' => [
|
||||
new OA\Server(['url' => 'http']),
|
||||
new OA\Server(['url' => 'https']),
|
||||
],
|
||||
'paths' => [
|
||||
new OA\PathItem([
|
||||
'path' => '/path/to/resource',
|
||||
'get' => new OA\Get([
|
||||
'responses' => [
|
||||
new OA\Response([
|
||||
'response' => '200',
|
||||
'ref' => '#/components/responses/default',
|
||||
]),
|
||||
],
|
||||
'requestBody' => new OA\RequestBody([
|
||||
'description' => 'request foo',
|
||||
'content' => [
|
||||
new OA\MediaType([
|
||||
'mediaType' => 'foo-request',
|
||||
'schema' => new OA\Schema([
|
||||
'type' => 'object',
|
||||
'required' => ['baz', 'bar'],
|
||||
]),
|
||||
]),
|
||||
],
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
'tags' => [
|
||||
new OA\Tag(['name' => 'baz']),
|
||||
new OA\Tag(['name' => 'foo']),
|
||||
new OA\Tag(['name' => 'baz']),
|
||||
new OA\Tag(['name' => 'foo']),
|
||||
new OA\Tag(['name' => 'foo']),
|
||||
],
|
||||
'components' => new OA\Components([
|
||||
'responses' => [
|
||||
new OA\Response([
|
||||
'response' => 'default',
|
||||
'description' => 'default response',
|
||||
'headers' => [
|
||||
new OA\Header([
|
||||
'header' => 'foo-header',
|
||||
'schema' => new OA\Schema([
|
||||
'type' => 'array',
|
||||
'items' => new OA\Items([
|
||||
'type' => 'string',
|
||||
'enum' => ['foo', 'bar', 'baz'],
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
]),
|
||||
],
|
||||
]),
|
||||
]),
|
||||
'assert' => array_merge(
|
||||
$assertDefaults,
|
||||
$merge,
|
||||
['tags' => \array_slice($merge['tags'], 0, 2, true)]
|
||||
),
|
||||
], ];
|
||||
}
|
||||
|
||||
public function assertIsNested(OA\AbstractAnnotation $parent, OA\AbstractAnnotation $child)
|
||||
{
|
||||
self::assertTrue($child->_context->is('nested'));
|
||||
self::assertSame($parent, $child->_context->nested);
|
||||
}
|
||||
|
||||
public function assertIsConnectedToRootContext(OA\AbstractAnnotation $annotation)
|
||||
{
|
||||
$this->assertSame($this->rootContext, $annotation->_context->getRootContext());
|
||||
}
|
||||
|
||||
private function getSetupPropertiesWithoutClass(array $setup)
|
||||
{
|
||||
return array_filter($setup, function ($k) {return 'class' !== $k; }, ARRAY_FILTER_USE_KEY);
|
||||
}
|
||||
|
||||
private function getNonDefaultProperties($object)
|
||||
{
|
||||
$objectVars = \get_object_vars($object);
|
||||
$classVars = \get_class_vars(\get_class($object));
|
||||
$props = [];
|
||||
foreach ($objectVars as $key => $value) {
|
||||
if ($value !== $classVars[$key] && 0 !== \strpos($key, '_')) {
|
||||
$props[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $props;
|
||||
}
|
||||
}
|
28
UPGRADE-4.0.md
Normal file
28
UPGRADE-4.0.md
Normal file
@ -0,0 +1,28 @@
|
||||
Upgrading From 3.x To 4.0
|
||||
=========================
|
||||
|
||||
Version 4 is a major change introducing OpenAPI 3.0 support, the rebranded swagger specification, which brings a set of new interesting features. Unfortunately this required a rework to a large part of the bundle, and introduces BC breaks.
|
||||
|
||||
The Visual guide to "[What's new in 3.0 spec](https://blog.readme.com/an-example-filled-guide-to-swagger-3-2/)" gives more information on OpenApi 3.0.
|
||||
|
||||
Version 4 does not support older versions of the specification. If you need to output swagger v2 documentation, you will need to use the latest 3.x release.
|
||||
|
||||
The Upgrade to Swagger 3.0
|
||||
--------------------------
|
||||
|
||||
The biggest part of the upgrade will most likely be the upgrade of the library `zircote/swagger-php` to `3.0` which introduces new annotations in order to support OpenAPI 3.0 but also changes
|
||||
their namespace from ``Swagger`` to ``OpenApi``.
|
||||
|
||||
They created a dedicated page to help you upgrade : https://zircote.github.io/swagger-php/Migrating-to-v3.html.
|
||||
|
||||
Here are some additional advices that are more likely to apply to NelmioApiDocBundle users:
|
||||
|
||||
- Upgrade all your ``use Swagger\Annotations as SWG`` statements to ``use OpenApi\Annotations as OA;`` (to simplify the upgrade you may also stick to the ``SWG`` aliasing).
|
||||
In case you changed ``SWG`` to ``OA``, upgrade all your annotations from ``@SWG\...`` to ``@OA\...``.
|
||||
|
||||
- Update your config in case you used inlined swagger docummentation (the field ``nelmio_api_doc.documentation``). [A tool](https://openapi-converter.herokuapp.com/) is available to help you convert it.
|
||||
|
||||
- In case you used ``@OA\Response(..., @OA\Schema(...))``, you should explicit your media type by using the annotation ``@OA\JsonContent`` or ``@OA\XmlContent`` instead of ``@OA\Schema``:
|
||||
``@OA\Response(..., @OA\JsonContent(...))`` or ``@OA\Response(..., @OA\XmlContent(...))``.
|
||||
|
||||
When you use ``@Model`` directly (``@OA\Response(..., @Model(...)))``), the media type is set by default to ``json``.
|
@ -16,33 +16,33 @@
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.1",
|
||||
"symfony/framework-bundle": "^3.4|^4.0|^5.0",
|
||||
"symfony/options-resolver": "^3.4.4|^4.0|^5.0",
|
||||
"symfony/property-info": "^3.4|^4.0|^5.0",
|
||||
"exsyst/swagger": "^0.4.1",
|
||||
"zircote/swagger-php": "^2.0.9",
|
||||
"ext-json": "*",
|
||||
"symfony/framework-bundle": "^4.0|^5.0",
|
||||
"symfony/options-resolver": "^4.0|^5.0",
|
||||
"symfony/property-info": "^4.0|^5.0",
|
||||
"zircote/swagger-php": "^3.0",
|
||||
"phpdocumentor/reflection-docblock": "^3.1|^4.0|^5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/templating": "^3.4|^4.0|^5.0",
|
||||
"symfony/twig-bundle": "^3.4|^4.0|^5.0",
|
||||
"symfony/asset": "^3.4|^4.0|^5.0",
|
||||
"symfony/console": "^3.4|^4.0|^5.0",
|
||||
"symfony/config": "^3.4|^4.0|^5.0",
|
||||
"symfony/validator": "^3.4|^4.0|^5.0",
|
||||
"symfony/property-access": "^3.4|^4.0|^5.0",
|
||||
"symfony/form": "^3.4|^4.0|^5.0",
|
||||
"symfony/dom-crawler": "^3.4|^4.0|^5.0",
|
||||
"symfony/browser-kit": "^3.4|^4.0|^5.0",
|
||||
"symfony/cache": "^3.4|^4.0|^5.0",
|
||||
"symfony/phpunit-bridge": "^3.4.24|^4.0|^5.0",
|
||||
"symfony/stopwatch": "^3.4|^4.0|^5.0",
|
||||
"symfony/routing": "^3.4|^4.0|^5.0",
|
||||
"sensio/framework-extra-bundle": "^3.0.13|^4.0|^5.0",
|
||||
"symfony/templating": "^4.0|^5.0",
|
||||
"symfony/twig-bundle": "^4.0|^5.0",
|
||||
"symfony/asset": "^4.0|^5.0",
|
||||
"symfony/console": "^4.0|^5.0",
|
||||
"symfony/config": "^4.0|^5.0",
|
||||
"symfony/validator": "^4.0|^5.0",
|
||||
"symfony/property-access": "^4.0|^5.0",
|
||||
"symfony/form": "^4.0|^5.0",
|
||||
"symfony/dom-crawler": "^4.0|^5.0",
|
||||
"symfony/browser-kit": "^4.0|^5.0",
|
||||
"symfony/cache": "^4.0|^5.0",
|
||||
"symfony/phpunit-bridge": "^4.0|^5.0",
|
||||
"symfony/stopwatch": "^4.0|^5.0",
|
||||
"symfony/routing": "^4.0|^5.0",
|
||||
"sensio/framework-extra-bundle": "^4.0|^5.0",
|
||||
"doctrine/annotations": "^1.2",
|
||||
"doctrine/common": "^2.4",
|
||||
|
||||
"api-platform/core": "^2.1.2",
|
||||
"api-platform/core": "^2.4",
|
||||
"friendsofsymfony/rest-bundle": "^2.0",
|
||||
"willdurand/hateoas-bundle": "^1.0|^2.0",
|
||||
"jms/serializer-bundle": "^2.3|^3.0",
|
||||
|
Loading…
x
Reference in New Issue
Block a user