diff --git a/.travis.yml b/.travis.yml index 36f2c9ed..d5889592 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: php php: - - 5.6 - - 7.0 - - 7.1 + - 7.1.3 + - 7.2 before_script: - travis_retry composer self-update diff --git a/composer.json b/composer.json index a7fa4a08..bb0fa47d 100644 --- a/composer.json +++ b/composer.json @@ -15,22 +15,24 @@ } ], "require": { - "php": ">=5.5.0", - "fzaninotto/faker": "~1.0", - "laravel/framework": "~5.4", + "php": ">=7.0.0", + "fzaninotto/faker": "~1.8", + "illuminate/routing": "5.5.* || 5.6.* || 5.7.*", + "illuminate/support": "5.5.* 5.6.* || 5.7.*", + "illuminate/console": "5.5.* 5.6.* || 5.7.*", "mpociot/documentarian": "^0.2.0", "mpociot/reflection-docblock": "^1.0", - "ramsey/uuid": "^3.0" + "ramsey/uuid": "^3.8" }, "require-dev": { - "orchestra/testbench": "~3.0", - "phpunit/phpunit": "~4.0 || ~5.0", - "dingo/api": "1.0.*@dev", - "mockery/mockery": "^0.9.5" + "orchestra/testbench": "3.5.* || 3.6.* || 3.7.*", + "phpunit/phpunit": "^6.0.0 || ^7.4.0", + "dingo/api": "2.0.0-alpha1", + "mockery/mockery": "^1.2.0" }, "autoload": { - "psr-0": { - "Mpociot\\ApiDoc": "src/" + "psr-4": { + "Mpociot\\ApiDoc\\": "src/" } }, "autoload-dev": { diff --git a/config/apidoc.php b/config/apidoc.php new file mode 100644 index 00000000..f2ff1585 --- /dev/null +++ b/config/apidoc.php @@ -0,0 +1,95 @@ + 'public/docs', + + + /* + * The router to be used (Laravel or Dingo). + */ + 'router' => 'laravel', + + /* + * Generate a Postman collection in addition to HTML docs. + */ + 'postman' => true, + + + /* + * The routes for which documentation should be generated. + * Each group contains rules defining which routes should be included ('match', 'include' and 'exclude' sections) + * and rules which should be applied to them ('apply' section). + */ + 'routes' => [ + [ + /* + * Specify conditions to determine what routes will be parsed in this group. + * A route must fulfill ALL conditions to pass. + */ + 'match' => [ + + /* + * Match only routes whose domains match this pattern (use * as a wildcard to match any characters). + */ + 'domains' => [ + '*', + // 'domain1.*', + ], + + /* + * Match only routes whose paths match this pattern (use * as a wildcard to match any characters). + */ + 'prefixes' => [ + '*', + // 'users/*', + ], + + /* + * Match only routes registered under this version. This option is ignored for Laravel router. + * Note that wildcards are not supported. + */ + 'versions' => [ + 'v1', + ], + ], + + /* + * Include these routes when generating documentation, + * even if they did not match the rules above. + * Note that the route must be referenced by name here. + */ + 'include' => [ + // 'users.index', + ], + + /* + * Exclude these routes when generating documentation, + * even if they matched the rules above. + * Note that the route must be referenced by name here. + */ + 'exclude' => [ + // 'users.create', + ], + + /* + * Specify rules to be applied to all the routes in this group when generating documentation + */ + 'apply' => [ + 'requests' => [ + + /* + * Specify headers to be added to the example requests + */ + 'headers' => [ + // 'Authorization' => 'Bearer: {token}', + // 'Api-Version' => 'v2', + ], + ], + ], + ], + ], +]; diff --git a/phpunit.xml b/phpunit.xml index 1458c056..8875a97f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,8 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="true" - syntaxCheck="false"> + stopOnFailure="true"> tests/ @@ -16,10 +15,10 @@ - src/Mpociot/ + src/ - src/Mpociot/ApiDoc/ApiDocGeneratorServiceProvider.php - src/resources/views/documentarian.blade.php + src/ApiDocGeneratorServiceProvider.php + resources/views/documentarian.blade.php diff --git a/src/resources/views/documentarian.blade.php b/resources/views/documentarian.blade.php similarity index 100% rename from src/resources/views/documentarian.blade.php rename to resources/views/documentarian.blade.php diff --git a/src/resources/views/partials/frontmatter.blade.php b/resources/views/partials/frontmatter.blade.php similarity index 100% rename from src/resources/views/partials/frontmatter.blade.php rename to resources/views/partials/frontmatter.blade.php diff --git a/src/resources/views/partials/info.blade.php b/resources/views/partials/info.blade.php similarity index 100% rename from src/resources/views/partials/info.blade.php rename to resources/views/partials/info.blade.php diff --git a/src/resources/views/partials/route.blade.php b/resources/views/partials/route.blade.php similarity index 100% rename from src/resources/views/partials/route.blade.php rename to resources/views/partials/route.blade.php diff --git a/src/ApiDocGeneratorServiceProvider.php b/src/ApiDocGeneratorServiceProvider.php new file mode 100644 index 00000000..ab62dcac --- /dev/null +++ b/src/ApiDocGeneratorServiceProvider.php @@ -0,0 +1,45 @@ +loadViewsFrom(__DIR__ . '/../resources/views/', 'apidoc'); + + $this->publishes([ + __DIR__ . '/../resources/views' => app()->basePath().'/resources/views/vendor/apidoc', + ], 'views'); + + $this->publishes([ + __DIR__.'/../config/apidoc.php' => config_path('apidoc.php'), + ], 'config'); + + if ($this->app->runningInConsole()) { + $this->commands([ + GenerateDocumentation::class, + UpdateDocumentation::class, + ]); + } + } + + /** + * Register the API doc commands. + * + * @return void + */ + public function register() + { + // + } +} diff --git a/src/Mpociot/ApiDoc/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php similarity index 63% rename from src/Mpociot/ApiDoc/Commands/GenerateDocumentation.php rename to src/Commands/GenerateDocumentation.php index 391b32c1..7b32afb7 100644 --- a/src/Mpociot/ApiDoc/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -2,6 +2,7 @@ namespace Mpociot\ApiDoc\Commands; +use Mpociot\ApiDoc\Tools\RouteMatcher; use ReflectionClass; use Illuminate\Routing\Route; use Illuminate\Console\Command; @@ -12,7 +13,6 @@ use Mpociot\ApiDoc\Generators\DingoGenerator; use Mpociot\ApiDoc\Generators\LaravelGenerator; use Mpociot\ApiDoc\Generators\AbstractGenerator; -use Illuminate\Support\Facades\Route as RouteFacade; class GenerateDocumentation extends Command { @@ -22,21 +22,7 @@ class GenerateDocumentation extends Command * @var string */ protected $signature = 'apidoc:generate - {--output=public/docs : The output path for the generated documentation} - {--routeDomain= : The route domain (or domains) to use for generation} - {--routePrefix= : The route prefix (or prefixes) to use for generation} - {--routes=* : The route names to use for generation} - {--middleware= : The middleware to use for generation} - {--noResponseCalls : Disable API response calls} - {--noPostmanCollection : Disable Postman collection creation} - {--useMiddlewares : Use all configured route middlewares} - {--authProvider=users : The authentication provider to use for API response calls} - {--authGuard=web : The authentication guard to use for API response calls} - {--actAsUserId= : The user ID to use for API response calls} - {--router=laravel : The router to be used (Laravel or Dingo)} {--force : Force rewriting of existing routes} - {--bindings= : Route Model Bindings} - {--header=* : Custom HTTP headers to add to the example requests. Separate the header name and value with ":"} '; /** @@ -46,14 +32,13 @@ class GenerateDocumentation extends Command */ protected $description = 'Generate your API documentation from existing Laravel routes.'; - /** - * Create a new command instance. - * - * @return void - */ - public function __construct() + + private $routeMatcher; + + public function __construct(RouteMatcher $routeMatcher) { parent::__construct(); + $this->routeMatcher = $routeMatcher; } /** @@ -63,39 +48,21 @@ public function __construct() */ public function handle() { + $routes = config('apidoc.router') == 'dingo' + ? $this->routeMatcher->getDingoRoutesToBeDocumented(config('apidoc.routes')) + : $this->routeMatcher->getLaravelRoutesToBeDocumented(config('apidoc.routes')); + if ($this->option('router') === 'laravel') { $generator = new LaravelGenerator(); } else { $generator = new DingoGenerator(); } - $allowedRoutes = $this->option('routes'); - $routeDomain = $this->option('routeDomain'); - $routePrefix = $this->option('routePrefix'); - $middleware = $this->option('middleware'); - - $this->setUserToBeImpersonated($this->option('actAsUserId')); - - if ($routePrefix === null && $routeDomain === null && ! count($allowedRoutes) && $middleware === null) { - $this->error('You must provide either a route prefix, a route domain, a route or a middleware to generate the documentation.'); - - return false; - } - - $generator->prepareMiddleware($this->option('useMiddlewares')); - - $routePrefixes = explode(',', $routePrefix ?: '*'); - $routeDomains = explode(',', $routeDomain ?: '*'); - - $parsedRoutes = []; - foreach ($routeDomains as $routeDomain) { - foreach ($routePrefixes as $routePrefix) { - $parsedRoutes += $this->processRoutes($generator, $allowedRoutes, $routeDomain, $routePrefix, $middleware); - } - } - $parsedRoutes = collect($parsedRoutes)->groupBy('resource')->sort(function ($a, $b) { - return strcmp($a->first()['resource'], $b->first()['resource']); + $parsedRoutes = $this->processRoutes($generator, $routes); + $parsedRoutes = collect($parsedRoutes)->groupBy('resource') + ->sort(function ($a, $b) { + return strcmp($a->first()['resource'], $b->first()['resource']); }); $this->writeMarkdown($parsedRoutes); @@ -210,85 +177,24 @@ private function writeMarkdown($parsedRoutes) } } - /** - * @return array - */ - private function getBindings() - { - $bindings = $this->option('bindings'); - if (empty($bindings)) { - return []; - } - - $bindings = explode('|', $bindings); - $resultBindings = []; - foreach ($bindings as $binding) { - list($name, $id) = explode(',', $binding); - $resultBindings[$name] = $id; - } - - return $resultBindings; - } - - /** - * @param $actAs - */ - private function setUserToBeImpersonated($actAs) - { - if (! empty($actAs)) { - if (version_compare($this->laravel->version(), '5.2.0', '<')) { - $userModel = config('auth.model'); - $user = $userModel::find($actAs); - $this->laravel['auth']->setUser($user); - } else { - $provider = $this->option('authProvider'); - $userModel = config("auth.providers.$provider.model"); - $user = $userModel::find($actAs); - $this->laravel['auth']->guard($this->option('authGuard'))->setUser($user); - } - } - } - - /** - * @return mixed - */ - private function getRoutes() - { - if ($this->option('router') === 'laravel') { - return RouteFacade::getRoutes(); - } else { - return app('Dingo\Api\Routing\Router')->getRoutes(); - } - } /** - * @param AbstractGenerator $generator - * @param $allowedRoutes - * @param $routeDomain - * @param $routePrefix - * + * @param AbstractGenerator $generator + * @param array $routes * @return array + * */ - private function processRoutes(AbstractGenerator $generator, array $allowedRoutes, $routeDomain, $routePrefix, $middleware) + private function processRoutes(AbstractGenerator $generator, array $routes) { - $withResponse = $this->option('noResponseCalls') == false; - $routes = $this->getRoutes(); - $bindings = $this->getBindings(); $parsedRoutes = []; - foreach ($routes as $route) { + foreach ($routes as ['route' => $route, 'apply' => $apply]) { /** @var Route $route */ - if (in_array($route->getName(), $allowedRoutes) - || (str_is($routeDomain, $generator->getDomain($route)) - && str_is($routePrefix, $generator->getUri($route))) - || in_array($middleware, $route->middleware()) - ) { if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route->getAction()['uses'])) { - $parsedRoutes[] = $generator->processRoute($route, $bindings, $this->option('header'), $withResponse && in_array('GET', $generator->getMethods($route))); + $parsedRoutes[] = $generator->processRoute($route, $apply); $this->info('Processed route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route)); } else { $this->warn('Skipping route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route)); } - } } return $parsedRoutes; diff --git a/src/Mpociot/ApiDoc/Commands/UpdateDocumentation.php b/src/Commands/UpdateDocumentation.php similarity index 100% rename from src/Mpociot/ApiDoc/Commands/UpdateDocumentation.php rename to src/Commands/UpdateDocumentation.php diff --git a/src/Generators/AbstractGenerator.php b/src/Generators/AbstractGenerator.php new file mode 100644 index 00000000..7b89b5c5 --- /dev/null +++ b/src/Generators/AbstractGenerator.php @@ -0,0 +1,371 @@ +domain() == null ? '*' : $route->domain(); + } + + /** + * @param Route $route + * + * @return mixed + */ + public function getUri(Route $route) + { + return $route->uri(); + } + + /** + * @param Route $route + * + * @return mixed + */ + public function getMethods(Route $route) + { + return array_diff($route->methods(), ['HEAD']); + } + + /** + * @param \Illuminate\Routing\Route $route + * @param array $apply Rules to apply when generating documentation for this route + * + * @return array + */ + public function processRoute($route, $apply = []) + { + $routeAction = $route->getAction(); + $routeGroup = $this->getRouteGroup($routeAction['uses']); + $routeDescription = $this->getRouteDescription($routeAction['uses']); + $showresponse = null; + + $response = null; + $docblockResponse = $this->getDocblockResponse($routeDescription['tags']); + if ($docblockResponse) { + // we have a response from the docblock ( @response ) + $response = $docblockResponse; + $showresponse = true; + } + if (! $response) { + $transformerResponse = $this->getTransformerResponse($routeDescription['tags']); + if ($transformerResponse) { + // we have a transformer response from the docblock ( @transformer || @transformercollection ) + $response = $transformerResponse; + $showresponse = true; + } + } + + $content = $this->getResponseContent($response); + + return [ + 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), + 'resource' => $routeGroup, + 'title' => $routeDescription['short'], + 'description' => $routeDescription['long'], + 'methods' => $this->getMethods($route), + 'uri' => $this->getUri($route), + 'parameters' => $this->getParametersFromDocBlock($routeAction['uses']), + 'response' => $content, + 'showresponse' => $showresponse, + ]; + } + + /** + * Prepares / Disables route middlewares. + * + * @param bool $disable + * + * @return void + */ + abstract public function prepareMiddleware($enable = false); + + /** + * Get the response from the docblock if available. + * + * @param array $tags + * + * @return mixed + */ + protected function getDocblockResponse($tags) + { + $responseTags = array_filter($tags, function ($tag) { + if (! ($tag instanceof Tag)) { + return false; + } + + return \strtolower($tag->getName()) == 'response'; + }); + if (empty($responseTags)) { + return; + } + $responseTag = \array_first($responseTags); + + return \response(json_encode($responseTag->getContent()), 200, ['Content-Type' => 'application/json']); + } + + /** + * @param array $routeAction + * @return array + */ + protected function getParametersFromDocBlock($routeAction) + { + return []; + } + + /** + * @param $route + * @param $bindings + * @param $headers + * + * @return \Illuminate\Http\Response + */ + protected function getRouteResponse($route, $bindings, $headers = []) + { + $uri = $this->addRouteModelBindings($route, $bindings); + + $methods = $this->getMethods($route); + + // Split headers into key - value pairs + $headers = collect($headers)->map(function ($value) { + $split = explode(':', $value); // explode to get key + values + $key = array_shift($split); // extract the key and keep the values in the array + $value = implode(':', $split); // implode values into string again + + return [trim($key) => trim($value)]; + })->collapse()->toArray(); + + //Changes url with parameters like /users/{user} to /users/1 + $uri = preg_replace('/{(.*?)}/', 1, $uri); // 1 is the default value for route parameters + + return $this->callRoute(array_shift($methods), $uri, [], [], [], $headers); + } + + /** + * @param $route + * @param array $bindings + * + * @return mixed + */ + protected function addRouteModelBindings($route, $bindings) + { + $uri = $this->getUri($route); + foreach ($bindings as $model => $id) { + $uri = str_replace('{'.$model.'}', $id, $uri); + $uri = str_replace('{'.$model.'?}', $id, $uri); + } + + return $uri; + } + + /** + * @param \Illuminate\Routing\Route $route + * + * @return array + */ + protected function getRouteDescription($route) + { + list($class, $method) = explode('@', $route); + $reflection = new ReflectionClass($class); + $reflectionMethod = $reflection->getMethod($method); + + $comment = $reflectionMethod->getDocComment(); + $phpdoc = new DocBlock($comment); + + return [ + 'short' => $phpdoc->getShortDescription(), + 'long' => $phpdoc->getLongDescription()->getContents(), + 'tags' => $phpdoc->getTags(), + ]; + } + + /** + * @param string $route + * + * @return string + */ + protected function getRouteGroup($route) + { + list($class, $method) = explode('@', $route); + $reflection = new ReflectionClass($class); + $comment = $reflection->getDocComment(); + if ($comment) { + $phpdoc = new DocBlock($comment); + foreach ($phpdoc->getTags() as $tag) { + if ($tag->getName() === 'resource') { + return $tag->getContent(); + } + } + } + + return 'general'; + } + + /** + * Call the given URI and return the Response. + * + * @param string $method + * @param string $uri + * @param array $parameters + * @param array $cookies + * @param array $files + * @param array $server + * @param string $content + * + * @return \Illuminate\Http\Response + */ + abstract public function callRoute($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null); + + /** + * Transform headers array to array of $_SERVER vars with HTTP_* format. + * + * @param array $headers + * + * @return array + */ + protected function transformHeadersToServerVars(array $headers) + { + $server = []; + $prefix = 'HTTP_'; + + foreach ($headers as $name => $value) { + $name = strtr(strtoupper($name), '-', '_'); + + if (! Str::startsWith($name, $prefix) && $name !== 'CONTENT_TYPE') { + $name = $prefix.$name; + } + + $server[$name] = $value; + } + + return $server; + } + + /** + * @param $response + * + * @return mixed + */ + private function getResponseContent($response) + { + if (empty($response)) { + return ''; + } + if ($response->headers->get('Content-Type') === 'application/json') { + $content = json_decode($response->getContent(), JSON_PRETTY_PRINT); + } else { + $content = $response->getContent(); + } + + return $content; + } + + /** + * Get a response from the transformer tags. + * + * @param array $tags + * + * @return mixed + */ + protected function getTransformerResponse($tags) + { + try { + $transFormerTags = array_filter($tags, function ($tag) { + if (! ($tag instanceof Tag)) { + return false; + } + + return \in_array(\strtolower($tag->getName()), ['transformer', 'transformercollection']); + }); + if (empty($transFormerTags)) { + // we didn't have any of the tags so goodbye + return false; + } + + $modelTag = array_first(array_filter($tags, function ($tag) { + if (! ($tag instanceof Tag)) { + return false; + } + + return \in_array(\strtolower($tag->getName()), ['transformermodel']); + })); + $tag = \array_first($transFormerTags); + $transformer = $tag->getContent(); + if (! \class_exists($transformer)) { + // if we can't find the transformer we can't generate a response + return; + } + $demoData = []; + + $reflection = new ReflectionClass($transformer); + $method = $reflection->getMethod('transform'); + $parameter = \array_first($method->getParameters()); + $type = null; + if ($modelTag) { + $type = $modelTag->getContent(); + } + if (version_compare(PHP_VERSION, '7.0.0') >= 0 && \is_null($type)) { + // we can only get the type with reflection for PHP 7 + if ($parameter->hasType() && + ! $parameter->getType()->isBuiltin() && + \class_exists((string) $parameter->getType())) { + //we have a type + $type = (string) $parameter->getType(); + } + } + if ($type) { + // we have a class so we try to create an instance + $demoData = new $type; + try { + // try a factory + $demoData = \factory($type)->make(); + } catch (\Exception $e) { + if ($demoData instanceof \Illuminate\Database\Eloquent\Model) { + // we can't use a factory but can try to get one from the database + try { + // check if we can find one + $newDemoData = $type::first(); + if ($newDemoData) { + $demoData = $newDemoData; + } + } catch (\Exception $e) { + // do nothing + } + } + } + } + + $fractal = new Manager(); + $resource = []; + if ($tag->getName() == 'transformer') { + // just one + $resource = new Item($demoData, new $transformer); + } + if ($tag->getName() == 'transformercollection') { + // a collection + $resource = new Collection([$demoData, $demoData], new $transformer); + } + + return \response($fractal->createData($resource)->toJson()); + } catch (\Exception $e) { + // it isn't possible to parse the transformer + return; + } + } +} diff --git a/src/Mpociot/ApiDoc/Generators/DingoGenerator.php b/src/Generators/DingoGenerator.php similarity index 100% rename from src/Mpociot/ApiDoc/Generators/DingoGenerator.php rename to src/Generators/DingoGenerator.php diff --git a/src/Mpociot/ApiDoc/Generators/LaravelGenerator.php b/src/Generators/LaravelGenerator.php similarity index 100% rename from src/Mpociot/ApiDoc/Generators/LaravelGenerator.php rename to src/Generators/LaravelGenerator.php diff --git a/src/Mpociot/ApiDoc/ApiDocGeneratorServiceProvider.php b/src/Mpociot/ApiDoc/ApiDocGeneratorServiceProvider.php deleted file mode 100644 index 125f5f14..00000000 --- a/src/Mpociot/ApiDoc/ApiDocGeneratorServiceProvider.php +++ /dev/null @@ -1,58 +0,0 @@ -loadViewsFrom(__DIR__.'/../../resources/views/', 'apidoc'); - $this->loadTranslationsFrom(__DIR__.'/../../resources/lang', 'apidoc'); - - $this->publishes([ - __DIR__.'/../../resources/lang' => $this->resource_path('lang/vendor/apidoc'), - __DIR__.'/../../resources/views' => $this->resource_path('views/vendor/apidoc'), - ]); - } - - /** - * Register the API doc commands. - * - * @return void - */ - public function register() - { - $this->app->singleton('apidoc.generate', function () { - return new GenerateDocumentation(); - }); - $this->app->singleton('apidoc.update', function () { - return new UpdateDocumentation(); - }); - - $this->commands([ - 'apidoc.generate', - 'apidoc.update', - ]); - } - - /** - * Return a fully qualified path to a given file. - * - * @param string $path - * - * @return string - */ - public function resource_path($path = '') - { - return app()->basePath().'/resources'.($path ? '/'.$path : $path); - } -} diff --git a/src/Mpociot/ApiDoc/Generators/AbstractGenerator.php b/src/Mpociot/ApiDoc/Generators/AbstractGenerator.php deleted file mode 100644 index 74594489..00000000 --- a/src/Mpociot/ApiDoc/Generators/AbstractGenerator.php +++ /dev/null @@ -1,790 +0,0 @@ -domain(); - } - - /** - * @param Route $route - * - * @return mixed - */ - public function getUri(Route $route) - { - return $route->uri(); - } - - /** - * @param Route $route - * - * @return mixed - */ - public function getMethods(Route $route) - { - return array_diff($route->methods(), ['HEAD']); - } - - /** - * @param \Illuminate\Routing\Route $route - * @param array $bindings - * @param bool $withResponse - * - * @return array - */ - public function processRoute($route, $bindings = [], $headers = [], $withResponse = true) - { - $routeDomain = $route->domain(); - $routeAction = $route->getAction(); - $routeGroup = $this->getRouteGroup($routeAction['uses']); - $routeDescription = $this->getRouteDescription($routeAction['uses']); - $showresponse = null; - - // set correct route domain - $headers[] = "HTTP_HOST: {$routeDomain}"; - $headers[] = "SERVER_NAME: {$routeDomain}"; - - $response = null; - $docblockResponse = $this->getDocblockResponse($routeDescription['tags']); - if ($docblockResponse) { - // we have a response from the docblock ( @response ) - $response = $docblockResponse; - $showresponse = true; - } - if (! $response) { - $transformerResponse = $this->getTransformerResponse($routeDescription['tags']); - if ($transformerResponse) { - // we have a transformer response from the docblock ( @transformer || @transformercollection ) - $response = $transformerResponse; - $showresponse = true; - } - } - if (! $response && $withResponse) { - try { - $response = $this->getRouteResponse($route, $bindings, $headers); - } catch (\Exception $e) { - echo "Couldn't get response for route: ".implode(',', $this->getMethods($route)).$route->uri().']: '.$e->getMessage()."\n"; - } - } - - $content = $this->getResponseContent($response); - - return $this->getParameters([ - 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), - 'resource' => $routeGroup, - 'title' => $routeDescription['short'], - 'description' => $routeDescription['long'], - 'methods' => $this->getMethods($route), - 'uri' => $this->getUri($route), - 'parameters' => [], - 'response' => $content, - 'showresponse' => $showresponse, - ], $routeAction, $bindings); - } - - /** - * Prepares / Disables route middlewares. - * - * @param bool $disable - * - * @return void - */ - abstract public function prepareMiddleware($enable = false); - - /** - * Get the response from the docblock if available. - * - * @param array $tags - * - * @return mixed - */ - protected function getDocblockResponse($tags) - { - $responseTags = array_filter($tags, function ($tag) { - if (! ($tag instanceof Tag)) { - return false; - } - - return \strtolower($tag->getName()) == 'response'; - }); - if (empty($responseTags)) { - return; - } - $responseTag = \array_first($responseTags); - - return \response(json_encode($responseTag->getContent()), 200, ['Content-Type' => 'application/json']); - } - - /** - * @param array $routeData - * @param array $routeAction - * @param array $bindings - * - * @return mixed - */ - protected function getParameters($routeData, $routeAction, $bindings) - { - $validationRules = $this->getRouteValidationRules($routeData['methods'], $routeAction['uses'], $bindings); - $rules = $this->simplifyRules($validationRules); - - foreach ($rules as $attribute => $ruleset) { - $attributeData = [ - 'required' => false, - 'type' => null, - 'default' => '', - 'value' => '', - 'description' => [], - ]; - foreach ($ruleset as $rule) { - $this->parseRule($rule, $attribute, $attributeData, $routeData['id'], $routeData); - } - $routeData['parameters'][$attribute] = $attributeData; - } - - return $routeData; - } - - /** - * Format the validation rules as a plain array. - * - * @param array $rules - * - * @return array - */ - protected function simplifyRules($rules) - { - // this will split all string rules into arrays of strings - $newRules = Validator::make([], $rules)->getRules(); - - // Laravel will ignore the nested array rules unless the key referenced exists and is an array - // So we'll create an empty array for each array attribute - $values = collect($newRules) - ->filter(function ($values) { - return in_array('array', $values); - })->map(function ($val, $key) { - return [str_random()]; - })->all(); - - // Now this will return the complete ruleset. - // Nested array parameters will be present, with '*' replaced by '0' - return Validator::make($values, $rules)->getRules(); - } - - /** - * @param $route - * @param $bindings - * @param $headers - * - * @return \Illuminate\Http\Response - */ - protected function getRouteResponse($route, $bindings, $headers = []) - { - $uri = $this->addRouteModelBindings($route, $bindings); - - $methods = $this->getMethods($route); - - // Split headers into key - value pairs - $headers = collect($headers)->map(function ($value) { - $split = explode(':', $value); // explode to get key + values - $key = array_shift($split); // extract the key and keep the values in the array - $value = implode(':', $split); // implode values into string again - - return [trim($key) => trim($value)]; - })->collapse()->toArray(); - - //Changes url with parameters like /users/{user} to /users/1 - $uri = preg_replace('/{(.*?)}/', 1, $uri); // 1 is the default value for route parameters - - return $this->callRoute(array_shift($methods), $uri, [], [], [], $headers); - } - - /** - * @param $route - * @param array $bindings - * - * @return mixed - */ - protected function addRouteModelBindings($route, $bindings) - { - $uri = $this->getUri($route); - foreach ($bindings as $model => $id) { - $uri = str_replace('{'.$model.'}', $id, $uri); - $uri = str_replace('{'.$model.'?}', $id, $uri); - } - - return $uri; - } - - /** - * @param \Illuminate\Routing\Route $route - * - * @return array - */ - protected function getRouteDescription($route) - { - list($class, $method) = explode('@', $route); - $reflection = new ReflectionClass($class); - $reflectionMethod = $reflection->getMethod($method); - - $comment = $reflectionMethod->getDocComment(); - $phpdoc = new DocBlock($comment); - - return [ - 'short' => $phpdoc->getShortDescription(), - 'long' => $phpdoc->getLongDescription()->getContents(), - 'tags' => $phpdoc->getTags(), - ]; - } - - /** - * @param string $route - * - * @return string - */ - protected function getRouteGroup($route) - { - list($class, $method) = explode('@', $route); - $reflection = new ReflectionClass($class); - $comment = $reflection->getDocComment(); - if ($comment) { - $phpdoc = new DocBlock($comment); - foreach ($phpdoc->getTags() as $tag) { - if ($tag->getName() === 'resource') { - return $tag->getContent(); - } - } - } - - return 'general'; - } - - /** - * @param array $routeMethods - * @param string $routeAction - * @param array $bindings - * - * @return array - */ - protected function getRouteValidationRules(array $routeMethods, $routeAction, $bindings) - { - list($controller, $method) = explode('@', $routeAction); - $reflection = new ReflectionClass($controller); - $reflectionMethod = $reflection->getMethod($method); - - foreach ($reflectionMethod->getParameters() as $parameter) { - $parameterType = $parameter->getClass(); - if (! is_null($parameterType) && class_exists($parameterType->name)) { - $className = $parameterType->name; - - if (is_subclass_of($className, FormRequest::class)) { - /** @var FormRequest $formRequest */ - $formRequest = new $className; - // Add route parameter bindings - $formRequest->setContainer(app()); - $formRequest->request->add($bindings); - $formRequest->query->add($bindings); - $formRequest->setMethod($routeMethods[0]); - - if (method_exists($formRequest, 'validator')) { - $factory = app(ValidationFactory::class); - - return call_user_func_array([$formRequest, 'validator'], [$factory]) - ->getRules(); - } else { - return call_user_func_array([$formRequest, 'rules'], []); - } - } - } - } - - return []; - } - - /** - * @param array $arr - * @param string $first - * @param string $last - * - * @return string - */ - protected function fancyImplode($arr, $first, $last) - { - $arr = array_map(function ($value) { - return '`'.$value.'`'; - }, $arr); - array_push($arr, implode($last, array_splice($arr, -2))); - - return implode($first, $arr); - } - - protected function splitValuePairs($parameters, $first = 'is ', $last = 'or ') - { - $attribute = ''; - collect($parameters)->map(function ($item, $key) use (&$attribute, $first, $last) { - $attribute .= '`'.$item.'` '; - if (($key + 1) % 2 === 0) { - $attribute .= $last; - } else { - $attribute .= $first; - } - }); - $attribute = rtrim($attribute, $last); - - return $attribute; - } - - /** - * @param string $rule - * @param string $attribute - * @param array $attributeData - * @param int $seed - * - * @return void - */ - protected function parseRule($rule, $attribute, &$attributeData, $seed, $routeData) - { - $faker = Factory::create(); - $faker->seed(crc32($seed)); - - $parsedRule = $this->parseStringRule($rule); - $parsedRule[0] = $this->normalizeRule($parsedRule[0]); - list($rule, $parameters) = $parsedRule; - - switch ($rule) { - case 'required': - $attributeData['required'] = true; - break; - case 'accepted': - $attributeData['required'] = true; - $attributeData['type'] = 'boolean'; - $attributeData['value'] = true; - break; - case 'after': - $attributeData['type'] = 'date'; - $format = isset($attributeData['format']) ? $attributeData['format'] : DATE_RFC850; - - if (strtotime($parameters[0]) === false) { - // the `after` date refers to another parameter in the request - $paramName = $parameters[0]; - $attributeData['description'][] = Description::parse($rule)->with($paramName)->getDescription(); - $attributeData['value'] = date($format, strtotime('+1 day', strtotime($routeData['parameters'][$paramName]['value']))); - } else { - $attributeData['description'][] = Description::parse($rule)->with(date($format, strtotime($parameters[0])))->getDescription(); - $attributeData['value'] = date($format, strtotime('+1 day', strtotime($parameters[0]))); - } - break; - case 'alpha': - $attributeData['description'][] = Description::parse($rule)->getDescription(); - $attributeData['value'] = $faker->word; - break; - case 'alpha_dash': - $attributeData['description'][] = Description::parse($rule)->getDescription(); - break; - case 'alpha_num': - $attributeData['description'][] = Description::parse($rule)->getDescription(); - break; - case 'in': - $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription(); - $attributeData['value'] = $faker->randomElement($parameters); - break; - case 'not_in': - $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription(); - $attributeData['value'] = $faker->word; - break; - case 'min': - $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription(); - if (Arr::get($attributeData, 'type') === 'numeric' || Arr::get($attributeData, 'type') === 'integer') { - $attributeData['value'] = $faker->numberBetween($parameters[0]); - } - break; - case 'max': - $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription(); - if (Arr::get($attributeData, 'type') === 'numeric' || Arr::get($attributeData, 'type') === 'integer') { - $attributeData['value'] = $faker->numberBetween(0, $parameters[0]); - } - break; - case 'between': - if (! isset($attributeData['type'])) { - $attributeData['type'] = 'numeric'; - } - $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription(); - $attributeData['value'] = $faker->numberBetween($parameters[0], $parameters[1]); - break; - case 'before': - $attributeData['type'] = 'date'; - $format = isset($attributeData['format']) ? $attributeData['format'] : DATE_RFC850; - - if (strtotime($parameters[0]) === false) { - // the `before` date refers to another parameter in the request - $paramName = $parameters[0]; - $attributeData['description'][] = Description::parse($rule)->with($paramName)->getDescription(); - $attributeData['value'] = date($format, strtotime('-1 day', strtotime($routeData['parameters'][$paramName]['value']))); - } else { - $attributeData['description'][] = Description::parse($rule)->with(date($format, strtotime($parameters[0])))->getDescription(); - $attributeData['value'] = date($format, strtotime('-1 day', strtotime($parameters[0]))); - } - break; - case 'date_format': - $attributeData['type'] = 'date'; - $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription(); - $attributeData['format'] = $parameters[0]; - $attributeData['value'] = date($attributeData['format']); - break; - case 'different': - $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription(); - break; - case 'digits': - $attributeData['type'] = 'numeric'; - $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription(); - $attributeData['value'] = ($parameters[0] < 9) ? $faker->randomNumber($parameters[0], true) : substr(mt_rand(100000000, mt_getrandmax()), 0, $parameters[0]); - break; - case 'digits_between': - $attributeData['type'] = 'numeric'; - $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription(); - break; - case 'file': - $attributeData['type'] = 'file'; - $attributeData['description'][] = Description::parse($rule)->getDescription(); - break; - case 'image': - $attributeData['type'] = 'image'; - $attributeData['description'][] = Description::parse($rule)->getDescription(); - break; - case 'json': - $attributeData['type'] = 'string'; - $attributeData['description'][] = Description::parse($rule)->getDescription(); - $attributeData['value'] = json_encode(['foo', 'bar', 'baz']); - break; - case 'mimetypes': - case 'mimes': - $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription(); - break; - case 'required_if': - $attributeData['description'][] = Description::parse($rule)->with($this->splitValuePairs($parameters))->getDescription(); - break; - case 'required_unless': - $attributeData['description'][] = Description::parse($rule)->with($this->splitValuePairs($parameters))->getDescription(); - break; - case 'required_with': - $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription(); - break; - case 'required_with_all': - $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' and '))->getDescription(); - break; - case 'required_without': - $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' or '))->getDescription(); - break; - case 'required_without_all': - $attributeData['description'][] = Description::parse($rule)->with($this->fancyImplode($parameters, ', ', ' and '))->getDescription(); - break; - case 'same': - $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription(); - break; - case 'size': - $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription(); - break; - case 'timezone': - $attributeData['description'][] = Description::parse($rule)->getDescription(); - $attributeData['value'] = $faker->timezone; - break; - case 'exists': - $fieldName = isset($parameters[1]) ? $parameters[1] : $attribute; - $attributeData['description'][] = Description::parse($rule)->with([Str::singular($parameters[0]), $fieldName])->getDescription(); - break; - case 'active_url': - $attributeData['type'] = 'url'; - $attributeData['value'] = $faker->url; - break; - case 'regex': - $attributeData['type'] = 'string'; - $attributeData['description'][] = Description::parse($rule)->with($parameters)->getDescription(); - break; - case 'boolean': - $attributeData['value'] = true; - $attributeData['type'] = $rule; - break; - case 'array': - $attributeData['value'] = [$faker->word]; - $attributeData['type'] = $rule; - $attributeData['description'][] = Description::parse($rule)->getDescription(); - break; - case 'date': - $attributeData['value'] = $faker->date(); - $attributeData['type'] = $rule; - break; - case 'email': - $attributeData['value'] = $faker->safeEmail; - $attributeData['type'] = $rule; - break; - case 'string': - $attributeData['value'] = $faker->word; - $attributeData['type'] = $rule; - break; - case 'integer': - $attributeData['value'] = $faker->randomNumber(); - $attributeData['type'] = $rule; - break; - case 'numeric': - $attributeData['value'] = $faker->randomNumber(); - $attributeData['type'] = $rule; - break; - case 'url': - $attributeData['value'] = $faker->url; - $attributeData['type'] = $rule; - break; - case 'ip': - $attributeData['value'] = $faker->ipv4; - $attributeData['type'] = $rule; - break; - default: - $unknownRuleDescription = Description::parse($rule)->getDescription(); - if ($unknownRuleDescription) { - $attributeData['description'][] = $unknownRuleDescription; - } - break; - } - - if ($attributeData['value'] === '') { - $attributeData['value'] = $faker->word; - } - - if (is_null($attributeData['type'])) { - $attributeData['type'] = 'string'; - } - } - - /** - * Call the given URI and return the Response. - * - * @param string $method - * @param string $uri - * @param array $parameters - * @param array $cookies - * @param array $files - * @param array $server - * @param string $content - * - * @return \Illuminate\Http\Response - */ - abstract public function callRoute($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null); - - /** - * Transform headers array to array of $_SERVER vars with HTTP_* format. - * - * @param array $headers - * - * @return array - */ - protected function transformHeadersToServerVars(array $headers) - { - $server = []; - $prefix = 'HTTP_'; - - foreach ($headers as $name => $value) { - $name = strtr(strtoupper($name), '-', '_'); - - if (! Str::startsWith($name, $prefix) && $name !== 'CONTENT_TYPE') { - $name = $prefix.$name; - } - - $server[$name] = $value; - } - - return $server; - } - - /** - * Parse a string based rule. - * - * @param string $rules - * - * @return array - */ - protected function parseStringRule($rules) - { - $parameters = []; - - // The format for specifying validation rules and parameters follows an - // easy {rule}:{parameters} formatting convention. For instance the - // rule "max:3" states that the value may only be three letters. - if (strpos($rules, ':') !== false) { - list($rules, $parameter) = explode(':', $rules, 2); - - $parameters = $this->parseParameters($rules, $parameter); - } - - return [strtolower(trim($rules)), $parameters]; - } - - /** - * Parse a parameter list. - * - * @param string $rule - * @param string $parameter - * - * @return array - */ - protected function parseParameters($rule, $parameter) - { - if (strtolower($rule) === 'regex') { - return [$parameter]; - } - - return str_getcsv($parameter); - } - - /** - * Normalizes a rule so that we can accept short types. - * - * @param string $rule - * - * @return string - */ - protected function normalizeRule($rule) - { - switch ($rule) { - case 'int': - return 'integer'; - case 'bool': - return 'boolean'; - default: - return $rule; - } - } - - /** - * @param $response - * - * @return mixed - */ - private function getResponseContent($response) - { - if (empty($response)) { - return ''; - } - if ($response->headers->get('Content-Type') === 'application/json') { - $content = json_decode($response->getContent(), JSON_PRETTY_PRINT); - } else { - $content = $response->getContent(); - } - - return $content; - } - - /** - * Get a response from the transformer tags. - * - * @param array $tags - * - * @return mixed - */ - protected function getTransformerResponse($tags) - { - try { - $transFormerTags = array_filter($tags, function ($tag) { - if (! ($tag instanceof Tag)) { - return false; - } - - return \in_array(\strtolower($tag->getName()), ['transformer', 'transformercollection']); - }); - if (empty($transFormerTags)) { - // we didn't have any of the tags so goodbye - return false; - } - - $modelTag = array_first(array_filter($tags, function ($tag) { - if (! ($tag instanceof Tag)) { - return false; - } - - return \in_array(\strtolower($tag->getName()), ['transformermodel']); - })); - $tag = \array_first($transFormerTags); - $transformer = $tag->getContent(); - if (! \class_exists($transformer)) { - // if we can't find the transformer we can't generate a response - return; - } - $demoData = []; - - $reflection = new ReflectionClass($transformer); - $method = $reflection->getMethod('transform'); - $parameter = \array_first($method->getParameters()); - $type = null; - if ($modelTag) { - $type = $modelTag->getContent(); - } - if (version_compare(PHP_VERSION, '7.0.0') >= 0 && \is_null($type)) { - // we can only get the type with reflection for PHP 7 - if ($parameter->hasType() && - ! $parameter->getType()->isBuiltin() && - \class_exists((string) $parameter->getType())) { - //we have a type - $type = (string) $parameter->getType(); - } - } - if ($type) { - // we have a class so we try to create an instance - $demoData = new $type; - try { - // try a factory - $demoData = \factory($type)->make(); - } catch (\Exception $e) { - if ($demoData instanceof \Illuminate\Database\Eloquent\Model) { - // we can't use a factory but can try to get one from the database - try { - // check if we can find one - $newDemoData = $type::first(); - if ($newDemoData) { - $demoData = $newDemoData; - } - } catch (\Exception $e) { - // do nothing - } - } - } - } - - $fractal = new Manager(); - $resource = []; - if ($tag->getName() == 'transformer') { - // just one - $resource = new Item($demoData, new $transformer); - } - if ($tag->getName() == 'transformercollection') { - // a collection - $resource = new Collection([$demoData, $demoData], new $transformer); - } - - return \response($fractal->createData($resource)->toJson()); - } catch (\Exception $e) { - // it isn't possible to parse the transformer - return; - } - } -} diff --git a/src/Mpociot/ApiDoc/Parsers/RuleDescriptionParser.php b/src/Mpociot/ApiDoc/Parsers/RuleDescriptionParser.php deleted file mode 100644 index 06aa335c..00000000 --- a/src/Mpociot/ApiDoc/Parsers/RuleDescriptionParser.php +++ /dev/null @@ -1,86 +0,0 @@ -rule = "apidoc::rules.{$rule}"; - } - - /** - * @return array|string - */ - public function getDescription() - { - return $this->ruleDescriptionExist() ? $this->makeDescription() : []; - } - - /** - * @param string|array $parameters - * - * @return $this - */ - public function with($parameters) - { - is_array($parameters) ? - $this->parameters += $parameters : - $this->parameters[] = $parameters; - - return $this; - } - - /** - * @return bool - */ - protected function ruleDescriptionExist() - { - return trans()->hasForLocale($this->rule) || trans()->hasForLocale($this->rule, self::DEFAULT_LOCALE); - } - - /** - * @return string - */ - protected function makeDescription() - { - $description = trans()->hasForLocale($this->rule) ? - trans()->get($this->rule) : - trans()->get($this->rule, [], self::DEFAULT_LOCALE); - - return $this->replaceAttributes($description); - } - - /** - * @param string $description$ - * - * @return string - */ - protected function replaceAttributes($description) - { - foreach ($this->parameters as $parameter) { - $description = preg_replace('/:attribute/', $parameter, $description, 1); - } - - return $description; - } - - /** - * @param null $rule - * - * @return static - */ - public static function parse($rule = null) - { - return new static($rule); - } -} diff --git a/src/Mpociot/ApiDoc/Postman/CollectionWriter.php b/src/Postman/CollectionWriter.php similarity index 100% rename from src/Mpociot/ApiDoc/Postman/CollectionWriter.php rename to src/Postman/CollectionWriter.php diff --git a/src/Tools/RouteMatcher.php b/src/Tools/RouteMatcher.php new file mode 100644 index 00000000..fd7f00b6 --- /dev/null +++ b/src/Tools/RouteMatcher.php @@ -0,0 +1,74 @@ +getRoutesToBeDocumented($routeRules,true); + } + + public function getLaravelRoutesToBeDocumented(array $routeRules) + { + return $this->getRoutesToBeDocumented($routeRules); + } + + public function getRoutesToBeDocumented(array $routeRules, bool $usingDingoRouter = false) + { + $matchedRoutes = []; + + foreach ($routeRules as $routeRule) { + $excludes = $routeRule['exclude'] ?? []; + $includes = $routeRule['include'] ?? []; + $allRoutes = $this->getAllRoutes($usingDingoRouter, $routeRule['match']['versions'] ?? []); + + foreach ($allRoutes as $route) { + /** @var Route $route */ + if (in_array($route->getName(), $excludes)) { + continue; + } + + if ($this->shouldIncludeRoute($route, $routeRule, $includes, $usingDingoRouter)) { + $matchedRoutes[] = [ + 'route' => $route, + 'apply' => $routeRule['apply'] ?? [], + ]; + continue; + } + } + } + + return $matchedRoutes; + } + + private function getAllRoutes(bool $usingDingoRouter, array $versions = []) + { + if (!$usingDingoRouter) { + return RouteFacade::getRoutes(); + } + + $allRouteCollections = app(\Dingo\Api\Routing\Router::class)->getRoutes(); + return collect($allRouteCollections) + ->flatMap(function (RouteCollection $collection) { + return $collection->getRoutes(); + })->toArray(); + } + + private function shouldIncludeRoute(Route $route, array $routeRule, array $mustIncludes, bool $usingDingoRouter) + { + $matchesVersion = $usingDingoRouter + ? !empty(array_intersect($route->versions(), $routeRule['match']['versions'] ?? [])) + : true; + + return in_array($route->getName(), $mustIncludes) + || (str_is($routeRule['match']['domains'] ?? [], $route->getDomain()) + && str_is($routeRule['match']['prefixes'] ?? [], $route->uri()) + && $matchesVersion); + } + +} diff --git a/src/resources/lang/en/rules.php b/src/resources/lang/en/rules.php deleted file mode 100644 index 5bab0bf7..00000000 --- a/src/resources/lang/en/rules.php +++ /dev/null @@ -1,35 +0,0 @@ - 'Must be a date after: `:attribute`', - 'alpha' => 'Only alphabetic characters allowed', - 'alpha_dash' => 'Allowed: alpha-numeric characters, as well as dashes and underscores.', - 'alpha_num' => 'Only alpha-numeric characters allowed', - 'array' => 'Must be an array', - 'in' => ':attribute', - 'not_in' => 'Not in: :attribute', - 'min' => 'Minimum: `:attribute`', - 'max' => 'Maximum: `:attribute`', - 'between' => 'Between: `:attribute` and `:attribute`', - 'before' => 'Must be a date preceding: `:attribute`', - 'date_format' => 'Date format: `:attribute`', - 'different' => 'Must have a different value than parameter: `:attribute`', - 'digits' => 'Must have an exact length of `:attribute`', - 'digits_between' => 'Must have a length between `:attribute` and `:attribute`', - 'file' => 'Must be a file upload', - 'image' => 'Must be an image (jpeg, png, bmp, gif, or svg)', - 'json' => 'Must be a valid JSON string.', - 'mimetypes' => 'Allowed mime types: :attribute', - 'mimes' => 'Allowed mime types: :attribute', - 'required_if' => 'Required if :attribute', - 'required_unless' => 'Required unless :attribute', - 'required_with' => 'Required if the parameters :attribute are present.', - 'required_with_all' => 'Required if the parameters :attribute are present.', - 'required_without' => 'Required if the parameters :attribute are not present.', - 'required_without_all' => 'Required if the parameters :attribute are not present.', - 'same' => 'Must be the same as `:attribute`', - 'size' => 'Must have the size of `:attribute`', - 'timezone' => 'Must be a valid timezone identifier', - 'exists' => 'Valid :attribute :attribute', - 'regex' => 'Must match this regular expression: `:attribute`', -]; diff --git a/tests/ApiDocGeneratorTest.php b/tests/ApiDocGeneratorTest.php index b3407514..95db8d4c 100644 --- a/tests/ApiDocGeneratorTest.php +++ b/tests/ApiDocGeneratorTest.php @@ -4,7 +4,6 @@ use Illuminate\Routing\Route; use Orchestra\Testbench\TestCase; -use Mpociot\ApiDoc\Tests\Fixtures\TestRequest; use Mpociot\ApiDoc\Generators\LaravelGenerator; use Mpociot\ApiDoc\Tests\Fixtures\TestController; use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider; @@ -76,277 +75,6 @@ public function testCanParseDependencyInjectionInControllerMethods() $this->assertTrue(is_array($parsed)); } - public function testCanParseFormRequestRules() - { - RouteFacade::post('/post', TestController::class.'@parseFormRequestRules'); - $route = new Route(['POST'], '/post', ['uses' => TestController::class.'@parseFormRequestRules']); - $parsed = $this->generator->processRoute($route); - $parameters = $parsed['parameters']; - - $testRequest = new TestRequest(); - $rules = $testRequest->rules(); - - foreach ($rules as $name => $rule) { - $attribute = $parameters[$name]; - - switch ($name) { - - case 'required': - $this->assertTrue($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'accepted': - $this->assertTrue($attribute['required']); - $this->assertSame('boolean', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'active_url': - $this->assertFalse($attribute['required']); - $this->assertSame('url', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'alpha': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Only alphabetic characters allowed', $attribute['description'][0]); - break; - case 'alpha_dash': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Allowed: alpha-numeric characters, as well as dashes and underscores.', $attribute['description'][0]); - break; - case 'alpha_num': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Only alpha-numeric characters allowed', $attribute['description'][0]); - break; - case 'array': - $this->assertFalse($attribute['required']); - $this->assertSame('array', $attribute['type']); - $this->assertCount(1, $attribute['description']); - break; - case 'between': - $this->assertFalse($attribute['required']); - $this->assertSame('numeric', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Between: `5` and `200`', $attribute['description'][0]); - break; - case 'string_between': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Between: `5` and `200`', $attribute['description'][0]); - break; - case 'before': - $this->assertFalse($attribute['required']); - $this->assertSame('date', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be a date preceding: `Saturday, 23-Apr-16 14:31:00 UTC`', $attribute['description'][0]); - break; - case 'boolean': - $this->assertFalse($attribute['required']); - $this->assertSame('boolean', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'date': - $this->assertFalse($attribute['required']); - $this->assertSame('date', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'date_format': - $this->assertFalse($attribute['required']); - $this->assertSame('date', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Date format: `j.n.Y H:iP`', $attribute['description'][0]); - break; - case 'different': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must have a different value than parameter: `alpha_num`', $attribute['description'][0]); - break; - case 'digits': - $this->assertFalse($attribute['required']); - $this->assertSame('numeric', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must have an exact length of `2`', $attribute['description'][0]); - break; - case 'digits_between': - $this->assertFalse($attribute['required']); - $this->assertSame('numeric', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must have a length between `2` and `10`', $attribute['description'][0]); - break; - case 'email': - $this->assertFalse($attribute['required']); - $this->assertSame('email', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'exists': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Valid user firstname', $attribute['description'][0]); - break; - case 'single_exists': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Valid user single_exists', $attribute['description'][0]); - break; - case 'file': - $this->assertFalse($attribute['required']); - $this->assertSame('file', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be a file upload', $attribute['description'][0]); - break; - case 'image': - $this->assertFalse($attribute['required']); - $this->assertSame('image', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be an image (jpeg, png, bmp, gif, or svg)', $attribute['description'][0]); - break; - case 'in': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('`jpeg`, `png`, `bmp`, `gif` or `svg`', $attribute['description'][0]); - break; - case 'integer': - $this->assertFalse($attribute['required']); - $this->assertSame('integer', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'ip': - $this->assertFalse($attribute['required']); - $this->assertSame('ip', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'json': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be a valid JSON string.', $attribute['description'][0]); - break; - case 'max': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Maximum: `10`', $attribute['description'][0]); - break; - case 'min': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Minimum: `20`', $attribute['description'][0]); - break; - case 'mimes': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Allowed mime types: `jpeg`, `bmp` or `png`', $attribute['description'][0]); - break; - case 'not_in': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Not in: `foo` or `bar`', $attribute['description'][0]); - break; - case 'numeric': - $this->assertFalse($attribute['required']); - $this->assertSame('numeric', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'regex': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must match this regular expression: `(.*)`', $attribute['description'][0]); - break; - case 'multiple_required_if': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if `foo` is `bar` or `baz` is `qux`', $attribute['description'][0]); - break; - case 'required_if': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if `foo` is `bar`', $attribute['description'][0]); - break; - case 'required_unless': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required unless `foo` is `bar`', $attribute['description'][0]); - break; - case 'required_with': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if the parameters `foo`, `bar` or `baz` are present.', $attribute['description'][0]); - break; - case 'required_with_all': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if the parameters `foo`, `bar` and `baz` are present.', $attribute['description'][0]); - break; - case 'required_without': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if the parameters `foo`, `bar` or `baz` are not present.', $attribute['description'][0]); - break; - case 'required_without_all': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if the parameters `foo`, `bar` and `baz` are not present.', $attribute['description'][0]); - break; - case 'same': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be the same as `foo`', $attribute['description'][0]); - break; - case 'size': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must have the size of `51`', $attribute['description'][0]); - break; - case 'timezone': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be a valid timezone identifier', $attribute['description'][0]); - break; - case 'url': - $this->assertFalse($attribute['required']); - $this->assertSame('url', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - - } - } - } - - public function testCustomFormRequestValidatorIsSupported() - { - RouteFacade::post('/post', TestController::class.'@customFormRequestValidator'); - $route = new Route(['POST'], '/post', ['uses' => TestController::class.'@customFormRequestValidator']); - $parsed = $this->generator->processRoute($route); - $parameters = $parsed['parameters']; - - $this->assertNotEmpty($parameters); - } - public function testCanParseResponseTag() { RouteFacade::post('/responseTag', TestController::class.'@responseTag'); diff --git a/tests/DingoGeneratorTest.php b/tests/DingoGeneratorTest.php index 2311498d..8f09ab57 100644 --- a/tests/DingoGeneratorTest.php +++ b/tests/DingoGeneratorTest.php @@ -37,10 +37,6 @@ public function setUp() public function testCanParseMethodDescription() { - if (version_compare($this->app->version(), '5.4', '>=')) { - $this->markTestSkipped('Dingo does not support Laravel 5.4'); - } - $api = app('Dingo\Api\Routing\Router'); $api->version('v1', function ($api) { $api->get('/api/test', TestController::class.'@parseMethodDescription'); @@ -55,10 +51,6 @@ public function testCanParseMethodDescription() public function testCanParseRouteMethods() { - if (version_compare($this->app->version(), '5.4', '>=')) { - $this->markTestSkipped('Dingo does not support Laravel 5.4'); - } - $api = app('Dingo\Api\Routing\Router'); $api->version('v1', function ($api) { $api->get('/get', TestController::class.'@dummy'); @@ -83,272 +75,4 @@ public function testCanParseRouteMethods() $this->assertSame(['DELETE'], $parsed['methods']); } - public function testCanParseFormRequestRules() - { - if (version_compare($this->app->version(), '5.4', '>=')) { - $this->markTestSkipped('Dingo does not support Laravel 5.4'); - } - - $api = app('Dingo\Api\Routing\Router'); - $api->version('v1', function ($api) { - $api->post('/post', DingoTestController::class.'@parseFormRequestRules'); - }); - - $route = app('Dingo\Api\Routing\Router')->getRoutes()['v1']->getRoutes()[0]; - $parsed = $this->generator->processRoute($route); - $parameters = $parsed['parameters']; - - $testRequest = new TestRequest(); - $rules = $testRequest->rules(); - - foreach ($rules as $name => $rule) { - $attribute = $parameters[$name]; - - switch ($name) { - - case 'required': - $this->assertTrue($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'accepted': - $this->assertTrue($attribute['required']); - $this->assertSame('boolean', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'active_url': - $this->assertFalse($attribute['required']); - $this->assertSame('url', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'alpha': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Only alphabetic characters allowed', $attribute['description'][0]); - break; - case 'alpha_dash': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Allowed: alpha-numeric characters, as well as dashes and underscores.', $attribute['description'][0]); - break; - case 'alpha_num': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Only alpha-numeric characters allowed', $attribute['description'][0]); - break; - case 'array': - $this->assertFalse($attribute['required']); - $this->assertSame('array', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'between': - $this->assertFalse($attribute['required']); - $this->assertSame('numeric', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Between: `5` and `200`', $attribute['description'][0]); - break; - case 'string_between': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Between: `5` and `200`', $attribute['description'][0]); - break; - case 'before': - $this->assertFalse($attribute['required']); - $this->assertSame('date', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be a date preceding: `Saturday, 23-Apr-16 14:31:00 UTC`', $attribute['description'][0]); - break; - case 'boolean': - $this->assertFalse($attribute['required']); - $this->assertSame('boolean', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'date': - $this->assertFalse($attribute['required']); - $this->assertSame('date', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'date_format': - $this->assertFalse($attribute['required']); - $this->assertSame('date', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Date format: `j.n.Y H:iP`', $attribute['description'][0]); - break; - case 'different': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must have a different value than parameter: `alpha_num`', $attribute['description'][0]); - break; - case 'digits': - $this->assertFalse($attribute['required']); - $this->assertSame('numeric', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must have an exact length of `2`', $attribute['description'][0]); - break; - case 'digits_between': - $this->assertFalse($attribute['required']); - $this->assertSame('numeric', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must have a length between `2` and `10`', $attribute['description'][0]); - break; - case 'email': - $this->assertFalse($attribute['required']); - $this->assertSame('email', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'exists': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Valid user firstname', $attribute['description'][0]); - break; - case 'single_exists': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Valid user single_exists', $attribute['description'][0]); - break; - case 'file': - $this->assertFalse($attribute['required']); - $this->assertSame('file', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be a file upload', $attribute['description'][0]); - break; - case 'image': - $this->assertFalse($attribute['required']); - $this->assertSame('image', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be an image (jpeg, png, bmp, gif, or svg)', $attribute['description'][0]); - break; - case 'in': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('`jpeg`, `png`, `bmp`, `gif` or `svg`', $attribute['description'][0]); - break; - case 'integer': - $this->assertFalse($attribute['required']); - $this->assertSame('integer', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'ip': - $this->assertFalse($attribute['required']); - $this->assertSame('ip', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'json': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be a valid JSON string.', $attribute['description'][0]); - break; - case 'max': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Maximum: `10`', $attribute['description'][0]); - break; - case 'min': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Minimum: `20`', $attribute['description'][0]); - break; - case 'mimes': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Allowed mime types: `jpeg`, `bmp` or `png`', $attribute['description'][0]); - break; - case 'not_in': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Not in: `foo` or `bar`', $attribute['description'][0]); - break; - case 'numeric': - $this->assertFalse($attribute['required']); - $this->assertSame('numeric', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - case 'regex': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must match this regular expression: `(.*)`', $attribute['description'][0]); - break; - case 'multiple_required_if': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if `foo` is `bar` or `baz` is `qux`', $attribute['description'][0]); - break; - case 'required_if': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if `foo` is `bar`', $attribute['description'][0]); - break; - case 'required_unless': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required unless `foo` is `bar`', $attribute['description'][0]); - break; - case 'required_with': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if the parameters `foo`, `bar` or `baz` are present.', $attribute['description'][0]); - break; - case 'required_with_all': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if the parameters `foo`, `bar` and `baz` are present.', $attribute['description'][0]); - break; - case 'required_without': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if the parameters `foo`, `bar` or `baz` are not present.', $attribute['description'][0]); - break; - case 'required_without_all': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Required if the parameters `foo`, `bar` and `baz` are not present.', $attribute['description'][0]); - break; - case 'same': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be the same as `foo`', $attribute['description'][0]); - break; - case 'size': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must have the size of `51`', $attribute['description'][0]); - break; - case 'timezone': - $this->assertFalse($attribute['required']); - $this->assertSame('string', $attribute['type']); - $this->assertCount(1, $attribute['description']); - $this->assertSame('Must be a valid timezone identifier', $attribute['description'][0]); - break; - case 'url': - $this->assertFalse($attribute['required']); - $this->assertSame('url', $attribute['type']); - $this->assertCount(0, $attribute['description']); - break; - - } - } - } } diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 957986dd..b2ab6a81 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -34,7 +34,7 @@ public function setUp() public function tearDown() { // delete the generated docs - compatible cross-platform - $dir = __DIR__.'/../public/docs'; + $dir = __DIR__.'/../public/docs';/* if (is_dir($dir)) { $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), @@ -46,7 +46,7 @@ public function tearDown() $todo($fileinfo->getRealPath()); } rmdir($dir); - } + }*/ } /** @@ -84,20 +84,16 @@ public function testConsoleCommandDoesNotWorkWithClosure() public function testConsoleCommandDoesNotWorkWithClosureUsingDingo() { - if (version_compare($this->app->version(), '5.4', '>=')) { - $this->markTestSkipped('Dingo does not support Laravel 5.4'); - } - $api = app('Dingo\Api\Routing\Router'); $api->version('v1', function ($api) { - $api->get('/closure', function () { + $api->get('v1/closure', function () { return 'foo'; }); - $api->get('/test', DingoTestController::class.'@parseMethodDescription'); + $api->get('v1/test', DingoTestController::class.'@parseMethodDescription'); $output = $this->artisan('apidoc:generate', [ '--router' => 'dingo', - '--routePrefix' => 'v1', + '--routePrefix' => 'v1/*', ]); $this->assertContains('Skipping route: [GET] closure', $output); $this->assertContains('Processed route: [GET] test', $output); diff --git a/tests/RouteMatcherTest.php b/tests/RouteMatcherTest.php new file mode 100644 index 00000000..ecdf573e --- /dev/null +++ b/tests/RouteMatcherTest.php @@ -0,0 +1,364 @@ +matcher = new RouteMatcher(); + } + + protected function getPackageProviders($app) + { + return [ + \Dingo\Api\Provider\LaravelServiceProvider::class, + ]; + } + + public function testRespectsDomainsRuleForLaravelRouter() + { + $this->registerLaravelRoutes(); + $routeRules[0]['match']['prefixes'] = ['*']; + + $routeRules[0]['match']['domains'] = ['*']; + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $this->assertCount(12, $routes); + + $routeRules[0]['match']['domains'] = ['domain1.*', 'domain2.*']; + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $this->assertCount(12, $routes); + + $routeRules[0]['match']['domains'] = ['domain1.*']; + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $this->assertCount(6, $routes); + foreach ($routes as $route){ + $this->assertContains('domain1', $route['route']->getDomain()); + } + + $routeRules[0]['match']['domains'] = ['domain2.*']; + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $this->assertCount(6, $routes); + foreach ($routes as $route){ + $this->assertContains('domain2', $route['route']->getDomain()); + } + } + + public function testRespectsDomainsRuleForDingoRouter() + { + $this->registerDingoRoutes(); + $routeRules[0]['match']['versions'] = ['v1']; + $routeRules[0]['match']['prefixes'] = ['*']; + + $routeRules[0]['match']['domains'] = ['*']; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(12, $routes); + + $routeRules[0]['match']['domains'] = ['domain1.*', 'domain2.*']; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(12, $routes); + + $routeRules[0]['match']['domains'] = ['domain1.*']; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(6, $routes); + foreach ($routes as $route){ + $this->assertContains('domain1', $route['route']->getDomain()); + } + + $routeRules[0]['match']['domains'] = ['domain2.*']; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(6, $routes); + foreach ($routes as $route){ + $this->assertContains('domain2', $route['route']->getDomain()); + } + } + + public function testRespectsPrefixesRuleForLaravelRouter() + { + $this->registerLaravelRoutes(); + $routeRules[0]['match']['domains'] = ['*']; + + $routeRules[0]['match']['prefixes'] = ['*']; + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $this->assertCount(12, $routes); + + $routeRules[0]['match']['prefixes'] = ['prefix1/*', 'prefix2/*']; + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $this->assertCount(8, $routes); + + $routeRules[0]['match']['prefixes'] = ['prefix1/*']; + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $this->assertCount(4, $routes); + foreach ($routes as $route){ + $this->assertTrue(str_is('prefix1/*', $route['route']->uri())); + } + + $routeRules[0]['match']['prefixes'] = ['prefix2/*']; + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $this->assertCount(4, $routes); + foreach ($routes as $route){ + $this->assertTrue(str_is('prefix2/*', $route['route']->uri())); + } + } + + public function testRespectsPrefixesRuleForDingoRouter() + { + $this->registerDingoRoutes(); + $routeRules[0]['match']['versions'] = ['v1']; + $routeRules[0]['match']['domains'] = ['*']; + + $routeRules[0]['match']['prefixes'] = ['*']; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(12, $routes); + + $routeRules[0]['match']['prefixes'] = ['prefix1/*', 'prefix2/*']; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(8, $routes); + + $routeRules[0]['match']['prefixes'] = ['prefix1/*']; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(4, $routes); + foreach ($routes as $route){ + $this->assertTrue(str_is('prefix1/*', $route['route']->uri())); + } + + $routeRules[0]['match']['prefixes'] = ['prefix2/*']; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(4, $routes); + foreach ($routes as $route){ + $this->assertTrue(str_is('prefix2/*', $route['route']->uri())); + } + } + + public function testRespectsVersionsRuleForDingoRouter() + { + $this->registerDingoRoutes(); + + $routeRules[0]['match']['versions'] = ['v2']; + $routeRules[0]['match']['domains'] = ['*']; + $routeRules[0]['match']['prefixes'] = ['*']; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(6, $routes); + foreach ($routes as $route){ + $this->assertNotEmpty(array_intersect($route['route']->versions(), ['v2'])); + } + + $routeRules[0]['match']['versions'] = ['v1', 'v2']; + $routeRules[0]['match']['domains'] = ['*']; + $routeRules[0]['match']['prefixes'] = ['*']; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(18, $routes); + } + + public function testWillIncludeRouteIfListedExplicitlyForLaravelRouter() + { + $this->registerLaravelRoutes(); + $mustInclude = 'domain1-1'; + $routeRules[0]['include'] = [$mustInclude]; + + $routeRules[0]['match']['domains'] = ['domain1.*']; + $routeRules[0]['match']['prefixes'] = ['prefix1/*']; + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $oddRuleOut = collect($routes)->filter(function ($route) use ($mustInclude) { + return $route['route']->getName() === $mustInclude; + }); + $this->assertCount(1, $oddRuleOut); + } + + public function testWillIncludeRouteIfListedExplicitlyForDingoRouter() + { + $this->registerDingoRoutes(); + + $mustInclude = 'v2.domain2'; + $routeRules = [ + [ + 'match' => [ + 'domains' => ['domain1.*'], + 'prefixes' => ['prefix1/*'], + 'versions' => ['v1'] + ], + 'include' => [$mustInclude], + ], + ]; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $oddRuleOut = collect($routes)->filter(function ($route) use ($mustInclude) { + return $route['route']->getName() === $mustInclude; + }); + $this->assertCount(1, $oddRuleOut); + } + + public function testWillExcludeRouteIfListedExplicitlyForLaravelRouter() + { + $this->registerLaravelRoutes(); + $mustNotInclude = 'prefix1.domain1-1'; + $routeRules[0]['exclude'] = [$mustNotInclude]; + + $routeRules[0]['match']['domains'] = ['domain1.*']; + $routeRules[0]['match']['prefixes'] = ['prefix1/*']; + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $oddRuleOut = collect($routes)->filter(function ($route) use ($mustNotInclude) { + return $route['route']->getName() === $mustNotInclude; + }); + $this->assertCount(0, $oddRuleOut); + } + + public function testWillExcludeRouteIfListedExplicitlyForDingoRouter() + { + $this->registerDingoRoutes(); + + $mustNotInclude = 'v2.domain2'; + $routeRules = [ + [ + 'match' => [ + 'domains' => ['domain2.*'], + 'prefixes' => ['*'], + 'versions' => ['v2'] + ], + 'exclude' => [$mustNotInclude], + ], + ]; + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $oddRuleOut = collect($routes)->filter(function ($route) use ($mustNotInclude) { + return $route['route']->getName() === $mustNotInclude; + }); + $this->assertCount(0, $oddRuleOut); + } + + public function testMergesRoutesFromDifferentRuleGroupsForLaravelRouter() + { + $this->registerLaravelRoutes(); + + $routeRules = [ + [ + 'match' => [ + 'domains' => ['domain1.*'], + 'prefixes' => ['prefix1/*'], + ], + ], + [ + 'match' => [ + 'domains' => ['domain2.*'], + 'prefixes' => ['prefix2*'], + ], + ], + ]; + + $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $this->assertCount(4, $routes); + + $routes = collect($routes); + $firstRuleGroup = $routes->filter(function ($route) { + return str_is('prefix1/*', $route['route']->uri()) + && str_is('domain1.*', $route['route']->getDomain()); + }); + $this->assertCount(2, $firstRuleGroup); + + $secondRuleGroup = $routes->filter(function ($route) { + return str_is('prefix2/*', $route['route']->uri()) + && str_is('domain2.*', $route['route']->getDomain()); + }); + $this->assertCount(2, $secondRuleGroup); + } + + public function testMergesRoutesFromDifferentRuleGroupsForDingoRouter() + { + $this->registerDingoRoutes(); + $routeRules = [ + [ + 'match' => [ + 'domains' => ['*'], + 'prefixes' => ['*'], + 'versions' => ['v1'], + ], + ], + [ + 'match' => [ + 'domains' => ['*'], + 'prefixes' => ['*'], + 'versions' => ['v2'], + ], + ], + ]; + + $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $this->assertCount(18, $routes); + + $routes = collect($routes); + $firstRuleGroup = $routes->filter(function ($route) { + return !empty(array_intersect($route['route']->versions(), ['v1'])); + }); + $this->assertCount(12, $firstRuleGroup); + + $secondRuleGroup = $routes->filter(function ($route) { + return !empty(array_intersect($route['route']->versions(), ['v2'])); + }); + $this->assertCount(6, $secondRuleGroup); + } + + private function registerLaravelRoutes() + { + RouteFacade::group(['domain' => 'domain1.app.test'], function () { + RouteFacade::post('/domain1-1', function () { return 'hi'; })->name('domain1-1'); + RouteFacade::get('domain1-2', function () { return 'hi'; })->name('domain1-2'); + RouteFacade::get('/prefix1/domain1-1', function () { return 'hi'; })->name('prefix1.domain1-1'); + RouteFacade::get('prefix1/domain1-2', function () { return 'hi'; })->name('prefix1.domain1-2'); + RouteFacade::get('/prefix2/domain1-1', function () { return 'hi'; })->name('prefix2.domain1-1'); + RouteFacade::get('prefix2/domain1-2', function () { return 'hi'; })->name('prefix2.domain1-2'); + }); + RouteFacade::group(['domain' => 'domain2.app.test'], function () { + RouteFacade::post('/domain2-1', function () { return 'hi'; })->name('domain2-1'); + RouteFacade::get('domain2-2', function () { return 'hi'; })->name('domain2-2'); + RouteFacade::get('/prefix1/domain2-1', function () { return 'hi'; })->name('prefix1.domain2-1'); + RouteFacade::get('prefix1/domain2-2', function () { return 'hi'; })->name('prefix1.domain2-2'); + RouteFacade::get('/prefix2/domain2-1', function () { return 'hi'; })->name('prefix2.domain2-1'); + RouteFacade::get('prefix2/domain2-2', function () { return 'hi'; })->name('prefix2.domain2-2'); + }); + } + + private function registerDingoRoutes() + { + + $api = app('api.router'); + $api->version('v1', function (Router $api) { + $api->group(['domain' => 'domain1.app.test'], function (Router $api) { + $api->post('/domain1-1', function () { return 'hi'; })->name('v1.domain1-1'); + $api->get('domain1-2', function () { return 'hi'; })->name('v1.domain1-2'); + $api->get('/prefix1/domain1-1', function () { return 'hi'; })->name('v1.prefix1.domain1-1'); + $api->get('prefix1/domain1-2', function () { return 'hi'; })->name('v1.prefix1.domain1-2'); + $api->get('/prefix2/domain1-1', function () { return 'hi'; })->name('v1.prefix2.domain1-1'); + $api->get('prefix2/domain1-2', function () { return 'hi'; })->name('v1.prefix2.domain1-2'); + }); + $api->group(['domain' => 'domain2.app.test'], function (Router $api) { + $api->post('/domain2-1', function () { return 'hi'; })->name('v1.domain2-1'); + $api->get('domain2-2', function () { return 'hi'; })->name('v1.domain2-2'); + $api->get('/prefix1/domain2-1', function () { return 'hi'; })->name('v1.prefix1.domain2-1'); + $api->get('prefix1/domain2-2', function () { return 'hi'; })->name('v1.prefix1.domain2-2'); + $api->get('/prefix2/domain2-1', function () { return 'hi'; })->name('v1.prefix2.domain2-1'); + $api->get('prefix2/domain2-2', function () { return 'hi'; })->name('v1.prefix2.domain2-2'); + }); + }); + $api->version('v2', function (Router $api) { + $api->group(['domain' => 'domain1.app.test'], function (Router $api) { + $api->post('/domain1', function () { return 'hi'; })->name('v2.domain1'); + $api->get('/prefix1/domain1', function () { return 'hi'; })->name('v2.prefix1.domain1'); + $api->get('/prefix2/domain1', function () { return 'hi'; })->name('v2.prefix2.domain1'); + }); + $api->group(['domain' => 'domain2.app.test'], function (Router $api) { + $api->post('/domain2', function () { return 'hi'; })->name('v2.domain2'); + $api->get('/prefix1/domain2', function () { return 'hi'; })->name('v2.prefix1.domain2'); + $api->get('/prefix2/domain2', function () { return 'hi'; })->name('v2.prefix2.domain2'); + }); + }); + } +} diff --git a/tests/RuleDescriptionParserTest.php b/tests/RuleDescriptionParserTest.php deleted file mode 100644 index f1f6f724..00000000 --- a/tests/RuleDescriptionParserTest.php +++ /dev/null @@ -1,118 +0,0 @@ -translatorMock = m::mock(Translator::class, [$fileLoaderMock, 'es']); - $this->app->instance('translator', $this->translatorMock); - } - - public function tearDown() - { - m::close(); - } - - public function testReturnsAnEmptyDescriptionIfARuleIsNotParsed() - { - $this->translatorMock->shouldReceive('hasForLocale')->twice()->andReturn(false); - - $description = new RuleDescriptionParser(); - - $this->assertEmpty($description->getDescription()); - } - - public function testProvidesANamedContructor() - { - $this->assertInstanceOf(RuleDescriptionParser::class, RuleDescriptionParser::parse()); - } - - public function testReturnsADescriptionInMainLanguageIfAvailable() - { - $this->translatorMock->shouldReceive('hasForLocale')->twice()->with('apidoc::rules.alpha')->andReturn(true); - $this->translatorMock->shouldReceive('get')->once()->with('apidoc::rules.alpha')->andReturn('Solo caracteres alfabeticos permitidos'); - - $description = RuleDescriptionParser::parse('alpha')->getDescription(); - - $this->assertEquals('Solo caracteres alfabeticos permitidos', $description); - } - - public function testReturnsDescriptionInDefaultLanguageIfNotAvailableInMainLanguage() - { - $this->translatorMock->shouldReceive('hasForLocale')->twice()->with('apidoc::rules.alpha')->andReturn(false); - $this->translatorMock->shouldReceive('hasForLocale')->once()->with('apidoc::rules.alpha', 'en')->andReturn(true); - $this->translatorMock->shouldReceive('get')->once()->with('apidoc::rules.alpha', [], 'en')->andReturn('Only alphabetic characters allowed'); - - $description = RuleDescriptionParser::parse('alpha')->getDescription(); - - $this->assertEquals('Only alphabetic characters allowed', $description); - } - - public function testReturnsAnEmptyDescriptionIfNotAvailable() - { - $this->translatorMock->shouldReceive('hasForLocale')->once()->with('apidoc::rules.dummy_rule')->andReturn(false); - $this->translatorMock->shouldReceive('hasForLocale')->once()->with('apidoc::rules.dummy_rule', 'en')->andReturn(false); - - $description = RuleDescriptionParser::parse('dummy_rule')->getDescription(); - - $this->assertEmpty($description); - } - - public function testAllowsToPassParametersToTheDescription() - { - $this->translatorMock->shouldReceive('hasForLocale')->twice()->with('apidoc::rules.digits')->andReturn(false); - $this->translatorMock->shouldReceive('hasForLocale')->once()->with('apidoc::rules.digits', 'en')->andReturn(true); - $this->translatorMock->shouldReceive('get')->once()->with('apidoc::rules.digits', [], 'en')->andReturn('Must have an exact length of `:attribute`'); - - $description = RuleDescriptionParser::parse('digits')->with(2)->getDescription(); - - $this->assertEquals('Must have an exact length of `2`', $description); - } - - public function testAllowsToPassMultipleParametersToTheDescription() - { - $this->translatorMock->shouldReceive('hasForLocale')->twice()->with('apidoc::rules.required_if')->andReturn(false); - $this->translatorMock->shouldReceive('hasForLocale')->once()->with('apidoc::rules.required_if', 'en')->andReturn(true); - $this->translatorMock->shouldReceive('get')->once()->with('apidoc::rules.required_if', [], 'en')->andReturn('Required if `:attribute` is `:attribute`'); - - $description = RuleDescriptionParser::parse('required_if')->with(['2 + 2', 4])->getDescription(); - - $this->assertEquals('Required if `2 + 2` is `4`', $description); - } - - /** - * @param \Illuminate\Foundation\Application $app - * - * @return array - */ - protected function getPackageProviders($app) - { - return [ApiDocGeneratorServiceProvider::class]; - } - - /** - * Define environment setup. - * - * @param \Illuminate\Foundation\Application $app - * - * @return void - */ - protected function getEnvironmentSetUp($app) - { - $app['config']->set('app.locale', 'es'); // Just to be different from default language. - $app['config']->set('app.fallback_locale', 'ch'); // Just to be different from default language. - } -}