diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 52105ae..e054835 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,15 +6,15 @@ jobs: php-cs-fixer: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 7.4 - coverage: none - tools: php-cs-fixer:2.19.3 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + coverage: none + tools: php-cs-fixer:2.19.3 - - name: Run PHP-CS-Fixer - run: php-cs-fixer fix --dry-run --diff + - name: Run PHP-CS-Fixer + run: php-cs-fixer fix --dry-run --diff diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 111b1ad..f53aaf1 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -4,44 +4,24 @@ on: [push, pull_request] jobs: tests: - name: "PHP ${{ matrix.php }} + Symfony ${{ matrix.symfony }} + Composer ${{ matrix.dependency }}" + name: "PHP${{ matrix.php }} Symfony${{ matrix.symfony}} ${{ matrix.composer-flags }}" runs-on: ubuntu-latest strategy: - fail-fast: true + fail-fast: false matrix: include: - # Lowest PHP with the lowest dependencies for every supported lowest major version of the Symfony - php: '7.1' - coverage: xdebug - dependency: lowest symfony: '^3.4' - - php: '7.1' - coverage: xdebug - dependency: lowest - symfony: '^4.0' - - php: '7.2' - coverage: xdebug - dependency: lowest - symfony: '^5.0' - - # All supported php versions with the highest dependencies for any supported Symfony version - - php: '7.1' - coverage: xdebug - dependency: highest - symfony: 'any' + coverage: 'xdebug' + composer-flags: '--prefer-lowest' - php: '7.2' - coverage: pcov - dependency: highest - symfony: 'any' + symfony: '^4.0' - php: '7.3' - coverage: pcov - dependency: highest - symfony: 'any' + symfony: '^5.0' - php: '7.4' - coverage: pcov - dependency: highest - symfony: 'any' + symfony: '^5.0' + coverage: 'pcov' steps: - name: Checkout source @@ -51,35 +31,30 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - coverage: ${{ matrix.coverage }} + coverage: ${{ matrix.coverage || 'none' }} - - name: Require Symfony version - if: matrix.symfony != 'any' - run: | - composer global require --no-interaction --no-progress symfony/flex:^1.11 - composer config extra.symfony.require ${{ matrix.symfony }} + - name: Install Symfony Flex + run: composer global require -o --no-interaction --no-progress symfony/flex:^1.11 - - name: Validate composer.json - run: composer validate + - name: Require Symfony version + run: composer config extra.symfony.require ${{ matrix.symfony }} - name: Update composer dependencies - uses: ramsey/composer-install@v1 - with: - dependency-versions: ${{ matrix.dependency }} + run: composer update -o --no-interaction --no-progress ${{ matrix.composer-flags }} - name: Run test suite run: ./vendor/bin/simple-phpunit -v - name: Install php-coveralls - run: composer global require --no-interaction --no-progress php-coveralls/php-coveralls + run: composer global require -o --no-interaction --no-progress php-coveralls/php-coveralls - name: Upload coverage results to Coveralls + if: matrix.coverage uses: nick-invision/retry@v2 env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_PARALLEL: true - COVERALLS_FLAG_NAME: | - "PHP ${{ matrix.php }} + Symfony ${{ matrix.symfony }} + Composer ${{ matrix.dependency }}" + COVERALLS_FLAG_NAME: "PHP${{ matrix.php }} Symfony${{ matrix.symfony}}" with: timeout_seconds: 60 max_attempts: 3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d94f7b..2fcc73a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Removed all unnecessary rows from the `.gitignore` according to `.gitignore_global` use instead. ### Fixed - Fixed incompatible with Symfony 5 according to new contract in `OptionResolver::offsetGet()`. +- Added conflict with `nelmio/api-doc-bundle` version lower than `3.4`. - Fixed error when `SwaggerResolver` without any validator. - Fixed incorrect behavior of the `multipleOf` validation when received value and `multipleOf` was a float type. - Fixed not worked array validation for the `multi` format. diff --git a/DependencyInjection/LinkinSwaggerResolverExtension.php b/DependencyInjection/LinkinSwaggerResolverExtension.php index ae6952c..7b38398 100644 --- a/DependencyInjection/LinkinSwaggerResolverExtension.php +++ b/DependencyInjection/LinkinSwaggerResolverExtension.php @@ -129,6 +129,7 @@ private function getConfigurationLoaderDefinition(ContainerBuilder $container, a return $loaderDefinition ->setClass(NelmioApiDocConfigurationLoader::class) ->addArgument(new Reference(sprintf('nelmio_api_doc.generator.%s', $this->globalAreaName))) + ->addArgument($container->getParameter('kernel.project_dir')) ; } diff --git a/Loader/AbstractAnnotationConfigurationLoader.php b/Loader/AbstractAnnotationConfigurationLoader.php deleted file mode 100644 index 994f562..0000000 --- a/Loader/AbstractAnnotationConfigurationLoader.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Linkin\Bundle\SwaggerResolverBundle\Loader; - -use Linkin\Bundle\SwaggerResolverBundle\Collection\SchemaOperationCollection; -use ReflectionClass; -use Symfony\Component\Config\Resource\FileResource; - -/** - * @author Viktor Linkin - */ -abstract class AbstractAnnotationConfigurationLoader extends AbstractSwaggerConfigurationLoader -{ - /** - * {@inheritdoc} - */ - protected function registerOperationResources(SchemaOperationCollection $operationCollection): void - { - foreach ($operationCollection->getIterator() as $routeName => $methodList) { - $route = $this->getRouter()->getRouteCollection()->get($routeName); - - if (null === $route) { - continue; - } - - $defaults = $route->getDefaults(); - $exploded = explode('::', $defaults['_controller']); - $controllerName = reset($exploded); - - $operationCollection->addSchemaResource($routeName, $this->getFileResource($controllerName)); - } - } - - protected function getFileResource(string $className): FileResource - { - $class = new ReflectionClass($className); - - return new FileResource($class->getFileName()); - } -} diff --git a/Loader/AbstractSwaggerConfigurationLoader.php b/Loader/AbstractSwaggerConfigurationLoader.php index 4bb4e31..6098ff3 100644 --- a/Loader/AbstractSwaggerConfigurationLoader.php +++ b/Loader/AbstractSwaggerConfigurationLoader.php @@ -21,6 +21,8 @@ use Linkin\Bundle\SwaggerResolverBundle\Collection\SchemaOperationCollection; use Linkin\Bundle\SwaggerResolverBundle\Exception\OperationNotFoundException; use Linkin\Bundle\SwaggerResolverBundle\Merger\OperationParameterMerger; +use ReflectionClass; +use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Routing\RouterInterface; /** @@ -38,11 +40,6 @@ abstract class AbstractSwaggerConfigurationLoader implements SwaggerConfiguratio */ private $operationCollection; - /** - * @var array - */ - private $mapPathToRouteName; - /** * @var OperationParameterMerger */ @@ -53,6 +50,16 @@ abstract class AbstractSwaggerConfigurationLoader implements SwaggerConfiguratio */ private $router; + /** + * @var string[][] + */ + private $mapPathToRouteName = []; + + /** + * @var FileResource[] + */ + private $mapRouteNameToSourceFile = []; + public function __construct(OperationParameterMerger $parameterMerger, RouterInterface $router) { $this->parameterMerger = $parameterMerger; @@ -96,39 +103,16 @@ abstract protected function registerDefinitionResources(SchemaDefinitionCollecti /** * Add file resources for swagger operations. */ - abstract protected function registerOperationResources(SchemaOperationCollection $operationCollection): void; - - protected function getRouter(): RouterInterface + protected function registerOperationResources(SchemaOperationCollection $operationCollection): void { - return $this->router; - } - - protected function getRouteNameByPath(string $path, string $method): string - { - if (!$this->mapPathToRouteName) { - $this->initMapPathToRouteName(); - } - - $route = $this->mapPathToRouteName[$path][$method] ?? null; - - if (!$route) { - throw new OperationNotFoundException($path, $method); + foreach ($operationCollection->getIterator() as $routeName => $methodList) { + $operationCollection->addSchemaResource($routeName, $this->mapRouteNameToSourceFile[$routeName]); } - - return (string) $route; } - /** - * Initialize map real path into appropriated route name. - */ - private function initMapPathToRouteName(): void + private function normalizeMethod(string $method): string { - foreach ($this->router->getRouteCollection() as $routeName => $route) { - foreach ($route->getMethods() as $method) { - $method = $this->normalizeMethod($method); - $this->mapPathToRouteName[$route->getPath()][$method] = $routeName; - } - } + return strtolower($method); } /** @@ -136,25 +120,32 @@ private function initMapPathToRouteName(): void */ private function registerCollections(): void { + $this->initRouteMaps(); $swaggerConfiguration = $this->loadConfiguration(); - $definitionCollection = new SchemaDefinitionCollection(); - $operationCollection = new SchemaOperationCollection(); + $this->definitionCollection = new SchemaDefinitionCollection(); + $this->operationCollection = new SchemaOperationCollection(); foreach ($swaggerConfiguration->getDefinitions()->getIterator() as $definitionName => $definition) { - $definitionCollection->addSchema($definitionName, $definition); + $this->definitionCollection->addSchema($definitionName, $definition); } - $this->registerDefinitionResources($definitionCollection); + $this->registerDefinitionResources($this->definitionCollection); /** @var Path $pathObject */ foreach ($swaggerConfiguration->getPaths()->getIterator() as $path => $pathObject) { /** @var Operation $operation */ foreach ($pathObject->getOperations() as $method => $operation) { $method = $this->normalizeMethod($method); + $routeName = $this->mapPathToRouteName[$path][$method] ?? null; + + if (null === $routeName) { + throw new OperationNotFoundException($path, $method); + } + $schema = $this->parameterMerger->merge($operation, $swaggerConfiguration->getDefinitions()); - $routeName = $this->getRouteNameByPath($path, $method); - $operationCollection->addSchema($routeName, $method, $schema); + + $this->operationCollection->addSchema($routeName, $method, $schema); /** @var Parameter $parameter */ foreach ($operation->getParameters()->getIterator() as $parameter) { @@ -167,21 +158,31 @@ private function registerCollections(): void $explodedName = explode('/', $ref); $definitionName = end($explodedName); - foreach ($definitionCollection->getSchemaResources($definitionName) as $fileResource) { - $operationCollection->addSchemaResource($routeName, $fileResource); + foreach ($this->definitionCollection->getSchemaResources($definitionName) as $fileResource) { + $this->operationCollection->addSchemaResource($routeName, $fileResource); } } } } - $this->registerOperationResources($operationCollection); - - $this->definitionCollection = $definitionCollection; - $this->operationCollection = $operationCollection; + $this->registerOperationResources($this->operationCollection); } - private function normalizeMethod(string $method): string + private function initRouteMaps(): void { - return strtolower($method); + $this->mapPathToRouteName = []; + $this->mapRouteNameToSourceFile = []; + + foreach ($this->router->getRouteCollection() as $routeName => $route) { + foreach ($route->getMethods() as $method) { + $defaults = $route->getDefaults(); + $exploded = explode('::', $defaults['_controller']); + $controllerName = reset($exploded); + $fullClassName = (new ReflectionClass($controllerName))->getFileName(); + + $this->mapPathToRouteName[$route->getPath()][$this->normalizeMethod($method)] = $routeName; + $this->mapRouteNameToSourceFile[$routeName] = new FileResource($fullClassName); + } + } } } diff --git a/Loader/NelmioApiDocConfigurationLoader.php b/Loader/NelmioApiDocConfigurationLoader.php index 9cbcf10..0bba594 100644 --- a/Loader/NelmioApiDocConfigurationLoader.php +++ b/Loader/NelmioApiDocConfigurationLoader.php @@ -17,25 +17,35 @@ use Linkin\Bundle\SwaggerResolverBundle\Collection\SchemaDefinitionCollection; use Linkin\Bundle\SwaggerResolverBundle\Merger\OperationParameterMerger; use Nelmio\ApiDocBundle\ApiDocGenerator; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Finder\Finder; use Symfony\Component\Routing\RouterInterface; /** * @author Viktor Linkin */ -class NelmioApiDocConfigurationLoader extends AbstractAnnotationConfigurationLoader +class NelmioApiDocConfigurationLoader extends AbstractSwaggerConfigurationLoader { /** - * Instance of nelmio Api configuration generator. - * * @var ApiDocGenerator */ private $apiDocGenerator; - public function __construct(OperationParameterMerger $merger, RouterInterface $router, ApiDocGenerator $generator) - { + /** + * @var string + */ + private $projectDir; + + public function __construct( + OperationParameterMerger $merger, + RouterInterface $router, + ApiDocGenerator $generator, + string $projectDir + ) { parent::__construct($merger, $router); $this->apiDocGenerator = $generator; + $this->projectDir = $projectDir; } /** @@ -51,24 +61,23 @@ protected function loadConfiguration(): Swagger */ protected function registerDefinitionResources(SchemaDefinitionCollection $definitionCollection): void { - $definitionNames = []; + $classMap = $this->getClassMap(); foreach ($definitionCollection->getIterator() as $definitionName => $definition) { - $definitionName = (string) $definitionName; - $definitionNames[$definitionName] = $definitionName; + $definitionCollection->addSchemaResource($definitionName, new FileResource($classMap[$definitionName])); } + } - foreach (get_declared_classes() as $fullClassName) { - $explodedClassName = explode('\\', $fullClassName); - $className = (string) end($explodedClassName); - - if (!isset($definitionNames[$className])) { - continue; - } + private function getClassMap(): array + { + $finder = (new Finder())->files()->in($this->projectDir)->exclude('vendor')->name('*.php'); + $classMap = []; - $definitionCollection->addSchemaResource($className, $this->getFileResource($fullClassName)); + foreach ($finder as $file) { + $name = (string) str_replace('.php', '', $file->getFilename()); + $classMap[$name] = $file->getPathname(); } - // TODO: Throw exception when class was never found + return $classMap; } } diff --git a/Loader/SwaggerPhpConfigurationLoader.php b/Loader/SwaggerPhpConfigurationLoader.php index 3c43832..d922458 100644 --- a/Loader/SwaggerPhpConfigurationLoader.php +++ b/Loader/SwaggerPhpConfigurationLoader.php @@ -23,7 +23,7 @@ /** * @author Viktor Linkin */ -class SwaggerPhpConfigurationLoader extends AbstractAnnotationConfigurationLoader +class SwaggerPhpConfigurationLoader extends AbstractSwaggerConfigurationLoader { /** * @var array diff --git a/Merger/Strategy/AbstractMergeStrategy.php b/Merger/Strategy/AbstractMergeStrategy.php index 067fbce..4079e49 100644 --- a/Merger/Strategy/AbstractMergeStrategy.php +++ b/Merger/Strategy/AbstractMergeStrategy.php @@ -23,12 +23,12 @@ abstract class AbstractMergeStrategy implements MergeStrategyInterface /** * @var array */ - protected $parameters; + protected $parameters = []; /** * @var array */ - protected $required; + protected $required = []; /** * {@inheritdoc} diff --git a/Tests/Fixtures/SwaggerPhp/Models/Cart.php b/Tests/Fixtures/SwaggerPhp/Models/Cart.php index c8b2474..69627ca 100644 --- a/Tests/Fixtures/SwaggerPhp/Models/Cart.php +++ b/Tests/Fixtures/SwaggerPhp/Models/Cart.php @@ -30,7 +30,7 @@ class Cart public $totalPrice; /** - * @var array + * @var CartItem[] * * @SWG\Property( * minItems=0, diff --git a/Tests/Fixtures/SwaggerPhp/Models/CustomerFull.php b/Tests/Fixtures/SwaggerPhp/Models/CustomerFull.php index ddd3ce4..06a9931 100644 --- a/Tests/Fixtures/SwaggerPhp/Models/CustomerFull.php +++ b/Tests/Fixtures/SwaggerPhp/Models/CustomerFull.php @@ -44,7 +44,7 @@ class CustomerFull public $secondName; /** - * @var array + * @var string[] * * @SWG\Property( * uniqueItems=true, diff --git a/Tests/Fixtures/SwaggerPhp/Models/CustomerNew.php b/Tests/Fixtures/SwaggerPhp/Models/CustomerNew.php index 29ea1c7..0b14d2d 100644 --- a/Tests/Fixtures/SwaggerPhp/Models/CustomerNew.php +++ b/Tests/Fixtures/SwaggerPhp/Models/CustomerNew.php @@ -37,7 +37,7 @@ class CustomerNew public $secondName; /** - * @var array + * @var string[] * * @SWG\Property( * uniqueItems=true, diff --git a/Tests/Functional/Bundle/TestBundle/Controller/CartController.php b/Tests/Functional/Bundle/TestBundle/Controller/CartController.php new file mode 100644 index 0000000..526966a --- /dev/null +++ b/Tests/Functional/Bundle/TestBundle/Controller/CartController.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Linkin\Bundle\SwaggerResolverBundle\Tests\Functional\Bundle\TestBundle\Controller; + +use Linkin\Bundle\SwaggerResolverBundle\Tests\Fixtures\SwaggerPhp\Models\Cart; +use Linkin\Bundle\SwaggerResolverBundle\Tests\Fixtures\SwaggerPhp\Models\CartItem; +use Linkin\Bundle\SwaggerResolverBundle\Tests\Fixtures\SwaggerPhp\Models\ResponseCreated; +use Nelmio\ApiDocBundle\Annotation\Model; +use Swagger\Annotations as SWG; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +/** + * @author Viktor Linkin + * + * @SWG\Tag(name="cart") + */ +class CartController +{ + /** + * Add new item into cart or increase count of existed. + * + * @Route(name="cart_add_item", path="/cart", methods={"PUT"}) + * + * @SWG\Parameter( + * name="x-auth-token", + * in="header", + * description="Alternative token for the authorization", + * required=true, + * type="string", + * pattern="^\w{36}$", + * ) + * @SWG\Parameter( + * name="cart", + * in="body", + * description="Item data to add to the cart", + * required=true, + * @Model(type=CartItem::class), + * ) + * @SWG\Response( + * response=201, + * description="New item into cart ID", + * @Model(type=ResponseCreated::class), + * ) + */ + public function addItem(): Response + { + return new Response(Response::HTTP_CREATED); + } + + /** + * Returns all items from the cart. + * + * @Route(name="cart_get", path="/cart", methods={"GET"}) + * + * @SWG\Parameter( + * name="x-auth-token", + * in="header", + * description="Alternative token for the authorization", + * required=true, + * type="string", + * pattern="^\w{36}$", + * ) + * @SWG\Response( + * response=200, + * description="Cart data", + * @Model(type=Cart::class), + * ) + */ + public function getCartData(): Response + { + return new Response(); + } +} diff --git a/Tests/Functional/Bundle/TestBundle/Controller/CustomerController.php b/Tests/Functional/Bundle/TestBundle/Controller/CustomerController.php new file mode 100644 index 0000000..f2c7fab --- /dev/null +++ b/Tests/Functional/Bundle/TestBundle/Controller/CustomerController.php @@ -0,0 +1,304 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Linkin\Bundle\SwaggerResolverBundle\Tests\Functional\Bundle\TestBundle\Controller; + +use Linkin\Bundle\SwaggerResolverBundle\Factory\SwaggerResolverFactory; +use Linkin\Bundle\SwaggerResolverBundle\Tests\Fixtures\SwaggerPhp\Models\CustomerFull; +use Linkin\Bundle\SwaggerResolverBundle\Tests\Fixtures\SwaggerPhp\Models\CustomerNew; +use Linkin\Bundle\SwaggerResolverBundle\Tests\Fixtures\SwaggerPhp\Models\ResponseCreated; +use Nelmio\ApiDocBundle\Annotation\Model; +use Swagger\Annotations as SWG; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +/** + * @author Viktor Linkin + * + * @SWG\Tag(name="customer") + */ +class CustomerController +{ + /** + * Returns all customers. + * + * @Route(name="customers_get", path="/customers", methods={"GET"}) + * + * @SWG\Parameter( + * name="x-auth-token", + * in="header", + * description="Alternative token for the authorization", + * required=true, + * type="string", + * pattern="^\w{36}$", + * ) + * @SWG\Parameter( + * name="page", + * in="query", + * description="Page number", + * required=false, + * type="integer", + * default=0, + * minimum=0, + * ) + * @SWG\Parameter( + * name="perPage", + * in="query", + * description="Items count for the single page", + * required=false, + * type="integer", + * enum={100, 500, 1000}, + * default=100, + * ) + * @SWG\Response( + * response=200, + * description="A list of customers", + * @SWG\Schema( + * type="array", + * @SWG\Items(ref=@Model(type=CustomerFull::class)), + * ), + * ) + */ + public function getAll(Request $request, SwaggerResolverFactory $factory): Response + { + $swaggerResolver = $factory->createForDefinition(CustomerFull::class); + $data = $swaggerResolver->resolve(json_decode($request->getContent(), true)); + + return new JsonResponse([$data]); + } + + /** + * Create new customer. + * + * @Route(name="customers_post", path="/customers", methods={"POST"}) + * + * @SWG\Parameter( + * name="x-auth-token", + * in="header", + * description="Alternative token for the authorization", + * required=true, + * type="string", + * pattern="^\w{36}$", + * ) + * @SWG\Parameter( + * name="roles", + * in="query", + * description="Deprecated - Old way to set user roles", + * required=false, + * type="array", + * collectionFormat="csv", + * uniqueItems=true, + * minItems=1, + * maxItems=3, + * @SWG\Items(type="string", enum={"guest", "user", "admin"}), + * ) + * @SWG\Parameter( + * name="customer", + * in="body", + * description="Customer to add to the system", + * required=true, + * @Model(type=CustomerNew::class), + * ) + * @SWG\Response( + * response=201, + * description="New customer ID", + * @Model(type=ResponseCreated::class), + * ) + */ + public function create(): Response + { + return new Response(Response::HTTP_CREATED); + } + + /** + * Return customer by ID. + * + * @Route(name="customers_get_one", path="/customers/{userId}", methods={"GET"}, requirements={"userId": "\d+"}) + * + * @SWG\Parameter( + * name="x-auth-token", + * in="header", + * description="Alternative token for the authorization", + * required=true, + * type="string", + * pattern="^\w{36}$", + * ) + * @SWG\Parameter( + * name="userId", + * in="path", + * description="Customer ID for retrieve data", + * required=true, + * type="integer", + * format="int64", + * minimum=0, + * exclusiveMinimum=true, + * ) + * @SWG\Response( + * response=200, + * description="Customer data", + * @Model(type=CustomerFull::class), + * ) + */ + public function getOne(): Response + { + return new Response(); + } + + /** + * Update customer. + * + * @Route(name="customers_update", path="/customers/{userId}", methods={"PUT"}, requirements={"userId": "\d+"}) + * + * @SWG\Parameter( + * name="x-auth-token", + * in="header", + * description="Alternative token for the authorization", + * required=true, + * type="string", + * pattern="^\w{36}$", + * ) + * @SWG\Parameter( + * name="userId", + * in="path", + * description="Customer ID to update", + * required=true, + * type="integer", + * format="int64", + * minimum=0, + * exclusiveMinimum=true, + * ) + * @SWG\Parameter( + * name="roles", + * in="query", + * description="Deprecated - Old way to set user roles", + * required=false, + * type="array", + * collectionFormat="csv", + * uniqueItems=true, + * minItems=1, + * maxItems=3, + * @SWG\Items(type="string", enum={"guest", "user", "admin"}), + * ) + * @SWG\Parameter( + * name="customer", + * in="body", + * description="Customer update", + * required=true, + * @Model(type=CustomerNew::class), + * ) + * @SWG\Response(response=204, description="Empty response when updated successfully") + */ + public function update(): Response + { + return new Response(Response::HTTP_NO_CONTENT); + } + + /** + * Partial customer update in formData style. + * + * @deprecated do not use this endpoint + * + * @Route(name="customers_patch", path="/customers/{userId}", methods={"PATCH"}, requirements={"userId": "\d+"}) + * + * @SWG\Parameter( + * name="x-auth-token", + * in="header", + * description="Alternative token for the authorization", + * required=true, + * type="string", + * pattern="^\w{36}$", + * ) + * @SWG\Parameter( + * name="userId", + * in="path", + * description="Customer ID to update", + * required=true, + * type="integer", + * format="int64", + * minimum=0, + * exclusiveMinimum=true, + * ) + * @SWG\Parameter( + * name="roles", + * in="query", + * description="Deprecated - Old way to set user roles", + * required=false, + * type="array", + * collectionFormat="csv", + * uniqueItems=true, + * minItems=1, + * maxItems=3, + * @SWG\Items(type="string", enum={"guest", "user", "admin"}), + * ) + * @SWG\Parameter( + * name="name", + * in="formData", + * description="Name of the Customer", + * required=true, + * type="string", + * minLength=2, + * maxLength=50, + * ) + * @SWG\Parameter( + * name="discount", + * in="formData", + * description="Size of the Customer's discount in percent", + * required=false, + * type="integer", + * format="int32", + * default=0, + * multipleOf=10, + * minimum=0, + * exclusiveMinimum=false, + * maximum=100, + * exclusiveMaximum=true, + * ) + * @SWG\Response(response=204, description="Empty response when updated successfully") + */ + public function updatePartial(): Response + { + return new Response(Response::HTTP_NO_CONTENT); + } + + /** + * Delete customer from the system. + * + * @Route(name="customers_delete", path="/customers/{userId}", methods={"DELETE"}, requirements={"userId": "\d+"}) + * + * @SWG\Parameter( + * name="x-auth-token", + * in="header", + * description="Alternative token for the authorization", + * required=true, + * type="string", + * pattern="^\w{36}$", + * ) + * @SWG\Parameter( + * name="userId", + * in="path", + * description="Customer ID to delete", + * required=true, + * type="integer", + * format="int64", + * minimum=0, + * exclusiveMinimum=true, + * ) + * @SWG\Response(response=204, description="Empty response when removed successfully") + */ + public function delete(): Response + { + return new Response(Response::HTTP_NO_CONTENT); + } +} diff --git a/Tests/Functional/Bundle/TestBundle/Controller/CustomerPasswordController.php b/Tests/Functional/Bundle/TestBundle/Controller/CustomerPasswordController.php new file mode 100644 index 0000000..a767043 --- /dev/null +++ b/Tests/Functional/Bundle/TestBundle/Controller/CustomerPasswordController.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Linkin\Bundle\SwaggerResolverBundle\Tests\Functional\Bundle\TestBundle\Controller; + +use Swagger\Annotations as SWG; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; + +/** + * @author Viktor Linkin + * + * @SWG\Tag(name="password") + */ +class CustomerPasswordController +{ + /** + * Create new password when not even set. + * + * @deprecated do not use this endpoint + * + * @Route(name="customers_password_create", path="/customers/{userId}/password", methods={"POST"}) + * + * @SWG\Parameter( + * name="x-auth-token", + * in="header", + * description="Alternative token for the authorization", + * required=true, + * type="string", + * pattern="^\w{36}$", + * ) + * @SWG\Parameter( + * name="userId", + * in="path", + * description="Customer ID to update", + * required=true, + * type="integer", + * format="int64", + * minimum=0, + * exclusiveMinimum=true, + * ) + * @SWG\Parameter( + * name="password", + * in="body", + * description="New password", + * required=true, + * @SWG\Schema(type="string", maxLength=30), + * ) + * @SWG\Response(response=204, description="Empty response when created successfully") + */ + public function create(): Response + { + return new Response(Response::HTTP_NO_CONTENT); + } + + /** + * Reset password. + * + * @deprecated do not use this endpoint + * + * @Route(name="customers_password_reset", path="/customers/{userId}/password", methods={"PUT"}) + * + * @SWG\Parameter( + * name="x-auth-token", + * in="header", + * description="Alternative token for the authorization", + * required=true, + * type="string", + * pattern="^\w{36}$", + * ) + * @SWG\Parameter( + * name="userId", + * in="path", + * description="Customer ID to update", + * required=true, + * type="integer", + * format="int64", + * minimum=0, + * exclusiveMinimum=true, + * ) + * @SWG\Parameter( + * name="password-reset", + * in="body", + * description="Body to change password", + * required=true, + * @SWG\Schema( + * type="object", + * required={"oldPassword", "newPassword"}, + * @SWG\Property( + * property="oldPassword", + * type="string", + * maxLength=30, + * ), + * @SWG\Property( + * property="newPassword", + * type="string", + * maxLength=30, + * ), + * ), + * ) + * @SWG\Response(response=204, description="Empty response when reset successfully") + */ + public function reset(): Response + { + return new Response(Response::HTTP_NO_CONTENT); + } +} diff --git a/Tests/Functional/Bundle/TestBundle/TestBundle.php b/Tests/Functional/Bundle/TestBundle/TestBundle.php new file mode 100644 index 0000000..f818009 --- /dev/null +++ b/Tests/Functional/Bundle/TestBundle/TestBundle.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Linkin\Bundle\SwaggerResolverBundle\Tests\Functional\Bundle\TestBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * @author Viktor Linkin + */ +class TestBundle extends Bundle +{ +} diff --git a/Tests/Functional/NelmioApiDocTest.php b/Tests/Functional/NelmioApiDocTest.php new file mode 100644 index 0000000..87632bc --- /dev/null +++ b/Tests/Functional/NelmioApiDocTest.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Linkin\Bundle\SwaggerResolverBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +/** + * @author Viktor Linkin + */ +class NelmioApiDocTest extends WebTestCase +{ + public function testCorrectlyLoaded(): void + { + $data = [ + 'id' => 1, + 'name' => 'Homer', + 'roles' => ['guest'], + 'email' => 'homer@crud.com', + 'isEmailConfirmed' => true, + 'registeredAt' => '2000-10-11T19:57:31Z', + ]; + + $client = self::createClient(); + $client->request('GET', '/api/customers', [], [], [], json_encode($data)); + + $response = $client->getResponse(); + self::assertEquals(200, $response->getStatusCode()); + } +} diff --git a/Tests/Functional/app/TestAppKernel.php b/Tests/Functional/app/TestAppKernel.php new file mode 100644 index 0000000..35b94f0 --- /dev/null +++ b/Tests/Functional/app/TestAppKernel.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Linkin\Bundle\SwaggerResolverBundle\Tests\Functional\app; + +use Linkin\Bundle\SwaggerResolverBundle\LinkinSwaggerResolverBundle; +use Linkin\Bundle\SwaggerResolverBundle\Tests\Functional\Bundle\TestBundle\TestBundle; +use Nelmio\ApiDocBundle\NelmioApiDocBundle; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\HttpKernel\Kernel; + +/** + * @author Viktor Linkin + */ +class TestAppKernel extends Kernel +{ + public function registerBundles(): array + { + return [ + new FrameworkBundle(), + new NelmioApiDocBundle(), + new LinkinSwaggerResolverBundle(), + new TestBundle(), + ]; + } + + public function getRootDir(): string + { + return $this->getProjectDir(); + } + + public function getCacheDir(): string + { + return sys_get_temp_dir().'/cache/'.$this->environment; + } + + public function getLogDir(): string + { + return sys_get_temp_dir().'/logs'; + } + + public function getProjectDir(): string + { + return parent::getProjectDir().'/Tests/'; + } + + public function registerContainerConfiguration(LoaderInterface $loader): void + { + $loader->load(__DIR__.'/config/config.yaml'); + } +} diff --git a/Tests/Functional/app/config/config.yaml b/Tests/Functional/app/config/config.yaml new file mode 100644 index 0000000..8c45fac --- /dev/null +++ b/Tests/Functional/app/config/config.yaml @@ -0,0 +1,43 @@ +framework: + secret: test + test: ~ + router: + resource: "%kernel.project_dir%/Functional/app/config/routing.yaml" + +nelmio_api_doc: + documentation: + swagger: 2.0 + host: localhost + schemes: + - https + info: + version: 1.0.0 + title: Customer API + description: Example API for work with customer + consumes: + - application/json + produces: + - application/json + + # TODO: project should work without areas definition + areas: + default: + path_patterns: + - '^/api/' + +linkin_swagger_resolver: + path_merge_strategy: Linkin\Bundle\SwaggerResolverBundle\Merger\Strategy\ReplaceLastWinMergeStrategy + +services: + logger: + class: Psr\Log\NullLogger + + _defaults: + autowire: true + autoconfigure: true + public: false + + Linkin\Bundle\SwaggerResolverBundle\Tests\Functional\Bundle\TestBundle\Controller\: + resource: '../../Bundle/TestBundle/Controller' + public: true + tags: ['controller.service_arguments'] diff --git a/Tests/Functional/app/config/routing.yaml b/Tests/Functional/app/config/routing.yaml new file mode 100644 index 0000000..10238b6 --- /dev/null +++ b/Tests/Functional/app/config/routing.yaml @@ -0,0 +1,4 @@ +app: + resource: '@TestBundle/Controller/' + prefix: api + type: annotation diff --git a/Tests/Loader/NelmioApiDocConfigurationLoaderTest.php b/Tests/Loader/NelmioApiDocConfigurationLoaderTest.php index 142cf41..5d7f3f4 100644 --- a/Tests/Loader/NelmioApiDocConfigurationLoaderTest.php +++ b/Tests/Loader/NelmioApiDocConfigurationLoaderTest.php @@ -13,9 +13,11 @@ namespace Linkin\Bundle\SwaggerResolverBundle\Tests\Loader; -use DG\BypassFinals; +use Closure; use EXSyst\Component\Swagger\Path; use EXSyst\Component\Swagger\Schema; +use EXSyst\Component\Swagger\Swagger; +use Linkin\Bundle\SwaggerResolverBundle\Exception\OperationNotFoundException; use Linkin\Bundle\SwaggerResolverBundle\Loader\NelmioApiDocConfigurationLoader; use Linkin\Bundle\SwaggerResolverBundle\Merger\OperationParameterMerger; use Linkin\Bundle\SwaggerResolverBundle\Merger\Strategy\ReplaceLastWinMergeStrategy; @@ -38,14 +40,26 @@ class NelmioApiDocConfigurationLoaderTest extends TestCase protected function setUp(): void { - BypassFinals::enable(); + $testsDir = __DIR__.'/..'; + $parameterMerger = new OperationParameterMerger(new ReplaceLastWinMergeStrategy()); + $router = new Router(new YamlFileLoader(new FileLocator($testsDir.'/Fixtures')), 'routing.yaml'); + $apiDocGenerator = $this->createApiDocGenerator(); + + $this->sut = new NelmioApiDocConfigurationLoader($parameterMerger, $router, $apiDocGenerator, $testsDir); + } + public function testFailWhenRouteNotFound(): void + { + $this->expectException(OperationNotFoundException::class); + + $testsDir = __DIR__.'/..'; $parameterMerger = new OperationParameterMerger(new ReplaceLastWinMergeStrategy()); - $router = new Router(new YamlFileLoader(new FileLocator(__DIR__.'/../Fixtures')), 'routing.yaml'); - $apiDocGenerator = $this->createMock(ApiDocGenerator::class); - $apiDocGenerator->method('generate')->willReturn(FixturesProvider::loadFromJson()); + $router = new Router(new YamlFileLoader(new FileLocator($testsDir.'/Fixtures')), 'routing.yaml'); + $router->getRouteCollection()->remove('customers_get'); + $apiDocGenerator = $this->createApiDocGenerator(); - $this->sut = new NelmioApiDocConfigurationLoader($parameterMerger, $router, $apiDocGenerator); + $sut = new NelmioApiDocConfigurationLoader($parameterMerger, $router, $apiDocGenerator, $testsDir); + $sut->getSchemaDefinitionCollection(); } public function testCanLoadDefinitionCollection(): void @@ -64,12 +78,11 @@ public function testCanLoadDefinitionCollection(): void $loadedDefinitionSchema = $definitionCollection->getSchema($name); self::assertSame($expectedSchema->toArray(), $loadedDefinitionSchema->toArray()); - // TODO: fix problem with loading resources in tests -// $loadedResources = $definitionCollection->getSchemaResources($name); -// self::assertCount(1, $loadedResources); -// -// $loadedResource = $loadedResources[0]; -// self::assertSame(FixturesProvider::getResourceByDefinition($name), $loadedResource->getResource()); + $loadedResources = $definitionCollection->getSchemaResources($name); + self::assertCount(1, $loadedResources); + + $loadedResource = $loadedResources[0]; + self::assertSame(FixturesProvider::getResourceByDefinition($name), $loadedResource->getResource()); } } @@ -102,4 +115,16 @@ public function testCanLoadOperationCollection(): void self::assertCount($expectedOperationsCount, $operationCollection->getIterator()); } + + private function createApiDocGenerator(): ApiDocGenerator + { + $apiDocGenerator = new ApiDocGenerator([], []); + $setSwagger = Closure::bind(function (Swagger $swagger) { + $this->swagger = $swagger; + }, $apiDocGenerator, ApiDocGenerator::class); + + $setSwagger(FixturesProvider::loadFromJson()); + + return $apiDocGenerator; + } } diff --git a/composer.json b/composer.json index 9274038..aa67dae 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "exsyst/swagger": "^0.4", "symfony/config": "^3.4||^4.0||^5.0", "symfony/dependency-injection": "^3.4||^4.0||^5.0", + "symfony/finder": "^3.4||^4.0||^5.0", "symfony/http-foundation": "^3.4||^4.0||^5.0", "symfony/http-kernel": "^3.4||^4.0||^5.0", "symfony/options-resolver": "^3.4||^4.0||^5.0", @@ -37,17 +38,22 @@ }, "require-dev": { - "dg/bypass-finals": "^1.3", "doctrine/annotations": "^1.2", "friendsofphp/php-cs-fixer": "^2.19", - "nelmio/api-doc-bundle": "^3.0", + "nelmio/api-doc-bundle": "^3.4", + "symfony/browser-kit": "^3.4||^4.0||^5.0", + "symfony/framework-bundle": "^3.4||^4.0||^5.0", "symfony/phpunit-bridge": "^3.4.38||^4.0||^5.0", - "zircote/swagger-php": "^2.0.9" + "zircote/swagger-php": "^2.0.15" + }, + + "conflict": { + "nelmio/api-doc-bundle": "<3.4", + "doctrine/annotations": "<1.7" }, "suggest": { "nelmio/api-doc-bundle": "Generates documentation for your REST API from annotations", - "symfony/symfony": "Allows more advanced functionality with full Symfony package", "zircote/swagger-php": "Allows to generate Swagger configuration by php annotations" }, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7b1e8e3..0f7f62a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,12 +5,11 @@ backupGlobals="false" colors="true" bootstrap="vendor/autoload.php" - failOnRisky="true" - failOnWarning="true" > +