From 7bdd05379d4174ed2e3af5a7d395cda1fd0a254b Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 12 Oct 2019 02:06:43 +0100 Subject: [PATCH 1/9] Add isStatic option. Store source in resources/docs and output in public --- config/apidoc.php | 11 +++- src/Commands/GenerateDocumentation.php | 81 +++++++++++++++++--------- tests/GenerateDocumentationTest.php | 25 ++++---- 3 files changed, 75 insertions(+), 42 deletions(-) diff --git a/config/apidoc.php b/config/apidoc.php index 0638a39c..621b49b7 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -1,12 +1,21 @@ 'static', /* * The output path for the generated documentation. * This path should be relative to the root of your application. */ - 'output' => 'public/docs', + 'output' => 'public/docs', // index.html, js, css, images, collection.json + // source => resources/docs/source + // laravel => resources/views/apidoc.blade.php, collection.json /* * The router to be used (Laravel or Dingo). diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index 703e67f1..a3220013 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -100,36 +100,28 @@ public function handle() 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'; + $isStatic = $this->docConfig->get('type') === 'static'; + $sourceOutputPath = "resources/docs"; + $targetFile = $sourceOutputPath.'/source/index.md'; + $compareFile = $sourceOutputPath.'/source/.compare.md'; + $prependFile = $sourceOutputPath.'/source/prepend.md'; + $appendFile = $sourceOutputPath.'/source/append.md'; + + // TODO for laravel, replace with {{ route("apidoc.collection") }} $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; - }); - }); + // Generate Markdown for each route + $parsedRouteOutput = $this->generateMarkdownOutputForEachRoute($parsedRoutes, $settings); $frontmatter = view('apidoc::partials.frontmatter') ->with('settings', $settings); /* - * In case the target file already exists, we should check if the documentation was modified + * 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)) { @@ -176,8 +168,8 @@ private function writeMarkdown($parsedRoutes) ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection()) ->with('parsedRoutes', $parsedRouteOutput); - if (! is_dir($outputPath)) { - $documentarian->create($outputPath); + if (! is_dir($sourceOutputPath)) { + $documentarian->create($sourceOutputPath); } // Write output file @@ -200,21 +192,32 @@ private function writeMarkdown($parsedRoutes) $this->info('Generating API HTML code'); - $documentarian->generate($outputPath); + $documentarian->generate($sourceOutputPath); + // Move index.html, css/style.css and js/all.js to public/docs + + if ($isStatic) { + if (!is_dir($outputPath)) { + mkdir($outputPath, 0777, true); + mkdir("{$outputPath}/css"); + mkdir("{$outputPath}/js"); + } + copy("{$sourceOutputPath}/index.html", "{$outputPath}/index.html"); + copy("{$sourceOutputPath}/css/style.css", "{$outputPath}/css/style.css"); + copy("{$sourceOutputPath}/js/all.js", "{$outputPath}/js/all.js"); + } - $this->info('Wrote HTML documentation to: '.$outputPath.'/index.html'); + $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)); + file_put_contents("{$outputPath}/collection.json", $this->generatePostmanCollection($parsedRoutes)); } if ($logo = $this->docConfig->get('logo')) { - copy( - $logo, - $outputPath.DIRECTORY_SEPARATOR.'images'.DIRECTORY_SEPARATOR.'logo.png' - ); + if ($isStatic) { + copy($logo, "{$outputPath}/images/logo.png"); + } } } @@ -310,4 +313,24 @@ private function shouldGeneratePostmanCollection() { return $this->docConfig->get('postman.enabled', is_bool($this->docConfig->get('postman')) ? $this->docConfig->get('postman') : false); } + + protected function generateMarkdownOutputForEachRoute(Collection $parsedRoutes, array $settings): Collection + { + $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; + }); + }); + return $parsedRouteOutput; +} } diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index e69bfd91..84347e97 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,7 +129,7 @@ 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); } @@ -156,7 +157,7 @@ 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', '<')) { @@ -172,7 +173,7 @@ 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); } @@ -202,8 +203,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 +222,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 +381,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 +393,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 +407,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 +438,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); } From d5fb1c0bcd0b113807731808731a5a3cce8200c4 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 13 Oct 2019 19:34:33 +0100 Subject: [PATCH 2/9] Reorganize code --- docs/plugins.md | 28 +-- src/Commands/GenerateDocumentation.php | 211 ++---------------- src/{Tools => Extracting}/Generator.php | 22 +- .../Traits => Extracting}/ParamHelpers.php | 44 +++- src/{Tools => Extracting}/RouteDocBlocker.php | 8 +- .../BodyParameters/GetFromBodyParamTag.php | 15 +- .../Strategies/Metadata/GetFromDocBlocks.php | 6 +- .../QueryParameters/GetFromQueryParamTag.php | 15 +- .../Strategies/Responses/ResponseCalls.php | 6 +- .../Responses/UseApiResourceTags.php | 6 +- .../Responses/UseResponseFileTag.php | 6 +- .../Strategies/Responses/UseResponseTag.php | 6 +- .../Responses/UseTransformerTags.php | 6 +- src/{ => Extracting}/Strategies/Strategy.php | 2 +- .../UrlParameters/GetFromUrlParamTag.php | 15 +- src/{Tools => Matching}/LumenRouteAdapter.php | 4 +- src/{Tools => Matching}/RouteMatcher.php | 35 ++- src/Tools/Traits/DocBlockParamHelpers.php | 45 ---- .../PostmanCollectionWriter.php} | 27 +-- tests/GenerateDocumentationTest.php | 18 +- tests/Unit/GeneratorPluginSystemTestCase.php | 15 +- tests/Unit/GeneratorTestCase.php | 32 +-- tests/Unit/RouteMatcherTest.php | 97 ++++---- 23 files changed, 253 insertions(+), 416 deletions(-) rename src/{Tools => Extracting}/Generator.php (89%) rename src/{Tools/Traits => Extracting}/ParamHelpers.php (62%) rename src/{Tools => Extracting}/RouteDocBlocker.php (89%) rename src/{ => Extracting}/Strategies/BodyParameters/GetFromBodyParamTag.php (91%) rename src/{ => Extracting}/Strategies/Metadata/GetFromDocBlocks.php (96%) rename src/{ => Extracting}/Strategies/QueryParameters/GetFromQueryParamTag.php (91%) rename src/{ => Extracting}/Strategies/Responses/ResponseCalls.php (98%) rename src/{ => Extracting}/Strategies/Responses/UseApiResourceTags.php (97%) rename src/{ => Extracting}/Strategies/Responses/UseResponseFileTag.php (93%) rename src/{ => Extracting}/Strategies/Responses/UseResponseTag.php (91%) rename src/{ => Extracting}/Strategies/Responses/UseTransformerTags.php (97%) rename src/{ => Extracting}/Strategies/Strategy.php (95%) rename src/{ => Extracting}/Strategies/UrlParameters/GetFromUrlParamTag.php (91%) rename src/{Tools => Matching}/LumenRouteAdapter.php (70%) rename src/{Tools => Matching}/RouteMatcher.php (75%) delete mode 100644 src/Tools/Traits/DocBlockParamHelpers.php rename src/{Postman/CollectionWriter.php => Writing/PostmanCollectionWriter.php} (77%) 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/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index a3220013..3f008823 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -9,12 +9,11 @@ use Mpociot\ApiDoc\Tools\Flags; use Mpociot\ApiDoc\Tools\Utils; use Mpociot\Reflection\DocBlock; +use Mpociot\ApiDoc\Writing\Writer; use Illuminate\Support\Collection; 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,162 +63,32 @@ 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'); - $isStatic = $this->docConfig->get('type') === 'static'; - - $sourceOutputPath = "resources/docs"; - $targetFile = $sourceOutputPath.'/source/index.md'; - $compareFile = $sourceOutputPath.'/source/.compare.md'; - $prependFile = $sourceOutputPath.'/source/prepend.md'; - $appendFile = $sourceOutputPath.'/source/append.md'; - - // TODO for laravel, replace with {{ route("apidoc.collection") }} - $infoText = view('apidoc::partials.info') - ->with('outputPath', ltrim($outputPath, 'public/')) - ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection()); - - $settings = ['languages' => $this->docConfig->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); - - 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($sourceOutputPath)) { - $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->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($sourceOutputPath); - // Move index.html, css/style.css and js/all.js to public/docs - - if ($isStatic) { - if (!is_dir($outputPath)) { - mkdir($outputPath, 0777, true); - mkdir("{$outputPath}/css"); - mkdir("{$outputPath}/js"); - } - copy("{$sourceOutputPath}/index.html", "{$outputPath}/index.html"); - copy("{$sourceOutputPath}/css/style.css", "{$outputPath}/css/style.css"); - copy("{$sourceOutputPath}/js/all.js", "{$outputPath}/js/all.js"); - } - - $this->info("Wrote HTML documentation to: {$outputPath}/index.html"); - - if ($this->shouldGeneratePostmanCollection()) { - $this->info('Generating Postman collection'); - - file_put_contents("{$outputPath}/collection.json", $this->generatePostmanCollection($parsedRoutes)); - } - - if ($logo = $this->docConfig->get('logo')) { - if ($isStatic) { - copy($logo, "{$outputPath}/images/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 @@ -290,47 +156,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); - } - - protected function generateMarkdownOutputForEachRoute(Collection $parsedRoutes, array $settings): Collection - { - $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; - }); - }); - return $parsedRouteOutput; -} } 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..64a1aed8 100644 --- a/src/Tools/Generator.php +++ b/src/Extracting/Generator.php @@ -1,7 +1,9 @@ [ - \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..8eb91414 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..baf0818a 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..9f030b3e 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 77% rename from src/Postman/CollectionWriter.php rename to src/Writing/PostmanCollectionWriter.php index 05acf2eb..93f6f3a5 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 = [ 'variables' => [], 'info' => [ - 'name' => config('apidoc.postman.name') ?: config('app.name').' API', + 'name' => config('apidoc.postman.name') ?: config('app.name') . ' API', '_postman_id' => Uuid::uuid4()->toString(), 'description' => config('apidoc.postman.description') ?: '', 'schema' => 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json', @@ -60,11 +55,11 @@ public function getCollection() return [ '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()))), + 'url' => url($route['uri']) . (collect($route['queryParameters'])->isEmpty() + ? '' + : ('?' . 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/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 84347e97..46475647 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -136,16 +136,8 @@ public function can_parse_resource_routes() /** @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) ->only(['index', 'create']); - } config(['apidoc.routes.0.match.prefixes' => ['api/*']]); config([ @@ -160,16 +152,9 @@ public function can_parse_partial_resource_routes() $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) ->only(['index', 'create']); - } + $this->artisan('apidoc:generate'); $fixtureMarkdown = __DIR__.'/Fixtures/partial_resource_index.md'; @@ -191,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([ diff --git a/tests/Unit/GeneratorPluginSystemTestCase.php b/tests/Unit/GeneratorPluginSystemTestCase.php index 0bc203a7..12f47358 100644 --- a/tests/Unit/GeneratorPluginSystemTestCase.php +++ b/tests/Unit/GeneratorPluginSystemTestCase.php @@ -5,8 +5,8 @@ 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\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Tools\DocumentationConfig; use Mpociot\ApiDoc\Tests\Fixtures\TestController; use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider; @@ -14,7 +14,7 @@ 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..29e9f627 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); From 997a066d179d6ddc4f549d11dd41f2da8662607a Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 13 Oct 2019 19:47:58 +0100 Subject: [PATCH 3/9] Add option for non-static docs --- config/apidoc.php | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/config/apidoc.php b/config/apidoc.php index 621b49b7..c9cdea92 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -9,14 +9,6 @@ */ 'type' => 'static', - /* - * The output path for the generated documentation. - * This path should be relative to the root of your application. - */ - 'output' => 'public/docs', // index.html, js, css, images, collection.json - // source => resources/docs/source - // laravel => resources/views/apidoc.blade.php, collection.json - /* * The router to be used (Laravel or Dingo). */ @@ -169,23 +161,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, ], ], From 2a99555f241954981a0ff632ade7957bf069f3b6 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 13 Oct 2019 20:03:08 +0100 Subject: [PATCH 4/9] Implement non-static docs --- .gitignore | 4 + config/apidoc.php | 7 +- src/Writing/Writer.php | 305 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 src/Writing/Writer.php 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/config/apidoc.php b/config/apidoc.php index c9cdea92..eb469a26 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -4,8 +4,8 @@ /** * The type of documentation output to generate. * - "static" will generate a static HTMl page in the /public/docs folder, - * - "laravel" will generate the documentation as Blade files, - * so you can add routing and authentication + * - "laravel" will generate the documentation as a Blade view, + * so you can add routing and authentication. */ 'type' => 'static', @@ -22,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' => [ /* diff --git a/src/Writing/Writer.php b/src/Writing/Writer.php new file mode 100644 index 00000000..42194f14 --- /dev/null +++ b/src/Writing/Writer.php @@ -0,0 +1,305 @@ +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}"); + } +} From 2ae821d7147b602f7818996da9fecabd23ca71c6 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 13 Oct 2019 20:03:20 +0100 Subject: [PATCH 5/9] Add routing --- src/ApiDoc.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/ApiDoc.php diff --git a/src/ApiDoc.php b/src/ApiDoc.php new file mode 100644 index 00000000..b31a89fe --- /dev/null +++ b/src/ApiDoc.php @@ -0,0 +1,26 @@ +get('apidoc/collection.json'), + 200, + ['Content-type' => 'application/json'] + + ); + } + + return view("apidoc.index"); + })->name('apidoc'); + } +} From f5277b0b579c2c80ad41b0b9a0f86a56d18befb3 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 13 Oct 2019 20:04:38 +0100 Subject: [PATCH 6/9] Update doc --- docs/config.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/config.md b/docs/config.md index 99b08218..87325008 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 usin `/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** From 98ed7d0535af44c9c8318224ddef819f5f5fa89b Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 13 Oct 2019 20:04:58 +0100 Subject: [PATCH 7/9] Fix typo --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 87325008..526280fa 100644 --- a/docs/config.md +++ b/docs/config.md @@ -12,7 +12,7 @@ If you're using `laravel` type, you can call `\Mpociot\ApiDoc\ApiDoc::routes()` ```php \Mpociot\ApiDoc\ApiDoc::routes("/apidoc")->middleware("auth.basic"); ``` -> Note: There is currently a known issue with usin `/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. +> 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. From ec2a721e31c2789a2a164e76d4798edb174ac24c Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Tue, 15 Oct 2019 21:21:20 +0000 Subject: [PATCH 8/9] Apply fixes from StyleCI --- config/apidoc.php | 2 +- src/ApiDoc.php | 7 ++- src/Commands/GenerateDocumentation.php | 4 +- src/Extracting/Generator.php | 4 +- src/Extracting/RouteDocBlocker.php | 2 +- .../BodyParameters/GetFromBodyParamTag.php | 4 +- .../Strategies/Metadata/GetFromDocBlocks.php | 2 +- .../QueryParameters/GetFromQueryParamTag.php | 4 +- .../Strategies/Responses/ResponseCalls.php | 2 +- .../Responses/UseApiResourceTags.php | 2 +- .../Responses/UseResponseFileTag.php | 2 +- .../Strategies/Responses/UseResponseTag.php | 2 +- .../Responses/UseTransformerTags.php | 2 +- .../UrlParameters/GetFromUrlParamTag.php | 4 +- src/Matching/RouteMatcher.php | 4 +- src/Writing/PostmanCollectionWriter.php | 10 ++-- src/Writing/Writer.php | 47 ++++++++++--------- tests/GenerateDocumentationTest.php | 4 +- tests/Unit/GeneratorPluginSystemTestCase.php | 2 +- tests/Unit/RouteMatcherTest.php | 30 ++++++------ 20 files changed, 69 insertions(+), 71 deletions(-) diff --git a/config/apidoc.php b/config/apidoc.php index eb469a26..d300a02d 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -1,7 +1,7 @@ get('apidoc/collection.json'), 200, @@ -20,7 +19,7 @@ public static function routes($path = "/doc") ); } - return view("apidoc.index"); + return view('apidoc.index'); })->name('apidoc'); } } diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index 3f008823..2404e652 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -9,8 +9,8 @@ use Mpociot\ApiDoc\Tools\Flags; use Mpociot\ApiDoc\Tools\Utils; use Mpociot\Reflection\DocBlock; -use Mpociot\ApiDoc\Writing\Writer; use Illuminate\Support\Collection; +use Mpociot\ApiDoc\Writing\Writer; use Illuminate\Support\Facades\URL; use Mpociot\ApiDoc\Extracting\Generator; use Mpociot\ApiDoc\Matching\RouteMatcher; @@ -71,7 +71,6 @@ public function handle() $generator = new Generator($this->docConfig); $parsedRoutes = $this->processRoutes($generator, $routes); - $groupedRoutes = collect($parsedRoutes) ->groupBy('metadata.groupName') ->sortBy(static function ($group) { @@ -155,5 +154,4 @@ private function isRouteVisibleForDocumentation(array $action) return true; } - } diff --git a/src/Extracting/Generator.php b/src/Extracting/Generator.php index 64a1aed8..283cf702 100644 --- a/src/Extracting/Generator.php +++ b/src/Extracting/Generator.php @@ -2,13 +2,13 @@ namespace Mpociot\ApiDoc\Extracting; -use Mpociot\ApiDoc\Tools\DocumentationConfig; -use Mpociot\ApiDoc\Tools\Utils; use ReflectionClass; use ReflectionMethod; use Illuminate\Support\Arr; use Illuminate\Support\Str; use Illuminate\Routing\Route; +use Mpociot\ApiDoc\Tools\Utils; +use Mpociot\ApiDoc\Tools\DocumentationConfig; class Generator { diff --git a/src/Extracting/RouteDocBlocker.php b/src/Extracting/RouteDocBlocker.php index 8eb91414..cdbd1acf 100644 --- a/src/Extracting/RouteDocBlocker.php +++ b/src/Extracting/RouteDocBlocker.php @@ -2,9 +2,9 @@ namespace Mpociot\ApiDoc\Extracting; -use Mpociot\ApiDoc\Tools\Utils; use ReflectionClass; use Illuminate\Routing\Route; +use Mpociot\ApiDoc\Tools\Utils; use Mpociot\Reflection\DocBlock; /** diff --git a/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php b/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php index 52e06fe9..7117981e 100644 --- a/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php +++ b/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php @@ -7,10 +7,10 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; +use Mpociot\ApiDoc\Extracting\ParamHelpers; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Dingo\Api\Http\FormRequest as DingoFormRequest; -use Mpociot\ApiDoc\Extracting\ParamHelpers; use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; class GetFromBodyParamTag extends Strategy diff --git a/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php b/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php index baf0818a..e74faf06 100644 --- a/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php +++ b/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php @@ -7,8 +7,8 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; class GetFromDocBlocks extends Strategy { diff --git a/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php b/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php index 1aadd963..f3943ee6 100644 --- a/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php +++ b/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php @@ -8,10 +8,10 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; +use Mpociot\ApiDoc\Extracting\ParamHelpers; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Dingo\Api\Http\FormRequest as DingoFormRequest; -use Mpociot\ApiDoc\Extracting\ParamHelpers; use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; class GetFromQueryParamTag extends Strategy diff --git a/src/Extracting/Strategies/Responses/ResponseCalls.php b/src/Extracting/Strategies/Responses/ResponseCalls.php index 9f030b3e..b1b39da1 100644 --- a/src/Extracting/Strategies/Responses/ResponseCalls.php +++ b/src/Extracting/Strategies/Responses/ResponseCalls.php @@ -9,8 +9,8 @@ use Illuminate\Routing\Route; use Mpociot\ApiDoc\Tools\Flags; use Mpociot\ApiDoc\Tools\Utils; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\ParamHelpers; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; /** * Make a call to the route and retrieve its response. diff --git a/src/Extracting/Strategies/Responses/UseApiResourceTags.php b/src/Extracting/Strategies/Responses/UseApiResourceTags.php index e2a5fe23..f1f2cbe8 100644 --- a/src/Extracting/Strategies/Responses/UseApiResourceTags.php +++ b/src/Extracting/Strategies/Responses/UseApiResourceTags.php @@ -15,9 +15,9 @@ use Mpociot\Reflection\DocBlock\Tag; use Illuminate\Database\Eloquent\Model; use League\Fractal\Resource\Collection; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; use Illuminate\Http\Resources\Json\JsonResource; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Illuminate\Http\Resources\Json\ResourceCollection; /** diff --git a/src/Extracting/Strategies/Responses/UseResponseFileTag.php b/src/Extracting/Strategies/Responses/UseResponseFileTag.php index 3f3e6ec6..9d559a5d 100644 --- a/src/Extracting/Strategies/Responses/UseResponseFileTag.php +++ b/src/Extracting/Strategies/Responses/UseResponseFileTag.php @@ -5,8 +5,8 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; /** * Get a response from from a file in the docblock ( @responseFile ). diff --git a/src/Extracting/Strategies/Responses/UseResponseTag.php b/src/Extracting/Strategies/Responses/UseResponseTag.php index a4324c7e..cee8869b 100644 --- a/src/Extracting/Strategies/Responses/UseResponseTag.php +++ b/src/Extracting/Strategies/Responses/UseResponseTag.php @@ -5,8 +5,8 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; /** * Get a response from the docblock ( @response ). diff --git a/src/Extracting/Strategies/Responses/UseTransformerTags.php b/src/Extracting/Strategies/Responses/UseTransformerTags.php index e79a0a66..edd41e27 100644 --- a/src/Extracting/Strategies/Responses/UseTransformerTags.php +++ b/src/Extracting/Strategies/Responses/UseTransformerTags.php @@ -15,8 +15,8 @@ use Mpociot\Reflection\DocBlock\Tag; use Illuminate\Database\Eloquent\Model; use League\Fractal\Resource\Collection; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; /** * Parse a transformer response from the docblock ( @transformer || @transformercollection ). diff --git a/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php b/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php index cdec1f31..90653c3e 100644 --- a/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php +++ b/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php @@ -8,10 +8,10 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; +use Mpociot\ApiDoc\Extracting\ParamHelpers; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Dingo\Api\Http\FormRequest as DingoFormRequest; -use Mpociot\ApiDoc\Extracting\ParamHelpers; use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; class GetFromUrlParamTag extends Strategy diff --git a/src/Matching/RouteMatcher.php b/src/Matching/RouteMatcher.php index d11f80d6..0de4b694 100644 --- a/src/Matching/RouteMatcher.php +++ b/src/Matching/RouteMatcher.php @@ -19,8 +19,7 @@ class RouteMatcher */ protected $routeRules; - - public function __construct(array $routeRules = [], string $router = "laravel") + public function __construct(array $routeRules = [], string $router = 'laravel') { $this->router = $router; $this->routeRules = $routeRules; @@ -29,6 +28,7 @@ public function __construct(array $routeRules = [], string $router = "laravel") public function getRoutes() { $usingDingoRouter = strtolower($this->router) == 'dingo'; + return $this->getRoutesToBeDocumented($this->routeRules, $usingDingoRouter); } diff --git a/src/Writing/PostmanCollectionWriter.php b/src/Writing/PostmanCollectionWriter.php index 93f6f3a5..4ef51928 100644 --- a/src/Writing/PostmanCollectionWriter.php +++ b/src/Writing/PostmanCollectionWriter.php @@ -40,7 +40,7 @@ public function getCollection() $collection = [ 'variables' => [], 'info' => [ - 'name' => config('apidoc.postman.name') ?: config('app.name') . ' API', + 'name' => config('apidoc.postman.name') ?: config('app.name').' API', '_postman_id' => Uuid::uuid4()->toString(), 'description' => config('apidoc.postman.description') ?: '', 'schema' => 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json', @@ -55,11 +55,11 @@ public function getCollection() return [ 'name' => $route['metadata']['title'] != '' ? $route['metadata']['title'] : url($route['uri']), 'request' => [ - 'url' => url($route['uri']) . (collect($route['queryParameters'])->isEmpty() + '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 index 42194f14..6a365763 100644 --- a/src/Writing/Writer.php +++ b/src/Writing/Writer.php @@ -1,9 +1,7 @@ config->get('type') === 'static'; - $sourceOutputPath = "resources/docs"; - $outputPath = $isStatic ? "public/docs" : "resources/views/apidoc"; + $sourceOutputPath = 'resources/docs'; + $outputPath = $isStatic ? 'public/docs' : 'resources/views/apidoc'; $this->writeMarkdownAndSourceFiles($this->routes, $sourceOutputPath); @@ -85,8 +83,8 @@ public function writeDocs() */ public function writeMarkdownAndSourceFiles(Collection $parsedRoutes, string $sourceOutputPath) { - $targetFile = $sourceOutputPath . '/source/index.md'; - $compareFile = $sourceOutputPath . '/source/.compare.md'; + $targetFile = $sourceOutputPath.'/source/index.md'; + $compareFile = $sourceOutputPath.'/source/.compare.md'; $infoText = view('apidoc::partials.info') ->with('outputPath', 'docs') @@ -110,14 +108,14 @@ public function writeMarkdownAndSourceFiles(Collection $parsedRoutes, string $so $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 (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']); + $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']); + $this->output->warn('Skipping modified route ['.implode(',', $route['methods']).'] '.$route['uri']); $route['modified_output'] = $existingRouteDoc[0]; } } @@ -140,9 +138,9 @@ public function writeMarkdownAndSourceFiles(Collection $parsedRoutes, string $so ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection) ->with('parsedRoutes', $parsedRouteOutput); - $this->output->info('Writing index.md and source files to: ' . $sourceOutputPath); + $this->output->info('Writing index.md and source files to: '.$sourceOutputPath); - if (!is_dir($sourceOutputPath)) { + if (! is_dir($sourceOutputPath)) { $documentarian = new Documentarian(); $documentarian->create($sourceOutputPath); } @@ -163,14 +161,14 @@ public function writeMarkdownAndSourceFiles(Collection $parsedRoutes, string $so file_put_contents($compareFile, $compareMarkdown); - $this->output->info('Wrote index.md and source files to: ' . $sourceOutputPath); + $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'])) { + 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'; } @@ -183,6 +181,7 @@ public function generateMarkdownOutputForEachRoute(Collection $parsedRoutes, arr return $route; }); }); + return $parsedRouteOutput; } @@ -225,11 +224,12 @@ public function generatePostmanCollection(Collection $routes) */ protected function getMarkdownToPrepend(string $sourceOutputPath): string { - $prependFile = $sourceOutputPath . '/source/prepend.md'; + $prependFile = $sourceOutputPath.'/source/prepend.md'; $prependFileContents = file_exists($prependFile) - ? file_get_contents($prependFile) . "\n" : ''; + ? file_get_contents($prependFile)."\n" : ''; + return $prependFileContents; -} + } /** * @param string $sourceOutputPath @@ -238,16 +238,17 @@ protected function getMarkdownToPrepend(string $sourceOutputPath): string */ protected function getMarkdownToAppend(string $sourceOutputPath): string { - $appendFile = $sourceOutputPath . '/source/append.md'; + $appendFile = $sourceOutputPath.'/source/append.md'; $appendFileContents = file_exists($appendFile) - ? "\n" . file_get_contents($appendFile) : ''; + ? "\n".file_get_contents($appendFile) : ''; + return $appendFileContents; -} + } protected function copyAssetsFromSourceFolderToPublicFolder(string $sourceOutputPath, bool $isStatic = true): void { - $publicPath = "public/docs"; - if (!is_dir($publicPath)) { + $publicPath = 'public/docs'; + if (! is_dir($publicPath)) { mkdir($publicPath, 0777, true); mkdir("{$publicPath}/css"); mkdir("{$publicPath}/js"); @@ -270,7 +271,7 @@ protected function moveOutputFromSourceFolderToTargetFolder(string $sourceOutput rename("{$sourceOutputPath}/index.html", "{$outputPath}/index.html"); } else { // Move output to resources/views - if (!is_dir($outputPath)) { + if (! is_dir($outputPath)) { mkdir($outputPath); } rename("{$sourceOutputPath}/index.html", "$outputPath/index.blade.php"); diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 46475647..3dfa1b2a 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -136,7 +136,7 @@ public function can_parse_resource_routes() /** @test */ public function can_parse_partial_resource_routes() { - RouteFacade::resource('/api/users', TestResourceController::class) + RouteFacade::resource('/api/users', TestResourceController::class) ->only(['index', 'create']); config(['apidoc.routes.0.match.prefixes' => ['api/*']]); @@ -152,7 +152,7 @@ public function can_parse_partial_resource_routes() $generatedMarkdown = __DIR__.'/../resources/docs/source/index.md'; $this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown); - RouteFacade::apiResource('/api/users', TestResourceController::class) + RouteFacade::apiResource('/api/users', TestResourceController::class) ->only(['index', 'create']); $this->artisan('apidoc:generate'); diff --git a/tests/Unit/GeneratorPluginSystemTestCase.php b/tests/Unit/GeneratorPluginSystemTestCase.php index 12f47358..5dbdac4b 100644 --- a/tests/Unit/GeneratorPluginSystemTestCase.php +++ b/tests/Unit/GeneratorPluginSystemTestCase.php @@ -6,10 +6,10 @@ use ReflectionMethod; use Illuminate\Routing\Route; use Mpociot\ApiDoc\Extracting\Generator; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; 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 { diff --git a/tests/Unit/RouteMatcherTest.php b/tests/Unit/RouteMatcherTest.php index 29e9f627..d800ee97 100644 --- a/tests/Unit/RouteMatcherTest.php +++ b/tests/Unit/RouteMatcherTest.php @@ -56,17 +56,17 @@ public function testRespectsDomainsRuleForDingoRouter() $routeRules[0]['match']['prefixes'] = ['*']; $routeRules[0]['match']['domains'] = ['*']; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(12, $routes); $routeRules[0]['match']['domains'] = ['domain1.*', 'domain2.*']; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(12, $routes); $routeRules[0]['match']['domains'] = ['domain1.*']; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(6, $routes); foreach ($routes as $route) { @@ -74,7 +74,7 @@ public function testRespectsDomainsRuleForDingoRouter() } $routeRules[0]['match']['domains'] = ['domain2.*']; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(6, $routes); foreach ($routes as $route) { @@ -121,17 +121,17 @@ public function testRespectsPrefixesRuleForDingoRouter() $routeRules[0]['match']['domains'] = ['*']; $routeRules[0]['match']['prefixes'] = ['*']; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(12, $routes); $routeRules[0]['match']['prefixes'] = ['prefix1/*', 'prefix2/*']; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(8, $routes); $routeRules[0]['match']['prefixes'] = ['prefix1/*']; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(4, $routes); foreach ($routes as $route) { @@ -139,7 +139,7 @@ public function testRespectsPrefixesRuleForDingoRouter() } $routeRules[0]['match']['prefixes'] = ['prefix2/*']; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(4, $routes); foreach ($routes as $route) { @@ -154,7 +154,7 @@ public function testRespectsVersionsRuleForDingoRouter() $routeRules[0]['match']['versions'] = ['v2']; $routeRules[0]['match']['domains'] = ['*']; $routeRules[0]['match']['prefixes'] = ['*']; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(6, $routes); foreach ($routes as $route) { @@ -164,7 +164,7 @@ public function testRespectsVersionsRuleForDingoRouter() $routeRules[0]['match']['versions'] = ['v1', 'v2']; $routeRules[0]['match']['domains'] = ['*']; $routeRules[0]['match']['prefixes'] = ['*']; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(18, $routes); } @@ -200,7 +200,7 @@ public function testWillIncludeRouteIfListedExplicitlyForDingoRouter() 'include' => [$mustInclude], ], ]; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustInclude) { return $route['route']->getName() === $mustInclude; @@ -241,7 +241,7 @@ public function testWillIncludeRouteIfMatchForAnIncludePatternForDingoRouter() 'include' => [$includePattern], ], ]; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustInclude) { return in_array($route['route']->getName(), $mustInclude); @@ -280,7 +280,7 @@ public function testWillExcludeRouteIfListedExplicitlyForDingoRouter() 'exclude' => [$mustNotInclude], ], ]; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustNotInclude) { return $route['route']->getName() === $mustNotInclude; @@ -321,7 +321,7 @@ public function testWillExcludeRouteIfMatchForAnExcludePatterForDingoRouter() 'exclude' => [$excludePattern], ], ]; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $oddRuleOut = collect($routes)->filter(function ($route) use ($mustNotInclude) { return in_array($route['route']->getName(), $mustNotInclude); @@ -386,7 +386,7 @@ public function testMergesRoutesFromDifferentRuleGroupsForDingoRouter() ], ]; - $matcher = new RouteMatcher($routeRules, "dingo"); + $matcher = new RouteMatcher($routeRules, 'dingo'); $routes = $matcher->getRoutes(); $this->assertCount(18, $routes); From 61163a7f0700d24e49a015e1c8c4ce367f502cde Mon Sep 17 00:00:00 2001 From: shalvah Date: Tue, 15 Oct 2019 22:32:03 +0100 Subject: [PATCH 9/9] Fix style --- CHANGELOG.md | 11 +++++ config/apidoc.php | 2 +- docs/migrating.md | 2 + src/ApiDoc.php | 7 ++-- src/Commands/GenerateDocumentation.php | 1 - src/Extracting/Generator.php | 4 +- src/Extracting/RouteDocBlocker.php | 2 +- .../BodyParameters/GetFromBodyParamTag.php | 4 +- .../Strategies/Metadata/GetFromDocBlocks.php | 2 +- .../QueryParameters/GetFromQueryParamTag.php | 4 +- .../Strategies/Responses/ResponseCalls.php | 2 +- .../Responses/UseApiResourceTags.php | 4 +- .../Responses/UseResponseFileTag.php | 2 +- .../Strategies/Responses/UseResponseTag.php | 2 +- .../Responses/UseTransformerTags.php | 2 +- .../UrlParameters/GetFromUrlParamTag.php | 4 +- src/Matching/RouteMatcher.php | 3 +- src/Writing/PostmanCollectionWriter.php | 8 ++-- src/Writing/Writer.php | 41 ++++++++++--------- 19 files changed, 59 insertions(+), 48 deletions(-) create mode 100644 docs/migrating.md 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 eb469a26..d300a02d 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -1,7 +1,7 @@ get('apidoc/collection.json'), 200, @@ -20,7 +19,7 @@ public static function routes($path = "/doc") ); } - return view("apidoc.index"); + return view('apidoc.index'); })->name('apidoc'); } } diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index 3f008823..f60f5b37 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -71,7 +71,6 @@ public function handle() $generator = new Generator($this->docConfig); $parsedRoutes = $this->processRoutes($generator, $routes); - $groupedRoutes = collect($parsedRoutes) ->groupBy('metadata.groupName') ->sortBy(static function ($group) { diff --git a/src/Extracting/Generator.php b/src/Extracting/Generator.php index 64a1aed8..283cf702 100644 --- a/src/Extracting/Generator.php +++ b/src/Extracting/Generator.php @@ -2,13 +2,13 @@ namespace Mpociot\ApiDoc\Extracting; -use Mpociot\ApiDoc\Tools\DocumentationConfig; -use Mpociot\ApiDoc\Tools\Utils; use ReflectionClass; use ReflectionMethod; use Illuminate\Support\Arr; use Illuminate\Support\Str; use Illuminate\Routing\Route; +use Mpociot\ApiDoc\Tools\Utils; +use Mpociot\ApiDoc\Tools\DocumentationConfig; class Generator { diff --git a/src/Extracting/RouteDocBlocker.php b/src/Extracting/RouteDocBlocker.php index 8eb91414..cdbd1acf 100644 --- a/src/Extracting/RouteDocBlocker.php +++ b/src/Extracting/RouteDocBlocker.php @@ -2,9 +2,9 @@ namespace Mpociot\ApiDoc\Extracting; -use Mpociot\ApiDoc\Tools\Utils; use ReflectionClass; use Illuminate\Routing\Route; +use Mpociot\ApiDoc\Tools\Utils; use Mpociot\Reflection\DocBlock; /** diff --git a/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php b/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php index 52e06fe9..7117981e 100644 --- a/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php +++ b/src/Extracting/Strategies/BodyParameters/GetFromBodyParamTag.php @@ -7,10 +7,10 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; +use Mpociot\ApiDoc\Extracting\ParamHelpers; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Dingo\Api\Http\FormRequest as DingoFormRequest; -use Mpociot\ApiDoc\Extracting\ParamHelpers; use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; class GetFromBodyParamTag extends Strategy diff --git a/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php b/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php index baf0818a..e74faf06 100644 --- a/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php +++ b/src/Extracting/Strategies/Metadata/GetFromDocBlocks.php @@ -7,8 +7,8 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; class GetFromDocBlocks extends Strategy { diff --git a/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php b/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php index 1aadd963..2d5e353c 100644 --- a/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php +++ b/src/Extracting/Strategies/QueryParameters/GetFromQueryParamTag.php @@ -8,10 +8,10 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; +use Mpociot\ApiDoc\Extracting\ParamHelpers; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; use Dingo\Api\Http\FormRequest as DingoFormRequest; -use Mpociot\ApiDoc\Extracting\ParamHelpers; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; class GetFromQueryParamTag extends Strategy diff --git a/src/Extracting/Strategies/Responses/ResponseCalls.php b/src/Extracting/Strategies/Responses/ResponseCalls.php index 9f030b3e..b1b39da1 100644 --- a/src/Extracting/Strategies/Responses/ResponseCalls.php +++ b/src/Extracting/Strategies/Responses/ResponseCalls.php @@ -9,8 +9,8 @@ use Illuminate\Routing\Route; use Mpociot\ApiDoc\Tools\Flags; use Mpociot\ApiDoc\Tools\Utils; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\ParamHelpers; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; /** * Make a call to the route and retrieve its response. diff --git a/src/Extracting/Strategies/Responses/UseApiResourceTags.php b/src/Extracting/Strategies/Responses/UseApiResourceTags.php index e2a5fe23..4de82e63 100644 --- a/src/Extracting/Strategies/Responses/UseApiResourceTags.php +++ b/src/Extracting/Strategies/Responses/UseApiResourceTags.php @@ -14,10 +14,8 @@ use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; use Illuminate\Database\Eloquent\Model; -use League\Fractal\Resource\Collection; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; -use Illuminate\Http\Resources\Json\JsonResource; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Illuminate\Http\Resources\Json\ResourceCollection; /** diff --git a/src/Extracting/Strategies/Responses/UseResponseFileTag.php b/src/Extracting/Strategies/Responses/UseResponseFileTag.php index 3f3e6ec6..9d559a5d 100644 --- a/src/Extracting/Strategies/Responses/UseResponseFileTag.php +++ b/src/Extracting/Strategies/Responses/UseResponseFileTag.php @@ -5,8 +5,8 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; /** * Get a response from from a file in the docblock ( @responseFile ). diff --git a/src/Extracting/Strategies/Responses/UseResponseTag.php b/src/Extracting/Strategies/Responses/UseResponseTag.php index a4324c7e..cee8869b 100644 --- a/src/Extracting/Strategies/Responses/UseResponseTag.php +++ b/src/Extracting/Strategies/Responses/UseResponseTag.php @@ -5,8 +5,8 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; /** * Get a response from the docblock ( @response ). diff --git a/src/Extracting/Strategies/Responses/UseTransformerTags.php b/src/Extracting/Strategies/Responses/UseTransformerTags.php index e79a0a66..edd41e27 100644 --- a/src/Extracting/Strategies/Responses/UseTransformerTags.php +++ b/src/Extracting/Strategies/Responses/UseTransformerTags.php @@ -15,8 +15,8 @@ use Mpociot\Reflection\DocBlock\Tag; use Illuminate\Database\Eloquent\Model; use League\Fractal\Resource\Collection; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; /** * Parse a transformer response from the docblock ( @transformer || @transformercollection ). diff --git a/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php b/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php index cdec1f31..90653c3e 100644 --- a/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php +++ b/src/Extracting/Strategies/UrlParameters/GetFromUrlParamTag.php @@ -8,10 +8,10 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Extracting\Strategies\Strategy; +use Mpociot\ApiDoc\Extracting\ParamHelpers; use Mpociot\ApiDoc\Extracting\RouteDocBlocker; +use Mpociot\ApiDoc\Extracting\Strategies\Strategy; use Dingo\Api\Http\FormRequest as DingoFormRequest; -use Mpociot\ApiDoc\Extracting\ParamHelpers; use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; class GetFromUrlParamTag extends Strategy diff --git a/src/Matching/RouteMatcher.php b/src/Matching/RouteMatcher.php index d11f80d6..f46b4349 100644 --- a/src/Matching/RouteMatcher.php +++ b/src/Matching/RouteMatcher.php @@ -20,7 +20,7 @@ class RouteMatcher protected $routeRules; - public function __construct(array $routeRules = [], string $router = "laravel") + public function __construct(array $routeRules = [], string $router = 'laravel') { $this->router = $router; $this->routeRules = $routeRules; @@ -29,6 +29,7 @@ public function __construct(array $routeRules = [], string $router = "laravel") public function getRoutes() { $usingDingoRouter = strtolower($this->router) == 'dingo'; + return $this->getRoutesToBeDocumented($this->routeRules, $usingDingoRouter); } diff --git a/src/Writing/PostmanCollectionWriter.php b/src/Writing/PostmanCollectionWriter.php index 93f6f3a5..175cb4a4 100644 --- a/src/Writing/PostmanCollectionWriter.php +++ b/src/Writing/PostmanCollectionWriter.php @@ -40,7 +40,7 @@ public function getCollection() $collection = [ 'variables' => [], 'info' => [ - 'name' => config('apidoc.postman.name') ?: config('app.name') . ' API', + 'name' => config('apidoc.postman.name') ?: config('app.name'). ' API', '_postman_id' => Uuid::uuid4()->toString(), 'description' => config('apidoc.postman.description') ?: '', 'schema' => 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json', @@ -55,11 +55,11 @@ public function getCollection() return [ 'name' => $route['metadata']['title'] != '' ? $route['metadata']['title'] : url($route['uri']), 'request' => [ - 'url' => url($route['uri']) . (collect($route['queryParameters'])->isEmpty() + 'url' => url($route['uri']).(collect($route['queryParameters'])->isEmpty() ? '' : ('?' . implode('&', collect($route['queryParameters'])->map(function ($parameter, $key) { - return urlencode($key) . '=' . urlencode($parameter['value'] ?? ''); - })->all()))), + 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 index 42194f14..591bb278 100644 --- a/src/Writing/Writer.php +++ b/src/Writing/Writer.php @@ -1,9 +1,7 @@ config->get('type') === 'static'; - $sourceOutputPath = "resources/docs"; - $outputPath = $isStatic ? "public/docs" : "resources/views/apidoc"; + $sourceOutputPath = 'resources/docs'; + $outputPath = $isStatic ? 'public/docs' : 'resources/views/apidoc'; $this->writeMarkdownAndSourceFiles($this->routes, $sourceOutputPath); @@ -85,8 +83,8 @@ public function writeDocs() */ public function writeMarkdownAndSourceFiles(Collection $parsedRoutes, string $sourceOutputPath) { - $targetFile = $sourceOutputPath . '/source/index.md'; - $compareFile = $sourceOutputPath . '/source/.compare.md'; + $targetFile = $sourceOutputPath.'/source/index.md'; + $compareFile = $sourceOutputPath.'/source/.compare.md'; $infoText = view('apidoc::partials.info') ->with('outputPath', 'docs') @@ -110,14 +108,14 @@ public function writeMarkdownAndSourceFiles(Collection $parsedRoutes, string $so $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 (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']); + $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']); + $this->output->warn('Skipping modified route ['.implode(',', $route['methods']).'] '.$route['uri']); $route['modified_output'] = $existingRouteDoc[0]; } } @@ -140,7 +138,7 @@ public function writeMarkdownAndSourceFiles(Collection $parsedRoutes, string $so ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection) ->with('parsedRoutes', $parsedRouteOutput); - $this->output->info('Writing index.md and source files to: ' . $sourceOutputPath); + $this->output->info('Writing index.md and source files to: '.$sourceOutputPath); if (!is_dir($sourceOutputPath)) { $documentarian = new Documentarian(); @@ -163,14 +161,14 @@ public function writeMarkdownAndSourceFiles(Collection $parsedRoutes, string $so file_put_contents($compareFile, $compareMarkdown); - $this->output->info('Wrote index.md and source files to: ' . $sourceOutputPath); + $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'])) { + 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'; } @@ -183,6 +181,7 @@ public function generateMarkdownOutputForEachRoute(Collection $parsedRoutes, arr return $route; }); }); + return $parsedRouteOutput; } @@ -225,9 +224,10 @@ public function generatePostmanCollection(Collection $routes) */ protected function getMarkdownToPrepend(string $sourceOutputPath): string { - $prependFile = $sourceOutputPath . '/source/prepend.md'; + $prependFile = $sourceOutputPath.'/source/prepend.md'; $prependFileContents = file_exists($prependFile) - ? file_get_contents($prependFile) . "\n" : ''; + ? file_get_contents($prependFile)."\n" : ''; + return $prependFileContents; } @@ -238,16 +238,17 @@ protected function getMarkdownToPrepend(string $sourceOutputPath): string */ protected function getMarkdownToAppend(string $sourceOutputPath): string { - $appendFile = $sourceOutputPath . '/source/append.md'; + $appendFile = $sourceOutputPath.'/source/append.md'; $appendFileContents = file_exists($appendFile) - ? "\n" . file_get_contents($appendFile) : ''; + ? "\n".file_get_contents($appendFile) : ''; + return $appendFileContents; } protected function copyAssetsFromSourceFolderToPublicFolder(string $sourceOutputPath, bool $isStatic = true): void { - $publicPath = "public/docs"; - if (!is_dir($publicPath)) { + $publicPath = 'public/docs'; + if (! is_dir($publicPath)) { mkdir($publicPath, 0777, true); mkdir("{$publicPath}/css"); mkdir("{$publicPath}/js"); @@ -270,7 +271,7 @@ protected function moveOutputFromSourceFolderToTargetFolder(string $sourceOutput rename("{$sourceOutputPath}/index.html", "{$outputPath}/index.html"); } else { // Move output to resources/views - if (!is_dir($outputPath)) { + if (! is_dir($outputPath)) { mkdir($outputPath); } rename("{$sourceOutputPath}/index.html", "$outputPath/index.blade.php");