diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f38912 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +vendor +composer.lock diff --git a/LICENCE.md b/LICENCE.md new file mode 100644 index 0000000..2c4e6a9 --- /dev/null +++ b/LICENCE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 iAdvize + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index a8f27b4..91ac161 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,125 @@ # php-swaggerize-fastroute-library A library to automatically create FastRoute routes based on swagger JSON documentation +# Install +To install with composer: +``` +composer require iadvize/php-swaggerize-fastroute-library +``` + +# Generate route File (FastRoute compatible) + +``` +vendor/bin/swaggerize swagger:scan path/to/swagger/json controllers\namespace [--routeFile=route/file/path] +``` + +# Dispatch generated file or simply use cache + +You can then use FastRoute cached dispatcher to use generated file or directly use a cache dispatcher (file will be generated at first call). + +```PHP + 'route/file/path']) { + \Iadvize\SwaggerizeFastRoute\addRoutes( + 'path/to/swagger/json', + $r, + $lumenOperationParser, + ['routeFile' => 'path/to/generated/route/file', 'cacheEnabled' => false] + ); +}); + +// Fetch method and URI from somewhere +// ... see FastRoute Dispatcher +``` + +Alternatively to generate routes, you can simply cache first parse by setting `'cacheEnabled' => true` in addRoute function. + +# Apply this to Lumen application + +To use this swagger routes in a Lumen Application (which use FastRoute as route library), you need to extends `Laravel\Lumen\Application` and override `createDispatcher` method. + +```PHP + +dispatcher ?: \FastRoute\simpleDispatcher(function ($r) { + foreach ($this->routes as $route) { + $r->addRoute($route['method'], $route['uri'], $route['action']); + } + + $operationParser = new \Iadvize\SwaggerizeFastRoute\OperationParser\LumenControllerOperationParser('My\Application\Http\Controllers'); + + \Iadvize\SwaggerizeFastRoute\addRoutes(storage_path('docs/definition.json'), $r, $operationParser, ['routeFile' => 'route/file/path']); + }); + } +} +``` + +# How handler is formed + +Handlers are formed from route defined in swagger as [Lumen](http://lumen.laravel.com/docs/routing#named-routes) define it for controller class : `Controller@method` +### Controller class generation + +Controller class is determined from path route with first character uppercased and with Controller at the end of file name + +This swagger JSON : + +```JSON +{ +// ... + "paths": { + "/pets": { + "get": { + // ... + } + "put": { + // ... + } + } + "/store": { + "post": { + // ... + } + } + } +// ... +} +``` + +will generates respectively this handlers: + +* `PetsController@get` +* `PetsController@update` +* `StoreController@create` + +### Method generation + +Controller method is mapped from HTTP method : +* `GET` => `get`, +* `POST` => `create`, +* `PUT` => `update`, +* `HEAD` => `head`, +* `OPTIONS` => `options`, +* `PATCH` => `patch`, +* `DELETE` => `delete`, + diff --git a/bin/swaggerize b/bin/swaggerize new file mode 100644 index 0000000..0189794 --- /dev/null +++ b/bin/swaggerize @@ -0,0 +1,7 @@ +#!/usr/bin/env php +add(new \Iadvize\SwaggerizeFastRoute\Command\Scan()); +$app->run(); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..86ed8d8 --- /dev/null +++ b/composer.json @@ -0,0 +1,46 @@ +{ + "name": "iadvize/php-swaggerize-fastroute-library", + "description": "A library to automatically create FastRoute routes based on swagger JSON documentation", + "authors": [ + { + "name": "Marc FRICOU", + "email": "marc.fricou@iadvize.com" + } + ], + + "repositories": [ + { + "type": "git", + "url": "git@github.com:iadvize/lumen-doctrine.git" + }, + { + "type": "git", + "url": "git@github.com:iadvize/api-swagger-ui.git" + }, + { + "type": "git", + "url": "git@github.com:iadvize/standards.git" + } + ], + "require": { + "php": ">=5.5.21", + "nikic/fast-route": "0.*", + "thefrozenfire/swagger": "^2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8", + "iadvize/standards": "dev-moveComposer", + "mockery/mockery": "~0.9", + "symfony/console": "^2.7" + }, + "autoload": { + "psr-4": { + "Iadvize\\SwaggerizeFastRoute\\": "src/", + "IadvizeTest\\SwaggerizeFastRoute\\": "tests/" + }, + "files": ["src/functions.php"] + }, + "bin": [ + "bin/swaggerize" + ] +} diff --git a/config/swaggerConfig.dist.php b/config/swaggerConfig.dist.php new file mode 100644 index 0000000..b1f34f8 --- /dev/null +++ b/config/swaggerConfig.dist.php @@ -0,0 +1,7 @@ + 'path/to/route/file', + 'cacheEnabled' => true, + 'namespace' => 'Iadvize\ServiceNamespace', +]; \ No newline at end of file diff --git a/src/Command/Scan.php b/src/Command/Scan.php new file mode 100644 index 0000000..6b7c746 --- /dev/null +++ b/src/Command/Scan.php @@ -0,0 +1,68 @@ +setName('swagger:scan') + ->setDescription('Scan swagger JSON file ') + ->addArgument( + 'swaggerFile', + InputArgument::REQUIRED, + 'Give JSON file to scan' + ) + ->addArgument( + 'controllerNamespace', + InputArgument::REQUIRED, + 'Controllers namespace that will handle route' + ) + ->addOption( + 'routeFile', + null, + InputOption::VALUE_REQUIRED, + 'Where FastRoute cache file should be write ? Default to output' + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $controllerNamespace = $input->getArgument('controllerNamespace'); + $swaggerFile = $input->getArgument('swaggerFile'); + $routeStream = $input->getOption('routeFile'); + + if (!$routeStream) { + $routeStream = 'php://output'; + } + + $operationParser = new LumenControllerOperationParser($controllerNamespace); + + $routes = \Iadvize\SwaggerizeFastRoute\scan($swaggerFile, $operationParser); + + \Iadvize\SwaggerizeFastRoute\cacheRoutes($routes, $routeStream); + + if ($routeStream !== 'php://output') { + $output->writeln('route file available at ' . $routeStream); + } + } +} diff --git a/src/OperationParser/LumenControllerOperationParser.php b/src/OperationParser/LumenControllerOperationParser.php new file mode 100644 index 0000000..c76e213 --- /dev/null +++ b/src/OperationParser/LumenControllerOperationParser.php @@ -0,0 +1,69 @@ + 'get', + 'POST' => 'create', + 'PUT' => 'update', + 'HEAD' => 'head', + 'OPTIONS' => 'options', + 'PATCH' => 'patch', + 'DELETE' => 'delete', + ]; + + /** + * Constructor + * + * @param string $controllerNamespace + */ + public function __construct($controllerNamespace) + { + if (substr('$controllerNamespace', -1) !== '\\') { + $controllerNamespace .= '\\'; + } + + $this->namespace = $controllerNamespace; + } + + /** + * Get Handler + * + * @param OperationReference $operation + * + * @return array + */ + public function getHandler(OperationReference $operation) + { + // remove route parameters + $path = preg_replace('/\/\{.*\}/', '', $operation->getPath()); + + // lowerCamelCase to UpperCamelCase + $paths = explode('/', $path); + // path start with a / + unset($paths[0]); + $paths = array_map(function ($path) { + return ucfirst($path); + }, $paths); + // path to 'relative' namespace + $path = implode('\\', $paths); + + $controller = $this->namespace . $path . 'Controller'; + + return ['uses' => $controller . '@' . $this->httpVerbToControllerMethod[strtoupper($operation->getMethod())], 'as' => $operation->getOperationId()]; + } +} diff --git a/src/OperationParser/OperationParserInterface.php b/src/OperationParser/OperationParserInterface.php new file mode 100644 index 0000000..a7a0949 --- /dev/null +++ b/src/OperationParser/OperationParserInterface.php @@ -0,0 +1,22 @@ +getOperationsById(); + + $routes = []; + + foreach ($operations as $operation) { + $routes[] = [ + 'method' => $operation->getMethod(), + 'uri' => $operation->getPath(), + 'action' => $operationParser->getHandler($operation), + ]; + } + + return $routes; +} + +/** + * Add route to route collector + * + * @param string $swaggerJson Swagger json file path (can be an URL) + * @param RouteCollector $routeCollector FastRoute route collector + * @param OperationParserInterface $operationParser Swagger operation parser. + * @param array $options Options (@see config/swaggerConfig.dist.php) + */ +function addRoutes($swaggerJson, RouteCollector $routeCollector, OperationParserInterface $operationParser, $options = []) +{ + if (!isset($options['routeFile']) || !file_exists($options['routeFile'])) { + $routes = scan($swaggerJson, $operationParser); + } else { + $routes = require $options['routeFile']; + } + + if (isset($options['routeFile']) && isset($options['cacheEnabled']) && $options['cacheEnabled']) { + cacheRoutes($routes, $options['routeFile']); + } + + foreach ($routes as $route) { + $routeCollector->addRoute($route['method'], $route['uri'], $route['action']); + } +} + +/** + * Write routes array into a file + * + * @param array $routes Routes + * @param string $stream File name or stream in which write routes. + */ +function cacheRoutes(array $routes, $stream) +{ + if (is_writable($stream)) { + throw new \LogicException($stream . ' is not writable'); + } + + $routeResource = fopen($stream, 'w'); + + $serializedRoutes = var_export($routes, true); + + fwrite($routeResource, 'operationParser = new LumenControllerOperationParser('Iadvize\Test'); + } + + /** + * Test: Get handler should return an Lumen compatible Array with controller@method + */ + public function testGetHandler() + { + $operationMock = \Mockery::mock(OperationReference::class); + $operationMock->shouldReceive('getPath')->andReturn('/marco'); + $operationMock->shouldReceive('getMethod')->andReturn('GET'); + $operationMock->shouldReceive('getOperationId')->andReturn('operationID'); + + $this->assertEquals(['uses' => 'Iadvize\Test\MarcoController@get', 'as' => 'operationID'], $this->operationParser->getHandler($operationMock)); + } + + /** + * Test: Get handler should return an Lumen compatible Array with controller@method + */ + public function testGetHandlerWithRouteParameter() + { + $operationMock = \Mockery::mock(OperationReference::class); + $operationMock->shouldReceive('getPath')->andReturn('/marco/{id}'); + $operationMock->shouldReceive('getMethod')->andReturn('PUT'); + $operationMock->shouldReceive('getOperationId')->andReturn('operationID'); + + $this->assertEquals(['uses' => 'Iadvize\Test\MarcoController@update', 'as' => 'operationID'], $this->operationParser->getHandler($operationMock)); + } + + /** + * Test: Get handler should return an Lumen compatible Array with controller@method + */ + public function testGetHandlerWithDeepPath() + { + $operationMock = \Mockery::mock(OperationReference::class); + $operationMock->shouldReceive('getPath')->andReturn('/marco/{id}/name/first'); + $operationMock->shouldReceive('getMethod')->andReturn('POST'); + $operationMock->shouldReceive('getOperationId')->andReturn('operationID'); + + $this->assertEquals(['uses' => 'Iadvize\Test\Marco\Name\FirstController@create', 'as' => 'operationID'], $this->operationParser->getHandler($operationMock)); + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml new file mode 100644 index 0000000..c8c336f --- /dev/null +++ b/tests/phpunit.xml @@ -0,0 +1,15 @@ + + + + + ./ + + +