diff --git a/.gitignore b/.gitignore index 115d9205..7973379d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,13 @@ composer.lock .php_cs.cache /vendor/ public/ +resources/docs/ +resources/views/apidoc/ tests/public/ .idea/ coverage.xml results.xml docs/_build docs/make.bat +tests/public/docs/ +tests/resources/** diff --git a/CHANGELOG.md b/CHANGELOG.md index c283d697..292724dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [4.0.0] ### Added +- Support for Eloquent API resources (https://github.com/mpociot/laravel-apidoc-generator/pull/601) - `bindings` replaced by `@urlParam` annotation (https://github.com/mpociot/laravel-apidoc-generator/pull/599) - Better support for arrays and objects in bodyParams (https://github.com/mpociot/laravel-apidoc-generator/pull/597) +### Modified +- Made ResponseCalls strategy only execute if no successful responses exist. (https://github.com/mpociot/laravel-apidoc-generator/pull/605) +- Hide null responses in examples. (https://github.com/mpociot/laravel-apidoc-generator/pull/605) +- Made `responses` stage additive (https://github.com/mpociot/laravel-apidoc-generator/pull/605) +- Renamed `query` and `body` in `response_calls` config to `queryParams` and `bodyParams` (https://github.com/mpociot/laravel-apidoc-generator/pull/603) + +### Removed +- Removed `apply.response_calls.headers` in favour of `apply.headers` (https://github.com/mpociot/laravel-apidoc-generator/pull/603) +- Removed bindings in response_calls (https://github.com/mpociot/laravel-apidoc-generator/pull/599) + ## [3.17.1] - Thursday, 12 September 2019 ### Fixed - ResponseCalls: Call Lumen application correctly since it does not use HttpKernel (https://github.com/mpociot/laravel-apidoc-generator/pull/585) diff --git a/config/apidoc.php b/config/apidoc.php index 0638a39c..d300a02d 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -1,12 +1,13 @@ 'public/docs', + 'type' => 'static', /* * The router to be used (Laravel or Dingo). @@ -21,6 +22,9 @@ /* * Generate a Postman collection in addition to HTML docs. + * For 'static' docs, the collection will be generated to public/docs/collection.json. + * For 'laravel' docs, it will be generated to storage/app/apidoc/collection.json. + * The `ApiDoc::routes()` helper will add routes for both the HTML and the Postman collection. */ 'postman' => [ /* @@ -160,23 +164,23 @@ 'strategies' => [ 'metadata' => [ - \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Extracting\Strategies\Metadata\GetFromDocBlocks::class, ], 'urlParameters' => [ - \Mpociot\ApiDoc\Strategies\UrlParameters\GetFromUrlParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\UrlParameters\GetFromUrlParamTag::class, ], 'queryParameters' => [ - \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\QueryParameters\GetFromQueryParamTag::class, ], 'bodyParameters' => [ - \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class, ], 'responses' => [ - \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, - \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class, - \Mpociot\ApiDoc\Strategies\Responses\UseApiResourceTags::class, - \Mpociot\ApiDoc\Strategies\Responses\UseTransformerTags::class, - \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseFileTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseApiResourceTags::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseTransformerTags::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\ResponseCalls::class, ], ], diff --git a/docs/config.md b/docs/config.md index 99b08218..526280fa 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2,8 +2,19 @@ Before you can generate your documentation, you'll need to configure a few things in your `config/apidoc.php`. If you aren't sure what an option does, it's best to leave it set to the default. If you don't have this config file, see the [installation instructions](index.html#installation). -## `output` -This is the file path where the generated documentation will be written to. Note that the documentation is generated as static HTML and CSS assets, so the route is accessed directly, and not via the Laravel routing mechanism. This path should be relative to the root of your application. Default: **public/docs** +## `type` +This is the type of documentation output to generate. +- `static` will generate a static HTMl page in the `public/docs` folder, so anyone can visit your documentation page by going to {yourapp.domain}/docs. +- `laravel` will generate the documentation as a Blade view within the `resources/views/apidoc` folder, so you can add routing and authentication. + +If you're using `laravel` type, you can call `\Mpociot\ApiDoc\ApiDoc::routes()` from your routes file (usually `routes/web.php`). This method will create a `/doc` route for your documentation, along with a `/doc.json` variant that will return the Postman collection, if you have that enabled. This method returns the route, so you can call additional methods to customise it (by adding middleware, for instance). You can also pass in the path you'd like to use instead. + +```php +\Mpociot\ApiDoc\ApiDoc::routes("/apidoc")->middleware("auth.basic"); +``` +> Note: There is currently a known issue with using `/docs` as the path for `laravel` docs. You should not use it, as it conflicts with the folder structure in the `public` folder and may confuse the webserver. + +You may, of course, set up your own routing instead of using the `routes()` helper. ## `router` The router to use when processing your routes (can be Laravel or Dingo. Defaults to **Laravel**) @@ -13,6 +24,8 @@ The base URL to be used in examples and the Postman collection. By default, this ## `postman` This package can automatically generate a Postman collection for your routes, along with the documentation. This section is where you can configure (or disable) that. +- For `static` docs (see [type](#type)), the collection will be created in `public/docs/collection.json`, so it can be accessed by visiting {yourapp.domain}/docs/colllection.json. +- For `laravel` docs, the collection will be generated to `storage/app/apidoc/collection.json`. The `ApiDoc::routes()` helper will add a `/docs.json` endpoint to fetch it.. ### `enabled` Whether or not to generate a Postman API collection. Default: **true** diff --git a/docs/migrating.md b/docs/migrating.md new file mode 100644 index 00000000..0f7382c0 --- /dev/null +++ b/docs/migrating.md @@ -0,0 +1,2 @@ +# Migrating +Rename your old config file. Publish the config file diff --git a/docs/plugins.md b/docs/plugins.md index 3086851b..3bfa17ec 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -16,7 +16,7 @@ There are a number of strategies inccluded with the package, so you don't have t > Note: The included ResponseCalls strategy is designed to stop if a response with a 2xx status code has already been gotten via any other strategy. ## Strategies -To create a strategy, create a class that extends `\Mpociot\ApiDoc\Strategies\Strategy`. +To create a strategy, create a class that extends `\Mpociot\ApiDoc\Extracting\Strategies\Strategy`. The `__invoke` method of the strategy is where you perform your actions and return data. It receives the following arguments: - the route (instance of `\Illuminate\Routing\Route`) @@ -31,7 +31,7 @@ The `__invoke` method of the strategy is where you perform your actions and retu [ 'metadata' => [ - \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Extracting\Strategies\Metadata\GetFromDocBlocks::class, ], 'urlParameters' => [ - \Mpociot\ApiDoc\Strategies\UrlParameters\GetFromUrlParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\UrlParameters\GetFromUrlParamTag::class, ], 'queryParameters' => [ - \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\QueryParameters\GetFromQueryParamTag::class, ], 'bodyParameters' => [ - \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class, ], 'responses' => [ - \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, - \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class, - \Mpociot\ApiDoc\Strategies\Responses\UseApiResourceTags::class, - \Mpociot\ApiDoc\Strategies\Responses\UseTransformerTags::class, - \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseFileTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseApiResourceTags::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseTransformerTags::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\ResponseCalls::class, ], ], ... @@ -82,7 +82,7 @@ You can add, replace or remove strategies from here. In our case, we're adding o ```php 'bodyParameters' => [ - \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class, AddOrganizationIdBodyParameter::class, ], ``` @@ -124,9 +124,9 @@ You are also provided with the instance pproperty `stage`, which is set to the n ## Utilities You have access to a number of tools when developing strategies. They include: -- The `RouteDocBlocker` class (in the `\Mpociot\ApiDoc\Tools` namespace) has a single public static method, `getDocBlocksFromRoute(Route $route)`. It allows you to retrieve the docblocks for a given route. It returns an array of with two keys: `method` and `class` containing the docblocks for the method and controller handling the route respectively. Both are instances of `\Mpociot\Reflection\DocBlock`. +- The `RouteDocBlocker` class (in the `\Mpociot\ApiDoc\Extracting` namespace) has a single public static method, `getDocBlocksFromRoute(Route $route)`. It allows you to retrieve the docblocks for a given route. It returns an array of with two keys: `method` and `class` containing the docblocks for the method and controller handling the route respectively. Both are instances of `\Mpociot\Reflection\DocBlock`. -- The `ParamsHelper` trait (in the `\Mpociot\ApiDoc\Tools` namespace) can be included in your strategies. It contains a number of useful methods for working with parameters, including type casting and generating dummy values. +- The `ParamsHelper` trait (in the `\Mpociot\ApiDoc\Extracting` namespace) can be included in your strategies. It contains a number of useful methods for working with parameters, including type casting and generating dummy values. ## API Each strategy class must implement the __invoke method with the parameters as described above. This method must return the needed data for the intended stage, or `null` to indicate failure. diff --git a/src/ApiDoc.php b/src/ApiDoc.php new file mode 100644 index 00000000..80929af4 --- /dev/null +++ b/src/ApiDoc.php @@ -0,0 +1,25 @@ +get('apidoc/collection.json'), + 200, + ['Content-type' => 'application/json'] + + ); + } + + return view('apidoc.index'); + })->name('apidoc'); + } +} diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index 703e67f1..2404e652 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -10,11 +10,10 @@ use Mpociot\ApiDoc\Tools\Utils; use Mpociot\Reflection\DocBlock; use Illuminate\Support\Collection; +use Mpociot\ApiDoc\Writing\Writer; use Illuminate\Support\Facades\URL; -use Mpociot\ApiDoc\Tools\Generator; -use Mpociot\ApiDoc\Tools\RouteMatcher; -use Mpociot\Documentarian\Documentarian; -use Mpociot\ApiDoc\Postman\CollectionWriter; +use Mpociot\ApiDoc\Extracting\Generator; +use Mpociot\ApiDoc\Matching\RouteMatcher; use Mpociot\ApiDoc\Tools\DocumentationConfig; class GenerateDocumentation extends Command @@ -35,8 +34,6 @@ class GenerateDocumentation extends Command */ protected $description = 'Generate your API documentation from existing Laravel routes.'; - private $routeMatcher; - /** * @var DocumentationConfig */ @@ -47,10 +44,9 @@ class GenerateDocumentation extends Command */ private $baseUrl; - public function __construct(RouteMatcher $routeMatcher) + public function __construct() { parent::__construct(); - $this->routeMatcher = $routeMatcher; } /** @@ -67,159 +63,31 @@ public function handle() $this->docConfig = new DocumentationConfig(config('apidoc')); $this->baseUrl = $this->docConfig->get('base_url') ?? config('app.url'); - try { - URL::forceRootUrl($this->baseUrl); - } catch (\Error $e) { - echo "Warning: Couldn't force base url as your version of Lumen doesn't have the forceRootUrl method.\n"; - echo "You should probably double check URLs in your generated documentation.\n"; - } + URL::forceRootUrl($this->baseUrl); - $usingDingoRouter = strtolower($this->docConfig->get('router')) == 'dingo'; - $routes = $this->docConfig->get('routes'); - $routes = $usingDingoRouter - ? $this->routeMatcher->getDingoRoutesToBeDocumented($routes) - : $this->routeMatcher->getLaravelRoutesToBeDocumented($routes); + $routeMatcher = new RouteMatcher($this->docConfig->get('routes'), $this->docConfig->get('router')); + $routes = $routeMatcher->getRoutes(); $generator = new Generator($this->docConfig); $parsedRoutes = $this->processRoutes($generator, $routes); + $groupedRoutes = collect($parsedRoutes) ->groupBy('metadata.groupName') ->sortBy(static function ($group) { /* @var $group Collection */ return $group->first()['metadata']['groupName']; }, SORT_NATURAL); - - $this->writeMarkdown($groupedRoutes); - } - - /** - * @param Collection $parsedRoutes - * - * @return void - */ - private function writeMarkdown($parsedRoutes) - { - $outputPath = $this->docConfig->get('output'); - $targetFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'index.md'; - $compareFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'.compare.md'; - $prependFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'prepend.md'; - $appendFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'append.md'; - - $infoText = view('apidoc::partials.info') - ->with('outputPath', ltrim($outputPath, 'public/')) - ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection()); - - $settings = ['languages' => $this->docConfig->get('example_languages')]; - $parsedRouteOutput = $parsedRoutes->map(function ($routeGroup) use ($settings) { - return $routeGroup->map(function ($route) use ($settings) { - if (count($route['cleanBodyParameters']) && ! isset($route['headers']['Content-Type'])) { - // Set content type if the user forgot to set it - $route['headers']['Content-Type'] = 'application/json'; - } - $route['output'] = (string) view('apidoc::partials.route') - ->with('route', $route) - ->with('settings', $settings) - ->with('baseUrl', $this->baseUrl) - ->render(); - - return $route; - }); - }); - - $frontmatter = view('apidoc::partials.frontmatter') - ->with('settings', $settings); - /* - * In case the target file already exists, we should check if the documentation was modified - * and skip the modified parts of the routes. - */ - if (file_exists($targetFile) && file_exists($compareFile)) { - $generatedDocumentation = file_get_contents($targetFile); - $compareDocumentation = file_get_contents($compareFile); - - if (preg_match('/---(.*)---\\s/is', $generatedDocumentation, $generatedFrontmatter)) { - $frontmatter = trim($generatedFrontmatter[1], "\n"); - } - - $parsedRouteOutput->transform(function ($routeGroup) use ($generatedDocumentation, $compareDocumentation) { - return $routeGroup->transform(function ($route) use ($generatedDocumentation, $compareDocumentation) { - if (preg_match('/(.*)/is', $generatedDocumentation, $existingRouteDoc)) { - $routeDocumentationChanged = (preg_match('/(.*)/is', $compareDocumentation, $lastDocWeGeneratedForThisRoute) && $lastDocWeGeneratedForThisRoute[1] !== $existingRouteDoc[1]); - if ($routeDocumentationChanged === false || $this->option('force')) { - if ($routeDocumentationChanged) { - $this->warn('Discarded manual changes for route ['.implode(',', $route['methods']).'] '.$route['uri']); - } - } else { - $this->warn('Skipping modified route ['.implode(',', $route['methods']).'] '.$route['uri']); - $route['modified_output'] = $existingRouteDoc[0]; - } - } - - return $route; - }); - }); - } - - $prependFileContents = file_exists($prependFile) - ? file_get_contents($prependFile)."\n" : ''; - $appendFileContents = file_exists($appendFile) - ? "\n".file_get_contents($appendFile) : ''; - - $documentarian = new Documentarian(); - - $markdown = view('apidoc::documentarian') - ->with('writeCompareFile', false) - ->with('frontmatter', $frontmatter) - ->with('infoText', $infoText) - ->with('prependMd', $prependFileContents) - ->with('appendMd', $appendFileContents) - ->with('outputPath', $this->docConfig->get('output')) - ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection()) - ->with('parsedRoutes', $parsedRouteOutput); - - if (! is_dir($outputPath)) { - $documentarian->create($outputPath); - } - - // Write output file - file_put_contents($targetFile, $markdown); - - // Write comparable markdown file - $compareMarkdown = view('apidoc::documentarian') - ->with('writeCompareFile', true) - ->with('frontmatter', $frontmatter) - ->with('infoText', $infoText) - ->with('prependMd', $prependFileContents) - ->with('appendMd', $appendFileContents) - ->with('outputPath', $this->docConfig->get('output')) - ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection()) - ->with('parsedRoutes', $parsedRouteOutput); - - file_put_contents($compareFile, $compareMarkdown); - - $this->info('Wrote index.md to: '.$outputPath); - - $this->info('Generating API HTML code'); - - $documentarian->generate($outputPath); - - $this->info('Wrote HTML documentation to: '.$outputPath.'/index.html'); - - if ($this->shouldGeneratePostmanCollection()) { - $this->info('Generating Postman collection'); - - file_put_contents($outputPath.DIRECTORY_SEPARATOR.'collection.json', $this->generatePostmanCollection($parsedRoutes)); - } - - if ($logo = $this->docConfig->get('logo')) { - copy( - $logo, - $outputPath.DIRECTORY_SEPARATOR.'images'.DIRECTORY_SEPARATOR.'logo.png' - ); - } + $writer = new Writer( + $groupedRoutes, + $this->option('force'), + $this, + $this->docConfig + ); + $writer->writeDocs(); } /** - * @param Generator $generator + * @param \Mpociot\ApiDoc\Extracting\Generator $generator * @param array $routes * * @return array @@ -286,28 +154,4 @@ private function isRouteVisibleForDocumentation(array $action) return true; } - - /** - * Generate Postman collection JSON file. - * - * @param Collection $routes - * - * @return string - */ - private function generatePostmanCollection(Collection $routes) - { - $writer = new CollectionWriter($routes, $this->baseUrl); - - return $writer->getCollection(); - } - - /** - * Checks config if it should generate Postman collection. - * - * @return bool - */ - private function shouldGeneratePostmanCollection() - { - return $this->docConfig->get('postman.enabled', is_bool($this->docConfig->get('postman')) ? $this->docConfig->get('postman') : false); - } } diff --git a/src/Tools/Generator.php b/src/Extracting/Generator.php similarity index 89% rename from src/Tools/Generator.php rename to src/Extracting/Generator.php index df2d96b6..283cf702 100644 --- a/src/Tools/Generator.php +++ b/src/Extracting/Generator.php @@ -1,12 +1,14 @@ [ - \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Extracting\Strategies\Metadata\GetFromDocBlocks::class, ], 'urlParameters' => [ - \Mpociot\ApiDoc\Strategies\UrlParameters\GetFromUrlParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\UrlParameters\GetFromUrlParamTag::class, ], 'queryParameters' => [ - \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\QueryParameters\GetFromQueryParamTag::class, ], 'bodyParameters' => [ - \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class, ], 'responses' => [ - \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, - \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class, - \Mpociot\ApiDoc\Strategies\Responses\UseApiResourceTags::class, - \Mpociot\ApiDoc\Strategies\Responses\UseTransformerTags::class, - \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseFileTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseApiResourceTags::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseTransformerTags::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\ResponseCalls::class, ], ]; diff --git a/src/Tools/Traits/ParamHelpers.php b/src/Extracting/ParamHelpers.php similarity index 62% rename from src/Tools/Traits/ParamHelpers.php rename to src/Extracting/ParamHelpers.php index 2afefb19..0c566758 100644 --- a/src/Tools/Traits/ParamHelpers.php +++ b/src/Extracting/ParamHelpers.php @@ -1,6 +1,6 @@ 'intval', - 'number' => 'floatval', + 'int' => 'intval', 'float' => 'floatval', + 'number' => 'floatval', + 'double' => 'floatval', 'boolean' => 'boolval', + 'bool' => 'boolval', ]; // First, we handle booleans. We can't use a regular cast, //because PHP considers string 'false' as true. - if ($value == 'false' && $type == 'boolean') { + if ($value == 'false' && ($type == 'boolean' || $type == 'bool')) { return false; } @@ -89,4 +92,39 @@ protected function normalizeParameterType(string $type) return $type ? ($typeMap[$type] ?? $type) : 'string'; } + + /** + * Allows users to specify that we shouldn't generate an example for the parameter + * by writing 'No-example'. + * + * @param string $description + * + * @return bool If true, don't generate an example for this. + */ + protected function shouldExcludeExample(string $description) + { + return strpos($description, ' No-example') !== false; + } + + /** + * Allows users to specify an example for the parameter by writing 'Example: the-example', + * to be used in example requests and response calls. + * + * @param string $description + * @param string $type The type of the parameter. Used to cast the example provided, if any. + * + * @return array The description and included example. + */ + protected function parseParamDescription(string $description, string $type) + { + $example = null; + if (preg_match('/(.*)\bExample:\s*(.+)\s*/', $description, $content)) { + $description = trim($content[1]); + + // examples are parsed as strings by default, we need to cast them properly + $example = $this->castToType($content[2], $type); + } + + return [$description, $example]; + } } diff --git a/src/Tools/RouteDocBlocker.php b/src/Extracting/RouteDocBlocker.php similarity index 89% rename from src/Tools/RouteDocBlocker.php rename to src/Extracting/RouteDocBlocker.php index 83f48169..cdbd1acf 100644 --- a/src/Tools/RouteDocBlocker.php +++ b/src/Extracting/RouteDocBlocker.php @@ -1,11 +1,17 @@ getBodyParametersFromDocBlock($methodDocBlock->getTags()); @@ -56,7 +57,7 @@ private function getBodyParametersFromDocBlock($tags) ->filter(function ($tag) { return $tag instanceof Tag && $tag->getName() === 'bodyParam'; }) - ->mapWithKeys(function ($tag) { + ->mapWithKeys(function (Tag $tag) { // Format: // @bodyParam <"required" (optional)> // Examples: @@ -81,7 +82,7 @@ private function getBodyParametersFromDocBlock($tags) $type = $this->normalizeParameterType($type); list($description, $example) = $this->parseParamDescription($description, $type); - $value = is_null($example) && ! $this->shouldExcludeExample($tag) + $value = is_null($example) && ! $this->shouldExcludeExample($tag->getContent()) ? $this->generateDummyValue($type) : $example; diff --git a/src/Strategies/Metadata/GetFromDocBlocks.php b/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php similarity index 96% rename from src/Strategies/Metadata/GetFromDocBlocks.php rename to src/Extracting/Strategies/Metadata/GetFromDocBlocks.php index dfa83bcd..e74faf06 100644 --- a/src/Strategies/Metadata/GetFromDocBlocks.php +++ b/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php @@ -1,14 +1,14 @@ getQueryParametersFromDocBlock($methodDocBlock->getTags()); @@ -57,7 +58,7 @@ private function getQueryParametersFromDocBlock($tags) ->filter(function ($tag) { return $tag instanceof Tag && $tag->getName() === 'queryParam'; }) - ->mapWithKeys(function ($tag) { + ->mapWithKeys(function (Tag $tag) { // Format: // @queryParam <"required" (optional)> // Examples: @@ -81,7 +82,7 @@ private function getQueryParametersFromDocBlock($tags) } list($description, $value) = $this->parseParamDescription($description, 'string'); - if (is_null($value) && ! $this->shouldExcludeExample($tag)) { + if (is_null($value) && ! $this->shouldExcludeExample($tag->getContent())) { $value = Str::contains($description, ['number', 'count', 'page']) ? $this->generateDummyValue('integer') : $this->generateDummyValue('string'); diff --git a/src/Strategies/Responses/ResponseCalls.php b/src/Extracting/Strategies/Responses/ResponseCalls.php similarity index 98% rename from src/Strategies/Responses/ResponseCalls.php rename to src/Extracting/Strategies/Responses/ResponseCalls.php index b04d22cf..b1b39da1 100644 --- a/src/Strategies/Responses/ResponseCalls.php +++ b/src/Extracting/Strategies/Responses/ResponseCalls.php @@ -1,6 +1,6 @@ getUrlParametersFromDocBlock($methodDocBlock->getTags()); @@ -57,7 +58,7 @@ private function getUrlParametersFromDocBlock($tags) ->filter(function ($tag) { return $tag instanceof Tag && $tag->getName() === 'urlParam'; }) - ->mapWithKeys(function ($tag) { + ->mapWithKeys(function (Tag $tag) { // Format: // @urlParam <"required" (optional)> // Examples: @@ -81,7 +82,7 @@ private function getUrlParametersFromDocBlock($tags) } list($description, $value) = $this->parseParamDescription($description, 'string'); - if (is_null($value) && ! $this->shouldExcludeExample($tag)) { + if (is_null($value) && ! $this->shouldExcludeExample($tag->getContent())) { $value = Str::contains($description, ['number', 'count', 'page']) ? $this->generateDummyValue('integer') : $this->generateDummyValue('string'); diff --git a/src/Tools/LumenRouteAdapter.php b/src/Matching/LumenRouteAdapter.php similarity index 70% rename from src/Tools/LumenRouteAdapter.php rename to src/Matching/LumenRouteAdapter.php index 03b89f98..762dae39 100644 --- a/src/Tools/LumenRouteAdapter.php +++ b/src/Matching/LumenRouteAdapter.php @@ -1,11 +1,13 @@ getRoutesToBeDocumented($routeRules, true); + $this->router = $router; + $this->routeRules = $routeRules; } - public function getLaravelRoutesToBeDocumented(array $routeRules) + public function getRoutes() { - return $this->getRoutesToBeDocumented($routeRules); + $usingDingoRouter = strtolower($this->router) == 'dingo'; + + return $this->getRoutesToBeDocumented($this->routeRules, $usingDingoRouter); } - public function getRoutesToBeDocumented(array $routeRules, bool $usingDingoRouter = false) + protected function getRoutesToBeDocumented(array $routeRules, bool $usingDingoRouter = false) { + $allRoutes = $this->getAllRoutes($usingDingoRouter); $matchedRoutes = []; foreach ($routeRules as $routeRule) { $includes = $routeRule['include'] ?? []; - $allRoutes = $this->getAllRoutes($usingDingoRouter, $routeRule['match']['versions'] ?? []); foreach ($allRoutes as $route) { if (is_array($route)) { @@ -49,10 +62,7 @@ public function getRoutesToBeDocumented(array $routeRules, bool $usingDingoRoute return $matchedRoutes; } - // TODO we should cache the results of this, for Laravel routes at least, - // to improve performance, since this method gets called - // for each ruleset in the config file. Not a high priority, though. - private function getAllRoutes(bool $usingDingoRouter, array $versions = []) + private function getAllRoutes(bool $usingDingoRouter) { if (! $usingDingoRouter) { return RouteFacade::getRoutes(); @@ -83,6 +93,9 @@ private function shouldExcludeRoute(Route $route, array $routeRule) { $excludes = $routeRule['exclude'] ?? []; + // Exclude this package's routes + $excludes[] = 'apidoc'; + // Exclude Laravel Telescope routes if (class_exists("Laravel\Telescope\Telescope")) { $excludes[] = 'telescope/*'; diff --git a/src/Tools/Traits/DocBlockParamHelpers.php b/src/Tools/Traits/DocBlockParamHelpers.php deleted file mode 100644 index a22168aa..00000000 --- a/src/Tools/Traits/DocBlockParamHelpers.php +++ /dev/null @@ -1,45 +0,0 @@ -getContent(), ' No-example') !== false; - } - - /** - * Allows users to specify an example for the parameter by writing 'Example: the-example', - * to be used in example requests and response calls. - * - * @param string $description - * @param string $type The type of the parameter. Used to cast the example provided, if any. - * - * @return array The description and included example. - */ - protected function parseParamDescription(string $description, string $type) - { - $example = null; - if (preg_match('/(.*)\bExample:\s*(.+)\s*/', $description, $content)) { - $description = trim($content[1]); - - // examples are parsed as strings by default, we need to cast them properly - $example = $this->castToType($content[2], $type); - } - - return [$description, $example]; - } -} diff --git a/src/Postman/CollectionWriter.php b/src/Writing/PostmanCollectionWriter.php similarity index 80% rename from src/Postman/CollectionWriter.php rename to src/Writing/PostmanCollectionWriter.php index 05acf2eb..4ef51928 100644 --- a/src/Postman/CollectionWriter.php +++ b/src/Writing/PostmanCollectionWriter.php @@ -1,13 +1,13 @@ baseUrl); - if (Str::startsWith($this->baseUrl, 'https://')) { - URL::forceScheme('https'); - } - } catch (\Error $e) { - echo "Warning: Couldn't force base url as your version of Lumen doesn't have the forceRootUrl method.\n"; - echo "You should probably double check URLs in your generated Postman collection.\n"; + URL::forceRootUrl($this->baseUrl); + if (Str::startsWith($this->baseUrl, 'https://')) { + URL::forceScheme('https'); } $collection = [ @@ -61,10 +56,10 @@ public function getCollection() 'name' => $route['metadata']['title'] != '' ? $route['metadata']['title'] : url($route['uri']), 'request' => [ 'url' => url($route['uri']).(collect($route['queryParameters'])->isEmpty() - ? '' - : ('?'.implode('&', collect($route['queryParameters'])->map(function ($parameter, $key) { - return urlencode($key).'='.urlencode($parameter['value'] ?? ''); - })->all()))), + ? '' + : ('?'.implode('&', collect($route['queryParameters'])->map(function ($parameter, $key) { + return urlencode($key).'='.urlencode($parameter['value'] ?? ''); + })->all()))), 'method' => $route['methods'][0], 'header' => collect($route['headers']) ->union([ diff --git a/src/Writing/Writer.php b/src/Writing/Writer.php new file mode 100644 index 00000000..6a365763 --- /dev/null +++ b/src/Writing/Writer.php @@ -0,0 +1,306 @@ +routes = $routes; + // If no config is injected, pull from global + $this->config = $config ?: new DocumentationConfig(config('apidoc')); + $this->baseUrl = $this->config->get('base_url') ?? config('app.url'); + $this->forceIt = $forceIt; + $this->output = $output; + $this->shouldGeneratePostmanCollection = $this->config->get('postman.enabled', false); + $this->documentarian = new Documentarian(); + } + + public function writeDocs() + { + // The source files (index.md, js/, css/, and images/) always go in resources/docs/source. + // The static assets (js/, css/, and images/) always go in public/docs/. + // For 'static' docs, the output files (index.html, collection.json) go in public/docs/. + // For 'laravel' docs, the output files (index.blade.php, collection.json) + // go in resources/views/apidoc/ and storage/app/apidoc/ respectively. + $isStatic = $this->config->get('type') === 'static'; + + $sourceOutputPath = 'resources/docs'; + $outputPath = $isStatic ? 'public/docs' : 'resources/views/apidoc'; + + $this->writeMarkdownAndSourceFiles($this->routes, $sourceOutputPath); + + $this->writeHtmlDocs($sourceOutputPath, $outputPath, $isStatic); + + $this->writePostmanCollection($this->routes, $outputPath, $isStatic); + } + + /** + * @param Collection $parsedRoutes + * + * @return void + */ + public function writeMarkdownAndSourceFiles(Collection $parsedRoutes, string $sourceOutputPath) + { + $targetFile = $sourceOutputPath.'/source/index.md'; + $compareFile = $sourceOutputPath.'/source/.compare.md'; + + $infoText = view('apidoc::partials.info') + ->with('outputPath', 'docs') + ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection); + + $settings = ['languages' => $this->config->get('example_languages')]; + // Generate Markdown for each route + $parsedRouteOutput = $this->generateMarkdownOutputForEachRoute($parsedRoutes, $settings); + + $frontmatter = view('apidoc::partials.frontmatter') + ->with('settings', $settings); + + /* + * If the target file already exists, + * we check if the documentation was modified + * and skip the modified parts of the routes. + */ + if (file_exists($targetFile) && file_exists($compareFile)) { + $generatedDocumentation = file_get_contents($targetFile); + $compareDocumentation = file_get_contents($compareFile); + + $parsedRouteOutput->transform(function (Collection $routeGroup) use ($generatedDocumentation, $compareDocumentation) { + return $routeGroup->transform(function (array $route) use ($generatedDocumentation, $compareDocumentation) { + if (preg_match('/(.*)/is', $generatedDocumentation, $existingRouteDoc)) { + $routeDocumentationChanged = (preg_match('/(.*)/is', $compareDocumentation, $lastDocWeGeneratedForThisRoute) && $lastDocWeGeneratedForThisRoute[1] !== $existingRouteDoc[1]); + if ($routeDocumentationChanged === false || $this->forceIt) { + if ($routeDocumentationChanged) { + $this->output->warn('Discarded manual changes for route ['.implode(',', $route['methods']).'] '.$route['uri']); + } + } else { + $this->output->warn('Skipping modified route ['.implode(',', $route['methods']).'] '.$route['uri']); + $route['modified_output'] = $existingRouteDoc[0]; + } + } + + return $route; + }); + }); + } + + $prependFileContents = $this->getMarkdownToPrepend($sourceOutputPath); + $appendFileContents = $this->getMarkdownToAppend($sourceOutputPath); + + $markdown = view('apidoc::documentarian') + ->with('writeCompareFile', false) + ->with('frontmatter', $frontmatter) + ->with('infoText', $infoText) + ->with('prependMd', $prependFileContents) + ->with('appendMd', $appendFileContents) + ->with('outputPath', $this->config->get('output')) + ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection) + ->with('parsedRoutes', $parsedRouteOutput); + + $this->output->info('Writing index.md and source files to: '.$sourceOutputPath); + + if (! is_dir($sourceOutputPath)) { + $documentarian = new Documentarian(); + $documentarian->create($sourceOutputPath); + } + + // Write output file + file_put_contents($targetFile, $markdown); + + // Write comparable markdown file + $compareMarkdown = view('apidoc::documentarian') + ->with('writeCompareFile', true) + ->with('frontmatter', $frontmatter) + ->with('infoText', $infoText) + ->with('prependMd', $prependFileContents) + ->with('appendMd', $appendFileContents) + ->with('outputPath', $this->config->get('output')) + ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection) + ->with('parsedRoutes', $parsedRouteOutput); + + file_put_contents($compareFile, $compareMarkdown); + + $this->output->info('Wrote index.md and source files to: '.$sourceOutputPath); + } + + public function generateMarkdownOutputForEachRoute(Collection $parsedRoutes, array $settings): Collection + { + $parsedRouteOutput = $parsedRoutes->map(function (Collection $routeGroup) use ($settings) { + return $routeGroup->map(function (array $route) use ($settings) { + if (count($route['cleanBodyParameters']) && ! isset($route['headers']['Content-Type'])) { + // Set content type if the user forgot to set it + $route['headers']['Content-Type'] = 'application/json'; + } + $route['output'] = (string) view('apidoc::partials.route') + ->with('route', $route) + ->with('settings', $settings) + ->with('baseUrl', $this->baseUrl) + ->render(); + + return $route; + }); + }); + + return $parsedRouteOutput; + } + + protected function writePostmanCollection(Collection $parsedRoutes, string $outputPath, bool $isStatic): void + { + if ($this->shouldGeneratePostmanCollection) { + $this->output->info('Generating Postman collection'); + + $collection = $this->generatePostmanCollection($parsedRoutes); + if ($isStatic) { + $collectionPath = "{$outputPath}/collection.json"; + file_put_contents($collectionPath, $collection); + } else { + Storage::disk('local')->put('apidoc/collection.json', $collection); + $collectionPath = 'storage/app/apidoc/collection.json'; + } + + $this->output->info("Wrote Postman collection to: {$collectionPath}"); + } + } + + /** + * Generate Postman collection JSON file. + * + * @param Collection $routes + * + * @return string + */ + public function generatePostmanCollection(Collection $routes) + { + $writer = new PostmanCollectionWriter($routes, $this->baseUrl); + + return $writer->getCollection(); + } + + /** + * @param string $sourceOutputPath + * + * @return string + */ + protected function getMarkdownToPrepend(string $sourceOutputPath): string + { + $prependFile = $sourceOutputPath.'/source/prepend.md'; + $prependFileContents = file_exists($prependFile) + ? file_get_contents($prependFile)."\n" : ''; + + return $prependFileContents; + } + + /** + * @param string $sourceOutputPath + * + * @return string + */ + protected function getMarkdownToAppend(string $sourceOutputPath): string + { + $appendFile = $sourceOutputPath.'/source/append.md'; + $appendFileContents = file_exists($appendFile) + ? "\n".file_get_contents($appendFile) : ''; + + return $appendFileContents; + } + + protected function copyAssetsFromSourceFolderToPublicFolder(string $sourceOutputPath, bool $isStatic = true): void + { + $publicPath = 'public/docs'; + if (! is_dir($publicPath)) { + mkdir($publicPath, 0777, true); + mkdir("{$publicPath}/css"); + mkdir("{$publicPath}/js"); + } + copy("{$sourceOutputPath}/js/all.js", "{$publicPath}/js/all.js"); + rcopy("{$sourceOutputPath}/images", "{$publicPath}/images"); + rcopy("{$sourceOutputPath}/css", "{$publicPath}/css"); + + if ($logo = $this->config->get('logo')) { + if ($isStatic) { + copy($logo, "{$publicPath}/images/logo.png"); + } + } + } + + protected function moveOutputFromSourceFolderToTargetFolder(string $sourceOutputPath, string $outputPath, bool $isStatic): void + { + if ($isStatic) { + // Move output (index.html, css/style.css and js/all.js) to public/docs + rename("{$sourceOutputPath}/index.html", "{$outputPath}/index.html"); + } else { + // Move output to resources/views + if (! is_dir($outputPath)) { + mkdir($outputPath); + } + rename("{$sourceOutputPath}/index.html", "$outputPath/index.blade.php"); + $contents = file_get_contents("$outputPath/index.blade.php"); + // + $contents = str_replace('href="css/style.css"', 'href="/docs/css/style.css"', $contents); + $contents = str_replace('src="js/all.js"', 'src="/docs/js/all.js"', $contents); + $contents = str_replace('src="images/', 'src="/docs/images/', $contents); + $contents = preg_replace('#href="http://.+?/docs/collection.json"#', 'href="{{ route("apidoc", ["format" => ".json"]) }}"', $contents); + file_put_contents("$outputPath/index.blade.php", $contents); + } + } + + /** + * @param string $sourceOutputPath + * @param string $outputPath + * @param bool $isStatic + */ + protected function writeHtmlDocs(string $sourceOutputPath, string $outputPath, bool $isStatic): void + { + $this->output->info('Generating API HTML code'); + + $this->documentarian->generate($sourceOutputPath); + + // Move assets to public folder + $this->copyAssetsFromSourceFolderToPublicFolder($sourceOutputPath, $isStatic); + + $this->moveOutputFromSourceFolderToTargetFolder($sourceOutputPath, $outputPath, $isStatic); + + $this->output->info("Wrote HTML documentation to: {$outputPath}"); + } +} diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index e69bfd91..3dfa1b2a 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -38,6 +38,7 @@ protected function setUp(): void public function tearDown(): void { Utils::deleteDirectoryAndContents('/public/docs'); + Utils::deleteDirectoryAndContents('/resources/docs'); } /** @@ -128,23 +129,15 @@ public function can_parse_resource_routes() $this->artisan('apidoc:generate'); $fixtureMarkdown = __DIR__.'/Fixtures/resource_index.md'; - $generatedMarkdown = __DIR__.'/../public/docs/source/index.md'; + $generatedMarkdown = __DIR__.'/../resources/docs/source/index.md'; $this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown); } /** @test */ public function can_parse_partial_resource_routes() { - if (version_compare(App::version(), '5.6', '<')) { - RouteFacade::resource('/api/users', TestResourceController::class, [ - 'only' => [ - 'index', 'create', - ], - ]); - } else { - RouteFacade::resource('/api/users', TestResourceController::class) + RouteFacade::resource('/api/users', TestResourceController::class) ->only(['index', 'create']); - } config(['apidoc.routes.0.match.prefixes' => ['api/*']]); config([ @@ -156,23 +149,16 @@ public function can_parse_partial_resource_routes() $this->artisan('apidoc:generate'); $fixtureMarkdown = __DIR__.'/Fixtures/partial_resource_index.md'; - $generatedMarkdown = __DIR__.'/../public/docs/source/index.md'; + $generatedMarkdown = __DIR__.'/../resources/docs/source/index.md'; $this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown); - if (version_compare(App::version(), '5.6', '<')) { - RouteFacade::apiResource('/api/users', TestResourceController::class, [ - 'only' => [ - 'index', 'create', - ], - ]); - } else { - RouteFacade::apiResource('/api/users', TestResourceController::class) + RouteFacade::apiResource('/api/users', TestResourceController::class) ->only(['index', 'create']); - } + $this->artisan('apidoc:generate'); $fixtureMarkdown = __DIR__.'/Fixtures/partial_resource_index.md'; - $generatedMarkdown = __DIR__.'/../public/docs/source/index.md'; + $generatedMarkdown = __DIR__.'/../resources/docs/source/index.md'; $this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown); } @@ -190,6 +176,7 @@ public function generated_markdown_file_is_correct() RouteFacade::get('/api/echoesUrlParameters/{param}-{param2}/{param3?}', [TestController::class, 'echoesUrlParameters']); // We want to have the same values for params each time + config(['apidoc.type' => 'static']); config(['apidoc.faker_seed' => 1234]); config(['apidoc.routes.0.match.prefixes' => ['api/*']]); config([ @@ -202,8 +189,8 @@ public function generated_markdown_file_is_correct() ]); $this->artisan('apidoc:generate'); - $generatedMarkdown = __DIR__.'/../public/docs/source/index.md'; - $compareMarkdown = __DIR__.'/../public/docs/source/.compare.md'; + $generatedMarkdown = __DIR__.'/../resources/docs/source/index.md'; + $compareMarkdown = __DIR__.'/../resources/docs/source/.compare.md'; $fixtureMarkdown = __DIR__.'/Fixtures/index.md'; $this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown); @@ -221,12 +208,12 @@ public function can_prepend_and_append_data_to_generated_markdown() $prependMarkdown = __DIR__.'/Fixtures/prepend.md'; $appendMarkdown = __DIR__.'/Fixtures/append.md'; - copy($prependMarkdown, __DIR__.'/../public/docs/source/prepend.md'); - copy($appendMarkdown, __DIR__.'/../public/docs/source/append.md'); + copy($prependMarkdown, __DIR__.'/../resources/docs/source/prepend.md'); + copy($appendMarkdown, __DIR__.'/../resources/docs/source/append.md'); $this->artisan('apidoc:generate'); - $generatedMarkdown = __DIR__.'/../public/docs/source/index.md'; + $generatedMarkdown = __DIR__.'/../resources/docs/source/index.md'; $this->assertContainsIgnoringWhitespace($this->getFileContents($prependMarkdown), $this->getFileContents($generatedMarkdown)); $this->assertContainsIgnoringWhitespace($this->getFileContents($appendMarkdown), $this->getFileContents($generatedMarkdown)); } @@ -380,7 +367,7 @@ public function can_append_custom_http_headers() ]); $this->artisan('apidoc:generate'); - $generatedMarkdown = $this->getFileContents(__DIR__.'/../public/docs/source/index.md'); + $generatedMarkdown = $this->getFileContents(__DIR__.'/../resources/docs/source/index.md'); $this->assertContainsIgnoringWhitespace('"Authorization": "customAuthToken","Custom-Header":"NotSoCustom"', $generatedMarkdown); } @@ -392,7 +379,7 @@ public function can_parse_utf8_response() config(['apidoc.routes.0.prefixes' => ['api/*']]); $this->artisan('apidoc:generate'); - $generatedMarkdown = file_get_contents(__DIR__.'/../public/docs/source/index.md'); + $generatedMarkdown = file_get_contents(__DIR__.'/../resources/docs/source/index.md'); $this->assertContains('Лорем ипсум долор сит амет', $generatedMarkdown); } @@ -406,7 +393,7 @@ public function sorts_group_naturally() config(['apidoc.routes.0.prefixes' => ['api/*']]); $this->artisan('apidoc:generate'); - $generatedMarkdown = file_get_contents(__DIR__.'/../public/docs/source/index.md'); + $generatedMarkdown = file_get_contents(__DIR__.'/../resources/docs/source/index.md'); $firstGroup1Occurrence = strpos($generatedMarkdown, '#1. Group 1'); $firstGroup2Occurrence = strpos($generatedMarkdown, '#2. Group 2'); @@ -437,7 +424,7 @@ public function supports_partial_resource_controller() } $this->assertNull($thrownException); - $generatedMarkdown = file_get_contents(__DIR__.'/../public/docs/source/index.md'); + $generatedMarkdown = file_get_contents(__DIR__.'/../resources/docs/source/index.md'); $this->assertContains('Group A', $generatedMarkdown); $this->assertContains('Group B', $generatedMarkdown); } diff --git a/tests/Unit/GeneratorPluginSystemTestCase.php b/tests/Unit/GeneratorPluginSystemTestCase.php index 0bc203a7..5dbdac4b 100644 --- a/tests/Unit/GeneratorPluginSystemTestCase.php +++ b/tests/Unit/GeneratorPluginSystemTestCase.php @@ -5,16 +5,16 @@ use ReflectionClass; use ReflectionMethod; use Illuminate\Routing\Route; -use Mpociot\ApiDoc\Tools\Generator; -use Mpociot\ApiDoc\Strategies\Strategy; +use Mpociot\ApiDoc\Extracting\Generator; use Mpociot\ApiDoc\Tools\DocumentationConfig; use Mpociot\ApiDoc\Tests\Fixtures\TestController; use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; class GeneratorPluginSystemTestCase extends LaravelGeneratorTest { /** - * @var \Mpociot\ApiDoc\Tools\Generator + * @var \Mpociot\ApiDoc\Extracting\Generator */ protected $generator; @@ -98,8 +98,7 @@ public function combines_results_from_different_strategies_in_same_stage() 'description' => 'dummy', 'authenticated' => false, ]; - $this->assertArraySubset($expectedMetadata, $parsed['metadata']); // Forwards-compatibility - $this->assertArraySubset($expectedMetadata, $parsed); // Backwards-compatibility + $this->assertArraySubset($expectedMetadata, $parsed['metadata']); } /** @test */ @@ -121,8 +120,7 @@ public function missing_metadata_is_filled_in() 'description' => 'dummy', 'authenticated' => false, ]; - $this->assertArraySubset($expectedMetadata, $parsed['metadata']); // Forwards-compatibility - $this->assertArraySubset($expectedMetadata, $parsed); // Backwards-compatibility + $this->assertArraySubset($expectedMetadata, $parsed['metadata']); } /** @test */ @@ -144,8 +142,7 @@ public function overwrites_metadat_from_previous_strategies_in_same_stage() 'description' => 'dummy', 'authenticated' => false, ]; - $this->assertArraySubset($expectedMetadata, $parsed['metadata']); // Forwards-compatibility - $this->assertArraySubset($expectedMetadata, $parsed); // Backwards-compatibility + $this->assertArraySubset($expectedMetadata, $parsed['metadata']); } public function dataResources() diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 14a3d374..ae5a8f1c 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -6,7 +6,7 @@ use Illuminate\Support\Arr; use Orchestra\Testbench\TestCase; -use Mpociot\ApiDoc\Tools\Generator; +use Mpociot\ApiDoc\Extracting\Generator; use Mpociot\ApiDoc\Tests\Fixtures\TestUser; use Mpociot\ApiDoc\Tools\DocumentationConfig; use Mpociot\ApiDoc\Tests\Fixtures\TestController; @@ -15,29 +15,29 @@ abstract class GeneratorTestCase extends TestCase { /** - * @var \Mpociot\ApiDoc\Tools\Generator + * @var \Mpociot\ApiDoc\Extracting\Generator */ protected $generator; private $config = [ 'strategies' => [ 'metadata' => [ - \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Extracting\Strategies\Metadata\GetFromDocBlocks::class, ], 'urlParameters' => [ - \Mpociot\ApiDoc\Strategies\UrlParameters\GetFromUrlParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\UrlParameters\GetFromUrlParamTag::class, ], 'queryParameters' => [ - \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\QueryParameters\GetFromQueryParamTag::class, ], 'bodyParameters' => [ - \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class, ], 'responses' => [ - \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, - \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class, - \Mpociot\ApiDoc\Strategies\Responses\UseApiResourceTags::class, - \Mpociot\ApiDoc\Strategies\Responses\UseTransformerTags::class, - \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseFileTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseApiResourceTags::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseTransformerTags::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\ResponseCalls::class, ], ], 'default_group' => 'general', @@ -398,7 +398,7 @@ public function can_parse_apiresource_tags() $route = $this->createRoute('POST', '/withEloquentApiResource', 'withEloquentApiResource'); $config = $this->config; - $config['strategies']['responses'] = [\Mpociot\ApiDoc\Strategies\Responses\UseApiResourceTags::class]; + $config['strategies']['responses'] = [\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseApiResourceTags::class]; $generator = new Generator(new DocumentationConfig($config)); $parsed = $this->generator->processRoute($route); @@ -421,7 +421,7 @@ public function can_parse_apiresourcecollection_tags() $route = $this->createRoute('POST', '/withEloquentApiResourceCollection', 'withEloquentApiResourceCollection'); $config = $this->config; - $config['strategies']['responses'] = [\Mpociot\ApiDoc\Strategies\Responses\UseApiResourceTags::class]; + $config['strategies']['responses'] = [\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseApiResourceTags::class]; $generator = new Generator(new DocumentationConfig($config)); $parsed = $this->generator->processRoute($route); @@ -451,7 +451,7 @@ public function can_parse_apiresourcecollection_tags_with_collection_class() $route = $this->createRoute('POST', '/withEloquentApiResourceCollectionClass', 'withEloquentApiResourceCollectionClass'); $config = $this->config; - $config['strategies']['responses'] = [\Mpociot\ApiDoc\Strategies\Responses\UseApiResourceTags::class]; + $config['strategies']['responses'] = [\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseApiResourceTags::class]; $generator = new Generator(new DocumentationConfig($config)); $parsed = $this->generator->processRoute($route); @@ -692,8 +692,8 @@ public function does_not_make_response_call_if_success_response_already_gotten() $config = [ 'strategies' => [ 'responses' => [ - \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, - \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseTag::class, + \Mpociot\ApiDoc\Extracting\Strategies\Responses\ResponseCalls::class, ], ], ]; diff --git a/tests/Unit/RouteMatcherTest.php b/tests/Unit/RouteMatcherTest.php index 2857add3..d800ee97 100644 --- a/tests/Unit/RouteMatcherTest.php +++ b/tests/Unit/RouteMatcherTest.php @@ -5,22 +5,11 @@ use Illuminate\Support\Str; use Dingo\Api\Routing\Router; use Orchestra\Testbench\TestCase; -use Mpociot\ApiDoc\Tools\RouteMatcher; +use Mpociot\ApiDoc\Matching\RouteMatcher; use Illuminate\Support\Facades\Route as RouteFacade; class RouteMatcherTest extends TestCase { - /** - * @var RouteMatcher - */ - private $matcher; - - protected function setUp(): void - { - parent::setUp(); - $this->matcher = new RouteMatcher(); - } - protected function getPackageProviders($app) { return [ @@ -34,22 +23,26 @@ public function testRespectsDomainsRuleForLaravelRouter() $routeRules[0]['match']['prefixes'] = ['*']; $routeRules[0]['match']['domains'] = ['*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $this->assertCount(12, $routes); $routeRules[0]['match']['domains'] = ['domain1.*', 'domain2.*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $this->assertCount(12, $routes); $routeRules[0]['match']['domains'] = ['domain1.*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $this->assertCount(6, $routes); foreach ($routes as $route) { $this->assertContains('domain1', $route['route']->getDomain()); } $routeRules[0]['match']['domains'] = ['domain2.*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $this->assertCount(6, $routes); foreach ($routes as $route) { $this->assertContains('domain2', $route['route']->getDomain()); @@ -63,22 +56,26 @@ public function testRespectsDomainsRuleForDingoRouter() $routeRules[0]['match']['prefixes'] = ['*']; $routeRules[0]['match']['domains'] = ['*']; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $this->assertCount(12, $routes); $routeRules[0]['match']['domains'] = ['domain1.*', 'domain2.*']; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $this->assertCount(12, $routes); $routeRules[0]['match']['domains'] = ['domain1.*']; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $this->assertCount(6, $routes); foreach ($routes as $route) { $this->assertContains('domain1', $route['route']->getDomain()); } $routeRules[0]['match']['domains'] = ['domain2.*']; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $this->assertCount(6, $routes); foreach ($routes as $route) { $this->assertContains('domain2', $route['route']->getDomain()); @@ -91,22 +88,26 @@ public function testRespectsPrefixesRuleForLaravelRouter() $routeRules[0]['match']['domains'] = ['*']; $routeRules[0]['match']['prefixes'] = ['*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $this->assertCount(12, $routes); $routeRules[0]['match']['prefixes'] = ['prefix1/*', 'prefix2/*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $this->assertCount(8, $routes); $routeRules[0]['match']['prefixes'] = ['prefix1/*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $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); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $this->assertCount(4, $routes); foreach ($routes as $route) { $this->assertTrue(Str::is('prefix2/*', $route['route']->uri())); @@ -120,22 +121,26 @@ public function testRespectsPrefixesRuleForDingoRouter() $routeRules[0]['match']['domains'] = ['*']; $routeRules[0]['match']['prefixes'] = ['*']; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $this->assertCount(12, $routes); $routeRules[0]['match']['prefixes'] = ['prefix1/*', 'prefix2/*']; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $this->assertCount(8, $routes); $routeRules[0]['match']['prefixes'] = ['prefix1/*']; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $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); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $this->assertCount(4, $routes); foreach ($routes as $route) { $this->assertTrue(Str::is('prefix2/*', $route['route']->uri())); @@ -149,7 +154,8 @@ public function testRespectsVersionsRuleForDingoRouter() $routeRules[0]['match']['versions'] = ['v2']; $routeRules[0]['match']['domains'] = ['*']; $routeRules[0]['match']['prefixes'] = ['*']; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $this->assertCount(6, $routes); foreach ($routes as $route) { $this->assertNotEmpty(array_intersect($route['route']->versions(), ['v2'])); @@ -158,7 +164,8 @@ public function testRespectsVersionsRuleForDingoRouter() $routeRules[0]['match']['versions'] = ['v1', 'v2']; $routeRules[0]['match']['domains'] = ['*']; $routeRules[0]['match']['prefixes'] = ['*']; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $this->assertCount(18, $routes); } @@ -170,7 +177,8 @@ public function testWillIncludeRouteIfListedExplicitlyForLaravelRouter() $routeRules[0]['match']['domains'] = ['domain1.*']; $routeRules[0]['match']['prefixes'] = ['prefix1/*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustInclude) { return $route['route']->getName() === $mustInclude; }); @@ -192,7 +200,8 @@ public function testWillIncludeRouteIfListedExplicitlyForDingoRouter() 'include' => [$mustInclude], ], ]; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustInclude) { return $route['route']->getName() === $mustInclude; }); @@ -208,7 +217,8 @@ public function testWillIncludeRouteIfMatchForAnIncludePatternForLaravelRouter() $routeRules[0]['match']['domains'] = ['domain1.*']; $routeRules[0]['match']['prefixes'] = ['prefix1/*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustInclude) { return in_array($route['route']->getName(), $mustInclude); }); @@ -231,7 +241,8 @@ public function testWillIncludeRouteIfMatchForAnIncludePatternForDingoRouter() 'include' => [$includePattern], ], ]; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustInclude) { return in_array($route['route']->getName(), $mustInclude); }); @@ -246,7 +257,8 @@ public function testWillExcludeRouteIfListedExplicitlyForLaravelRouter() $routeRules[0]['match']['domains'] = ['domain1.*']; $routeRules[0]['match']['prefixes'] = ['prefix1/*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustNotInclude) { return $route['route']->getName() === $mustNotInclude; }); @@ -268,7 +280,8 @@ public function testWillExcludeRouteIfListedExplicitlyForDingoRouter() 'exclude' => [$mustNotInclude], ], ]; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustNotInclude) { return $route['route']->getName() === $mustNotInclude; }); @@ -284,7 +297,8 @@ public function testWillExcludeRouteIfMatchForAnExcludePatternForLaravelRouter() $routeRules[0]['match']['domains'] = ['domain1.*']; $routeRules[0]['match']['prefixes'] = ['prefix1/*']; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustNotInclude) { return in_array($route['route']->getName(), $mustNotInclude); }); @@ -307,7 +321,8 @@ public function testWillExcludeRouteIfMatchForAnExcludePatterForDingoRouter() 'exclude' => [$excludePattern], ], ]; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustNotInclude) { return in_array($route['route']->getName(), $mustNotInclude); }); @@ -333,7 +348,8 @@ public function testMergesRoutesFromDifferentRuleGroupsForLaravelRouter() ], ]; - $routes = $this->matcher->getRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules); + $routes = $matcher->getRoutes(); $this->assertCount(4, $routes); $routes = collect($routes); @@ -370,7 +386,8 @@ public function testMergesRoutesFromDifferentRuleGroupsForDingoRouter() ], ]; - $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); + $matcher = new RouteMatcher($routeRules, 'dingo'); + $routes = $matcher->getRoutes(); $this->assertCount(18, $routes); $routes = collect($routes);