From e46e1610186ee5df27e4cc53f2e0c1527702cdd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro=20Jerry?= Date: Tue, 11 Jun 2019 13:58:53 -0300 Subject: [PATCH 001/312] Include Python as example language --- .../example-requests/python.blade.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 resources/views/partials/example-requests/python.blade.php diff --git a/resources/views/partials/example-requests/python.blade.php b/resources/views/partials/example-requests/python.blade.php new file mode 100644 index 00000000..f6a0e3bc --- /dev/null +++ b/resources/views/partials/example-requests/python.blade.php @@ -0,0 +1,27 @@ +```python +import requests +import json + +url = '{{ trim(config('app.docs_url') ?: config('app.url'), '/')}}/{{ ltrim($route['uri'], '/') }}' +@if(count($route['bodyParameters'])) +payload = { + @foreach($route['bodyParameters'] as $attribute => $parameter) + '{{ $attribute }}': '{{ $parameter['value'] }}'@if(!($loop->last)),@endif {{ !$parameter['required'] ? '# optional' : '' }} + @endforeach +} +@endif +@if(count($route['queryParameters'])) +params = { + @foreach($route['queryParameters'] as $attribute => $parameter) + '{{ $attribute }}': '{{ $parameter['value'] }}'@if(!($loop->last)),@endif {{ !$parameter['required'] ? '# optional' : '' }} + @endforeach +} +@endif +headers = { + @foreach($route['headers'] as $header => $value) + '{{$header}}': '{{$value}}'@if(!($loop->last)),@endif + @endforeach +} +response = requests.request('{{$route['methods'][0]}}', url, headers=headers{{ count($route['bodyParameters']) ? ', data=payload' : '' }}{{ count($route['queryParameters']) ? ', params=params' : ''}}) +response.json() +``` From 3578242674bed095b57be14947934ac38b1cbfe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro=20Jerry?= Date: Wed, 12 Jun 2019 09:41:12 -0300 Subject: [PATCH 002/312] Allow to add new non-native language examples --- resources/views/partials/route.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/partials/route.blade.php b/resources/views/partials/route.blade.php index 0843f60a..b5ce510c 100644 --- a/resources/views/partials/route.blade.php +++ b/resources/views/partials/route.blade.php @@ -12,7 +12,7 @@ > Example request: @foreach($settings['languages'] as $language) -@include("apidoc::partials.example-requests.$language") +@includeFirst(["apidoc::partials.example-requests.$language"], ["vendor.apidoc.partials.example-requests.$language"]) @endforeach From 87d3cdeea7e14e3ec6796073ae4043823a18218d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro=20Jerry?= Date: Wed, 12 Jun 2019 09:58:47 -0300 Subject: [PATCH 003/312] Fix compatibility @includeFirst --- resources/views/partials/route.blade.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/views/partials/route.blade.php b/resources/views/partials/route.blade.php index b5ce510c..23fb7c20 100644 --- a/resources/views/partials/route.blade.php +++ b/resources/views/partials/route.blade.php @@ -12,7 +12,11 @@ > Example request: @foreach($settings['languages'] as $language) -@includeFirst(["apidoc::partials.example-requests.$language"], ["vendor.apidoc.partials.example-requests.$language"]) +@if(view()->exists("apidoc::partials.example-requests.$language")) + @include("apidoc::partials.example-requests.$language") +@else + @include("vendor.apidoc.partials.example-requests.$language") +@endif @endforeach From b66db123690fe3aca67867d6b765b383028a1e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro=20Jerry?= Date: Wed, 12 Jun 2019 10:43:40 -0300 Subject: [PATCH 004/312] Remove unwanted white space --- resources/views/partials/route.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/partials/route.blade.php b/resources/views/partials/route.blade.php index 23fb7c20..6935540b 100644 --- a/resources/views/partials/route.blade.php +++ b/resources/views/partials/route.blade.php @@ -13,9 +13,9 @@ @foreach($settings['languages'] as $language) @if(view()->exists("apidoc::partials.example-requests.$language")) - @include("apidoc::partials.example-requests.$language") +@include("apidoc::partials.example-requests.$language") @else - @include("vendor.apidoc.partials.example-requests.$language") +@include("vendor.apidoc.partials.example-requests.$language") @endif @endforeach From aeb34bdfc82485451d0deb6f4025575fb75d5985 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 22 Jun 2019 15:02:54 +0100 Subject: [PATCH 005/312] Add --verbose flag to log output of failed response calls, etc --- src/Commands/GenerateDocumentation.php | 14 +++++++++----- src/Tools/Flags.php | 10 ++++++++++ .../ResponseStrategies/ResponseCallStrategy.php | 8 ++++++-- .../ResponseStrategies/TransformerTagsStrategy.php | 8 ++++++++ 4 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 src/Tools/Flags.php diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index a0ec1f8d..58c4ba98 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -6,6 +6,7 @@ use ReflectionException; use Illuminate\Routing\Route; use Illuminate\Console\Command; +use Mpociot\ApiDoc\Tools\Flags; use Mpociot\ApiDoc\Tools\Utils; use Mpociot\Reflection\DocBlock; use Illuminate\Support\Collection; @@ -25,6 +26,7 @@ class GenerateDocumentation extends Command */ protected $signature = 'apidoc:generate {--force : Force rewriting of existing routes} + {--verbose : Show verbose output} '; /** @@ -54,6 +56,9 @@ public function __construct(RouteMatcher $routeMatcher) */ public function handle() { + // Using a global static variable here, so fuck off if you don't like it + Flags::$shouldBeVerbose = $this->option('verbose'); + $this->docConfig = new DocumentationConfig(config('apidoc')); try { @@ -62,13 +67,12 @@ public function handle() 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"; } + $usingDingoRouter = strtolower($this->docConfig->get('router')) == 'dingo'; $routes = $this->docConfig->get('routes'); - if ($usingDingoRouter) { - $routes = $this->routeMatcher->getDingoRoutesToBeDocumented($routes); - } else { - $routes = $this->routeMatcher->getLaravelRoutesToBeDocumented($routes); - } + $routes = $usingDingoRouter + ? $this->routeMatcher->getDingoRoutesToBeDocumented($routes) + : $this->routeMatcher->getLaravelRoutesToBeDocumented($routes); $generator = new Generator($this->docConfig); $parsedRoutes = $this->processRoutes($generator, $routes); diff --git a/src/Tools/Flags.php b/src/Tools/Flags.php new file mode 100644 index 00000000..75719781 --- /dev/null +++ b/src/Tools/Flags.php @@ -0,0 +1,10 @@ +makeApiCall($request)]; } catch (\Exception $e) { echo 'Response call failed for ['.implode(',', $route->methods)."] {$route->uri}"; - // TODO - // echo "Run this again with the --debug flag for details + if (Flags::$shouldBeVerbose) { + dump($e); + } else { + echo "Run this again with the --verbose flag for details"; + } $response = null; } finally { $this->finish(); diff --git a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php index d1fc1b5c..933c3f24 100644 --- a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php +++ b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php @@ -2,6 +2,7 @@ namespace Mpociot\ApiDoc\Tools\ResponseStrategies; +use Mpociot\ApiDoc\Tools\Flags; use ReflectionClass; use ReflectionMethod; use League\Fractal\Manager; @@ -108,6 +109,10 @@ protected function instantiateTransformerModel(string $type) // try Eloquent model factory return factory($type)->make(); } catch (\Exception $e) { + if (Flags::$shouldBeVerbose) { + echo "Eloquent model factory failed to instantiate {$type}; trying to fetch from database"; + } + $instance = new $type; if ($instance instanceof \Illuminate\Database\Eloquent\Model) { try { @@ -118,6 +123,9 @@ protected function instantiateTransformerModel(string $type) } } catch (\Exception $e) { // okay, we'll stick with `new` + if (Flags::$shouldBeVerbose) { + echo "Failed to fetch first {$type} from database; using `new` to instantiate"; + } } } } From 0adf6a1670f5a70c122eb395054530037c88d0a0 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 22 Jun 2019 15:19:12 +0100 Subject: [PATCH 006/312] Add --verbose flag to log output of failed response calls, etc --- src/Commands/GenerateDocumentation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index 58c4ba98..d4331156 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -26,7 +26,6 @@ class GenerateDocumentation extends Command */ protected $signature = 'apidoc:generate {--force : Force rewriting of existing routes} - {--verbose : Show verbose output} '; /** @@ -57,6 +56,7 @@ public function __construct(RouteMatcher $routeMatcher) public function handle() { // Using a global static variable here, so fuck off if you don't like it + // Also, the --verbose option is included with all Artisan commands Flags::$shouldBeVerbose = $this->option('verbose'); $this->docConfig = new DocumentationConfig(config('apidoc')); From dc987f296e5a3d073f56c67911b2cb61ae47e9dc Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 23 Jun 2019 20:08:54 +0100 Subject: [PATCH 007/312] Add --verbose flag to log output of failed response calls, etc --- TODO.md | 1 - composer.json | 3 ++- src/Tools/ResponseStrategies/ResponseCallStrategy.php | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index aa508229..2504e689 100644 --- a/TODO.md +++ b/TODO.md @@ -3,4 +3,3 @@ - Add tests on output (deterministic) - Bring `bindings` outside of `response_calls` - Should `routes.*.apply.response_calls.headers` be replaced by `routes.*.apply.headers`? -- Implement debug flag diff --git a/composer.json b/composer.json index cbc31be8..6400bd4c 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,8 @@ "illuminate/console": "5.5.* || 5.6.* || 5.7.* || 5.8.*", "mpociot/documentarian": "^0.2.0", "mpociot/reflection-docblock": "^1.0.1", - "ramsey/uuid": "^3.8" + "ramsey/uuid": "^3.8", + "nunomaduro/collision": "^3.0" }, "require-dev": { "orchestra/testbench": "3.5.* || 3.6.* || 3.7.*", diff --git a/src/Tools/ResponseStrategies/ResponseCallStrategy.php b/src/Tools/ResponseStrategies/ResponseCallStrategy.php index 8800feb4..328f56a0 100644 --- a/src/Tools/ResponseStrategies/ResponseCallStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseCallStrategy.php @@ -8,7 +8,9 @@ use Illuminate\Routing\Route; use Mpociot\ApiDoc\Tools\Flags; use Mpociot\ApiDoc\Tools\Utils; +use NunoMaduro\Collision\Handler; use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; +use Whoops\Exception\Inspector; /** * Make a call to the route and retrieve its response. @@ -37,11 +39,14 @@ public function __invoke(Route $route, array $tags, array $routeProps) try { $response = [$this->makeApiCall($request)]; } catch (\Exception $e) { - echo 'Response call failed for ['.implode(',', $route->methods)."] {$route->uri}"; + echo 'Exception thrown during response call for ['.implode(',', $route->methods)."] {$route->uri}.\n"; if (Flags::$shouldBeVerbose) { - dump($e); + $handler = new Handler; + $handler->setInspector(new Inspector($e)); + $handler->setException($e); + $handler->handle(); } else { - echo "Run this again with the --verbose flag for details"; + echo "Run this again with the --verbose flag to see the exception.\n"; } $response = null; } finally { From c7297b74f14b007129ce9265193fbc7a96a65dae Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Sun, 23 Jun 2019 19:09:09 +0000 Subject: [PATCH 008/312] Apply fixes from StyleCI --- src/Tools/Flags.php | 6 ++---- src/Tools/ResponseStrategies/ResponseCallStrategy.php | 2 +- src/Tools/ResponseStrategies/TransformerTagsStrategy.php | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Tools/Flags.php b/src/Tools/Flags.php index 75719781..00a80285 100644 --- a/src/Tools/Flags.php +++ b/src/Tools/Flags.php @@ -1,10 +1,8 @@ Date: Sun, 23 Jun 2019 21:06:36 +0100 Subject: [PATCH 009/312] Add tests on output, config and bindings --- TODO.md | 6 +- resources/views/partials/route.blade.php | 1 + src/Tools/Utils.php | 25 +++- tests/Fixtures/TestController.php | 14 ++ tests/Fixtures/index.md | 170 ++++++++++++----------- tests/GenerateDocumentationTest.php | 67 +-------- tests/TestHelpers.php | 53 +++++++ tests/Unit/GeneratorTestCase.php | 130 ++++++++++++++--- 8 files changed, 303 insertions(+), 163 deletions(-) create mode 100644 tests/TestHelpers.php diff --git a/TODO.md b/TODO.md index 2504e689..e9345e7d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,5 @@ -- Add tests for bindings and bindings prefixes -- Add tests for config overrides -- Add tests on output (deterministic) +- Replace filesystem manipulation with lib + +Major - Bring `bindings` outside of `response_calls` - Should `routes.*.apply.response_calls.headers` be replaced by `routes.*.apply.headers`? diff --git a/resources/views/partials/route.blade.php b/resources/views/partials/route.blade.php index 0843f60a..0588433e 100644 --- a/resources/views/partials/route.blade.php +++ b/resources/views/partials/route.blade.php @@ -14,6 +14,7 @@ @foreach($settings['languages'] as $language) @include("apidoc::partials.example-requests.$language") + @endforeach @if(in_array('GET',$route['methods']) || (isset($route['showresponse']) && $route['showresponse'])) diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php index c34b19be..38b80961 100644 --- a/src/Tools/Utils.php +++ b/src/Tools/Utils.php @@ -4,6 +4,10 @@ use Illuminate\Support\Str; use Illuminate\Routing\Route; +use League\Flysystem\Adapter\Local; +use League\Flysystem\Filesystem; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; class Utils { @@ -45,7 +49,7 @@ public static function getRouteActionUses(array $action) * * @return mixed */ - protected static function replaceUrlParameterBindings(string $uri, array $bindings) + public static function replaceUrlParameterBindings(string $uri, array $bindings) { foreach ($bindings as $path => $binding) { // So we can support partial bindings like @@ -63,4 +67,23 @@ protected static function replaceUrlParameterBindings(string $uri, array $bindin return $uri; } + + public static function deleteDirectoryAndContents($dir) + { + if (is_dir($dir)) { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($files as $fileinfo) { + $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink'); + $todo($fileinfo->getRealPath()); + } + rmdir($dir); + } + /* + $adapter = new Local(__DIR__.'../../'); + $filesystem = new Filesystem($adapter);*/ + } } diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index 46a9cfe7..a38f46f9 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -112,6 +112,20 @@ public function shouldFetchRouteResponse() ]; } + public function echoesConfig() + { + return [ + 'app.env' => config('app.env') + ]; + } + + public function echoesUrlPathParameters($param) + { + return [ + 'param' => $param + ]; + } + public function shouldFetchRouteResponseWithEchoedSettings($id) { return [ diff --git a/tests/Fixtures/index.md b/tests/Fixtures/index.md index 752a0388..e5f1dea5 100644 --- a/tests/Fixtures/index.md +++ b/tests/Fixtures/index.md @@ -20,10 +20,10 @@ Welcome to the generated API reference. -#general +#Group A ## Example title. - + This will be the long description. It can also be multiple lines long. @@ -31,15 +31,16 @@ It can also be multiple lines long. ```bash curl -X GET -G "http://localhost/api/withDescription" \ - -H "Accept: application/json" \ -H "Authorization: customAuthToken" \ - -H "Custom-Header: NotSoCustom" + -H "Custom-Header: NotSoCustom" ``` ```javascript -const url = new URL("http://localhost/api/users"); +const url = new URL("http://localhost/api/withDescription"); let headers = { + "Authorization": "customAuthToken", + "Custom-Header": "NotSoCustom", "Accept": "application/json", "Content-Type": "application/json", } @@ -52,7 +53,8 @@ fetch(url, { .then(json => console.log(json)); ``` -> Example response: + +> Example response (200): ```json null @@ -66,35 +68,34 @@ null ## api/withResponseTag - > Example request: ```bash curl -X GET -G "http://localhost/api/withResponseTag" \ - -H "Accept: application/json" \ -H "Authorization: customAuthToken" \ - -H "Custom-Header: NotSoCustom" + -H "Custom-Header: NotSoCustom" ``` ```javascript -var settings = { - "async": true, - "crossDomain": true, - "url": "http://localhost/api/withResponseTag", - "method": "GET", - "headers": { - "accept": "application/json", - "Authorization": "customAuthToken", - "Custom-Header": "NotSoCustom", - } +const url = new URL("http://localhost/api/withResponseTag"); + +let headers = { + "Authorization": "customAuthToken", + "Custom-Header": "NotSoCustom", + "Accept": "application/json", + "Content-Type": "application/json", } -$.ajax(settings).done(function (response) { - console.log(response); -}); +fetch(url, { + method: "GET", + headers: headers, +}) + .then(response => response.json()) + .then(json => console.log(json)); ``` -> Example response: + +> Example response (200): ```json { @@ -114,49 +115,61 @@ $.ajax(settings).done(function (response) { ## api/withBodyParameters - > Example request: ```bash curl -X GET -G "http://localhost/api/withBodyParameters" \ - -H "Accept: application/json" \ -H "Authorization: customAuthToken" \ - -H "Custom-Header: NotSoCustom" \ - -d "user_id"=20 \ - -d "room_id"=6DZyNcBgezdjdAIs \ - -d "forever"= \ - -d "another_one"=2153.4 \ - -d "yet_another_param"={} \ - -d "even_more_param"=[] + -H "Custom-Header: NotSoCustom" \ + -H "Content-Type: application/json" \ + -d '{"user_id":9,"room_id":"consequatur","forever":false,"another_one":11613.31890586,"yet_another_param":{},"even_more_param":[],"book":{"name":"consequatur","author_id":17,"pages_count":17},"ids":[17],"users":[{"first_name":"John","last_name":"Doe"}]}' + ``` ```javascript -var settings = { - "async": true, - "crossDomain": true, - "url": "http://localhost/api/withBodyParameters", - "method": "GET", - "data": { - "user_id": 20, - "room_id": "6DZyNcBgezdjdAIs", - "forever": false, - "another_one": 2153.4, - "yet_another_param": "{}", - "even_more_param": "[]" +const url = new URL("http://localhost/api/withBodyParameters"); + +let headers = { + "Authorization": "customAuthToken", + "Custom-Header": "NotSoCustom", + "Content-Type": "application/json", + "Accept": "application/json", +} + +let body = { + "user_id": 9, + "room_id": "consequatur", + "forever": false, + "another_one": 11613.31890586, + "yet_another_param": {}, + "even_more_param": [], + "book": { + "name": "consequatur", + "author_id": 17, + "pages_count": 17 }, - "headers": { - "accept": "application/json", - "Authorization": "customAuthToken", - "Custom-Header": "NotSoCustom", - } + "ids": [ + 17 + ], + "users": [ + { + "first_name": "John", + "last_name": "Doe" + } + ] } -$.ajax(settings).done(function (response) { - console.log(response); -}); +fetch(url, { + method: "GET", + headers: headers, + body: body +}) + .then(response => response.json()) + .then(json => console.log(json)); ``` -> Example response: + +> Example response (200): ```json null @@ -165,7 +178,7 @@ null ### HTTP Request `GET api/withBodyParameters` -#### Parameters +#### Body Parameters Parameter | Type | Status | Description --------- | ------- | ------- | ------- | ----------- @@ -175,49 +188,46 @@ Parameter | Type | Status | Description another_one | number | optional | Just need something here. yet_another_param | object | required | even_more_param | array | optional | + book.name | string | optional | + book.author_id | integer | optional | + book[pages_count] | integer | optional | + ids.* | integer | optional | + users.*.first_name | string | optional | The first name of the user. + users.*.last_name | string | optional | The last name of the user. ## api/withAuthTag - Requires authentication - +
Requires authentication > Example request: ```bash curl -X GET -G "http://localhost/api/withAuthTag" \ - -H "Accept: application/json" \ -H "Authorization: customAuthToken" \ - -H "Custom-Header: NotSoCustom" + -H "Custom-Header: NotSoCustom" ``` ```javascript -var settings = { - "async": true, - "crossDomain": true, - "url": "http://localhost/api/withAuthTag", - "method": "GET", - "headers": { - "accept": "application/json", - "Authorization": "customAuthToken", - "Custom-Header": "NotSoCustom", - } +const url = new URL("http://localhost/api/withAuthTag"); + +let headers = { + "Authorization": "customAuthToken", + "Custom-Header": "NotSoCustom", + "Accept": "application/json", + "Content-Type": "application/json", } -$.ajax(settings).done(function (response) { - console.log(response); -}); +fetch(url, { + method: "GET", + headers: headers, +}) + .then(response => response.json()) + .then(json => console.log(json)); ``` -> Example response: + +> Example response (200): ```json null diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 9c1bd0d5..3284ccc6 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -2,6 +2,7 @@ namespace Mpociot\ApiDoc\Tests; +use Mpociot\ApiDoc\Tools\Utils; use ReflectionException; use Illuminate\Support\Str; use RecursiveIteratorIterator; @@ -9,7 +10,6 @@ use Orchestra\Testbench\TestCase; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; -use Illuminate\Contracts\Console\Kernel; use Mpociot\ApiDoc\Tests\Fixtures\TestController; use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider; use Illuminate\Support\Facades\Route as RouteFacade; @@ -19,6 +19,8 @@ class GenerateDocumentationTest extends TestCase { + use TestHelpers; + /** * Setup the test environment. */ @@ -29,20 +31,7 @@ public function setUp() public function tearDown() { - // delete the generated docs - compatible cross-platform - $dir = __DIR__.'/../public/docs'; - if (is_dir($dir)) { - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($files as $fileinfo) { - $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink'); - $todo($fileinfo->getRealPath()); - } - rmdir($dir); - } + Utils::deleteDirectoryAndContents(__DIR__.'/../public/docs'); } /** @@ -206,6 +195,8 @@ public function generated_markdown_file_is_correct() RouteFacade::get('/api/withBodyParameters', TestController::class.'@withBodyParameters'); RouteFacade::get('/api/withAuthTag', TestController::class.'@withAuthenticatedTag'); + // We want to have the same values for params each time + config(['apidoc.faker_seed' => 1234]); config(['apidoc.routes.0.match.prefixes' => ['api/*']]); config([ 'apidoc.routes.0.apply.headers' => [ @@ -219,7 +210,6 @@ public function generated_markdown_file_is_correct() $compareMarkdown = __DIR__.'/../public/docs/source/.compare.md'; $fixtureMarkdown = __DIR__.'/Fixtures/index.md'; - $this->markTestSkipped('Test is non-deterministic since example values for body parameters are random.'); $this->assertFilesHaveSameContent($fixtureMarkdown, $generatedMarkdown); $this->assertFilesHaveSameContent($fixtureMarkdown, $compareMarkdown); } @@ -365,49 +355,4 @@ public function supports_partial_resource_controller() $this->assertContains('Group A', $generatedMarkdown); $this->assertContains('Group B', $generatedMarkdown); } - - /** - * @param string $command - * @param array $parameters - * - * @return mixed - */ - public function artisan($command, $parameters = []) - { - $this->app[Kernel::class]->call($command, $parameters); - - return $this->app[Kernel::class]->output(); - } - - private function assertFilesHaveSameContent($pathToExpected, $pathToActual) - { - $actual = $this->getFileContents($pathToActual); - $expected = $this->getFileContents($pathToExpected); - $this->assertSame($expected, $actual); - } - - /** - * Get the contents of a file in a cross-platform-compatible way. - * - * @param $path - * - * @return string - */ - private function getFileContents($path) - { - return str_replace("\r\n", "\n", file_get_contents($path)); - } - - /** - * Assert that a string contains another string, ignoring all whitespace. - * - * @param $needle - * @param $haystack - */ - private function assertContainsIgnoringWhitespace($needle, $haystack) - { - $haystack = preg_replace('/\s/', '', $haystack); - $needle = preg_replace('/\s/', '', $needle); - $this->assertContains($needle, $haystack); - } } diff --git a/tests/TestHelpers.php b/tests/TestHelpers.php new file mode 100644 index 00000000..415679cb --- /dev/null +++ b/tests/TestHelpers.php @@ -0,0 +1,53 @@ +app[Kernel::class]->call($command, $parameters); + + return $this->app[Kernel::class]->output(); + } + + private function assertFilesHaveSameContent($pathToExpected, $pathToActual) + { + $actual = $this->getFileContents($pathToActual); + $expected = $this->getFileContents($pathToExpected); + $this->assertSame($expected, $actual); + } + + /** + * Get the contents of a file in a cross-platform-compatible way. + * + * @param $path + * + * @return string + */ + private function getFileContents($path) + { + return str_replace("\r\n", "\n", file_get_contents($path)); + } + + /** + * Assert that a string contains another string, ignoring all whitespace. + * + * @param $needle + * @param $haystack + */ + private function assertContainsIgnoringWhitespace($needle, $haystack) + { + $haystack = preg_replace('/\s/', '', $haystack); + $needle = preg_replace('/\s/', '', $needle); + $this->assertContains($needle, $haystack); + } +} \ No newline at end of file diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 9cf941d3..7a62d3e6 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -348,20 +348,6 @@ public function can_parse_transformer_tag($serializer, $expected) ); } - public function dataResources() - { - return [ - [ - null, - '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}', - ], - [ - 'League\Fractal\Serializer\JsonApiSerializer', - '{"data":{"type":null,"id":"1","attributes":{"description":"Welcome on this test versions","name":"TestName"}}}', - ], - ]; - } - /** @test */ public function can_parse_transformer_tag_with_model() { @@ -449,6 +435,104 @@ public function can_call_route_and_generate_response() ], json_decode($response['content'], true)); } + /** @test */ + public function can_override_config_during_response_call() + { + $route = $this->createRoute('POST', '/echoesConfig', 'echoesConfig', true); + + $rules = [ + 'response_calls' => [ + 'methods' => ['*'], + ], + ]; + $parsed = $this->generator->processRoute($route, $rules); + $response = json_decode(array_first($parsed['response'])['content'], true); + $originalValue = $response['app.env']; + + $now = time(); + $rules = [ + 'response_calls' => [ + 'methods' => ['*'], + 'config' => [ + 'app.env' => $now, + ], + ], + ]; + $parsed = $this->generator->processRoute($route, $rules); + $response = json_decode(array_first($parsed['response'])['content'], true); + $newValue = $response['app.env']; + $this->assertEquals($now, $newValue); + $this->assertNotEquals($originalValue, $newValue); + } + + /** @test */ + public function can_override_url_path_parameters_with_bindings() + { + $route = $this->createRoute('POST', '/echoesUrlPathParameters/{param}', 'echoesUrlPathParameters', true); + + $rand = rand(); + $rules = [ + 'response_calls' => [ + 'methods' => ['*'], + 'bindings' => [ + '{param}' => $rand, + ], + ], + ]; + $parsed = $this->generator->processRoute($route, $rules); + $response = json_decode(array_first($parsed['response'])['content'], true); + $param = $response['param']; + $this->assertEquals($rand, $param); + } + + /** @test */ + public function replaces_optional_url_path_parameters_with_bindings() + { + $route = $this->createRoute('POST', '/echoesUrlPathParameters/{param?}', 'echoesUrlPathParameters', true); + + $rand = rand(); + $rules = [ + 'response_calls' => [ + 'methods' => ['*'], + 'bindings' => [ + '{param?}' => $rand, + ], + ], + ]; + $parsed = $this->generator->processRoute($route, $rules); + $response = json_decode(array_first($parsed['response'])['content'], true); + $param = $response['param']; + $this->assertEquals($rand, $param); + } + + /** @test */ + public function uses_correct_bindings_by_prefix() + { + $route1 = $this->createRoute('POST', '/echoesUrlPathParameters/first/{param}', 'echoesUrlPathParameters', true); + $route2 = $this->createRoute('POST', '/echoesUrlPathParameters/second/{param}', 'echoesUrlPathParameters', true); + + $rand1 = rand(); + $rand2 = rand(); + $rules = [ + 'response_calls' => [ + 'methods' => ['*'], + 'bindings' => [ + 'first/{param}' => $rand1, + 'second/{param}' => $rand2, + ], + ], + ]; + $parsed = $this->generator->processRoute($route1, $rules); + $response = json_decode(array_first($parsed['response'])['content'], true); + $param = $response['param']; + $this->assertEquals($rand1, $param); + + $parsed = $this->generator->processRoute($route2, $rules); + $response = json_decode(array_first($parsed['response'])['content'], true); + $param = $response['param']; + $this->assertEquals($rand2, $param); + } + /** @test */ public function can_parse_response_file_tag() { @@ -574,9 +658,6 @@ public function uses_configured_settings_when_calling_route() 'bindings' => [ '{id}' => 3, ], - 'env' => [ - 'APP_ENV' => 'documentation', - ], 'query' => [ 'queryParam' => 'queryValue', ], @@ -595,7 +676,6 @@ public function uses_configured_settings_when_calling_route() $this->assertEquals(200, $response['status']); $responseContent = json_decode($response['content'], true); $this->assertEquals(3, $responseContent['{id}']); - $this->assertEquals('documentation', $responseContent['APP_ENV']); $this->assertEquals('queryValue', $responseContent['queryParam']); $this->assertEquals('bodyValue', $responseContent['bodyParam']); $this->assertEquals('value', $responseContent['header']); @@ -615,4 +695,18 @@ public function can_use_arrays_in_routes_uses() abstract public function createRoute(string $httpMethod, string $path, string $controllerMethod, $register = false); abstract public function createRouteUsesArray(string $httpMethod, string $path, string $controllerMethod, $register = false); + + public function dataResources() + { + return [ + [ + null, + '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}', + ], + [ + 'League\Fractal\Serializer\JsonApiSerializer', + '{"data":{"type":null,"id":"1","attributes":{"description":"Welcome on this test versions","name":"TestName"}}}', + ], + ]; + } } From 5d2be82a57c5d0ce11eeb41c894cc9c1c782187d Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 23 Jun 2019 21:23:39 +0100 Subject: [PATCH 010/312] Replace file/folder manipulation with Flysystem library --- CHANGELOG.md | 4 ++++ TODO.md | 2 -- composer.json | 3 ++- src/Tools/Utils.php | 22 ++++------------------ tests/GenerateDocumentationTest.php | 2 +- 5 files changed, 11 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4511fbbe..2e84715e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [3.10.0] - Sunday, 23 June 2019 +### Added +- `--verbose` flag to show exception encountered when making response call. + ## [3.9.0] - Saturday, 8 June 2019 ### Modified - Postman collections and URLs in example requests now use the `apidoc.base_url` config variable (https://github.com/mpociot/laravel-apidoc-generator/pull/523) diff --git a/TODO.md b/TODO.md index e9345e7d..3479e3d8 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,3 @@ -- Replace filesystem manipulation with lib - Major - Bring `bindings` outside of `response_calls` - Should `routes.*.apply.response_calls.headers` be replaced by `routes.*.apply.headers`? diff --git a/composer.json b/composer.json index 6400bd4c..da57a78a 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "mpociot/documentarian": "^0.2.0", "mpociot/reflection-docblock": "^1.0.1", "ramsey/uuid": "^3.8", - "nunomaduro/collision": "^3.0" + "nunomaduro/collision": "^3.0", + "league/flysystem": "^1.0" }, "require-dev": { "orchestra/testbench": "3.5.* || 3.6.* || 3.7.*", diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php index 38b80961..1eb2a8a6 100644 --- a/src/Tools/Utils.php +++ b/src/Tools/Utils.php @@ -4,10 +4,8 @@ use Illuminate\Support\Str; use Illuminate\Routing\Route; -use League\Flysystem\Adapter\Local; use League\Flysystem\Filesystem; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; +use League\Flysystem\Adapter\Local; class Utils { @@ -70,20 +68,8 @@ public static function replaceUrlParameterBindings(string $uri, array $bindings) public static function deleteDirectoryAndContents($dir) { - if (is_dir($dir)) { - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($files as $fileinfo) { - $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink'); - $todo($fileinfo->getRealPath()); - } - rmdir($dir); - } - /* - $adapter = new Local(__DIR__.'../../'); - $filesystem = new Filesystem($adapter);*/ + $adapter = new Local(realpath(__DIR__."/../../")); + $fs = new Filesystem($adapter); + $fs->deleteDir($dir); } } diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 3284ccc6..07b4c8a2 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -31,7 +31,7 @@ public function setUp() public function tearDown() { - Utils::deleteDirectoryAndContents(__DIR__.'/../public/docs'); + Utils::deleteDirectoryAndContents('/public/docs'); } /** From 9be9cd2ecb109b7dec5d43bb5e6d563eeee5580d Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Sun, 23 Jun 2019 20:25:02 +0000 Subject: [PATCH 011/312] Apply fixes from StyleCI --- src/Tools/Utils.php | 2 +- tests/Fixtures/TestController.php | 4 ++-- tests/GenerateDocumentationTest.php | 4 +--- tests/TestHelpers.php | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php index 1eb2a8a6..2cd70e69 100644 --- a/src/Tools/Utils.php +++ b/src/Tools/Utils.php @@ -68,7 +68,7 @@ public static function replaceUrlParameterBindings(string $uri, array $bindings) public static function deleteDirectoryAndContents($dir) { - $adapter = new Local(realpath(__DIR__."/../../")); + $adapter = new Local(realpath(__DIR__.'/../../')); $fs = new Filesystem($adapter); $fs->deleteDir($dir); } diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index a38f46f9..473437fb 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -115,14 +115,14 @@ public function shouldFetchRouteResponse() public function echoesConfig() { return [ - 'app.env' => config('app.env') + 'app.env' => config('app.env'), ]; } public function echoesUrlPathParameters($param) { return [ - 'param' => $param + 'param' => $param, ]; } diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 07b4c8a2..a86b24ca 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -2,11 +2,9 @@ namespace Mpociot\ApiDoc\Tests; -use Mpociot\ApiDoc\Tools\Utils; use ReflectionException; use Illuminate\Support\Str; -use RecursiveIteratorIterator; -use RecursiveDirectoryIterator; +use Mpociot\ApiDoc\Tools\Utils; use Orchestra\Testbench\TestCase; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; diff --git a/tests/TestHelpers.php b/tests/TestHelpers.php index 415679cb..a4425c1d 100644 --- a/tests/TestHelpers.php +++ b/tests/TestHelpers.php @@ -50,4 +50,4 @@ private function assertContainsIgnoringWhitespace($needle, $haystack) $needle = preg_replace('/\s/', '', $needle); $this->assertContains($needle, $haystack); } -} \ No newline at end of file +} From 19d56564133e8e25db9f8d48858a4e23f43da90e Mon Sep 17 00:00:00 2001 From: Shalvah Date: Sun, 23 Jun 2019 21:53:34 +0100 Subject: [PATCH 012/312] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e84715e..a822803d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.10.0] - Sunday, 23 June 2019 ### Added -- `--verbose` flag to show exception encountered when making response call. +- `--verbose` flag to show exception encountered when making response calls (https://github.com/mpociot/laravel-apidoc-generator/commit/dc987f296e5a3d073f56c67911b2cb61ae47e9dc) ## [3.9.0] - Saturday, 8 June 2019 ### Modified From 2f8ee8fec5fcd891b935393593bcb63d0fd88ecb Mon Sep 17 00:00:00 2001 From: Icaro Jerry Date: Tue, 25 Jun 2019 11:32:12 -0300 Subject: [PATCH 013/312] makes the suggested changes --- config/apidoc.php | 2 +- docs/config.md | 2 +- docs/generating-documentation.md | 2 +- .../views/partials/example-requests/python.blade.php | 12 ++++++------ resources/views/partials/route.blade.php | 5 ----- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/config/apidoc.php b/config/apidoc.php index df21372e..90c28e40 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -198,7 +198,7 @@ /* * Example requests for each endpoint will be shown in each of these languages. - * Supported options are: bash, javascript, php + * Supported options are: bash, javascript, php, python * You can add a language of your own, but you must publish the package's views * and define a corresponding view for it in the partials/example-requests directory. * See https://laravel-apidoc-generator.readthedocs.io/en/latest/generating-documentation.html diff --git a/docs/config.md b/docs/config.md index bb25f846..413c079d 100644 --- a/docs/config.md +++ b/docs/config.md @@ -35,7 +35,7 @@ If you want to use this, please note that the image size must be 230 x 52. When [documenting your api](documenting.md), you use `@group` annotations to group API endpoints. Endpoints which do not have a ggroup annotation will be grouped under the `default_group`. Defaults to **"general"**. ## `example_languages` -For each endpoint, an example request is shown in each of the languages specified in this array. Currently only `bash`, `javascript` and `php` are supported. You can add your own language, but you must also define the corresponding view (see [Specifying languages for examples](generating-documentation.html#specifying-language-for-examples)). Default: `["bash", "javascript"]` +For each endpoint, an example request is shown in each of the languages specified in this array. Currently only `bash`, `javascript`, `php` and `python` are supported. You can add your own language, but you must also define the corresponding view (see [Specifying languages for examples](generating-documentation.html#specifying-language-for-examples)). Default: `["bash", "javascript"]` ## `faker_seed` When generating example requests, this package uses fzanninoto/faker to generate random values. If you would like the package to generate the same example values for parameters on each run, set this to any number (eg. 1234). (Note: alternatively, you can set example values for parameters when [documenting them.](documenting.html#specifying-request-parameters)) diff --git a/docs/generating-documentation.md b/docs/generating-documentation.md index 44ba89a4..e0d0af54 100644 --- a/docs/generating-documentation.md +++ b/docs/generating-documentation.md @@ -47,7 +47,7 @@ php artisan apidoc:rebuild This will copy the views files to `\resources\views\vendor\apidoc`. - - Next, create a file called {language-name}.blade.php (for example, python.blade.php) in the partials/example-requests directory. You can then write Markdown with Blade templating that describes how the example request for the language should be rendered. You have the `$route` variable available to you. This variable is an array with the following keys: + - Next, create a file called {language-name}.blade.php (for example, ruby.blade.php) in the partials/example-requests directory. You can then write Markdown with Blade templating that describes how the example request for the language should be rendered. You have the `$route` variable available to you. This variable is an array with the following keys: - `methods`: an array of the HTTP methods for that route - `boundUri`: the complete URL for the route, with any url parameters replaced (/users/{id} -> /users/1) - `headers`: key-value array of headers to be sent with route (according to your configuration) diff --git a/resources/views/partials/example-requests/python.blade.php b/resources/views/partials/example-requests/python.blade.php index f6a0e3bc..9d143a35 100644 --- a/resources/views/partials/example-requests/python.blade.php +++ b/resources/views/partials/example-requests/python.blade.php @@ -2,17 +2,17 @@ import requests import json -url = '{{ trim(config('app.docs_url') ?: config('app.url'), '/')}}/{{ ltrim($route['uri'], '/') }}' -@if(count($route['bodyParameters'])) +url = '{{ rtrim($baseUrl, '/') }}/{{ ltrim($route['boundUri'], '/') }}' +@if(count($route['cleanBodyParameters'])) payload = { - @foreach($route['bodyParameters'] as $attribute => $parameter) + @foreach($route['cleancleanBodyParameters'] as $attribute => $parameter) '{{ $attribute }}': '{{ $parameter['value'] }}'@if(!($loop->last)),@endif {{ !$parameter['required'] ? '# optional' : '' }} @endforeach } @endif -@if(count($route['queryParameters'])) +@if(count($route['cleanQueryParameters'])) params = { - @foreach($route['queryParameters'] as $attribute => $parameter) + @foreach($route['cleanQueryParameters'] as $attribute => $parameter) '{{ $attribute }}': '{{ $parameter['value'] }}'@if(!($loop->last)),@endif {{ !$parameter['required'] ? '# optional' : '' }} @endforeach } @@ -22,6 +22,6 @@ '{{$header}}': '{{$value}}'@if(!($loop->last)),@endif @endforeach } -response = requests.request('{{$route['methods'][0]}}', url, headers=headers{{ count($route['bodyParameters']) ? ', data=payload' : '' }}{{ count($route['queryParameters']) ? ', params=params' : ''}}) +response = requests.request('{{$route['methods'][0]}}', url, headers=headers{{ count($route['cleanBodyParameters']) ? ', data=payload' : '' }}{{ count($route['cleanQueryParameters']) ? ', params=params' : ''}}) response.json() ``` diff --git a/resources/views/partials/route.blade.php b/resources/views/partials/route.blade.php index af5e7458..0843f60a 100644 --- a/resources/views/partials/route.blade.php +++ b/resources/views/partials/route.blade.php @@ -12,12 +12,7 @@ > Example request: @foreach($settings['languages'] as $language) -@if(view()->exists("apidoc::partials.example-requests.$language")) @include("apidoc::partials.example-requests.$language") -@else -@include("vendor.apidoc.partials.example-requests.$language") -@endif - @endforeach From d0de8fb178d9f7c3373de426334448b1d284a04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro=20Jerry?= Date: Tue, 25 Jun 2019 11:55:49 -0300 Subject: [PATCH 014/312] undo change not required --- resources/views/partials/route.blade.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/views/partials/route.blade.php b/resources/views/partials/route.blade.php index 0843f60a..0588433e 100644 --- a/resources/views/partials/route.blade.php +++ b/resources/views/partials/route.blade.php @@ -14,6 +14,7 @@ @foreach($settings['languages'] as $language) @include("apidoc::partials.example-requests.$language") + @endforeach @if(in_array('GET',$route['methods']) || (isset($route['showresponse']) && $route['showresponse'])) From 481968b99ea08faa8b7e7aaca7627544a99a5e4e Mon Sep 17 00:00:00 2001 From: Icaro Jerry Date: Tue, 9 Jul 2019 19:06:39 -0300 Subject: [PATCH 015/312] Fix request --- resources/views/partials/example-requests/python.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/partials/example-requests/python.blade.php b/resources/views/partials/example-requests/python.blade.php index 9d143a35..24e73a7d 100644 --- a/resources/views/partials/example-requests/python.blade.php +++ b/resources/views/partials/example-requests/python.blade.php @@ -22,6 +22,6 @@ '{{$header}}': '{{$value}}'@if(!($loop->last)),@endif @endforeach } -response = requests.request('{{$route['methods'][0]}}', url, headers=headers{{ count($route['cleanBodyParameters']) ? ', data=payload' : '' }}{{ count($route['cleanQueryParameters']) ? ', params=params' : ''}}) +response = requests.request('{{$route['methods'][0]}}', url, headers=headers{{ count($route['cleanBodyParameters']) ? ', json=payload' : '' }}{{ count($route['cleanQueryParameters']) ? ', params=params' : ''}}) response.json() ``` From c0492eeae153142a4d86ff033282d7a548845b39 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 11 Jul 2019 20:40:11 +0100 Subject: [PATCH 016/312] Fix tests --- composer.json | 2 +- tests/Fixtures/partial_resource_index.md | 4 ++++ tests/Fixtures/resource_index.md | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index da57a78a..af9bfeff 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "mpociot/documentarian": "^0.2.0", "mpociot/reflection-docblock": "^1.0.1", "ramsey/uuid": "^3.8", - "nunomaduro/collision": "^3.0", + "nunomaduro/collision": "^2.0 || ^3.0", "league/flysystem": "^1.0" }, "require-dev": { diff --git a/tests/Fixtures/partial_resource_index.md b/tests/Fixtures/partial_resource_index.md index 88ac1ee4..9ae056a8 100644 --- a/tests/Fixtures/partial_resource_index.md +++ b/tests/Fixtures/partial_resource_index.md @@ -30,6 +30,7 @@ Welcome to the generated API reference. curl -X GET -G "http://localhost/api/users" \ -H "Accept: application/json" ``` + ```javascript const url = new URL("http://localhost/api/users"); @@ -46,6 +47,7 @@ fetch(url, { .then(json => console.log(json)); ``` + > Example response (200): ```json @@ -69,6 +71,7 @@ fetch(url, { curl -X GET -G "http://localhost/api/users/create" \ -H "Accept: application/json" ``` + ```javascript const url = new URL("http://localhost/api/users/create"); @@ -85,6 +88,7 @@ fetch(url, { .then(json => console.log(json)); ``` + > Example response (200): ```json diff --git a/tests/Fixtures/resource_index.md b/tests/Fixtures/resource_index.md index 2f2a9310..628bba23 100644 --- a/tests/Fixtures/resource_index.md +++ b/tests/Fixtures/resource_index.md @@ -30,6 +30,7 @@ Welcome to the generated API reference. curl -X GET -G "http://localhost/api/users" \ -H "Accept: application/json" ``` + ```javascript const url = new URL("http://localhost/api/users"); @@ -46,6 +47,7 @@ fetch(url, { .then(json => console.log(json)); ``` + > Example response (200): ```json @@ -69,6 +71,7 @@ fetch(url, { curl -X GET -G "http://localhost/api/users/create" \ -H "Accept: application/json" ``` + ```javascript const url = new URL("http://localhost/api/users/create"); @@ -85,6 +88,7 @@ fetch(url, { .then(json => console.log(json)); ``` + > Example response (200): ```json @@ -108,6 +112,7 @@ fetch(url, { curl -X POST "http://localhost/api/users" \ -H "Accept: application/json" ``` + ```javascript const url = new URL("http://localhost/api/users"); @@ -125,6 +130,7 @@ fetch(url, { ``` + ### HTTP Request `POST api/users` @@ -140,6 +146,7 @@ fetch(url, { curl -X GET -G "http://localhost/api/users/1" \ -H "Accept: application/json" ``` + ```javascript const url = new URL("http://localhost/api/users/1"); @@ -156,6 +163,7 @@ fetch(url, { .then(json => console.log(json)); ``` + > Example response (200): ```json @@ -179,6 +187,7 @@ fetch(url, { curl -X GET -G "http://localhost/api/users/1/edit" \ -H "Accept: application/json" ``` + ```javascript const url = new URL("http://localhost/api/users/1/edit"); @@ -195,6 +204,7 @@ fetch(url, { .then(json => console.log(json)); ``` + > Example response (200): ```json @@ -218,6 +228,7 @@ fetch(url, { curl -X PUT "http://localhost/api/users/1" \ -H "Accept: application/json" ``` + ```javascript const url = new URL("http://localhost/api/users/1"); @@ -235,6 +246,7 @@ fetch(url, { ``` + ### HTTP Request `PUT api/users/{user}` @@ -252,6 +264,7 @@ fetch(url, { curl -X DELETE "http://localhost/api/users/1" \ -H "Accept: application/json" ``` + ```javascript const url = new URL("http://localhost/api/users/1"); @@ -269,6 +282,7 @@ fetch(url, { ``` + ### HTTP Request `DELETE api/users/{user}` From 0c1c8d77a945e842e4e518d8ec4882b833087c9b Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 11 Jul 2019 20:46:36 +0100 Subject: [PATCH 017/312] Fix tests --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index af9bfeff..7754066e 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "mpociot/documentarian": "^0.2.0", "mpociot/reflection-docblock": "^1.0.1", "ramsey/uuid": "^3.8", - "nunomaduro/collision": "^2.0 || ^3.0", + "nunomaduro/collision": "^1.1 || ^3.0", "league/flysystem": "^1.0" }, "require-dev": { From 2f3a2144e1a4f1eb0229aea8b4d11707cb4aabbf Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 11 Jul 2019 20:57:42 +0100 Subject: [PATCH 018/312] Fix build - remove collision package by default --- composer.json | 4 ++-- .../ResponseStrategies/ResponseCallStrategy.php | 7 +------ src/Tools/Utils.php | 13 +++++++++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index 7754066e..0da1d5a8 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,6 @@ "mpociot/documentarian": "^0.2.0", "mpociot/reflection-docblock": "^1.0.1", "ramsey/uuid": "^3.8", - "nunomaduro/collision": "^1.1 || ^3.0", "league/flysystem": "^1.0" }, "require-dev": { @@ -34,7 +33,8 @@ "league/fractal": "^0.17.0" }, "suggest": { - "league/fractal": "Required for transformers support" + "league/fractal": "Required for transformers support", + "nunomaduro/collision": "For better reporting of errors that are thrpwn when generating docs" }, "autoload": { "psr-4": { diff --git a/src/Tools/ResponseStrategies/ResponseCallStrategy.php b/src/Tools/ResponseStrategies/ResponseCallStrategy.php index dfd59f51..32fe7284 100644 --- a/src/Tools/ResponseStrategies/ResponseCallStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseCallStrategy.php @@ -8,8 +8,6 @@ use Illuminate\Routing\Route; use Mpociot\ApiDoc\Tools\Flags; use Mpociot\ApiDoc\Tools\Utils; -use Whoops\Exception\Inspector; -use NunoMaduro\Collision\Handler; use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; /** @@ -41,10 +39,7 @@ public function __invoke(Route $route, array $tags, array $routeProps) } catch (\Exception $e) { echo 'Exception thrown during response call for ['.implode(',', $route->methods)."] {$route->uri}.\n"; if (Flags::$shouldBeVerbose) { - $handler = new Handler; - $handler->setInspector(new Inspector($e)); - $handler->setException($e); - $handler->handle(); + Utils::dumpException($e); } else { echo "Run this again with the --verbose flag to see the exception.\n"; } diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php index 2cd70e69..5962f189 100644 --- a/src/Tools/Utils.php +++ b/src/Tools/Utils.php @@ -66,6 +66,19 @@ public static function replaceUrlParameterBindings(string $uri, array $bindings) return $uri; } + public function dumpException(\Exception $e) + { + if (class_exists(\NunoMaduro\Collision\Handler::class)) { + $handler = new \NunoMaduro\Collision\Handler; + $handler->setInspector(new \Whoops\Exception\Inspector($e)); + $handler->setException($e); + $handler->handle(); + } else { + dump($e); + echo "You can get better exception output by installing the library \nunomaduro/collision (PHP 7.1+ only).\n"; + } + } + public static function deleteDirectoryAndContents($dir) { $adapter = new Local(realpath(__DIR__.'/../../')); From 6f28592e38da95c99597d46fdee15400b4a8366e Mon Sep 17 00:00:00 2001 From: Icaro Jerry Date: Thu, 11 Jul 2019 20:39:17 -0300 Subject: [PATCH 019/312] Fix attribute name --- resources/views/partials/example-requests/python.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/partials/example-requests/python.blade.php b/resources/views/partials/example-requests/python.blade.php index 24e73a7d..4633aa2b 100644 --- a/resources/views/partials/example-requests/python.blade.php +++ b/resources/views/partials/example-requests/python.blade.php @@ -5,7 +5,7 @@ url = '{{ rtrim($baseUrl, '/') }}/{{ ltrim($route['boundUri'], '/') }}' @if(count($route['cleanBodyParameters'])) payload = { - @foreach($route['cleancleanBodyParameters'] as $attribute => $parameter) + @foreach($route['cleanBodyParameters'] as $attribute => $parameter) '{{ $attribute }}': '{{ $parameter['value'] }}'@if(!($loop->last)),@endif {{ !$parameter['required'] ? '# optional' : '' }} @endforeach } From e927f702faa0ccb91e18b6634772c7f4e08080e9 Mon Sep 17 00:00:00 2001 From: Daniel Alm Date: Mon, 15 Jul 2019 12:44:46 +0200 Subject: [PATCH 020/312] Make a few `ResponseCallStrategy` methods `protected`. Motivation: This lets me subclass `ResponseCallStrategy` and modify e.g. `makeApiCall` to do extra work before/after the request has been performed. Examples for "preflight" work would be creating a temporary user for the API call, preparing "fixtures" that the API call can return, etc.. "Post-flight" work could be sanitizing the output for inclusion in the API docs, e.g. if I want my response to a "POST" call to always have the same ID for the new object. --- src/Tools/ResponseStrategies/ResponseCallStrategy.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tools/ResponseStrategies/ResponseCallStrategy.php b/src/Tools/ResponseStrategies/ResponseCallStrategy.php index 32fe7284..4857315e 100644 --- a/src/Tools/ResponseStrategies/ResponseCallStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseCallStrategy.php @@ -71,7 +71,7 @@ private function configureEnvironment(array $rulesToApply) * * @return Request */ - private function prepareRequest(Route $route, array $rulesToApply, array $bodyParams, array $queryParams) + protected function prepareRequest(Route $route, array $rulesToApply, array $bodyParams, array $queryParams) { $uri = Utils::getFullUrl($route, $rulesToApply['bindings'] ?? []); $routeMethods = $this->getMethods($route); @@ -261,7 +261,7 @@ private function addBodyParameters(Request $request, array $body) * * @return \Illuminate\Http\JsonResponse|mixed|\Symfony\Component\HttpFoundation\Response */ - private function makeApiCall(Request $request) + protected function makeApiCall(Request $request) { if (config('apidoc.router') == 'dingo') { $response = $this->callDingoRoute($request); @@ -279,7 +279,7 @@ private function makeApiCall(Request $request) * * @return \Symfony\Component\HttpFoundation\Response */ - private function callLaravelRoute(Request $request): \Symfony\Component\HttpFoundation\Response + protected function callLaravelRoute(Request $request): \Symfony\Component\HttpFoundation\Response { $kernel = app(\Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle($request); From 573e9efb63026f1169a00f6b9c56541fd93a3fd2 Mon Sep 17 00:00:00 2001 From: Daniel Alm Date: Tue, 16 Jul 2019 11:42:46 +0200 Subject: [PATCH 021/312] Include query parameters and headers in the generated Postman collection. (#537) * Include query parameters in the generated Postman collection. * Style fixes * Also include headers in the Postman collection. * Also include JSON content type headers for good measure. * Update the tests. * Add a test to validate the body parameters in a Postman collection. * Remove the "Content-Type" header again, as we are actually sending content as form data. * Add a query test and make the tests more deterministic. * PR fixes. --- src/Postman/CollectionWriter.php | 19 ++- tests/Fixtures/collection.json | 56 ++++++++- tests/Fixtures/collection_updated_url.json | 56 ++++++++- .../collection_with_body_parameters.json | 109 ++++++++++++++++++ .../collection_with_custom_headers.json | 44 +++++++ .../collection_with_query_parameters.json | 36 ++++++ tests/GenerateDocumentationTest.php | 66 ++++++++++- 7 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 tests/Fixtures/collection_with_body_parameters.json create mode 100644 tests/Fixtures/collection_with_custom_headers.json create mode 100644 tests/Fixtures/collection_with_query_parameters.json diff --git a/src/Postman/CollectionWriter.php b/src/Postman/CollectionWriter.php index a8d1e364..990adafe 100644 --- a/src/Postman/CollectionWriter.php +++ b/src/Postman/CollectionWriter.php @@ -56,14 +56,29 @@ public function getCollection() return [ 'name' => $route['title'] != '' ? $route['title'] : url(https://github.com/$route['uri']), 'request' => [ - 'url' => url(https://github.com/$route['uri']), + 'url' => url(https://github.com/$route['uri']).(collect($route['queryParameters'])->isEmpty() + ? '' + : ('?'.implode('&', collect($route['queryParameters'])->map(function ($parameter, $key) { + return $key.'='.($parameter['value'] ?? ''); + })->all()))), 'method' => $route['methods'][0], + 'header' => collect($route['headers']) + ->union([ + 'Accept' => 'application/json', + ]) + ->map(function ($value, $header) { + return [ + 'key' => $header, + 'value' => $value, + ]; + }) + ->values()->all(), 'body' => [ 'mode' => $mode, $mode => collect($route['bodyParameters'])->map(function ($parameter, $key) { return [ 'key' => $key, - 'value' => isset($parameter['value']) ? $parameter['value'] : '', + 'value' => $parameter['value'] ?? '', 'type' => 'text', 'enabled' => true, ]; diff --git a/tests/Fixtures/collection.json b/tests/Fixtures/collection.json index 2f56d052..03168bcc 100644 --- a/tests/Fixtures/collection.json +++ b/tests/Fixtures/collection.json @@ -1 +1,55 @@ -{"variables":[],"info":{"name":"Laravel API","_postman_id":"","description":"","schema":"https:\/\/schema.getpostman.com\/json\/collection\/v2.0.0\/collection.json"},"item":[{"name":"Group A","description":"","item":[{"name":"Example title.","request":{"url":"http:\/\/localhost\/api\/test","method":"GET","body":{"mode":"formdata","formdata":[]},"description":"This will be the long description.\nIt can also be multiple lines long.","response":[]}},{"name":"http:\/\/localhost\/api\/responseTag","request":{"url":"http:\/\/localhost\/api\/responseTag","method":"POST","body":{"mode":"formdata","formdata":[]},"description":"","response":[]}}]}]} +{ + "variables": [], + "info": { + "name": "Laravel API", + "_postman_id": "", + "description": "", + "schema": "https:\/\/schema.getpostman.com\/json\/collection\/v2.0.0\/collection.json" + }, + "item": [ + { + "name": "Group A", + "description": "", + "item": [ + { + "name": "Example title.", + "request": { + "url": "http:\/\/localhost\/api\/test", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "This will be the long description.\nIt can also be multiple lines long.", + "response": [] + } + }, + { + "name": "http:\/\/localhost\/api\/responseTag", + "request": { + "url": "http:\/\/localhost\/api\/responseTag", + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "", + "response": [] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Fixtures/collection_updated_url.json b/tests/Fixtures/collection_updated_url.json index f2397710..fbcccef3 100644 --- a/tests/Fixtures/collection_updated_url.json +++ b/tests/Fixtures/collection_updated_url.json @@ -1 +1,55 @@ -{"variables":[],"info":{"name":"Laravel API","_postman_id":"","description":"","schema":"https:\/\/schema.getpostman.com\/json\/collection\/v2.0.0\/collection.json"},"item":[{"name":"Group A","description":"","item":[{"name":"Example title.","request":{"url":"http:\/\/yourapp.app\/api\/test","method":"GET","body":{"mode":"formdata","formdata":[]},"description":"This will be the long description.\nIt can also be multiple lines long.","response":[]}},{"name":"http:\/\/yourapp.app\/api\/responseTag","request":{"url":"http:\/\/yourapp.app\/api\/responseTag","method":"POST","body":{"mode":"formdata","formdata":[]},"description":"","response":[]}}]}]} +{ + "variables": [], + "info": { + "name": "Laravel API", + "_postman_id": "", + "description": "", + "schema": "https:\/\/schema.getpostman.com\/json\/collection\/v2.0.0\/collection.json" + }, + "item": [ + { + "name": "Group A", + "description": "", + "item": [ + { + "name": "Example title.", + "request": { + "url": "http:\/\/yourapp.app\/api\/test", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "This will be the long description.\nIt can also be multiple lines long.", + "response": [] + } + }, + { + "name": "http:\/\/yourapp.app\/api\/responseTag", + "request": { + "url": "http:\/\/yourapp.app\/api\/responseTag", + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "", + "response": [] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Fixtures/collection_with_body_parameters.json b/tests/Fixtures/collection_with_body_parameters.json new file mode 100644 index 00000000..438f2f88 --- /dev/null +++ b/tests/Fixtures/collection_with_body_parameters.json @@ -0,0 +1,109 @@ +{ + "variables": [], + "info": { + "name": "Laravel API", + "_postman_id": "", + "description": "", + "schema": "https:\/\/schema.getpostman.com\/json\/collection\/v2.0.0\/collection.json" + }, + "item": [ + { + "name": "Group A", + "description": "", + "item": [ + { + "name": "http://localhost/api/withBodyParameters", + "request": { + "url": "http:\/\/localhost\/api\/withBodyParameters", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "user_id", + "value": 9, + "type": "text", + "enabled": true + }, + { + "key": "room_id", + "value": "consequatur", + "type": "text", + "enabled": true + }, + { + "key": "forever", + "value": false, + "type": "text", + "enabled": true + }, + { + "key": "another_one", + "value": 11613.31890586, + "type": "text", + "enabled": true + }, + { + "key": "yet_another_param", + "value": [], + "type": "text", + "enabled": true + }, + { + "key": "even_more_param", + "value": [], + "type": "text", + "enabled": true + }, + { + "key": "book.name", + "value": "consequatur", + "type": "text", + "enabled": true + }, + { + "key": "book.author_id", + "value": 17, + "type": "text", + "enabled": true + }, + { + "key": "book[pages_count]", + "value": 17, + "type": "text", + "enabled": true + }, + { + "key": "ids.*", + "value": 17, + "type": "text", + "enabled": true + }, + { + "key": "users.*.first_name", + "value": "John", + "type": "text", + "enabled": true + }, + { + "key": "users.*.last_name", + "value": "Doe", + "type": "text", + "enabled": true + } + ] + }, + "description": "", + "response": [] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Fixtures/collection_with_custom_headers.json b/tests/Fixtures/collection_with_custom_headers.json new file mode 100644 index 00000000..fd8d17f2 --- /dev/null +++ b/tests/Fixtures/collection_with_custom_headers.json @@ -0,0 +1,44 @@ +{ + "variables": [], + "info": { + "name": "Laravel API", + "_postman_id": "", + "description": "", + "schema": "https:\/\/schema.getpostman.com\/json\/collection\/v2.0.0\/collection.json" + }, + "item": [ + { + "name": "Group A", + "description": "", + "item": [ + { + "name": "http://localhost/api/headers", + "request": { + "url": "http:\/\/localhost\/api\/headers", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "customAuthToken" + }, + { + "key": "Custom-Header", + "value": "NotSoCustom" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "", + "response": [] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Fixtures/collection_with_query_parameters.json b/tests/Fixtures/collection_with_query_parameters.json new file mode 100644 index 00000000..83a58ecc --- /dev/null +++ b/tests/Fixtures/collection_with_query_parameters.json @@ -0,0 +1,36 @@ +{ + "variables": [], + "info": { + "name": "Laravel API", + "_postman_id": "", + "description": "", + "schema": "https:\/\/schema.getpostman.com\/json\/collection\/v2.0.0\/collection.json" + }, + "item": [ + { + "name": "Group A", + "description": "", + "item": [ + { + "name": "http://localhost/api/withQueryParameters", + "request": { + "url": "http:\/\/localhost\/api\/withQueryParameters?location_id=consequatur&user_id=me&page=4&filters=consequatur", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "", + "response": [] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index a86b24ca..d8a8b165 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -242,9 +242,10 @@ public function generated_postman_collection_file_is_correct() config(['apidoc.routes.0.match.prefixes' => ['api/*']]); $this->artisan('apidoc:generate'); - $generatedCollection = json_decode(file_get_contents(__DIR__.'/../public/docs/collection.json')); - $generatedCollection->info->_postman_id = ''; - $fixtureCollection = json_decode(file_get_contents(__DIR__.'/Fixtures/collection.json')); + $generatedCollection = json_decode(file_get_contents(__DIR__.'/../public/docs/collection.json'), true); + // The Postman ID varies from call to call; erase it to make the test data reproducible. + $generatedCollection['info']['_postman_id'] = ''; + $fixtureCollection = json_decode(file_get_contents(__DIR__.'/Fixtures/collection.json'), true); $this->assertEquals($generatedCollection, $fixtureCollection); } @@ -273,9 +274,62 @@ public function generated_postman_collection_can_have_custom_url() config(['apidoc.routes.0.match.prefixes' => ['api/*']]); $this->artisan('apidoc:generate'); - $generatedCollection = json_decode(file_get_contents(__DIR__.'/../public/docs/collection.json')); - $generatedCollection->info->_postman_id = ''; - $fixtureCollection = json_decode(file_get_contents(__DIR__.'/Fixtures/collection_updated_url.json')); + $generatedCollection = json_decode(file_get_contents(__DIR__.'/../public/docs/collection.json'), true); + // The Postman ID varies from call to call; erase it to make the test data reproducible. + $generatedCollection['info']['_postman_id'] = ''; + $fixtureCollection = json_decode(file_get_contents(__DIR__.'/Fixtures/collection_updated_url.json'), true); + $this->assertEquals($generatedCollection, $fixtureCollection); + } + + /** @test */ + public function generated_postman_collection_can_append_custom_http_headers() + { + RouteFacade::get('/api/headers', TestController::class.'@checkCustomHeaders'); + config(['apidoc.routes.0.match.prefixes' => ['api/*']]); + config([ + 'apidoc.routes.0.apply.headers' => [ + 'Authorization' => 'customAuthToken', + 'Custom-Header' => 'NotSoCustom', + ], + ]); + $this->artisan('apidoc:generate'); + + $generatedCollection = json_decode(file_get_contents(__DIR__.'/../public/docs/collection.json'), true); + // The Postman ID varies from call to call; erase it to make the test data reproducible. + $generatedCollection['info']['_postman_id'] = ''; + $fixtureCollection = json_decode(file_get_contents(__DIR__.'/Fixtures/collection_with_custom_headers.json'), true); + $this->assertEquals($generatedCollection, $fixtureCollection); + } + + /** @test */ + public function generated_postman_collection_can_have_query_parameters() + { + RouteFacade::get('/api/withQueryParameters', TestController::class.'@withQueryParameters'); + // We want to have the same values for params each time + config(['apidoc.faker_seed' => 1234]); + config(['apidoc.routes.0.match.prefixes' => ['api/*']]); + $this->artisan('apidoc:generate'); + + $generatedCollection = json_decode(file_get_contents(__DIR__.'/../public/docs/collection.json'), true); + // The Postman ID varies from call to call; erase it to make the test data reproducible. + $generatedCollection['info']['_postman_id'] = ''; + $fixtureCollection = json_decode(file_get_contents(__DIR__.'/Fixtures/collection_with_query_parameters.json'), true); + $this->assertEquals($generatedCollection, $fixtureCollection); + } + + /** @test */ + public function generated_postman_collection_can_add_body_parameters() + { + RouteFacade::get('/api/withBodyParameters', TestController::class.'@withBodyParameters'); + // We want to have the same values for params each time + config(['apidoc.faker_seed' => 1234]); + config(['apidoc.routes.0.match.prefixes' => ['api/*']]); + $this->artisan('apidoc:generate'); + + $generatedCollection = json_decode(file_get_contents(__DIR__.'/../public/docs/collection.json'), true); + // The Postman ID varies from call to call; erase it to make the test data reproducible. + $generatedCollection['info']['_postman_id'] = ''; + $fixtureCollection = json_decode(file_get_contents(__DIR__.'/Fixtures/collection_with_body_parameters.json'), true); $this->assertEquals($generatedCollection, $fixtureCollection); } From 45310bf9a3c69848c4c900b491904ce43009b5be Mon Sep 17 00:00:00 2001 From: Daniel Alm Date: Tue, 16 Jul 2019 20:17:38 +0200 Subject: [PATCH 022/312] Only require the package for dev environments by default. Given that the API docs are just static files, the generator does not need to be shipped in production environments. Therefore, this change updates the `composer require` command in the README to only require the package in dev environments. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd7b865e..c4df853f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Automatically generate your API documentation from your existing Laravel/Lumen/[ PHP 7 and Laravel 5.5 or higher are required. ```sh -composer require mpociot/laravel-apidoc-generator +composer require --dev mpociot/laravel-apidoc-generator ``` ### Laravel @@ -28,6 +28,7 @@ php artisan vendor:publish --provider="Mpociot\ApiDoc\ApiDocGeneratorServiceProv This will create an `apidoc.php` file in your `config` folder. ### Lumen +- When using Lumen, you will need to run `composer require mpociot/laravel-apidoc-generator` instead. - Register the service provider in your `bootstrap/app.php`: ```php From 6bc4b53f8cce3ac71fe72ea831c800c1d1862092 Mon Sep 17 00:00:00 2001 From: Daniel Alm Date: Mon, 29 Jul 2019 11:33:01 +0200 Subject: [PATCH 023/312] Add support for query parameters in the bash template and ensure that all query parameters are URL-encoded. --- .../partials/example-requests/bash.blade.php | 5 +- src/Postman/CollectionWriter.php | 2 +- tests/Fixtures/TestController.php | 1 + .../collection_with_query_parameters.json | 2 +- tests/Fixtures/index.md | 59 +++++++++++++++++++ tests/GenerateDocumentationTest.php | 1 + 6 files changed, 67 insertions(+), 3 deletions(-) diff --git a/resources/views/partials/example-requests/bash.blade.php b/resources/views/partials/example-requests/bash.blade.php index 8924da1c..dde5c265 100644 --- a/resources/views/partials/example-requests/bash.blade.php +++ b/resources/views/partials/example-requests/bash.blade.php @@ -1,5 +1,8 @@ ```bash -curl -X {{$route['methods'][0]}} {{$route['methods'][0] == 'GET' ? '-G ' : ''}}"{{ rtrim($baseUrl, '/')}}/{{ ltrim($route['boundUri'], '/') }}" @if(count($route['headers']))\ +curl -X {{$route['methods'][0]}} {{$route['methods'][0] == 'GET' ? '-G ' : ''}}"{{ rtrim($baseUrl, '/')}}/{{ ltrim($route['boundUri'], '/') }}@if(count($route['queryParameters']))?@foreach($route['queryParameters'] as $attribute => $parameter) +{{ urlencode($attribute) }}={{ urlencode($parameter['value']) }}@if(!$loop->last)&@endif +@endforeach +@endif" @if(count($route['headers']))\ @foreach($route['headers'] as $header => $value) -H "{{$header}}: {{$value}}"@if(! ($loop->last) || ($loop->last && count($route['bodyParameters']))) \ @endif diff --git a/src/Postman/CollectionWriter.php b/src/Postman/CollectionWriter.php index 990adafe..64019789 100644 --- a/src/Postman/CollectionWriter.php +++ b/src/Postman/CollectionWriter.php @@ -59,7 +59,7 @@ public function getCollection() 'url' => url(https://github.com/$route['uri']).(collect($route['queryParameters'])->isEmpty() ? '' : ('?'.implode('&', collect($route['queryParameters'])->map(function ($parameter, $key) { - return $key.'='.($parameter['value'] ?? ''); + return urlencode($key).'='.urlencode($parameter['value'] ?? ''); })->all()))), 'method' => $route['methods'][0], 'header' => collect($route['headers']) diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index 473437fb..024f43fc 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -75,6 +75,7 @@ public function withNonCommentedFormRequestParameter(TestNonCommentedRequest $re * @queryParam user_id required The id of the user. Example: me * @queryParam page required The page number. Example: 4 * @queryParam filters The filters. + * @queryParam url_encoded Used for testing that URL parameters will be URL-encoded where needed. Example: + []&= */ public function withQueryParameters() { diff --git a/tests/Fixtures/collection_with_query_parameters.json b/tests/Fixtures/collection_with_query_parameters.json index 83a58ecc..54274a0e 100644 --- a/tests/Fixtures/collection_with_query_parameters.json +++ b/tests/Fixtures/collection_with_query_parameters.json @@ -14,7 +14,7 @@ { "name": "http://localhost/api/withQueryParameters", "request": { - "url": "http:\/\/localhost\/api\/withQueryParameters?location_id=consequatur&user_id=me&page=4&filters=consequatur", + "url": "http:\/\/localhost\/api\/withQueryParameters?location_id=consequatur&user_id=me&page=4&filters=consequatur&url_encoded=%2B+%5B%5D%26%3D", "method": "GET", "header": [ { diff --git a/tests/Fixtures/index.md b/tests/Fixtures/index.md index e5f1dea5..5d97e980 100644 --- a/tests/Fixtures/index.md +++ b/tests/Fixtures/index.md @@ -197,6 +197,65 @@ Parameter | Type | Status | Description + +## api/withQueryParameters +> Example request: + +```bash +curl -X GET -G "http://localhost/api/withQueryParameters?location_id=consequatur&user_id=me&page=4&filters=consequatur&url_encoded=%2B+%5B%5D%26%3D" \ + -H "Authorization: customAuthToken" \ + -H "Custom-Header: NotSoCustom" +``` + +```javascript +const url = new URL("http://localhost/api/withQueryParameters"); + + let params = { + "location_id": "consequatur", + "user_id": "me", + "page": "4", + "filters": "consequatur", + "url_encoded": "+ []&=", + }; + Object.keys(params).forEach(key => url.searchParams.append(key, params[key])); + +let headers = { + "Authorization": "customAuthToken", + "Custom-Header": "NotSoCustom", + "Accept": "application/json", + "Content-Type": "application/json", +} + +fetch(url, { + method: "GET", + headers: headers, +}) + .then(response => response.json()) + .then(json => console.log(json)); +``` + + +> Example response (200): + +```json +null +``` + +### HTTP Request +`GET api/withQueryParameters` + +#### Query Parameters + +Parameter | Status | Description +--------- | ------- | ------- | ----------- + location_id | required | The id of the location. + user_id | required | The id of the user. + page | required | The page number. + filters | optional | The filters. + url_encoded | optional | Used for testing that URL parameters will be URL-encoded where needed. + + + ## api/withAuthTag
Requires authentication diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index d8a8b165..012ebae9 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -191,6 +191,7 @@ public function generated_markdown_file_is_correct() RouteFacade::get('/api/withDescription', TestController::class.'@withEndpointDescription'); RouteFacade::get('/api/withResponseTag', TestController::class.'@withResponseTag'); RouteFacade::get('/api/withBodyParameters', TestController::class.'@withBodyParameters'); + RouteFacade::get('/api/withQueryParameters', TestController::class.'@withQueryParameters'); RouteFacade::get('/api/withAuthTag', TestController::class.'@withAuthenticatedTag'); // We want to have the same values for params each time From b3a93deda0ae11447cb543386aa0c06492f7a98d Mon Sep 17 00:00:00 2001 From: Guilherme Pressutto Date: Fri, 2 Aug 2019 09:38:32 -0300 Subject: [PATCH 024/312] Not using config helper inside config file --- config/apidoc.php | 2 +- src/Commands/GenerateDocumentation.php | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/config/apidoc.php b/config/apidoc.php index 90c28e40..aba55635 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -17,7 +17,7 @@ * The base URL to be used in examples and the Postman collection. * By default, this will be the value of config('app.url'). */ - 'base_url' => config('app.url'), + 'base_url' => null, /* * Generate a Postman collection in addition to HTML docs. diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index d4331156..016372e7 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -42,6 +42,11 @@ class GenerateDocumentation extends Command */ private $docConfig; + /** + * @var string + */ + private $baseUrl; + public function __construct(RouteMatcher $routeMatcher) { parent::__construct(); @@ -60,9 +65,10 @@ public function handle() Flags::$shouldBeVerbose = $this->option('verbose'); $this->docConfig = new DocumentationConfig(config('apidoc')); + $this->baseUrl = $this->docConfig->get('base_url') ?? config('app.url'); try { - URL::forceRootUrl($this->docConfig->get('base_url')); + 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"; @@ -111,7 +117,7 @@ private function writeMarkdown($parsedRoutes) $route['output'] = (string) view('apidoc::partials.route') ->with('route', $route) ->with('settings', $settings) - ->with('baseUrl', $this->docConfig->get('base_url')) + ->with('baseUrl', $this->baseUrl) ->render(); return $route; @@ -288,7 +294,7 @@ private function isRouteVisibleForDocumentation($action) */ private function generatePostmanCollection(Collection $routes) { - $writer = new CollectionWriter($routes, $this->docConfig->get('base_url')); + $writer = new CollectionWriter($routes, $this->baseUrl); return $writer->getCollection(); } From 921a365f5b9859f782cf34c8dba8f61609d4447a Mon Sep 17 00:00:00 2001 From: Shalvah Date: Fri, 9 Aug 2019 19:43:04 +0100 Subject: [PATCH 025/312] Update CHANGELOG.md --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a822803d..e36ea8b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [3.11.0] - Friday, 9 August 2019 +### Added +- Support for query parameters in the bash template (https://github.com/mpociot/laravel-apidoc-generator/pull/545) +- Include query parameters and headers in the generated Postman collection (https://github.com/mpociot/laravel-apidoc-generator/pull/537) +- Include Python out of the box as example language (https://github.com/mpociot/laravel-apidoc-generator/pull/524) + +### Changed +- Moved nunomaduro/collision to "suggested" so it doesn't break PHP 7.0 (https://github.com/mpociot/laravel-apidoc-generator/commit/2f3a2144e1a4f1eb0229aea8b4d11707cb4aabbf) + +### Fixed +- Stopped using config helper inside config file (https://github.com/mpociot/laravel-apidoc-generator/pull/548) + ## [3.10.0] - Sunday, 23 June 2019 ### Added - `--verbose` flag to show exception encountered when making response calls (https://github.com/mpociot/laravel-apidoc-generator/commit/dc987f296e5a3d073f56c67911b2cb61ae47e9dc) From 1df54a8c8d921b4d5ca686212d684480984e8221 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Fri, 16 Aug 2019 10:47:00 +0200 Subject: [PATCH 026/312] Allows users to exclude a parameter from having an example - Includes 'No-example' in the doc-block - Useful for optional params in the request, or those that conflict with other params (ie only one of two are allowed) - Implement with a simple `strpos()` search --- src/Tools/Generator.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 8f22b167..19fce33d 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -130,6 +130,9 @@ protected function getBodyParametersFromDocBlock(array $tags) ->filter(function ($tag) { return $tag instanceof Tag && $tag->getName() === 'bodyParam'; }) + ->filter(function (Tag $tag) { + return !$this->shouldExcludeExample($tag); + }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); if (empty($content)) { @@ -205,6 +208,9 @@ protected function getQueryParametersFromDocBlock(array $tags) ->filter(function ($tag) { return $tag instanceof Tag && $tag->getName() === 'queryParam'; }) + ->filter(function (Tag $tag) { + return !$this->shouldExcludeExample($tag); + }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); if (empty($content)) { @@ -367,6 +373,19 @@ private function parseDescription(string $description, string $type) return [$description, $example]; } + /** + * Allows users to specify that we shouldn't generate an example for the parameter + * by writing 'No-example' + * + * @param Tag $tag + * + * @return bool Whether no example should be generated + */ + private function shouldExcludeExample(Tag $tag) + { + return strpos($tag->getContent(), ' No-example') !== false; + } + /** * Cast a value from a string to a specified type. * From 2901a02e7bcef435d957d5a7e0ab4ebf0dfc1222 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Fri, 16 Aug 2019 10:50:11 +0200 Subject: [PATCH 027/312] Style-ci fixes --- src/Tools/Generator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 19fce33d..3bdb974a 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -131,7 +131,7 @@ protected function getBodyParametersFromDocBlock(array $tags) return $tag instanceof Tag && $tag->getName() === 'bodyParam'; }) ->filter(function (Tag $tag) { - return !$this->shouldExcludeExample($tag); + return ! $this->shouldExcludeExample($tag); }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); @@ -209,7 +209,7 @@ protected function getQueryParametersFromDocBlock(array $tags) return $tag instanceof Tag && $tag->getName() === 'queryParam'; }) ->filter(function (Tag $tag) { - return !$this->shouldExcludeExample($tag); + return ! $this->shouldExcludeExample($tag); }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); @@ -375,7 +375,7 @@ private function parseDescription(string $description, string $type) /** * Allows users to specify that we shouldn't generate an example for the parameter - * by writing 'No-example' + * by writing 'No-example'. * * @param Tag $tag * From 682fd1d40b6f2191e4b4d46b827d78ba6acf2686 Mon Sep 17 00:00:00 2001 From: Khuong Van Cong Date: Wed, 21 Aug 2019 10:46:46 +0700 Subject: [PATCH 028/312] Correct URI in example request for PHP --- resources/views/partials/example-requests/php.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/partials/example-requests/php.blade.php b/resources/views/partials/example-requests/php.blade.php index 2b37514e..a713157e 100644 --- a/resources/views/partials/example-requests/php.blade.php +++ b/resources/views/partials/example-requests/php.blade.php @@ -1,7 +1,7 @@ ```php $client = new \GuzzleHttp\Client(); -$response = $client->{{ strtolower($route['methods'][0]) }}("{{ rtrim($baseUrl, '/') . '/' . $route['boundUri'] }}", [ +$response = $client->{{ strtolower($route['methods'][0]) }}("{{ rtrim($baseUrl, '/') . '/' . ltrim($route['boundUri'], '/') }}", [ @if(!empty($route['headers'])) 'headers' => [ @foreach($route['headers'] as $header => $value) From abb7ef5b60cebc497b852fb9acbd582d3e07fbfe Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 21:23:11 +0100 Subject: [PATCH 029/312] Separate endpoint group anem frmo group description and key by group name. Fixes #535 --- resources/views/documentarian.blade.php | 8 ++-- src/Commands/GenerateDocumentation.php | 4 +- src/Tools/Generator.php | 21 +++++++--- tests/Fixtures/TestGroupController.php | 44 ++++++++++++++++++++ tests/Fixtures/TestNaturalSortController.php | 21 ---------- tests/Fixtures/index.md | 2 + tests/Fixtures/partial_resource_index.md | 2 + tests/Fixtures/resource_index.md | 2 + tests/Unit/GeneratorTestCase.php | 4 +- 9 files changed, 74 insertions(+), 34 deletions(-) create mode 100644 tests/Fixtures/TestGroupController.php delete mode 100644 tests/Fixtures/TestNaturalSortController.php diff --git a/resources/views/documentarian.blade.php b/resources/views/documentarian.blade.php index d1bb3463..ab5b890c 100644 --- a/resources/views/documentarian.blade.php +++ b/resources/views/documentarian.blade.php @@ -5,10 +5,12 @@ {!! $infoText !!} {!! $prependMd !!} -@foreach($parsedRoutes as $group => $routes) -@if($group) -#{!! $group !!} +@foreach($parsedRoutes as $groupName => $routes) +@if($groupName) +#{!! $groupName !!} @endif +{{-- We pick the first non-empty description we see. --}} +{!! array_first($routes, function ($route) { return $route['groupDescription'] !== ''; })['groupDescription'] ?? '' !!} @foreach($routes as $parsedRoute) @if($writeCompareFile === true) {!! $parsedRoute['output'] !!} diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index 016372e7..fcb7751d 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -82,10 +82,10 @@ public function handle() $generator = new Generator($this->docConfig); $parsedRoutes = $this->processRoutes($generator, $routes); - $parsedRoutes = collect($parsedRoutes)->groupBy('group') + $parsedRoutes = collect($parsedRoutes)->groupBy('groupName') ->sortBy(static function ($group) { /* @var $group Collection */ - return $group->first()['group']; + return $group->first()['groupName']; }, SORT_NATURAL); $this->writeMarkdown($parsedRoutes); diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 8f22b167..a6886bc8 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -57,7 +57,8 @@ public function processRoute(Route $route, array $rulesToApply = []) $controller = new ReflectionClass($class); $method = $controller->getMethod($method); - $routeGroup = $this->getRouteGroup($controller, $method); + list($routeGroupName, $routeGroupDescription) = $this->getRouteGroup($controller, $method); + $docBlock = $this->parseDocBlock($method); $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']); $queryParameters = $this->getQueryParameters($method, $docBlock['tags']); @@ -69,7 +70,8 @@ public function processRoute(Route $route, array $rulesToApply = []) $parsedRoute = [ 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), - 'group' => $routeGroup, + 'groupName' => $routeGroupName, + 'groupDescription' => $routeGroupDescription, 'title' => $docBlock['short'], 'description' => $docBlock['long'], 'methods' => $this->getMethods($route), @@ -271,7 +273,7 @@ protected function parseDocBlock(ReflectionMethod $method) * @param ReflectionClass $controller * @param ReflectionMethod $method * - * @return string + * @return array The route group name and description */ protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $method) { @@ -281,7 +283,11 @@ protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $ $phpdoc = new DocBlock($docBlockComment); foreach ($phpdoc->getTags() as $tag) { if ($tag->getName() === 'group') { - return $tag->getContent(); + $routeGroup = trim($tag->getContent()); + $routeGroupParts = explode("\n", $tag->getContent()); + $routeGroupName = array_shift($routeGroupParts); + $routeGroupDescription = implode("\n", $routeGroupParts); + return [$routeGroupName, $routeGroupDescription]; } } } @@ -291,12 +297,15 @@ protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $ $phpdoc = new DocBlock($docBlockComment); foreach ($phpdoc->getTags() as $tag) { if ($tag->getName() === 'group') { - return $tag->getContent(); + $routeGroupParts = explode("\n", $tag->getContent()); + $routeGroupName = array_shift($routeGroupParts); + $routeGroupDescription = implode("\n", $routeGroupParts); + return [$routeGroupName, $routeGroupDescription]; } } } - return $this->config->get(('default_group')); + return [$this->config->get(('default_group')), '']; } private function normalizeParameterType($type) diff --git a/tests/Fixtures/TestGroupController.php b/tests/Fixtures/TestGroupController.php new file mode 100644 index 00000000..6c16e053 --- /dev/null +++ b/tests/Fixtures/TestGroupController.php @@ -0,0 +1,44 @@ + #Group A + + ## Example title. diff --git a/tests/Fixtures/partial_resource_index.md b/tests/Fixtures/partial_resource_index.md index 9ae056a8..2fc47beb 100644 --- a/tests/Fixtures/partial_resource_index.md +++ b/tests/Fixtures/partial_resource_index.md @@ -21,6 +21,8 @@ Welcome to the generated API reference. #general + + ## Display a listing of the resource. diff --git a/tests/Fixtures/resource_index.md b/tests/Fixtures/resource_index.md index 628bba23..0fde3fd9 100644 --- a/tests/Fixtures/resource_index.md +++ b/tests/Fixtures/resource_index.md @@ -21,6 +21,8 @@ Welcome to the generated API reference. #general + + ## Display a listing of the resource. diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 7a62d3e6..d98701f1 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -214,7 +214,7 @@ public function can_parse_query_parameters() public function can_parse_route_group() { $route = $this->createRoute('GET', '/api/test', 'dummy'); - $routeGroup = $this->generator->processRoute($route)['group']; + $routeGroup = $this->generator->processRoute($route)['groupName']; $this->assertSame('Group A', $routeGroup); } @@ -223,7 +223,7 @@ public function can_parse_route_group() public function method_can_override_controller_group() { $route = $this->createRoute('GET', '/api/test', 'withGroupOverride'); - $routeGroup = $this->generator->processRoute($route)['group']; + $routeGroup = $this->generator->processRoute($route)['groupName']; $this->assertSame('Group B', $routeGroup); } From 6458e4d5f861fa67a76aadf2011530abc6badaff Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 21:23:44 +0100 Subject: [PATCH 030/312] Remove Dingo callable tuple test - Dingo doesn't support it --- tests/GenerateDocumentationTest.php | 30 +++++++---------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 012ebae9..191f890a 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -12,7 +12,7 @@ use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider; use Illuminate\Support\Facades\Route as RouteFacade; use Mpociot\ApiDoc\Tests\Fixtures\TestResourceController; -use Mpociot\ApiDoc\Tests\Fixtures\TestNaturalSortController; +use Mpociot\ApiDoc\Tests\Fixtures\TestGroupController; use Mpociot\ApiDoc\Tests\Fixtures\TestPartialResourceController; class GenerateDocumentationTest extends TestCase @@ -29,7 +29,7 @@ public function setUp() public function tearDown() { - Utils::deleteDirectoryAndContents('/public/docs'); + //Utils::deleteDirectoryAndContents('/public/docs'); } /** @@ -81,7 +81,7 @@ public function console_command_does_not_work_with_closure_using_dingo() } /** @test */ - public function console_command_work_with_routes_uses_array() + public function console_command_work_with_routes_callable_tuple() { RouteFacade::get('/api/array/test', [TestController::class, 'withEndpointDescription']); @@ -92,23 +92,6 @@ public function console_command_work_with_routes_uses_array() $this->assertContains('Processed route: [GET] api/array/test', $output); } - /** @test */ - public function console_command_work_with_dingo_routes_uses_array() - { - $api = app(\Dingo\Api\Routing\Router::class); - $api->version('v1', function ($api) { - $api->get('/array/dingo/test', [TestController::class, 'withEndpointDescription']); - }); - - config(['apidoc.router' => 'dingo']); - config(['apidoc.routes.0.match.prefixes' => ['*']]); - config(['apidoc.routes.0.match.versions' => ['v1']]); - $output = $this->artisan('apidoc:generate'); - - $this->assertNotContains('Skipping route: [GET] array/dingo/test', $output); - $this->assertContains('Processed route: [GET] array/dingo/test', $output); - } - /** @test */ public function can_skip_single_routes() { @@ -367,9 +350,10 @@ public function can_parse_utf8_response() /** @test */ public function sorts_group_naturally() { - RouteFacade::get('/api/action1', TestNaturalSortController::class.'@action1'); - RouteFacade::get('/api/action2', TestNaturalSortController::class.'@action2'); - RouteFacade::get('/api/action10', TestNaturalSortController::class.'@action10'); + RouteFacade::get('/api/action1', TestGroupController::class.'@action1'); + RouteFacade::get('/api/action1b', TestGroupController::class.'@action1b'); + RouteFacade::get('/api/action2', TestGroupController::class.'@action2'); + RouteFacade::get('/api/action10', TestGroupController::class.'@action10'); config(['apidoc.routes.0.prefixes' => ['api/*']]); $this->artisan('apidoc:generate'); From e1c5619aa044d481fa1b93c624815911fc79e7c5 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 21:27:54 +0100 Subject: [PATCH 031/312] Propagate verbosity level when error handling to Collision library --- src/Tools/Utils.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php index 5962f189..42e8a2e2 100644 --- a/src/Tools/Utils.php +++ b/src/Tools/Utils.php @@ -6,6 +6,8 @@ use Illuminate\Routing\Route; use League\Flysystem\Filesystem; use League\Flysystem\Adapter\Local; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; class Utils { @@ -66,10 +68,11 @@ public static function replaceUrlParameterBindings(string $uri, array $bindings) return $uri; } - public function dumpException(\Exception $e) + public static function dumpException(\Exception $e) { if (class_exists(\NunoMaduro\Collision\Handler::class)) { - $handler = new \NunoMaduro\Collision\Handler; + $output = new ConsoleOutput(OutputInterface::VERBOSITY_VERBOSE); + $handler = new \NunoMaduro\Collision\Handler(new \NunoMaduro\Collision\Writer($output)); $handler->setInspector(new \Whoops\Exception\Inspector($e)); $handler->setException($e); $handler->handle(); From e91ba99e6f3b4c4f3c7612aa8d85ff1430811f0a Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 21:28:18 +0100 Subject: [PATCH 032/312] Fix build config - pass string rather than number as PHP version --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e6780ada..0dcff014 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: php php: - 7.0.0 - 7.1.3 - - 7.2 + - 7.2.0 env: matrix: From 41d2b161cd0b2706846585b37693d4e44ac84005 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 21:28:31 +0100 Subject: [PATCH 033/312] Update Dingo version --- composer.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 0da1d5a8..a74779a5 100644 --- a/composer.json +++ b/composer.json @@ -23,12 +23,13 @@ "mpociot/documentarian": "^0.2.0", "mpociot/reflection-docblock": "^1.0.1", "ramsey/uuid": "^3.8", - "league/flysystem": "^1.0" + "league/flysystem": "^1.0", + "nunomaduro/collision": "^3.0" }, "require-dev": { "orchestra/testbench": "3.5.* || 3.6.* || 3.7.*", "phpunit/phpunit": "^6.0.0 || ^7.4.0", - "dingo/api": "2.0.0-alpha1", + "dingo/api": "^2", "mockery/mockery": "^1.2.0", "league/fractal": "^0.17.0" }, @@ -47,7 +48,8 @@ } }, "scripts": { - "test-ci": "phpunit --coverage-clover=coverage.xml" + "test-ci": "phpunit --coverage-clover=coverage.xml", + "test": "phpunit --stop-on-failure" }, "extra": { "laravel": { From 448c224e03ecc8330e01eb6d268cbca663c6e19d Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 21:31:55 +0100 Subject: [PATCH 034/312] Code cleanup --- src/Commands/GenerateDocumentation.php | 2 +- tests/GenerateDocumentationTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index fcb7751d..fcfb7257 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -276,7 +276,7 @@ private function isRouteVisibleForDocumentation($action) $phpdoc = new DocBlock($comment); return collect($phpdoc->getTags()) - ->filter(function ($tag) use ($action) { + ->filter(function ($tag) { return $tag->getName() === 'hideFromAPIDocumentation'; }) ->isEmpty(); diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 191f890a..c37096fc 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -29,7 +29,7 @@ public function setUp() public function tearDown() { - //Utils::deleteDirectoryAndContents('/public/docs'); + Utils::deleteDirectoryAndContents('/public/docs'); } /** From d5cedf430bad30e1ff24a1d0dd81ea1845246fa0 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 21:40:12 +0100 Subject: [PATCH 035/312] Remove collision installation --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index a74779a5..78d43499 100644 --- a/composer.json +++ b/composer.json @@ -23,15 +23,15 @@ "mpociot/documentarian": "^0.2.0", "mpociot/reflection-docblock": "^1.0.1", "ramsey/uuid": "^3.8", - "league/flysystem": "^1.0", - "nunomaduro/collision": "^3.0" + "league/flysystem": "^1.0" }, "require-dev": { "orchestra/testbench": "3.5.* || 3.6.* || 3.7.*", "phpunit/phpunit": "^6.0.0 || ^7.4.0", "dingo/api": "^2", "mockery/mockery": "^1.2.0", - "league/fractal": "^0.17.0" + "league/fractal": "^0.17.0", + "phpstan/phpstan": "^0.11.15" }, "suggest": { "league/fractal": "Required for transformers support", @@ -48,8 +48,8 @@ } }, "scripts": { - "test-ci": "phpunit --coverage-clover=coverage.xml", - "test": "phpunit --stop-on-failure" + "test": "phpunit --stop-on-failure", + "test-ci": "phpunit --coverage-clover=coverage.xml" }, "extra": { "laravel": { From 69121bb2001876c88b8c161c39fea443240dc4fc Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Sun, 25 Aug 2019 20:42:15 +0000 Subject: [PATCH 036/312] Apply fixes from StyleCI --- src/Tools/Generator.php | 2 ++ tests/Fixtures/TestGroupController.php | 3 +-- tests/GenerateDocumentationTest.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index a6886bc8..a96f2ed2 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -287,6 +287,7 @@ protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $ $routeGroupParts = explode("\n", $tag->getContent()); $routeGroupName = array_shift($routeGroupParts); $routeGroupDescription = implode("\n", $routeGroupParts); + return [$routeGroupName, $routeGroupDescription]; } } @@ -300,6 +301,7 @@ protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $ $routeGroupParts = explode("\n", $tag->getContent()); $routeGroupName = array_shift($routeGroupParts); $routeGroupDescription = implode("\n", $routeGroupParts); + return [$routeGroupName, $routeGroupDescription]; } } diff --git a/tests/Fixtures/TestGroupController.php b/tests/Fixtures/TestGroupController.php index 6c16e053..0bce4cd6 100644 --- a/tests/Fixtures/TestGroupController.php +++ b/tests/Fixtures/TestGroupController.php @@ -9,7 +9,6 @@ */ class TestGroupController { - /** * Some endpoint. * @@ -20,7 +19,7 @@ public function action1() } /** - * Another endpoint + * Another endpoint. * * Here we specify a group. This is also in Group 1. * diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index c37096fc..04793d6c 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -11,8 +11,8 @@ use Mpociot\ApiDoc\Tests\Fixtures\TestController; use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider; use Illuminate\Support\Facades\Route as RouteFacade; -use Mpociot\ApiDoc\Tests\Fixtures\TestResourceController; use Mpociot\ApiDoc\Tests\Fixtures\TestGroupController; +use Mpociot\ApiDoc\Tests\Fixtures\TestResourceController; use Mpociot\ApiDoc\Tests\Fixtures\TestPartialResourceController; class GenerateDocumentationTest extends TestCase From d46957715757ca671f4aca0476443feead33c83d Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 22:00:50 +0100 Subject: [PATCH 037/312] Update PHPStan version constraint to support 7.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 78d43499..e5851655 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "dingo/api": "^2", "mockery/mockery": "^1.2.0", "league/fractal": "^0.17.0", - "phpstan/phpstan": "^0.11.15" + "phpstan/phpstan": "^0.9.0" }, "suggest": { "league/fractal": "Required for transformers support", From ddf134c1e73b0eef3bc49e5c6ba817e8e23a02e3 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 22:28:46 +0100 Subject: [PATCH 038/312] Exclude the broken dingo/api 2.1.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e5851655..ac773929 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "require-dev": { "orchestra/testbench": "3.5.* || 3.6.* || 3.7.*", "phpunit/phpunit": "^6.0.0 || ^7.4.0", - "dingo/api": "^2", + "dingo/api": "^2.0.0 <2.1.0 || ^2.0.0 >2.1.0", "mockery/mockery": "^1.2.0", "league/fractal": "^0.17.0", "phpstan/phpstan": "^0.9.0" From 54dc2aa40e6d193fb92fe8c83ac1a94660a9cc9b Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 22:39:58 +0100 Subject: [PATCH 039/312] Revert Dingo version change - shit is broken and I don't know why. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ac773929..90ad1974 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "require-dev": { "orchestra/testbench": "3.5.* || 3.6.* || 3.7.*", "phpunit/phpunit": "^6.0.0 || ^7.4.0", - "dingo/api": "^2.0.0 <2.1.0 || ^2.0.0 >2.1.0", + "dingo/api": "2.0.0-alpha1", "mockery/mockery": "^1.2.0", "league/fractal": "^0.17.0", "phpstan/phpstan": "^0.9.0" From e1a43b0143c839c41c0109cf8389b9d6c690b8ac Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 22:47:11 +0100 Subject: [PATCH 040/312] PHPStan linting --- .travis.yml | 1 + composer.json | 1 + docs/architecture.md | 4 ++++ phpstan.neon | 8 ++++++++ src/Commands/GenerateDocumentation.php | 6 +++--- src/Tools/Generator.php | 4 ++-- src/Tools/ResponseResolver.php | 16 ++++++++-------- .../ResponseStrategies/ResponseCallStrategy.php | 4 ++-- .../ResponseStrategies/ResponseFileStrategy.php | 4 ++-- .../ResponseStrategies/ResponseTagStrategy.php | 2 +- .../TransformerTagsStrategy.php | 4 ++-- src/Tools/Utils.php | 2 +- 12 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 docs/architecture.md create mode 100644 phpstan.neon diff --git a/.travis.yml b/.travis.yml index 0dcff014..30d605d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ before_script: - travis_retry composer update --no-interaction --prefer-dist $PREFER_LOWEST script: + - composer lint - composer test-ci before_install: diff --git a/composer.json b/composer.json index 90ad1974..e15d2012 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ } }, "scripts": { + "lint": "phpstan analyse -c phpstan.neon src", "test": "phpunit --stop-on-failure", "test-ci": "phpunit --coverage-clover=coverage.xml" }, diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..984e1e84 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,4 @@ +# Architecutre +Read this page if you want a deeper understanding of how this works (for instance, for the purpose of contributing). + +- Documentarian takes in a markdwown file and transforms it into HTML. diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..0d17cfcf --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +parameters: + level: 5 + reportUnmatchedIgnoredErrors: false + inferPrivatePropertyTypeFromConstructor:: true + ignoreErrors: + - '#Call to an undefined static method Illuminate\\Support\\Facades\\Route::getRoutes().#' + - '#Call to an undefined static method Illuminate\\Support\\Facades\\URL::forceRootUrl()#' + - '#Call to an undefined method Illuminate\\Routing\\Route::versions().#' diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index fcfb7257..13116266 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -240,7 +240,7 @@ private function processRoutes(Generator $generator, array $routes) } /** - * @param $route + * @param Route $route * * @return bool */ @@ -255,13 +255,13 @@ private function isValidRoute(Route $route) } /** - * @param $action + * @param array $action * * @throws ReflectionException * * @return bool */ - private function isRouteVisibleForDocumentation($action) + private function isRouteVisibleForDocumentation(array $action) { list($class, $method) = Utils::getRouteActionUses($action); $reflection = new ReflectionClass($class); diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index a96f2ed2..5cb068e3 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -46,8 +46,8 @@ public function getMethods(Route $route) } /** - * @param \Illuminate\Routing\Route $route - * @param array $apply Rules to apply when generating documentation for this route + * @param \Illuminate\Routing\Route $route + * @param array $rulesToApply Rules to apply when generating documentation for this route * * @return array */ diff --git a/src/Tools/ResponseResolver.php b/src/Tools/ResponseResolver.php index 319bc9c4..3d37140b 100644 --- a/src/Tools/ResponseResolver.php +++ b/src/Tools/ResponseResolver.php @@ -57,24 +57,24 @@ private function resolve(array $tags, array $routeProps) } /** - * @param $route - * @param $tags - * @param $routeProps + * @param Route $route + * @param array $tags + * @param array $routeProps * * @return array */ - public static function getResponse($route, $tags, $routeProps) + public static function getResponse(Route $route, array $tags, array $routeProps) { return (new static($route))->resolve($tags, $routeProps); } /** - * @param $response + * @param Response $response * - * @return mixed + * @return string */ - private function getResponseContent($response) + private function getResponseContent(Response $response) { - return $response ? $response->getContent() : ''; + return $response->getContent() ?: ''; } } diff --git a/src/Tools/ResponseStrategies/ResponseCallStrategy.php b/src/Tools/ResponseStrategies/ResponseCallStrategy.php index 4857315e..bf232615 100644 --- a/src/Tools/ResponseStrategies/ResponseCallStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseCallStrategy.php @@ -28,7 +28,7 @@ public function __invoke(Route $route, array $tags, array $routeProps) { $rulesToApply = $routeProps['rules']['response_calls'] ?? []; if (! $this->shouldMakeApiCall($route, $rulesToApply)) { - return; + return null; } $this->configureEnvironment($rulesToApply); @@ -91,7 +91,7 @@ protected function prepareRequest(Route $route, array $rulesToApply, array $body } /** - * @param array $config + * @param array $env * * @return void * diff --git a/src/Tools/ResponseStrategies/ResponseFileStrategy.php b/src/Tools/ResponseStrategies/ResponseFileStrategy.php index b186fe69..1b780559 100644 --- a/src/Tools/ResponseStrategies/ResponseFileStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseFileStrategy.php @@ -32,7 +32,7 @@ public function __invoke(Route $route, array $tags, array $routeProps) */ protected function getFileResponses(array $tags) { - // avoid "holes" in the keys of the filtered array, by using array_values on the filtered array + // Avoid "holes" in the keys of the filtered array, by using array_values on the filtered array $responseFileTags = array_values( array_filter($tags, function ($tag) { return $tag instanceof Tag && strtolower($tag->getName()) === 'responsefile'; @@ -40,7 +40,7 @@ protected function getFileResponses(array $tags) ); if (empty($responseFileTags)) { - return; + return null; } return array_map(function (Tag $responseFileTag) { diff --git a/src/Tools/ResponseStrategies/ResponseTagStrategy.php b/src/Tools/ResponseStrategies/ResponseTagStrategy.php index 913a117e..9cd2ff09 100644 --- a/src/Tools/ResponseStrategies/ResponseTagStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseTagStrategy.php @@ -39,7 +39,7 @@ protected function getDocBlockResponses(array $tags) ); if (empty($responseTags)) { - return; + return null; } return array_map(function (Tag $responseTag) { diff --git a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php index 82fa5432..62ab3e9a 100644 --- a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php +++ b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php @@ -39,7 +39,7 @@ protected function getTransformerResponse(array $tags) { try { if (empty($transformerTag = $this->getTransformerTag($tags))) { - return; + return null; } $transformer = $this->getTransformerClass($transformerTag); @@ -58,7 +58,7 @@ protected function getTransformerResponse(array $tags) return [response($fractal->createData($resource)->toJson())]; } catch (\Exception $e) { - return; + return null; } } diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php index 42e8a2e2..b4e30930 100644 --- a/src/Tools/Utils.php +++ b/src/Tools/Utils.php @@ -63,7 +63,7 @@ public static function replaceUrlParameterBindings(string $uri, array $bindings) } } // Replace any unbound parameters with '1' - $uri = preg_replace('/{(.+?)}/', 1, $uri); + $uri = preg_replace('/{(.+?)}/', '1', $uri); return $uri; } From 374e8c2a290e2ca8ea0b9a5a189e12f809255cae Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Sun, 25 Aug 2019 21:47:37 +0000 Subject: [PATCH 041/312] Apply fixes from StyleCI --- src/Tools/ResponseStrategies/ResponseCallStrategy.php | 2 +- src/Tools/ResponseStrategies/ResponseFileStrategy.php | 2 +- src/Tools/ResponseStrategies/ResponseTagStrategy.php | 2 +- src/Tools/ResponseStrategies/TransformerTagsStrategy.php | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tools/ResponseStrategies/ResponseCallStrategy.php b/src/Tools/ResponseStrategies/ResponseCallStrategy.php index bf232615..8ab5cc62 100644 --- a/src/Tools/ResponseStrategies/ResponseCallStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseCallStrategy.php @@ -28,7 +28,7 @@ public function __invoke(Route $route, array $tags, array $routeProps) { $rulesToApply = $routeProps['rules']['response_calls'] ?? []; if (! $this->shouldMakeApiCall($route, $rulesToApply)) { - return null; + return; } $this->configureEnvironment($rulesToApply); diff --git a/src/Tools/ResponseStrategies/ResponseFileStrategy.php b/src/Tools/ResponseStrategies/ResponseFileStrategy.php index 1b780559..ed65f531 100644 --- a/src/Tools/ResponseStrategies/ResponseFileStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseFileStrategy.php @@ -40,7 +40,7 @@ protected function getFileResponses(array $tags) ); if (empty($responseFileTags)) { - return null; + return; } return array_map(function (Tag $responseFileTag) { diff --git a/src/Tools/ResponseStrategies/ResponseTagStrategy.php b/src/Tools/ResponseStrategies/ResponseTagStrategy.php index 9cd2ff09..913a117e 100644 --- a/src/Tools/ResponseStrategies/ResponseTagStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseTagStrategy.php @@ -39,7 +39,7 @@ protected function getDocBlockResponses(array $tags) ); if (empty($responseTags)) { - return null; + return; } return array_map(function (Tag $responseTag) { diff --git a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php index 62ab3e9a..82fa5432 100644 --- a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php +++ b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php @@ -39,7 +39,7 @@ protected function getTransformerResponse(array $tags) { try { if (empty($transformerTag = $this->getTransformerTag($tags))) { - return null; + return; } $transformer = $this->getTransformerClass($transformerTag); @@ -58,7 +58,7 @@ protected function getTransformerResponse(array $tags) return [response($fractal->createData($resource)->toJson())]; } catch (\Exception $e) { - return null; + return; } } From 16cfc4f4da85500a380f2873b454fb3a8d9ee474 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 22:54:00 +0100 Subject: [PATCH 042/312] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e36ea8b9..ea04cd9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [3.12.0] - Sunday, 25 August 2019 +### Fixed +- Specifying an `@group` for a method no longer requires you to add the description. (https://github.com/mpociot/laravel-apidoc-generator/pull/556) +- Pass the verbosity level down to the Collision library. (https://github.com/mpociot/laravel-apidoc-generator/pull/556) + ## [3.11.0] - Friday, 9 August 2019 ### Added - Support for query parameters in the bash template (https://github.com/mpociot/laravel-apidoc-generator/pull/545) From 9b55aedcf65b5de8912cd2d6cacdff9f83a4eee3 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 23:03:07 +0100 Subject: [PATCH 043/312] Update PHPStan config --- composer.json | 2 +- src/Tools/ResponseStrategies/ResponseCallStrategy.php | 2 +- src/Tools/ResponseStrategies/ResponseFileStrategy.php | 2 +- src/Tools/ResponseStrategies/ResponseTagStrategy.php | 2 +- src/Tools/ResponseStrategies/TransformerTagsStrategy.php | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index e15d2012..710b61d3 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ } }, "scripts": { - "lint": "phpstan analyse -c phpstan.neon src", + "lint": "phpstan analyse src", "test": "phpunit --stop-on-failure", "test-ci": "phpunit --coverage-clover=coverage.xml" }, diff --git a/src/Tools/ResponseStrategies/ResponseCallStrategy.php b/src/Tools/ResponseStrategies/ResponseCallStrategy.php index 8ab5cc62..bf232615 100644 --- a/src/Tools/ResponseStrategies/ResponseCallStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseCallStrategy.php @@ -28,7 +28,7 @@ public function __invoke(Route $route, array $tags, array $routeProps) { $rulesToApply = $routeProps['rules']['response_calls'] ?? []; if (! $this->shouldMakeApiCall($route, $rulesToApply)) { - return; + return null; } $this->configureEnvironment($rulesToApply); diff --git a/src/Tools/ResponseStrategies/ResponseFileStrategy.php b/src/Tools/ResponseStrategies/ResponseFileStrategy.php index ed65f531..1b780559 100644 --- a/src/Tools/ResponseStrategies/ResponseFileStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseFileStrategy.php @@ -40,7 +40,7 @@ protected function getFileResponses(array $tags) ); if (empty($responseFileTags)) { - return; + return null; } return array_map(function (Tag $responseFileTag) { diff --git a/src/Tools/ResponseStrategies/ResponseTagStrategy.php b/src/Tools/ResponseStrategies/ResponseTagStrategy.php index 913a117e..9cd2ff09 100644 --- a/src/Tools/ResponseStrategies/ResponseTagStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseTagStrategy.php @@ -39,7 +39,7 @@ protected function getDocBlockResponses(array $tags) ); if (empty($responseTags)) { - return; + return null; } return array_map(function (Tag $responseTag) { diff --git a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php index 82fa5432..62ab3e9a 100644 --- a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php +++ b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php @@ -39,7 +39,7 @@ protected function getTransformerResponse(array $tags) { try { if (empty($transformerTag = $this->getTransformerTag($tags))) { - return; + return null; } $transformer = $this->getTransformerClass($transformerTag); @@ -58,7 +58,7 @@ protected function getTransformerResponse(array $tags) return [response($fractal->createData($resource)->toJson())]; } catch (\Exception $e) { - return; + return null; } } From 1f5a15c3f9da435b28cfe3da820b3bd322506397 Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Sun, 25 Aug 2019 22:03:24 +0000 Subject: [PATCH 044/312] Apply fixes from StyleCI --- src/Tools/ResponseStrategies/ResponseCallStrategy.php | 2 +- src/Tools/ResponseStrategies/ResponseFileStrategy.php | 2 +- src/Tools/ResponseStrategies/ResponseTagStrategy.php | 2 +- src/Tools/ResponseStrategies/TransformerTagsStrategy.php | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tools/ResponseStrategies/ResponseCallStrategy.php b/src/Tools/ResponseStrategies/ResponseCallStrategy.php index bf232615..8ab5cc62 100644 --- a/src/Tools/ResponseStrategies/ResponseCallStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseCallStrategy.php @@ -28,7 +28,7 @@ public function __invoke(Route $route, array $tags, array $routeProps) { $rulesToApply = $routeProps['rules']['response_calls'] ?? []; if (! $this->shouldMakeApiCall($route, $rulesToApply)) { - return null; + return; } $this->configureEnvironment($rulesToApply); diff --git a/src/Tools/ResponseStrategies/ResponseFileStrategy.php b/src/Tools/ResponseStrategies/ResponseFileStrategy.php index 1b780559..ed65f531 100644 --- a/src/Tools/ResponseStrategies/ResponseFileStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseFileStrategy.php @@ -40,7 +40,7 @@ protected function getFileResponses(array $tags) ); if (empty($responseFileTags)) { - return null; + return; } return array_map(function (Tag $responseFileTag) { diff --git a/src/Tools/ResponseStrategies/ResponseTagStrategy.php b/src/Tools/ResponseStrategies/ResponseTagStrategy.php index 9cd2ff09..913a117e 100644 --- a/src/Tools/ResponseStrategies/ResponseTagStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseTagStrategy.php @@ -39,7 +39,7 @@ protected function getDocBlockResponses(array $tags) ); if (empty($responseTags)) { - return null; + return; } return array_map(function (Tag $responseTag) { diff --git a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php index 62ab3e9a..82fa5432 100644 --- a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php +++ b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php @@ -39,7 +39,7 @@ protected function getTransformerResponse(array $tags) { try { if (empty($transformerTag = $this->getTransformerTag($tags))) { - return null; + return; } $transformer = $this->getTransformerClass($transformerTag); @@ -58,7 +58,7 @@ protected function getTransformerResponse(array $tags) return [response($fractal->createData($resource)->toJson())]; } catch (\Exception $e) { - return null; + return; } } From 4bafe50c2eada7205412bc1f8534c9a9a2c318b0 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 23:12:38 +0100 Subject: [PATCH 045/312] Fucking StyleCI and the fucking Laravel style guide --- .styleci.yml | 3 ++- src/Tools/ResponseStrategies/ResponseCallStrategy.php | 2 +- src/Tools/ResponseStrategies/ResponseFileStrategy.php | 2 +- src/Tools/ResponseStrategies/ResponseTagStrategy.php | 2 +- src/Tools/ResponseStrategies/TransformerTagsStrategy.php | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.styleci.yml b/.styleci.yml index 5651629b..058bfc24 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -6,4 +6,5 @@ enabled: - unalign_double_arrow disabled: - - short_list_syntax \ No newline at end of file + - short_list_syntax + - simplified_null_return diff --git a/src/Tools/ResponseStrategies/ResponseCallStrategy.php b/src/Tools/ResponseStrategies/ResponseCallStrategy.php index 8ab5cc62..bf232615 100644 --- a/src/Tools/ResponseStrategies/ResponseCallStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseCallStrategy.php @@ -28,7 +28,7 @@ public function __invoke(Route $route, array $tags, array $routeProps) { $rulesToApply = $routeProps['rules']['response_calls'] ?? []; if (! $this->shouldMakeApiCall($route, $rulesToApply)) { - return; + return null; } $this->configureEnvironment($rulesToApply); diff --git a/src/Tools/ResponseStrategies/ResponseFileStrategy.php b/src/Tools/ResponseStrategies/ResponseFileStrategy.php index ed65f531..1b780559 100644 --- a/src/Tools/ResponseStrategies/ResponseFileStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseFileStrategy.php @@ -40,7 +40,7 @@ protected function getFileResponses(array $tags) ); if (empty($responseFileTags)) { - return; + return null; } return array_map(function (Tag $responseFileTag) { diff --git a/src/Tools/ResponseStrategies/ResponseTagStrategy.php b/src/Tools/ResponseStrategies/ResponseTagStrategy.php index 913a117e..9cd2ff09 100644 --- a/src/Tools/ResponseStrategies/ResponseTagStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseTagStrategy.php @@ -39,7 +39,7 @@ protected function getDocBlockResponses(array $tags) ); if (empty($responseTags)) { - return; + return null; } return array_map(function (Tag $responseTag) { diff --git a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php index 82fa5432..62ab3e9a 100644 --- a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php +++ b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php @@ -39,7 +39,7 @@ protected function getTransformerResponse(array $tags) { try { if (empty($transformerTag = $this->getTransformerTag($tags))) { - return; + return null; } $transformer = $this->getTransformerClass($transformerTag); @@ -58,7 +58,7 @@ protected function getTransformerResponse(array $tags) return [response($fractal->createData($resource)->toJson())]; } catch (\Exception $e) { - return; + return null; } } From c4c51c5f41f6a5480bb7fab66f243595c61bffb2 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 23:13:21 +0100 Subject: [PATCH 046/312] Update Travis build config --- .travis.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 30d605d5..f2e1f988 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,13 +10,19 @@ env: - PREFER_LOWEST="--prefer-lowest" - PREFER_LOWEST="" -before_script: - - travis_retry composer self-update - - travis_retry composer update --no-interaction --prefer-dist $PREFER_LOWEST +stages: + - name: test + - name: lint +jobs: + include: + - stage: test + script: composer test-ci + before_script: + - travis_retry composer self-update + - travis_retry composer update --no-interaction --prefer-dist $PREFER_LOWEST -script: - - composer lint - - composer test-ci + - stage: lint + script: composer lint before_install: - pip install --user codecov From bf7e45c256f3e15b56807fca985dbb92fa98f874 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 23:15:05 +0100 Subject: [PATCH 047/312] Update PHPStan config - ignore errors relating to optional package --- phpstan.neon | 1 + 1 file changed, 1 insertion(+) diff --git a/phpstan.neon b/phpstan.neon index 0d17cfcf..4850c070 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,3 +6,4 @@ parameters: - '#Call to an undefined static method Illuminate\\Support\\Facades\\Route::getRoutes().#' - '#Call to an undefined static method Illuminate\\Support\\Facades\\URL::forceRootUrl()#' - '#Call to an undefined method Illuminate\\Routing\\Route::versions().#' + - '#(.*)NunoMaduro\\Collision\\Handler(.*)#' From df78b512d57a2f71555f2ec43b6ed6ff29d8fa8b Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 23:25:37 +0100 Subject: [PATCH 048/312] Travis config --- .travis.yml | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index f2e1f988..7cdd253e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,22 @@ language: php php: - - 7.0.0 - - 7.1.3 - 7.2.0 + - 7.1.3 + - 7.0.0 env: - matrix: - - PREFER_LOWEST="--prefer-lowest" - - PREFER_LOWEST="" + - PREFER_LOWEST="" + - PREFER_LOWEST="--prefer-lowest" + +script: composer test-ci +before_script: + - travis_retry composer self-update + - travis_retry composer update --no-interaction --prefer-dist $PREFER_LOWEST -stages: - - name: test - - name: lint jobs: include: - - stage: test - script: composer test-ci - before_script: - - travis_retry composer self-update - - travis_retry composer update --no-interaction --prefer-dist $PREFER_LOWEST - - - stage: lint - script: composer lint + - script: composer lint before_install: - pip install --user codecov From d86e04ff240701faa38a2b6023ee7256146c299a Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 23:32:11 +0100 Subject: [PATCH 049/312] Travis config --- .travis.yml | 11 ++++++----- composer.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7cdd253e..19302eac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,15 +9,16 @@ env: - PREFER_LOWEST="" - PREFER_LOWEST="--prefer-lowest" -script: composer test-ci -before_script: - - travis_retry composer self-update - - travis_retry composer update --no-interaction --prefer-dist $PREFER_LOWEST - jobs: include: - script: composer lint + name: "Lint code" +script: composer test-ci +before_script: + - travis_retry composer self-update + - travis_retry composer update --no-interaction --prefer-dist $PREFER_LOWEST + - before_install: - pip install --user codecov diff --git a/composer.json b/composer.json index 710b61d3..a6aba12c 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ } }, "scripts": { - "lint": "phpstan analyse src", + "lint": "phpstan analyse -c ./phpstan.neon src", "test": "phpunit --stop-on-failure", "test-ci": "phpunit --coverage-clover=coverage.xml" }, From 03859be66048c15d6138f136b2142058d6680b21 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 23:39:37 +0100 Subject: [PATCH 050/312] Update PHPStan version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a6aba12c..672b61ac 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "dingo/api": "2.0.0-alpha1", "mockery/mockery": "^1.2.0", "league/fractal": "^0.17.0", - "phpstan/phpstan": "^0.9.0" + "phpstan/phpstan": "^0.11.0" }, "suggest": { "league/fractal": "Required for transformers support", From 9d1abc604c6189235021d3fa05106d9f3e8d9c3f Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 23:40:08 +0100 Subject: [PATCH 051/312] Update PHPStan version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 672b61ac..1d3b78c1 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "dingo/api": "2.0.0-alpha1", "mockery/mockery": "^1.2.0", "league/fractal": "^0.17.0", - "phpstan/phpstan": "^0.11.0" + "phpstan/phpstan": "^0.11.15" }, "suggest": { "league/fractal": "Required for transformers support", From d5eef26cc525cda9280f54ec539be5d022841e48 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 25 Aug 2019 23:43:42 +0100 Subject: [PATCH 052/312] Fix PHPStan config --- phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 4850c070..d8f5d764 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,4 +6,4 @@ parameters: - '#Call to an undefined static method Illuminate\\Support\\Facades\\Route::getRoutes().#' - '#Call to an undefined static method Illuminate\\Support\\Facades\\URL::forceRootUrl()#' - '#Call to an undefined method Illuminate\\Routing\\Route::versions().#' - - '#(.*)NunoMaduro\\Collision\\Handler(.*)#' + - '#(.*)NunoMaduro\\Collision(.*)#' From e31dfdf1f1e04bc350d26afcc145d8a280374d47 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 09:41:04 +0200 Subject: [PATCH 053/312] Add tests for excluding query and body params - Test that a valid result is returned - Test that all query params are ignored - Test for non-existence of specific body param --- tests/Fixtures/TestController.php | 10 ++++++++++ tests/Unit/GeneratorTestCase.php | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index 024f43fc..6a3f6070 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -82,6 +82,16 @@ public function withQueryParameters() return ''; } + /** + * @bodyParam included string required Exists in examples. Example: 'Here' + * @bodyParam excluded_body_param int Does not exist in examples. No-example + * @queryParam excluded_query_param Does not exist in examples. No-example + */ + public function withExcludedExamples() + { + return ''; + } + /** * @authenticated */ diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 7a62d3e6..710d5312 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -210,6 +210,28 @@ public function can_parse_query_parameters() ], $queryParameters); } + /** @test */ + public function it_ignores_excluded_params() + { + $route = $this->createRoute('GET', '/api/test', 'withExcludedExamples'); + $parsed = $this->generator->processRoute($route); + $bodyParameters = $parsed['bodyParameters']; + $queryParameters = $parsed['queryParameters']; + + $this->assertArraySubset([ + 'included' => [ + 'required' => true, + 'type' => 'string', + 'description' => 'Exists in examples.', + ], + ], $bodyParameters); + + $this->assertArrayNotHasKey('excluded_body_param', $bodyParameters); + + $this->assertEmpty($queryParameters); + + } + /** @test */ public function can_parse_route_group() { From 725aff30e817ef03cd8b344c9e8a180e0947d683 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 09:44:11 +0200 Subject: [PATCH 054/312] Style-ci fixes --- tests/Unit/GeneratorTestCase.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 710d5312..9a7f5f61 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -229,7 +229,6 @@ public function it_ignores_excluded_params() $this->assertArrayNotHasKey('excluded_body_param', $bodyParameters); $this->assertEmpty($queryParameters); - } /** @test */ From 76b8c3e61286a97ba2887c297d8baa0cf4771ad8 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 11:17:41 +0200 Subject: [PATCH 055/312] Do not completely exclude tags with examples in them - We will filter them out later in the code execution --- src/Tools/Generator.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 3bdb974a..c66eb6db 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -130,9 +130,6 @@ protected function getBodyParametersFromDocBlock(array $tags) ->filter(function ($tag) { return $tag instanceof Tag && $tag->getName() === 'bodyParam'; }) - ->filter(function (Tag $tag) { - return ! $this->shouldExcludeExample($tag); - }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); if (empty($content)) { @@ -208,9 +205,6 @@ protected function getQueryParametersFromDocBlock(array $tags) ->filter(function ($tag) { return $tag instanceof Tag && $tag->getName() === 'queryParam'; }) - ->filter(function (Tag $tag) { - return ! $this->shouldExcludeExample($tag); - }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); if (empty($content)) { From d50365a5b74ec5b91b85b57849f790d7fd80b1f1 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 11:19:02 +0200 Subject: [PATCH 056/312] If a tag is marked as `No-example`, do not generate a value and filter it out of the `cleaned..Parameters` variables --- src/Tools/Generator.php | 4 ++-- src/Tools/Traits/ParamHelpers.php | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index c66eb6db..4a14a498 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -149,7 +149,7 @@ protected function getBodyParametersFromDocBlock(array $tags) $type = $this->normalizeParameterType($type); list($description, $example) = $this->parseDescription($description, $type); - $value = is_null($example) ? $this->generateDummyValue($type) : $example; + $value = is_null($example) && !$this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; return [$name => compact('type', 'description', 'required', 'value')]; })->toArray(); @@ -223,7 +223,7 @@ protected function getQueryParametersFromDocBlock(array $tags) } list($description, $value) = $this->parseDescription($description, 'string'); - if (is_null($value)) { + if (is_null($value) && !$this->shouldExcludeExample($tag)) { $value = str_contains($description, ['number', 'count', 'page']) ? $this->generateDummyValue('integer') : $this->generateDummyValue('string'); diff --git a/src/Tools/Traits/ParamHelpers.php b/src/Tools/Traits/ParamHelpers.php index d81dfc26..28c918c9 100644 --- a/src/Tools/Traits/ParamHelpers.php +++ b/src/Tools/Traits/ParamHelpers.php @@ -14,6 +14,10 @@ trait ParamHelpers protected function cleanParams(array $params) { $values = []; + $params = array_filter($params, function ($details) { + return is_string($details['value']) && strlen($details['value']); + }); + foreach ($params as $name => $details) { $this->cleanValueFrom($name, $details['value'], $values); } From 47558ce9792d7294f4311a6bf0c680f63d723615 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 11:19:55 +0200 Subject: [PATCH 057/312] Ensure we clean out `No-example` from the description tet we render --- src/Tools/Generator.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 4a14a498..9f5bd7f5 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -132,6 +132,7 @@ protected function getBodyParametersFromDocBlock(array $tags) }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); + $content = preg_replace('/\s?No-example.?/', '', $content); if (empty($content)) { // this means only name and type were supplied list($name, $type) = preg_split('/\s+/', $tag->getContent()); @@ -207,6 +208,7 @@ protected function getQueryParametersFromDocBlock(array $tags) }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); + $content = preg_replace('/\s?No-example.?/', '', $content); if (empty($content)) { // this means only name was supplied list($name) = preg_split('/\s+/', $tag->getContent()); From 3fb2220b3ae67635cc49761aab18c780034966a4 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 11:20:57 +0200 Subject: [PATCH 058/312] Use only the `cleaned...Parameters` for examples - The `...Parameters` contain all values - The `No-example` values are excluded from `cleaned...Parameters` --- resources/views/partials/example-requests/bash.blade.php | 6 +++--- .../views/partials/example-requests/javascript.blade.php | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/views/partials/example-requests/bash.blade.php b/resources/views/partials/example-requests/bash.blade.php index dde5c265..0cb272a0 100644 --- a/resources/views/partials/example-requests/bash.blade.php +++ b/resources/views/partials/example-requests/bash.blade.php @@ -1,6 +1,6 @@ ```bash -curl -X {{$route['methods'][0]}} {{$route['methods'][0] == 'GET' ? '-G ' : ''}}"{{ rtrim($baseUrl, '/')}}/{{ ltrim($route['boundUri'], '/') }}@if(count($route['queryParameters']))?@foreach($route['queryParameters'] as $attribute => $parameter) -{{ urlencode($attribute) }}={{ urlencode($parameter['value']) }}@if(!$loop->last)&@endif +curl -X {{$route['methods'][0]}} {{$route['methods'][0] == 'GET' ? '-G ' : ''}}"{{ rtrim($baseUrl, '/')}}/{{ ltrim($route['boundUri'], '/') }}@if(count($route['cleanQueryParameters']))?@foreach($route['cleanQueryParameters'] as $parameter => $value) +{{ urlencode($parameter) }}={{ urlencode($value) }}@if(!$loop->last)&@endif @endforeach @endif" @if(count($route['headers']))\ @foreach($route['headers'] as $header => $value) @@ -12,4 +12,4 @@ -d '{!! json_encode($route['cleanBodyParameters']) !!}' @endif -``` \ No newline at end of file +``` diff --git a/resources/views/partials/example-requests/javascript.blade.php b/resources/views/partials/example-requests/javascript.blade.php index a8d324c9..37b56c9d 100644 --- a/resources/views/partials/example-requests/javascript.blade.php +++ b/resources/views/partials/example-requests/javascript.blade.php @@ -1,10 +1,10 @@ ```javascript const url = new URL("{{ rtrim($baseUrl, '/') }}/{{ ltrim($route['boundUri'], '/') }}"); -@if(count($route['queryParameters'])) +@if(count($route['cleanQueryParameters'])) let params = { - @foreach($route['queryParameters'] as $attribute => $parameter) - "{{ $attribute }}": "{{ $parameter['value'] }}", + @foreach($route['cleanQueryParameters'] as $parameter => $value) + "{{ $parameter }}": "{{ $value }}", @endforeach }; Object.keys(params).forEach(key => url.searchParams.append(key, params[key])); @@ -35,4 +35,4 @@ }) .then(response => response.json()) .then(json => console.log(json)); -``` \ No newline at end of file +``` From 54afbe20b328b5cbd23cb9efeb82e375fbe9b088 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 11:26:50 +0200 Subject: [PATCH 059/312] Test using the the `cleaned...Parameters` for `No-example` tests - The `...Parameters` contain all values - The `No-example` values are excluded from `cleaned...Parameters` --- tests/Unit/GeneratorTestCase.php | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 9a7f5f61..940cff15 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -215,20 +215,12 @@ public function it_ignores_excluded_params() { $route = $this->createRoute('GET', '/api/test', 'withExcludedExamples'); $parsed = $this->generator->processRoute($route); - $bodyParameters = $parsed['bodyParameters']; - $queryParameters = $parsed['queryParameters']; + $cleanBodyParameters = $parsed['cleanBodyParameters']; + $cleanQueryParameters = $parsed['cleanQueryParameters']; - $this->assertArraySubset([ - 'included' => [ - 'required' => true, - 'type' => 'string', - 'description' => 'Exists in examples.', - ], - ], $bodyParameters); - - $this->assertArrayNotHasKey('excluded_body_param', $bodyParameters); - - $this->assertEmpty($queryParameters); + $this->assertArrayHasKey('included', $cleanBodyParameters); + $this->assertArrayNotHasKey('excluded_body_param', $cleanBodyParameters); + $this->assertEmpty($cleanQueryParameters); } /** @test */ From 910b06f085174b03245cae9bba9934ec738757b3 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 11:30:53 +0200 Subject: [PATCH 060/312] Test that all parasm exist in the `...Parameters` of a parsed route - The `...Parameters` contain all values - The `No-example` values are excluded from `cleaned...Parameters` --- tests/Unit/GeneratorTestCase.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 940cff15..9e76fb6b 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -217,10 +217,30 @@ public function it_ignores_excluded_params() $parsed = $this->generator->processRoute($route); $cleanBodyParameters = $parsed['cleanBodyParameters']; $cleanQueryParameters = $parsed['cleanQueryParameters']; + $bodyParameters = $parsed['bodyParameters']; + $queryParameters = $parsed['queryParameters']; $this->assertArrayHasKey('included', $cleanBodyParameters); $this->assertArrayNotHasKey('excluded_body_param', $cleanBodyParameters); $this->assertEmpty($cleanQueryParameters); + + $this->assertArraySubset([ + 'included' => [ + 'required' => true, + 'type' => 'string', + 'description' => 'Exists in examples.', + ], + 'excluded_body_param' => [ + 'type' => 'integer', + 'description' => 'Does not exist in examples.' + ], + ], $bodyParameters); + + $this->assertArraySubset([ + 'excluded_query_param' => [ + 'description' => 'Does not exist in examples.' + ], + ], $queryParameters); } /** @test */ From 1f06c378056a762924bf3e14e1e35b23d1e16275 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 11:32:37 +0200 Subject: [PATCH 061/312] Style-ci fixes --- src/Tools/Generator.php | 4 ++-- tests/Unit/GeneratorTestCase.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 9f5bd7f5..f2d3ddc5 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -150,7 +150,7 @@ protected function getBodyParametersFromDocBlock(array $tags) $type = $this->normalizeParameterType($type); list($description, $example) = $this->parseDescription($description, $type); - $value = is_null($example) && !$this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; + $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; return [$name => compact('type', 'description', 'required', 'value')]; })->toArray(); @@ -225,7 +225,7 @@ protected function getQueryParametersFromDocBlock(array $tags) } list($description, $value) = $this->parseDescription($description, 'string'); - if (is_null($value) && !$this->shouldExcludeExample($tag)) { + if (is_null($value) && ! $this->shouldExcludeExample($tag)) { $value = str_contains($description, ['number', 'count', 'page']) ? $this->generateDummyValue('integer') : $this->generateDummyValue('string'); diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 9e76fb6b..157cec15 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -232,13 +232,13 @@ public function it_ignores_excluded_params() ], 'excluded_body_param' => [ 'type' => 'integer', - 'description' => 'Does not exist in examples.' + 'description' => 'Does not exist in examples.', ], ], $bodyParameters); $this->assertArraySubset([ 'excluded_query_param' => [ - 'description' => 'Does not exist in examples.' + 'description' => 'Does not exist in examples.', ], ], $queryParameters); } From 634da1cc4e3d806d519e7cb1696036eb7aa63ce7 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 11:33:45 +0200 Subject: [PATCH 062/312] Style-ci fixes --- src/Tools/Generator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index f2d3ddc5..4534af09 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -150,7 +150,7 @@ protected function getBodyParametersFromDocBlock(array $tags) $type = $this->normalizeParameterType($type); list($description, $example) = $this->parseDescription($description, $type); - $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; + $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; return [$name => compact('type', 'description', 'required', 'value')]; })->toArray(); From 88652a35611851f185a6e6602480043ff582f352 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 11:43:54 +0200 Subject: [PATCH 063/312] Add `No-example` to documentation --- docs/documenting.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/documenting.md b/docs/documenting.md index e85897ca..48bf20be 100644 --- a/docs/documenting.md +++ b/docs/documenting.md @@ -97,6 +97,19 @@ Note: a random value will be used as the value of each parameter in the example */ ``` +Note: To exclude a particular parameter from the generated examples (for all languages), you can annotate it with `No-example`. For instance: +```php + /** + * @queryParam location_id required The id of the location. Example: 1 + * @queryParam user_id required The id of the user. No-example + * @queryParam page required The page number. Example: 4 + */ +``` +Outputs: +```bash +curl -X GET -G "https://example.com/api?location_id=1&page=4" +``` + Note: You can also add the `@queryParam` and `@bodyParam` annotations to a `\Illuminate\Foundation\Http\FormRequest` subclass instead, if you are using one in your controller method ```php From a8303a5500c521147b96b65f3f38b841c4ac43d1 Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Mon, 26 Aug 2019 12:06:34 +0200 Subject: [PATCH 064/312] [bugfix] Remove extra newline that makes the tests fail when comparing md to fixtures --- resources/views/partials/route.blade.php | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/views/partials/route.blade.php b/resources/views/partials/route.blade.php index 0588433e..0843f60a 100644 --- a/resources/views/partials/route.blade.php +++ b/resources/views/partials/route.blade.php @@ -14,7 +14,6 @@ @foreach($settings['languages'] as $language) @include("apidoc::partials.example-requests.$language") - @endforeach @if(in_array('GET',$route['methods']) || (isset($route['showresponse']) && $route['showresponse'])) From 789d382d04c03993840d4c592990d34d2212b19f Mon Sep 17 00:00:00 2001 From: Marnu Lombard Date: Tue, 27 Aug 2019 10:36:28 +0200 Subject: [PATCH 065/312] [bugfix] Only check for `is_null()` to filter out excluded values - Using any other method is essentially a breaking change --- src/Tools/Traits/ParamHelpers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/Traits/ParamHelpers.php b/src/Tools/Traits/ParamHelpers.php index 28c918c9..5033952d 100644 --- a/src/Tools/Traits/ParamHelpers.php +++ b/src/Tools/Traits/ParamHelpers.php @@ -15,7 +15,7 @@ protected function cleanParams(array $params) { $values = []; $params = array_filter($params, function ($details) { - return is_string($details['value']) && strlen($details['value']); + return ! is_null($details['value']); }); foreach ($params as $name => $details) { From f5ea7d46f0ffdb41df0ff087dd64e7e5dad84cab Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 29 Aug 2019 08:04:08 +0100 Subject: [PATCH 066/312] Tweak Travis config --- .travis.yml | 9 ++++++--- composer.json | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 19302eac..a7c55c6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,19 +6,22 @@ php: - 7.0.0 env: - - PREFER_LOWEST="" - PREFER_LOWEST="--prefer-lowest" jobs: include: - - script: composer lint + - before_script: composer update --no-interaction --prefer-dist $PREFER_LOWEST + script: composer lint name: "Lint code" + env: + - PREFER_LOWEST="" + -php: 7.2.0 script: composer test-ci before_script: - travis_retry composer self-update - travis_retry composer update --no-interaction --prefer-dist $PREFER_LOWEST - - + before_install: - pip install --user codecov diff --git a/composer.json b/composer.json index 1d3b78c1..d2caa1d5 100644 --- a/composer.json +++ b/composer.json @@ -31,11 +31,11 @@ "dingo/api": "2.0.0-alpha1", "mockery/mockery": "^1.2.0", "league/fractal": "^0.17.0", - "phpstan/phpstan": "^0.11.15" + "phpstan/phpstan": "^0.9.0" }, "suggest": { "league/fractal": "Required for transformers support", - "nunomaduro/collision": "For better reporting of errors that are thrpwn when generating docs" + "nunomaduro/collision": "For better reporting of errors that are thrown when generating docs" }, "autoload": { "psr-4": { From bc2dfbbc2bcf649d8423e1604fce96a0ce42e855 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 29 Aug 2019 08:06:48 +0100 Subject: [PATCH 067/312] Tweak Travis config --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a7c55c6e..8b247914 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ jobs: name: "Lint code" env: - PREFER_LOWEST="" - -php: 7.2.0 + php: 7.2.0 script: composer test-ci before_script: From 1f41af4aeb0c3a9c2a7f67993e85b2456dd5aeb5 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 29 Aug 2019 08:08:21 +0100 Subject: [PATCH 068/312] Tweak Travis config --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8b247914..f30a39eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,10 @@ env: jobs: include: - - before_script: composer update --no-interaction --prefer-dist $PREFER_LOWEST + - before_script: composer install --no-interaction --prefer-dist $PREFER_LOWEST script: composer lint name: "Lint code" - env: - - PREFER_LOWEST="" + env: PREFER_LOWEST="" php: 7.2.0 script: composer test-ci From 6cb39cb955bc148ee86daf32f619687b5bda65d6 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 29 Aug 2019 08:12:47 +0100 Subject: [PATCH 069/312] Tweak Travis config --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f30a39eb..5e545583 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ jobs: - before_script: composer install --no-interaction --prefer-dist $PREFER_LOWEST script: composer lint name: "Lint code" - env: PREFER_LOWEST="" php: 7.2.0 script: composer test-ci From 6951db5006a7306b8d79aa933168484980cf2954 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 29 Aug 2019 08:17:23 +0100 Subject: [PATCH 070/312] Tweak Travis config --- .travis.yml | 1 - composer.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5e545583..97ff2c0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ jobs: - before_script: composer install --no-interaction --prefer-dist $PREFER_LOWEST script: composer lint name: "Lint code" - php: 7.2.0 script: composer test-ci before_script: diff --git a/composer.json b/composer.json index d2caa1d5..90512c6a 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "dingo/api": "2.0.0-alpha1", "mockery/mockery": "^1.2.0", "league/fractal": "^0.17.0", - "phpstan/phpstan": "^0.9.0" + "phpstan/phpstan": "^0.9.0 || ^0.10.0 || ^0.11.15" }, "suggest": { "league/fractal": "Required for transformers support", From 0c221d8b0790d6c8ec68fa13ee6ad364a69ac4b5 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 29 Aug 2019 08:19:34 +0100 Subject: [PATCH 071/312] Tweak Travis config --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 97ff2c0f..1b2631ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,7 @@ env: jobs: include: - - before_script: composer install --no-interaction --prefer-dist $PREFER_LOWEST - script: composer lint + - script: composer lint name: "Lint code" script: composer test-ci From 3dc0c30b5d0a01a4dde7dc9aa42a459071f80326 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 29 Aug 2019 08:23:19 +0100 Subject: [PATCH 072/312] Tweak Travis config --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1b2631ab..74dbffc5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,15 @@ php: - 7.0.0 env: + - PREFER_LOWEST="" - PREFER_LOWEST="--prefer-lowest" jobs: include: - script: composer lint name: "Lint code" + php: 7.2.0 + env: PREFER_LOWEST="" script: composer test-ci before_script: From fdc7a725314030bd3cad4b86300591c8243ae316 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 29 Aug 2019 08:32:57 +0100 Subject: [PATCH 073/312] Update PHPStan config --- phpstan.neon | 1 + 1 file changed, 1 insertion(+) diff --git a/phpstan.neon b/phpstan.neon index d8f5d764..f07d6f9a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,3 +7,4 @@ parameters: - '#Call to an undefined static method Illuminate\\Support\\Facades\\URL::forceRootUrl()#' - '#Call to an undefined method Illuminate\\Routing\\Route::versions().#' - '#(.*)NunoMaduro\\Collision(.*)#' + - '#Instantiated class Whoops\\Exception\\Inspector not found\.#' From 5647eda35ebb7f8aed35b31790c5f220b736e985 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 31 Aug 2019 11:43:08 +0100 Subject: [PATCH 074/312] Parse @group tag on method properly - fixes #564, #561 --- TODO.md | 1 + resources/views/documentarian.blade.php | 2 - src/Commands/GenerateDocumentation.php | 9 +++-- src/Tools/Generator.php | 50 ++++++++++++++++--------- src/Tools/RouteMatcher.php | 3 ++ tests/Fixtures/TestController.php | 33 ++++++++++++++++ tests/Unit/GeneratorTestCase.php | 27 ++++++++++--- 7 files changed, 97 insertions(+), 28 deletions(-) diff --git a/TODO.md b/TODO.md index 3479e3d8..282ab3e6 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,4 @@ Major - Bring `bindings` outside of `response_calls` - Should `routes.*.apply.response_calls.headers` be replaced by `routes.*.apply.headers`? +- Should we move HTML generation from Blade to fully PHP? (L) diff --git a/resources/views/documentarian.blade.php b/resources/views/documentarian.blade.php index ab5b890c..90151a73 100644 --- a/resources/views/documentarian.blade.php +++ b/resources/views/documentarian.blade.php @@ -6,9 +6,7 @@ {!! $prependMd !!} @foreach($parsedRoutes as $groupName => $routes) -@if($groupName) #{!! $groupName !!} -@endif {{-- We pick the first non-empty description we see. --}} {!! array_first($routes, function ($route) { return $route['groupDescription'] !== ''; })['groupDescription'] ?? '' !!} @foreach($routes as $parsedRoute) diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index 13116266..a4f1cdf2 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -60,8 +60,8 @@ public function __construct(RouteMatcher $routeMatcher) */ public function handle() { - // Using a global static variable here, so fuck off if you don't like it - // Also, the --verbose option is included with all Artisan commands + // Using a global static variable here, so fuck off if you don't like it. + // Also, the --verbose option is included with all Artisan commands. Flags::$shouldBeVerbose = $this->option('verbose'); $this->docConfig = new DocumentationConfig(config('apidoc')); @@ -82,13 +82,14 @@ public function handle() $generator = new Generator($this->docConfig); $parsedRoutes = $this->processRoutes($generator, $routes); - $parsedRoutes = collect($parsedRoutes)->groupBy('groupName') + $groupedRoutes = collect($parsedRoutes) + ->groupBy('groupName') ->sortBy(static function ($group) { /* @var $group Collection */ return $group->first()['groupName']; }, SORT_NATURAL); - $this->writeMarkdown($parsedRoutes); + $this->writeMarkdown($groupedRoutes); } /** diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 5cb068e3..000025a1 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -57,9 +57,9 @@ public function processRoute(Route $route, array $rulesToApply = []) $controller = new ReflectionClass($class); $method = $controller->getMethod($method); - list($routeGroupName, $routeGroupDescription) = $this->getRouteGroup($controller, $method); $docBlock = $this->parseDocBlock($method); + list($routeGroupName, $routeGroupDescription, $routeTitle) = $this->getRouteGroup($controller, $docBlock); $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']); $queryParameters = $this->getQueryParameters($method, $docBlock['tags']); $content = ResponseResolver::getResponse($route, $docBlock['tags'], [ @@ -72,7 +72,7 @@ public function processRoute(Route $route, array $rulesToApply = []) 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), 'groupName' => $routeGroupName, 'groupDescription' => $routeGroupDescription, - 'title' => $docBlock['short'], + 'title' => $routeTitle ?: $docBlock['short'], 'description' => $docBlock['long'], 'methods' => $this->getMethods($route), 'uri' => $this->getUri($route), @@ -271,24 +271,40 @@ protected function parseDocBlock(ReflectionMethod $method) /** * @param ReflectionClass $controller - * @param ReflectionMethod $method + * @param array $methodDocBlock * - * @return array The route group name and description + * @return array The route group name, the group description, ad the route title */ - protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $method) + protected function getRouteGroup(ReflectionClass $controller, array $methodDocBlock) { // @group tag on the method overrides that on the controller - $docBlockComment = $method->getDocComment(); - if ($docBlockComment) { - $phpdoc = new DocBlock($docBlockComment); - foreach ($phpdoc->getTags() as $tag) { + if (!empty($methodDocBlock['tags'])) { + foreach ($methodDocBlock['tags'] as $tag) { if ($tag->getName() === 'group') { - $routeGroup = trim($tag->getContent()); - $routeGroupParts = explode("\n", $tag->getContent()); + $routeGroupParts = explode("\n", trim($tag->getContent())); $routeGroupName = array_shift($routeGroupParts); - $routeGroupDescription = implode("\n", $routeGroupParts); - - return [$routeGroupName, $routeGroupDescription]; + $routeGroupDescription = trim(implode("\n", $routeGroupParts)); + + // If the route has no title (aka "short"), + // we'll assume the routeGroupDescription is actually the title + // Something like this: + // /** + // * Fetch cars. <-- This is route title. + // * @group Cars <-- This is group name. + // * APIs for cars. <-- This is group description (not required). + // **/ + // VS + // /** + // * @group Cars <-- This is group name. + // * Fetch cars. <-- This is route title, NOT group description. + // **/ + + // BTW, this is a spaghetti way of doing this. + // It shall be refactored soon. Deus vult!💪 + if (empty($methodDocBlock['short'])) { + return [$routeGroupName, '', $routeGroupDescription]; + } + return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']]; } } } @@ -298,16 +314,16 @@ protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $ $phpdoc = new DocBlock($docBlockComment); foreach ($phpdoc->getTags() as $tag) { if ($tag->getName() === 'group') { - $routeGroupParts = explode("\n", $tag->getContent()); + $routeGroupParts = explode("\n", trim($tag->getContent())); $routeGroupName = array_shift($routeGroupParts); $routeGroupDescription = implode("\n", $routeGroupParts); - return [$routeGroupName, $routeGroupDescription]; + return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']]; } } } - return [$this->config->get(('default_group')), '']; + return [$this->config->get(('default_group')), '', $methodDocBlock['short']]; } private function normalizeParameterType($type) diff --git a/src/Tools/RouteMatcher.php b/src/Tools/RouteMatcher.php index cc948370..c98885f0 100644 --- a/src/Tools/RouteMatcher.php +++ b/src/Tools/RouteMatcher.php @@ -48,6 +48,9 @@ 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 = []) { if (! $usingDingoRouter) { diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index 024f43fc..e41d849b 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -33,6 +33,39 @@ public function withGroupOverride() return ''; } + /** + * This is also in Group B. No route description. Route title before gropp. + * + * @group Group B + */ + public function withGroupOverride2() + { + return ''; + } + + /** + * @group Group B + * + * This is also in Group B. Route title after group. + */ + public function withGroupOverride3() + { + return ''; + } + + /** + * This is in Group C. Route title before group. + * + * @group Group C + * + * Group description after group. + * + */ + public function withGroupOverride4() + { + return ''; + } + /** * @bodyParam user_id int required The id of the user. Example: 9 * @bodyParam room_id string The id of the room. diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index d98701f1..105a12ba 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -4,7 +4,6 @@ use Orchestra\Testbench\TestCase; use Mpociot\ApiDoc\Tools\Generator; -use Illuminate\Support\Facades\Storage; use Mpociot\ApiDoc\Tools\DocumentationConfig; use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider; @@ -222,10 +221,28 @@ public function can_parse_route_group() /** @test */ public function method_can_override_controller_group() { - $route = $this->createRoute('GET', '/api/test', 'withGroupOverride'); - $routeGroup = $this->generator->processRoute($route)['groupName']; - - $this->assertSame('Group B', $routeGroup); + $route = $this->createRoute('GET', '/group/1', 'withGroupOverride'); + $parsedRoute = $this->generator->processRoute($route); + $this->assertSame('Group B', $parsedRoute['groupName']); + $this->assertSame('', $parsedRoute['groupDescription']); + + $route = $this->createRoute('GET', '/group/2', 'withGroupOverride2'); + $parsedRoute = $this->generator->processRoute($route); + $this->assertSame('Group B', $parsedRoute['groupName']); + $this->assertSame('', $parsedRoute['groupDescription']); + $this->assertSame('This is also in Group B. No route description. Route title before gropp.', $parsedRoute['title']); + + $route = $this->createRoute('GET', '/group/3', 'withGroupOverride3'); + $parsedRoute = $this->generator->processRoute($route); + $this->assertSame('Group B', $parsedRoute['groupName']); + $this->assertSame('', $parsedRoute['groupDescription']); + $this->assertSame('This is also in Group B. Route title after group.', $parsedRoute['title']); + + $route = $this->createRoute('GET', '/group/4', 'withGroupOverride4'); + $parsedRoute = $this->generator->processRoute($route); + $this->assertSame('Group C', $parsedRoute['groupName']); + $this->assertSame('Group description after group.', $parsedRoute['groupDescription']); + $this->assertSame('This is in Group C. Route title before group.', $parsedRoute['title']); } /** @test */ From 77ed9943f85edf3177a5557351708742fddd6c4f Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Sat, 31 Aug 2019 10:43:25 +0000 Subject: [PATCH 075/312] Apply fixes from StyleCI --- src/Tools/Generator.php | 4 ++-- tests/Fixtures/TestController.php | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 000025a1..2316be00 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -57,7 +57,6 @@ public function processRoute(Route $route, array $rulesToApply = []) $controller = new ReflectionClass($class); $method = $controller->getMethod($method); - $docBlock = $this->parseDocBlock($method); list($routeGroupName, $routeGroupDescription, $routeTitle) = $this->getRouteGroup($controller, $docBlock); $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']); @@ -278,7 +277,7 @@ protected function parseDocBlock(ReflectionMethod $method) protected function getRouteGroup(ReflectionClass $controller, array $methodDocBlock) { // @group tag on the method overrides that on the controller - if (!empty($methodDocBlock['tags'])) { + if (! empty($methodDocBlock['tags'])) { foreach ($methodDocBlock['tags'] as $tag) { if ($tag->getName() === 'group') { $routeGroupParts = explode("\n", trim($tag->getContent())); @@ -304,6 +303,7 @@ protected function getRouteGroup(ReflectionClass $controller, array $methodDocBl if (empty($methodDocBlock['short'])) { return [$routeGroupName, '', $routeGroupDescription]; } + return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']]; } } diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index e41d849b..9266ea9a 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -59,7 +59,6 @@ public function withGroupOverride3() * @group Group C * * Group description after group. - * */ public function withGroupOverride4() { From f2eb99d061cd5656972767512148a98177400bc9 Mon Sep 17 00:00:00 2001 From: Shalvah Date: Sat, 31 Aug 2019 11:51:50 +0100 Subject: [PATCH 076/312] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea04cd9f..78be32b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [3.14.0] - Saturday, 31 August 2019 +### Fixed +- Backwards compatibility for the changes to `@group` introduced in 3.12.0 (https://github.com/mpociot/laravel-apidoc-generator/pull/556) + +## [3.13.0] - Sunday, 25 August 2019 + ## [3.12.0] - Sunday, 25 August 2019 ### Fixed - Specifying an `@group` for a method no longer requires you to add the description. (https://github.com/mpociot/laravel-apidoc-generator/pull/556) From dd840854b1915f078f8cf4bf841812d0e91056ca Mon Sep 17 00:00:00 2001 From: Shalvah Date: Sat, 31 Aug 2019 12:13:39 +0100 Subject: [PATCH 077/312] Update GeneratorTestCase.php --- tests/Unit/GeneratorTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 157cec15..fe4a0824 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -211,7 +211,7 @@ public function can_parse_query_parameters() } /** @test */ - public function it_ignores_excluded_params() + public function it_does_not_generate_values_for_excluded_params_and_excludes_them_from_clean_params() { $route = $this->createRoute('GET', '/api/test', 'withExcludedExamples'); $parsed = $this->generator->processRoute($route); From e48003edf3c8fd6afbe537ac451d2523a787f7c1 Mon Sep 17 00:00:00 2001 From: Shalvah Date: Sat, 31 Aug 2019 12:14:39 +0100 Subject: [PATCH 078/312] Update docblock on cleanParameters --- src/Tools/Traits/ParamHelpers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/Traits/ParamHelpers.php b/src/Tools/Traits/ParamHelpers.php index 5033952d..6f72be10 100644 --- a/src/Tools/Traits/ParamHelpers.php +++ b/src/Tools/Traits/ParamHelpers.php @@ -5,7 +5,7 @@ trait ParamHelpers { /** - * Create proper arrays from dot-noted parameter names. + * Create proper arrays from dot-noted parameter names. Also filter out parameters which were excluded from having examples. * * @param array $params * From 6079fd37da9bda9a6c00b59523702c39a5949a79 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 31 Aug 2019 12:29:59 +0100 Subject: [PATCH 079/312] Update docs --- docs/architecture.md | 12 ++++++++++-- docs/documenting.md | 5 +++-- docs/generating-documentation.md | 4 ++-- docs/index.md | 1 + 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 984e1e84..aec01017 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,4 +1,12 @@ -# Architecutre +# Architecture Read this page if you want a deeper understanding of how this works (for instance, for the purpose of contributing). -- Documentarian takes in a markdwown file and transforms it into HTML. +- When the `generate` command is run, it fetches all your application's routes from Laravel's (or DIngo's) Route facade. +- Next, the RouteMatcher uses the rules in your config to determine what routes to generate documentation for, as well as extract any specific configuration for them. This configuration is passed to the next stages. +- The Generator processes each route. This entails: + - Fetching the route action (controller, method) via Reflection (along with their corresponding docblocks). These are used in the remaining stages below. + - Determining and obtaining info on body parameters, query parameters and headers to be added to the route's documentation. + - Obtaining a sample response. +- The generate command uses information from these parsed routes and other configuration to generate a Markdown file via Blade templating. +- This Markdown file is passed to Documentarian, which transforms it into HTML, CSS and JavaScript assets. +- If enabled, a Postman collection is generated as well. diff --git a/docs/documenting.md b/docs/documenting.md index 48bf20be..51f8a5f3 100644 --- a/docs/documenting.md +++ b/docs/documenting.md @@ -84,7 +84,8 @@ They will be included in the generated documentation text and example requests. ![](body_params.png) -Note: a random value will be used as the value of each parameter in the example requests. If you'd like to specify an example value, you can do so by adding `Example: your-example` to the end of your description. For instance: +### Example parameters +For each parameter in your request, this package will generate a random value to be used in the example requests. If you'd like to specify an example value, you can do so by adding `Example: your-example` to the end of your description. For instance: ```php /** @@ -97,7 +98,7 @@ Note: a random value will be used as the value of each parameter in the example */ ``` -Note: To exclude a particular parameter from the generated examples (for all languages), you can annotate it with `No-example`. For instance: +You can also exclude a particular parameter from the generated examples (for all languages) by annotating it with `No-example`. For instance: ```php /** * @queryParam location_id required The id of the location. Example: 1 diff --git a/docs/generating-documentation.md b/docs/generating-documentation.md index e0d0af54..eb05758e 100644 --- a/docs/generating-documentation.md +++ b/docs/generating-documentation.md @@ -51,8 +51,8 @@ php artisan apidoc:rebuild - `methods`: an array of the HTTP methods for that route - `boundUri`: the complete URL for the route, with any url parameters replaced (/users/{id} -> /users/1) - `headers`: key-value array of headers to be sent with route (according to your configuration) - - `cleanQueryParameters`: key-value array of query parameters (with example values) to be sent with the request - - `cleanBodyParameters`: key-value array of body parameters (with example values) to be sent with the request + - `cleanQueryParameters`: key-value array of query parameters with example values to be sent with the request. Parameters which have been excluded from the example requests (see [Example Parameters](documenting.html#example-parameters)) will not be present here. + - `cleanBodyParameters`: key-value array of body parameters with example values to be sent with the request. Parameters which have been excluded from the example requests (see [Example Parameters](documenting.html#example-parameters)) will not be present here. - Add the language to the `example_languages` array in the package config. diff --git a/docs/index.md b/docs/index.md index b4bc8fb1..decb22c5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,6 +9,7 @@ Automatically generate your API documentation from your existing Laravel/Lumen/[ * [Configuration](config.md) * [Generating Documentation](generating-documentation.md) * [Documenting Your API](documenting.md) +* [Internal Architecture](architecture.md) ## Installation > Note: PHP 7 and Laravel 5.5 or higher are required. From b7bc937e2d1ac1d6262f23d91d3d4b59b6ad148d Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 31 Aug 2019 12:33:31 +0100 Subject: [PATCH 080/312] Update changelog --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78be32b6..8c1f1f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [3.15.0] - Saturday, 31 August 2019 +### Added +- Ability to exclude a query or body parameter from being included in the example requests (https://github.com/mpociot/laravel-apidoc-generator/pull/552) + ## [3.14.0] - Saturday, 31 August 2019 ### Fixed -- Backwards compatibility for the changes to `@group` introduced in 3.12.0 (https://github.com/mpociot/laravel-apidoc-generator/pull/556) +- Backwards compatibility for the changes to `@group` introduced in 3.12.0 (https://github.com/mpociot/laravel-apidoc-generator/commit/5647eda35ebb7f8aed35b31790c5f220b736e985) -## [3.13.0] - Sunday, 25 August 2019 +## [3.13.0] (deleted) ## [3.12.0] - Sunday, 25 August 2019 ### Fixed From 67c61fef8db0c94563938307cd4a677c4b6231d2 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sun, 1 Sep 2019 23:40:28 +0100 Subject: [PATCH 081/312] First implementation of plugin architecture - Split route processing into stages: metadata, bodyParameters, queryParameters, responses - Provide tool to fetch route docblocks (with caching) --- config/apidoc.php | 17 +++ src/Commands/GenerateDocumentation.php | 4 +- src/Strategies/Metadata/GetFromDocBlocks.php | 109 ++++++++++++++ src/Tools/Generator.php | 141 +++++-------------- src/Tools/RouteDocBlocker.php | 56 ++++++++ src/Tools/Utils.php | 6 +- 6 files changed, 222 insertions(+), 111 deletions(-) create mode 100644 src/Strategies/Metadata/GetFromDocBlocks.php create mode 100644 src/Tools/RouteDocBlocker.php diff --git a/config/apidoc.php b/config/apidoc.php index aba55635..8067fc4e 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -1,5 +1,7 @@ [ + 'metadata' => [ + GetFromDocBlocks::class, + ], + 'bodyParameters' => [ + + ], + 'queryParameters' => [ + + ], + 'responses' => [ + + ], + ], + /* * Custom logo path. The logo will be copied from this location * during the generate process. Set this to false to use the default logo. diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index a4f1cdf2..e9dda396 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -247,7 +247,7 @@ private function processRoutes(Generator $generator, array $routes) */ private function isValidRoute(Route $route) { - $action = Utils::getRouteActionUses($route->getAction()); + $action = Utils::getRouteClassAndMethodNames($route->getAction()); if (is_array($action)) { $action = implode('@', $action); } @@ -264,7 +264,7 @@ private function isValidRoute(Route $route) */ private function isRouteVisibleForDocumentation(array $action) { - list($class, $method) = Utils::getRouteActionUses($action); + list($class, $method) = Utils::getRouteClassAndMethodNames($action); $reflection = new ReflectionClass($class); if (! $reflection->hasMethod($method)) { diff --git a/src/Strategies/Metadata/GetFromDocBlocks.php b/src/Strategies/Metadata/GetFromDocBlocks.php new file mode 100644 index 00000000..a8acf328 --- /dev/null +++ b/src/Strategies/Metadata/GetFromDocBlocks.php @@ -0,0 +1,109 @@ +config = $config; + } + + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method) + { + $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); + /** @var DocBlock $methodDocBlock */ + $methodDocBlock = $docBlocks['method']; + + list($routeGroupName, $routeGroupDescription, $routeTitle) = $this->getRouteGroupDescriptionAndTitle($methodDocBlock, $docBlocks['class']); + + return [ + 'groupName' => $routeGroupName, + 'groupDescription' => $routeGroupDescription, + 'title' => $routeTitle ?: $methodDocBlock->getShortDescription(), + 'description' => $methodDocBlock->getLongDescription()->getContents(), + 'authenticated' => $this->getAuthStatusFromDocBlock($methodDocBlock->getTags()), + ]; + } + + /** + * @param array $tags Tags in the method doc block + * + * @return bool + */ + protected function getAuthStatusFromDocBlock(array $tags) + { + $authTag = collect($tags) + ->first(function ($tag) { + return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated'; + }); + + return (bool) $authTag; + } + + /** + * @param DocBlock $methodDocBlock + * + * @param DocBlock $controllerDocBlock + * + * @return array The route group name, the group description, ad the route title + */ + protected function getRouteGroupDescriptionAndTitle(DocBlock $methodDocBlock, DocBlock $controllerDocBlock) + { + // @group tag on the method overrides that on the controller + if (! empty($methodDocBlock->getTags())) { + foreach ($methodDocBlock->getTags() as $tag) { + if ($tag->getName() === 'group') { + $routeGroupParts = explode("\n", trim($tag->getContent())); + $routeGroupName = array_shift($routeGroupParts); + $routeGroupDescription = trim(implode("\n", $routeGroupParts)); + + // If the route has no title (the methodDocBlock's "short description"), + // we'll assume the routeGroupDescription is actually the title + // Something like this: + // /** + // * Fetch cars. <-- This is route title. + // * @group Cars <-- This is group name. + // * APIs for cars. <-- This is group description (not required). + // **/ + // VS + // /** + // * @group Cars <-- This is group name. + // * Fetch cars. <-- This is route title, NOT group description. + // **/ + + // BTW, this is a spaghetti way of doing this. + // It shall be refactored soon. Deus vult!💪 + if (empty($methodDocBlock->getShortDescription())) { + return [$routeGroupName, '', $routeGroupDescription]; + } + + return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()]; + } + } + } + + foreach ($controllerDocBlock->getTags() as $tag) { + if ($tag->getName() === 'group') { + $routeGroupParts = explode("\n", trim($tag->getContent())); + $routeGroupName = array_shift($routeGroupParts); + $routeGroupDescription = implode("\n", $routeGroupParts); + + return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()]; + } + } + + return [$this->config->get('default_group'), '', $methodDocBlock->getShortDescription()]; + } +} diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 9153e14d..dc11d4ad 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -53,15 +53,21 @@ public function getMethods(Route $route) */ public function processRoute(Route $route, array $rulesToApply = []) { - list($class, $method) = Utils::getRouteActionUses($route->getAction()); - $controller = new ReflectionClass($class); - $method = $controller->getMethod($method); - - $docBlock = $this->parseDocBlock($method); - list($routeGroupName, $routeGroupDescription, $routeTitle) = $this->getRouteGroup($controller, $docBlock); - $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']); - $queryParameters = $this->getQueryParameters($method, $docBlock['tags']); - $content = ResponseResolver::getResponse($route, $docBlock['tags'], [ + list($controllerName, $methodName) = Utils::getRouteClassAndMethodNames($route->getAction()); + $controller = new ReflectionClass($controllerName); + $method = $controller->getMethod($methodName); + + $metadata = $this->fetchMetadata($controller, $method, $route); + // $this->fetchBodyParameters(); + // $this->fetchQueryParameters(); + // $this->fetchResponse(); + + $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); + /** @var DocBlock $methodDocBlock */ + $methodDocBlock = $docBlocks['method']; + $bodyParameters = $this->getBodyParameters($method, $methodDocBlock->getTags()); + $queryParameters = $this->getQueryParameters($method, $methodDocBlock->getTags()); + $content = ResponseResolver::getResponse($route, $methodDocBlock->getTags(), [ 'rules' => $rulesToApply, 'body' => $bodyParameters, 'query' => $queryParameters, @@ -69,10 +75,6 @@ public function processRoute(Route $route, array $rulesToApply = []) $parsedRoute = [ 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), - 'groupName' => $routeGroupName, - 'groupDescription' => $routeGroupDescription, - 'title' => $routeTitle ?: $docBlock['short'], - 'description' => $docBlock['long'], 'methods' => $this->getMethods($route), 'uri' => $this->getUri($route), 'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])), @@ -80,15 +82,30 @@ public function processRoute(Route $route, array $rulesToApply = []) 'bodyParameters' => $bodyParameters, 'cleanBodyParameters' => $this->cleanParams($bodyParameters), 'cleanQueryParameters' => $this->cleanParams($queryParameters), - 'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']), 'response' => $content, 'showresponse' => ! empty($content), ]; $parsedRoute['headers'] = $rulesToApply['headers'] ?? []; + $parsedRoute += $metadata; return $parsedRoute; } + protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route) + { + $metadataStrategies = $this->config->get('strategies.metadata', []); + $results = []; + + foreach ($metadataStrategies as $strategyClass) { + $strategy = new $strategyClass($this->config); + $results = $strategy($route, $controller, $method); + if (count($results)) { + break; + } + } + return count($results) ? $results : []; + } + protected function getBodyParameters(ReflectionMethod $method, array $tags) { foreach ($method->getParameters() as $param) { @@ -150,7 +167,7 @@ protected function getBodyParametersFromDocBlock(array $tags) } $type = $this->normalizeParameterType($type); - list($description, $example) = $this->parseDescription($description, $type); + list($description, $example) = $this->parseParamDescription($description, $type); $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; return [$name => compact('type', 'description', 'required', 'value')]; @@ -225,7 +242,7 @@ protected function getQueryParametersFromDocBlock(array $tags) $required = trim($required) == 'required' ? true : false; } - list($description, $value) = $this->parseDescription($description, 'string'); + list($description, $value) = $this->parseParamDescription($description, 'string'); if (is_null($value) && ! $this->shouldExcludeExample($tag)) { $value = str_contains($description, ['number', 'count', 'page']) ? $this->generateDummyValue('integer') @@ -238,96 +255,6 @@ protected function getQueryParametersFromDocBlock(array $tags) return $parameters; } - /** - * @param array $tags - * - * @return bool - */ - protected function getAuthStatusFromDocBlock(array $tags) - { - $authTag = collect($tags) - ->first(function ($tag) { - return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated'; - }); - - return (bool) $authTag; - } - - /** - * @param ReflectionMethod $method - * - * @return array - */ - protected function parseDocBlock(ReflectionMethod $method) - { - $comment = $method->getDocComment(); - $phpdoc = new DocBlock($comment); - - return [ - 'short' => $phpdoc->getShortDescription(), - 'long' => $phpdoc->getLongDescription()->getContents(), - 'tags' => $phpdoc->getTags(), - ]; - } - - /** - * @param ReflectionClass $controller - * @param array $methodDocBlock - * - * @return array The route group name, the group description, ad the route title - */ - protected function getRouteGroup(ReflectionClass $controller, array $methodDocBlock) - { - // @group tag on the method overrides that on the controller - if (! empty($methodDocBlock['tags'])) { - foreach ($methodDocBlock['tags'] as $tag) { - if ($tag->getName() === 'group') { - $routeGroupParts = explode("\n", trim($tag->getContent())); - $routeGroupName = array_shift($routeGroupParts); - $routeGroupDescription = trim(implode("\n", $routeGroupParts)); - - // If the route has no title (aka "short"), - // we'll assume the routeGroupDescription is actually the title - // Something like this: - // /** - // * Fetch cars. <-- This is route title. - // * @group Cars <-- This is group name. - // * APIs for cars. <-- This is group description (not required). - // **/ - // VS - // /** - // * @group Cars <-- This is group name. - // * Fetch cars. <-- This is route title, NOT group description. - // **/ - - // BTW, this is a spaghetti way of doing this. - // It shall be refactored soon. Deus vult!💪 - if (empty($methodDocBlock['short'])) { - return [$routeGroupName, '', $routeGroupDescription]; - } - - return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']]; - } - } - } - - $docBlockComment = $controller->getDocComment(); - if ($docBlockComment) { - $phpdoc = new DocBlock($docBlockComment); - foreach ($phpdoc->getTags() as $tag) { - if ($tag->getName() === 'group') { - $routeGroupParts = explode("\n", trim($tag->getContent())); - $routeGroupName = array_shift($routeGroupParts); - $routeGroupDescription = implode("\n", $routeGroupParts); - - return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']]; - } - } - } - - return [$this->config->get(('default_group')), '', $methodDocBlock['short']]; - } - private function normalizeParameterType($type) { $typeMap = [ @@ -383,7 +310,7 @@ private function generateDummyValue(string $type) * * @return array The description and included example. */ - private function parseDescription(string $description, string $type) + private function parseParamDescription(string $description, string $type) { $example = null; if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) { diff --git a/src/Tools/RouteDocBlocker.php b/src/Tools/RouteDocBlocker.php new file mode 100644 index 00000000..e96ba033 --- /dev/null +++ b/src/Tools/RouteDocBlocker.php @@ -0,0 +1,56 @@ +hasMethod($methodName)) { + throw new \Exception("Error while fetching docblock for route: Class $className does not contain method $methodName"); + } + + $docBlocks = [ + 'method' => new DocBlock($class->getMethod($methodName)->getDocComment() ?: ''), + 'class' => new DocBlock($class->getDocComment() ?: '') + ]; + self::cacheDocBlocks($route, $className, $methodName, $docBlocks); + return $docBlocks; + } + + protected static function getCachedDocBlock(Route $route, string $className, string $methodName) + { + $routeId = self::getRouteId($route, $className, $methodName); + return self::$docBlocks[$routeId] ?? null; + } + + protected static function cacheDocBlocks(Route $route, string $className, string $methodName, array $docBlocks) + { + $routeId = self::getRouteId($route, $className, $methodName); + self::$docBlocks[$routeId] = $docBlocks; + } + + private static function getRouteId(Route $route, string $className, string $methodName) + { + return $route->uri() + .':' + .implode(array_diff($route->methods(), ['HEAD'])) + .$className + .$methodName; + } +} diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php index b4e30930..ed3a50de 100644 --- a/src/Tools/Utils.php +++ b/src/Tools/Utils.php @@ -19,12 +19,14 @@ public static function getFullUrl(Route $route, array $bindings = []): string } /** - * @param array $action + * @param array|Route $routeOrAction * * @return array|null */ - public static function getRouteActionUses(array $action) + public static function getRouteClassAndMethodNames($routeOrAction) { + $action = $routeOrAction instanceof Route ? $routeOrAction->getAction() : $routeOrAction; + if ($action['uses'] !== null) { if (is_array($action['uses'])) { return $action['uses']; From 29df46880c6d2d7b152c03e69e90968e88d97b77 Mon Sep 17 00:00:00 2001 From: shalvah Date: Mon, 2 Sep 2019 00:24:56 +0100 Subject: [PATCH 082/312] Move Query and Body Params to strategies TODO: move the "clean" functionality --- config/apidoc.php | 9 +- .../BodyParameters/GetFromDocBlocks.php | 104 +++++++ src/Strategies/Metadata/GetFromDocBlocks.php | 11 +- .../QueryParameters/GetFromDocBlocks.php | 107 +++++++ src/Tools/Generator.php | 278 ++---------------- src/Tools/Traits/ParamHelpers.php | 114 +++++++ tests/Unit/GeneratorTestCase.php | 34 ++- 7 files changed, 387 insertions(+), 270 deletions(-) create mode 100644 src/Strategies/BodyParameters/GetFromDocBlocks.php create mode 100644 src/Strategies/QueryParameters/GetFromDocBlocks.php diff --git a/config/apidoc.php b/config/apidoc.php index 8067fc4e..bb2dbd17 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -1,7 +1,5 @@ [ 'metadata' => [ - GetFromDocBlocks::class, + \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, ], 'bodyParameters' => [ - + \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromDocBlocks::class, ], 'queryParameters' => [ - + \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromDocBlocks::class, ], 'responses' => [ - ], ], diff --git a/src/Strategies/BodyParameters/GetFromDocBlocks.php b/src/Strategies/BodyParameters/GetFromDocBlocks.php new file mode 100644 index 00000000..7758e75d --- /dev/null +++ b/src/Strategies/BodyParameters/GetFromDocBlocks.php @@ -0,0 +1,104 @@ +config = $config; + } + + /** + * @param Route $route + * @param ReflectionClass $controller + * @param ReflectionMethod $method + * @param array $routeConfig Array of rules for the ruleset which this route belongs to. + * + * @return array + * @throws \Exception + */ + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeConfig) + { + foreach ($method->getParameters() as $param) { + $paramType = $param->getType(); + if ($paramType === null) { + continue; + } + + $parameterClassName = version_compare(phpversion(), '7.1.0', '<') + ? $paramType->__toString() + : $paramType->getName(); + + try { + $parameterClass = new ReflectionClass($parameterClassName); + } catch (\ReflectionException $e) { + continue; + } + + // If there's a FormRequest, we check there for @bodyParam tags. + if (class_exists(LaravelFormRequest::class) && $parameterClass->isSubclassOf(LaravelFormRequest::class) + || class_exists(DingoFormRequest::class) && $parameterClass->isSubclassOf(DingoFormRequest::class)) { + $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); + $bodyParametersFromDocBlock = $this->getBodyParametersFromDocBlock($formRequestDocBlock->getTags()); + + if (count($bodyParametersFromDocBlock)) { + return $bodyParametersFromDocBlock; + } + } + } + + $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method']; + return $this->getBodyParametersFromDocBlock($methodDocBlock->getTags()); + } + + private function getBodyParametersFromDocBlock($tags) + { + $parameters = collect($tags) + ->filter(function ($tag) { + return $tag instanceof Tag && $tag->getName() === 'bodyParam'; + }) + ->mapWithKeys(function ($tag) { + preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); + $content = preg_replace('/\s?No-example.?/', '', $content); + if (empty($content)) { + // this means only name and type were supplied + list($name, $type) = preg_split('/\s+/', $tag->getContent()); + $required = false; + $description = ''; + } else { + list($_, $name, $type, $required, $description) = $content; + $description = trim($description); + if ($description == 'required' && empty(trim($required))) { + $required = $description; + $description = ''; + } + $required = trim($required) == 'required' ? true : false; + } + + $type = $this->normalizeParameterType($type); + list($description, $example) = $this->parseParamDescription($description, $type); + $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; + + return [$name => compact('type', 'description', 'required', 'value')]; + })->toArray(); + + return $parameters; + } +} diff --git a/src/Strategies/Metadata/GetFromDocBlocks.php b/src/Strategies/Metadata/GetFromDocBlocks.php index a8acf328..3ccd64fe 100644 --- a/src/Strategies/Metadata/GetFromDocBlocks.php +++ b/src/Strategies/Metadata/GetFromDocBlocks.php @@ -20,7 +20,16 @@ public function __construct(DocumentationConfig $config) $this->config = $config; } - public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method) + /** + * @param Route $route + * @param ReflectionClass $controller + * @param ReflectionMethod $method + * @param array $routeConfig Array of rules for the ruleset which this route belongs to. + * + * @return array + * @throws \Exception + */ + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeConfig) { $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); /** @var DocBlock $methodDocBlock */ diff --git a/src/Strategies/QueryParameters/GetFromDocBlocks.php b/src/Strategies/QueryParameters/GetFromDocBlocks.php new file mode 100644 index 00000000..81340273 --- /dev/null +++ b/src/Strategies/QueryParameters/GetFromDocBlocks.php @@ -0,0 +1,107 @@ +config = $config; + } + + /** + * @param Route $route + * @param ReflectionClass $controller + * @param ReflectionMethod $method + * @param array $routeConfig Array of rules for the ruleset which this route belongs to. + * + * @return array + * @throws \Exception + */ + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeConfig) + { + foreach ($method->getParameters() as $param) { + $paramType = $param->getType(); + if ($paramType === null) { + continue; + } + + $parameterClassName = version_compare(phpversion(), '7.1.0', '<') + ? $paramType->__toString() + : $paramType->getName(); + + try { + $parameterClass = new ReflectionClass($parameterClassName); + } catch (\ReflectionException $e) { + continue; + } + + // If there's a FormRequest, we check there for @queryParam tags. + if (class_exists(LaravelFormRequest::class) && $parameterClass->isSubclassOf(LaravelFormRequest::class) + || class_exists(DingoFormRequest::class) && $parameterClass->isSubclassOf(DingoFormRequest::class)) { + $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); + $queryParametersFromDocBlock = $this->getqueryParametersFromDocBlock($formRequestDocBlock->getTags()); + + if (count($queryParametersFromDocBlock)) { + return $queryParametersFromDocBlock; + } + } + } + + $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method']; + return $this->getqueryParametersFromDocBlock($methodDocBlock->getTags()); + } + + private function getQueryParametersFromDocBlock($tags) + { + $parameters = collect($tags) + ->filter(function ($tag) { + return $tag instanceof Tag && $tag->getName() === 'queryParam'; + }) + ->mapWithKeys(function ($tag) { + preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); + $content = preg_replace('/\s?No-example.?/', '', $content); + if (empty($content)) { + // this means only name was supplied + list($name) = preg_split('/\s+/', $tag->getContent()); + $required = false; + $description = ''; + } else { + list($_, $name, $required, $description) = $content; + $description = trim($description); + if ($description == 'required' && empty(trim($required))) { + $required = $description; + $description = ''; + } + $required = trim($required) == 'required' ? true : false; + } + + list($description, $value) = $this->parseParamDescription($description, 'string'); + if (is_null($value) && ! $this->shouldExcludeExample($tag)) { + $value = str_contains($description, ['number', 'count', 'page']) + ? $this->generateDummyValue('integer') + : $this->generateDummyValue('string'); + } + + return [$name => compact('description', 'required', 'value')]; + })->toArray(); + + return $parameters; + } +} diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index dc11d4ad..4850bfbf 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -57,16 +57,14 @@ public function processRoute(Route $route, array $rulesToApply = []) $controller = new ReflectionClass($controllerName); $method = $controller->getMethod($methodName); - $metadata = $this->fetchMetadata($controller, $method, $route); - // $this->fetchBodyParameters(); - // $this->fetchQueryParameters(); + $metadata = $this->fetchMetadata($controller, $method, $route, $rulesToApply); + $bodyParameters = $this->fetchBodyParameters($controller, $method, $route, $rulesToApply); + $queryParameters = $this->fetchQueryParameters($controller, $method, $route, $rulesToApply); // $this->fetchResponse(); $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); /** @var DocBlock $methodDocBlock */ $methodDocBlock = $docBlocks['method']; - $bodyParameters = $this->getBodyParameters($method, $methodDocBlock->getTags()); - $queryParameters = $this->getQueryParameters($method, $methodDocBlock->getTags()); $content = ResponseResolver::getResponse($route, $methodDocBlock->getTags(), [ 'rules' => $rulesToApply, 'body' => $bodyParameters, @@ -91,14 +89,14 @@ public function processRoute(Route $route, array $rulesToApply = []) return $parsedRoute; } - protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route) + protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply) { $metadataStrategies = $this->config->get('strategies.metadata', []); $results = []; foreach ($metadataStrategies as $strategyClass) { $strategy = new $strategyClass($this->config); - $results = $strategy($route, $controller, $method); + $results = $strategy($route, $controller, $method, $rulesToApply); if (count($results)) { break; } @@ -106,263 +104,33 @@ protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $ return count($results) ? $results : []; } - protected function getBodyParameters(ReflectionMethod $method, array $tags) + protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply) { - foreach ($method->getParameters() as $param) { - $paramType = $param->getType(); - if ($paramType === null) { - continue; - } - - $parameterClassName = version_compare(phpversion(), '7.1.0', '<') - ? $paramType->__toString() - : $paramType->getName(); - - try { - $parameterClass = new ReflectionClass($parameterClassName); - } catch (\ReflectionException $e) { - continue; - } - - if (class_exists('\Illuminate\Foundation\Http\FormRequest') && $parameterClass->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class) || class_exists('\Dingo\Api\Http\FormRequest') && $parameterClass->isSubclassOf(\Dingo\Api\Http\FormRequest::class)) { - $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); - $bodyParametersFromDocBlock = $this->getBodyParametersFromDocBlock($formRequestDocBlock->getTags()); + $bodyParametersStrategies = $this->config->get('strategies.bodyParameters', []); + $results = []; - if (count($bodyParametersFromDocBlock)) { - return $bodyParametersFromDocBlock; - } + foreach ($bodyParametersStrategies as $strategyClass) { + $strategy = new $strategyClass($this->config); + $results = $strategy($route, $controller, $method, $rulesToApply); + if (count($results)) { + break; } } - - return $this->getBodyParametersFromDocBlock($tags); + return count($results) ? $results : []; } - /** - * @param array $tags - * - * @return array - */ - protected function getBodyParametersFromDocBlock(array $tags) + protected function fetchQueryParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply) { - $parameters = collect($tags) - ->filter(function ($tag) { - return $tag instanceof Tag && $tag->getName() === 'bodyParam'; - }) - ->mapWithKeys(function ($tag) { - preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); - $content = preg_replace('/\s?No-example.?/', '', $content); - if (empty($content)) { - // this means only name and type were supplied - list($name, $type) = preg_split('/\s+/', $tag->getContent()); - $required = false; - $description = ''; - } else { - list($_, $name, $type, $required, $description) = $content; - $description = trim($description); - if ($description == 'required' && empty(trim($required))) { - $required = $description; - $description = ''; - } - $required = trim($required) == 'required' ? true : false; - } - - $type = $this->normalizeParameterType($type); - list($description, $example) = $this->parseParamDescription($description, $type); - $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; - - return [$name => compact('type', 'description', 'required', 'value')]; - })->toArray(); - - return $parameters; - } - - /** - * @param ReflectionMethod $method - * @param array $tags - * - * @return array - */ - protected function getQueryParameters(ReflectionMethod $method, array $tags) - { - foreach ($method->getParameters() as $param) { - $paramType = $param->getType(); - if ($paramType === null) { - continue; - } - - $parameterClassName = version_compare(phpversion(), '7.1.0', '<') - ? $paramType->__toString() - : $paramType->getName(); - - try { - $parameterClass = new ReflectionClass($parameterClassName); - } catch (\ReflectionException $e) { - continue; - } - - if (class_exists('\Illuminate\Foundation\Http\FormRequest') && $parameterClass->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class) || class_exists('\Dingo\Api\Http\FormRequest') && $parameterClass->isSubclassOf(\Dingo\Api\Http\FormRequest::class)) { - $formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); - $queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags()); + $queryParametersStrategies = $this->config->get('strategies.queryParameters', []); + $results = []; - if (count($queryParametersFromDocBlock)) { - return $queryParametersFromDocBlock; - } + foreach ($queryParametersStrategies as $strategyClass) { + $strategy = new $strategyClass($this->config); + $results = $strategy($route, $controller, $method, $rulesToApply); + if (count($results)) { + break; } } - - return $this->getQueryParametersFromDocBlock($tags); - } - - /** - * @param array $tags - * - * @return array - */ - protected function getQueryParametersFromDocBlock(array $tags) - { - $parameters = collect($tags) - ->filter(function ($tag) { - return $tag instanceof Tag && $tag->getName() === 'queryParam'; - }) - ->mapWithKeys(function ($tag) { - preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); - $content = preg_replace('/\s?No-example.?/', '', $content); - if (empty($content)) { - // this means only name was supplied - list($name) = preg_split('/\s+/', $tag->getContent()); - $required = false; - $description = ''; - } else { - list($_, $name, $required, $description) = $content; - $description = trim($description); - if ($description == 'required' && empty(trim($required))) { - $required = $description; - $description = ''; - } - $required = trim($required) == 'required' ? true : false; - } - - list($description, $value) = $this->parseParamDescription($description, 'string'); - if (is_null($value) && ! $this->shouldExcludeExample($tag)) { - $value = str_contains($description, ['number', 'count', 'page']) - ? $this->generateDummyValue('integer') - : $this->generateDummyValue('string'); - } - - return [$name => compact('description', 'required', 'value')]; - })->toArray(); - - return $parameters; - } - - private function normalizeParameterType($type) - { - $typeMap = [ - 'int' => 'integer', - 'bool' => 'boolean', - 'double' => 'float', - ]; - - return $type ? ($typeMap[$type] ?? $type) : 'string'; - } - - private function generateDummyValue(string $type) - { - $faker = Factory::create(); - if ($this->config->get('faker_seed')) { - $faker->seed($this->config->get('faker_seed')); - } - $fakeFactories = [ - 'integer' => function () use ($faker) { - return $faker->numberBetween(1, 20); - }, - 'number' => function () use ($faker) { - return $faker->randomFloat(); - }, - 'float' => function () use ($faker) { - return $faker->randomFloat(); - }, - 'boolean' => function () use ($faker) { - return $faker->boolean(); - }, - 'string' => function () use ($faker) { - return $faker->word; - }, - 'array' => function () { - return []; - }, - 'object' => function () { - return new \stdClass; - }, - ]; - - $fakeFactory = $fakeFactories[$type] ?? $fakeFactories['string']; - - return $fakeFactory(); - } - - /** - * 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. - */ - private function parseParamDescription(string $description, string $type) - { - $example = null; - if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) { - $description = $content[1]; - - // examples are parsed as strings by default, we need to cast them properly - $example = $this->castToType($content[2], $type); - } - - return [$description, $example]; - } - - /** - * Allows users to specify that we shouldn't generate an example for the parameter - * by writing 'No-example'. - * - * @param Tag $tag - * - * @return bool Whether no example should be generated - */ - private function shouldExcludeExample(Tag $tag) - { - return strpos($tag->getContent(), ' No-example') !== false; - } - - /** - * Cast a value from a string to a specified type. - * - * @param string $value - * @param string $type - * - * @return mixed - */ - private function castToType(string $value, string $type) - { - $casts = [ - 'integer' => 'intval', - 'number' => 'floatval', - 'float' => 'floatval', - 'boolean' => 'boolval', - ]; - - // First, we handle booleans. We can't use a regular cast, - //because PHP considers string 'false' as true. - if ($value == 'false' && $type == 'boolean') { - return false; - } - - if (isset($casts[$type])) { - return $casts[$type]($value); - } - - return $value; + return count($results) ? $results : []; } } diff --git a/src/Tools/Traits/ParamHelpers.php b/src/Tools/Traits/ParamHelpers.php index 6f72be10..b168aa81 100644 --- a/src/Tools/Traits/ParamHelpers.php +++ b/src/Tools/Traits/ParamHelpers.php @@ -2,6 +2,9 @@ namespace Mpociot\ApiDoc\Tools\Traits; +use Faker\Factory; +use Mpociot\Reflection\DocBlock\Tag; + trait ParamHelpers { /** @@ -41,4 +44,115 @@ protected function cleanValueFrom($name, $value, array &$values = []) } array_set($values, str_replace('.*', '.0', $name), $value); } + + /** + * Allows users to specify that we shouldn't generate an example for the parameter + * by writing 'No-example'. + * + * @param Tag $tag + * + * @return bool Whether no example should be generated + */ + private function shouldExcludeExample(Tag $tag) + { + return strpos($tag->getContent(), ' No-example') !== false; + } + + private function generateDummyValue(string $type) + { + $faker = Factory::create(); + if ($this->config->get('faker_seed')) { + $faker->seed($this->config->get('faker_seed')); + } + $fakeFactories = [ + 'integer' => function () use ($faker) { + return $faker->numberBetween(1, 20); + }, + 'number' => function () use ($faker) { + return $faker->randomFloat(); + }, + 'float' => function () use ($faker) { + return $faker->randomFloat(); + }, + 'boolean' => function () use ($faker) { + return $faker->boolean(); + }, + 'string' => function () use ($faker) { + return $faker->word; + }, + 'array' => function () { + return []; + }, + 'object' => function () { + return new \stdClass; + }, + ]; + + $fakeFactory = $fakeFactories[$type] ?? $fakeFactories['string']; + + return $fakeFactory(); + } + + /** + * 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. + */ + private function parseParamDescription(string $description, string $type) + { + $example = null; + if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) { + $description = $content[1]; + + // examples are parsed as strings by default, we need to cast them properly + $example = $this->castToType($content[2], $type); + } + + return [$description, $example]; + } + + /** + * Cast a value from a string to a specified type. + * + * @param string $value + * @param string $type + * + * @return mixed + */ + private function castToType(string $value, string $type) + { + $casts = [ + 'integer' => 'intval', + 'number' => 'floatval', + 'float' => 'floatval', + 'boolean' => 'boolval', + ]; + + // First, we handle booleans. We can't use a regular cast, + //because PHP considers string 'false' as true. + if ($value == 'false' && $type == 'boolean') { + return false; + } + + if (isset($casts[$type])) { + return $casts[$type]($value); + } + + return $value; + } + + private function normalizeParameterType(string $type) + { + $typeMap = [ + 'int' => 'integer', + 'bool' => 'boolean', + 'double' => 'float', + ]; + + return $type ? ($typeMap[$type] ?? $type) : 'string'; + } } diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 4a44090b..e19f6d99 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -13,6 +13,24 @@ abstract class GeneratorTestCase extends TestCase * @var \Mpociot\ApiDoc\Tools\Generator */ protected $generator; + private $config = [ + 'strategies' => [ + 'metadata' => [ + \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, + ], + 'bodyParameters' => [ + \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromDocBlocks::class, + ], + 'queryParameters' => [ + \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromDocBlocks::class, + ], + 'responses' => [ + + ], + ], + 'default_group' => 'general', + + ]; protected function getPackageProviders($app) { @@ -28,7 +46,7 @@ public function setUp() { parent::setUp(); - $this->generator = new Generator(); + $this->generator = new Generator(new DocumentationConfig($this->config)); } /** @test */ @@ -430,7 +448,7 @@ public function can_parse_transformer_collection_tag() $this->assertEquals(200, $response['status']); $this->assertSame( $response['content'], - '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},'. + '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},' . '{"id":1,"description":"Welcome on this test versions","name":"TestName"}]}' ); } @@ -449,7 +467,7 @@ public function can_parse_transformer_collection_tag_with_model() $this->assertEquals(200, $response['status']); $this->assertSame( $response['content'], - '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},'. + '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},' . '{"id":1,"description":"Welcome on this test versions","name":"TestName"}]}' ); } @@ -587,7 +605,7 @@ public function uses_correct_bindings_by_prefix() public function can_parse_response_file_tag() { // copy file to storage - $filePath = __DIR__.'/../Fixtures/response_test.json'; + $filePath = __DIR__ . '/../Fixtures/response_test.json'; $fixtureFileJson = file_get_contents($filePath); copy($filePath, storage_path('response_test.json')); @@ -612,7 +630,7 @@ public function can_parse_response_file_tag() public function can_add_or_replace_key_value_pair_in_response_file() { // copy file to storage - $filePath = __DIR__.'/../Fixtures/response_test.json'; + $filePath = __DIR__ . '/../Fixtures/response_test.json'; $fixtureFileJson = file_get_contents($filePath); copy($filePath, storage_path('response_test.json')); @@ -637,10 +655,10 @@ public function can_add_or_replace_key_value_pair_in_response_file() public function can_parse_multiple_response_file_tags_with_status_codes() { // copy file to storage - $successFilePath = __DIR__.'/../Fixtures/response_test.json'; + $successFilePath = __DIR__ . '/../Fixtures/response_test.json'; $successFixtureFileJson = file_get_contents($successFilePath); copy($successFilePath, storage_path('response_test.json')); - $errorFilePath = __DIR__.'/../Fixtures/response_error_test.json'; + $errorFilePath = __DIR__ . '/../Fixtures/response_error_test.json'; $errorFixtureFileJson = file_get_contents($errorFilePath); copy($errorFilePath, storage_path('response_error_test.json')); @@ -682,7 +700,7 @@ public function generates_consistent_examples_when_faker_seed_is_set() // Examples should have different values $this->assertNotEquals(count($results), 1); - $generator = new Generator(new DocumentationConfig(['faker_seed' => 12345])); + $generator = new Generator(new DocumentationConfig($this->config + ['faker_seed' => 12345])); $results = []; $results[$generator->processRoute($route)['cleanBodyParameters'][$paramName]] = true; $results[$generator->processRoute($route)['cleanBodyParameters'][$paramName]] = true; From c0ecc1b7f72b9c73d18d0285c8b5efbe19a35f9b Mon Sep 17 00:00:00 2001 From: shalvah Date: Mon, 2 Sep 2019 01:02:14 +0100 Subject: [PATCH 083/312] Add base Strategy class --- .../BodyParameters/GetFromDocBlocks.php | 22 ++------------ src/Strategies/Metadata/GetFromDocBlocks.php | 23 ++------------ .../QueryParameters/GetFromDocBlocks.php | 22 ++------------ src/Strategies/Strategy.php | 30 +++++++++++++++++++ 4 files changed, 39 insertions(+), 58 deletions(-) create mode 100644 src/Strategies/Strategy.php diff --git a/src/Strategies/BodyParameters/GetFromDocBlocks.php b/src/Strategies/BodyParameters/GetFromDocBlocks.php index 7758e75d..a6c37795 100644 --- a/src/Strategies/BodyParameters/GetFromDocBlocks.php +++ b/src/Strategies/BodyParameters/GetFromDocBlocks.php @@ -7,34 +7,18 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; +use Mpociot\ApiDoc\Strategies\Strategy; use Mpociot\ApiDoc\Tools\RouteDocBlocker; -use Mpociot\ApiDoc\Tools\DocumentationConfig; use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; use Dingo\Api\Http\FormRequest as DingoFormRequest; use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; -class GetFromDocBlocks +class GetFromDocBlocks extends Strategy { use ParamHelpers; - public $config; - - public function __construct(DocumentationConfig $config) - { - $this->config = $config; - } - - /** - * @param Route $route - * @param ReflectionClass $controller - * @param ReflectionMethod $method - * @param array $routeConfig Array of rules for the ruleset which this route belongs to. - * - * @return array - * @throws \Exception - */ - public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeConfig) + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) { foreach ($method->getParameters() as $param) { $paramType = $param->getType(); diff --git a/src/Strategies/Metadata/GetFromDocBlocks.php b/src/Strategies/Metadata/GetFromDocBlocks.php index 3ccd64fe..da92676a 100644 --- a/src/Strategies/Metadata/GetFromDocBlocks.php +++ b/src/Strategies/Metadata/GetFromDocBlocks.php @@ -7,29 +7,12 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Tools\DocumentationConfig; +use Mpociot\ApiDoc\Strategies\Strategy; use Mpociot\ApiDoc\Tools\RouteDocBlocker; -class GetFromDocBlocks +class GetFromDocBlocks extends Strategy { - - public $config; - - public function __construct(DocumentationConfig $config) - { - $this->config = $config; - } - - /** - * @param Route $route - * @param ReflectionClass $controller - * @param ReflectionMethod $method - * @param array $routeConfig Array of rules for the ruleset which this route belongs to. - * - * @return array - * @throws \Exception - */ - public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeConfig) + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) { $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); /** @var DocBlock $methodDocBlock */ diff --git a/src/Strategies/QueryParameters/GetFromDocBlocks.php b/src/Strategies/QueryParameters/GetFromDocBlocks.php index 81340273..d770dbd7 100644 --- a/src/Strategies/QueryParameters/GetFromDocBlocks.php +++ b/src/Strategies/QueryParameters/GetFromDocBlocks.php @@ -7,34 +7,18 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; +use Mpociot\ApiDoc\Strategies\Strategy; use Mpociot\ApiDoc\Tools\RouteDocBlocker; -use Mpociot\ApiDoc\Tools\DocumentationConfig; use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; use Dingo\Api\Http\FormRequest as DingoFormRequest; use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; -class GetFromDocBlocks +class GetFromDocBlocks extends Strategy { use ParamHelpers; - public $config; - - public function __construct(DocumentationConfig $config) - { - $this->config = $config; - } - - /** - * @param Route $route - * @param ReflectionClass $controller - * @param ReflectionMethod $method - * @param array $routeConfig Array of rules for the ruleset which this route belongs to. - * - * @return array - * @throws \Exception - */ - public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeConfig) + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) { foreach ($method->getParameters() as $param) { $paramType = $param->getType(); diff --git a/src/Strategies/Strategy.php b/src/Strategies/Strategy.php new file mode 100644 index 00000000..3e59fd40 --- /dev/null +++ b/src/Strategies/Strategy.php @@ -0,0 +1,30 @@ +config = $config; + } + + /** + * @param Route $route + * @param ReflectionClass $controller + * @param ReflectionMethod $method + * @param array $routeRules Array of rules for the ruleset which this route belongs to. + * @param array $context Results from the previous stages + * + * @return array + * @throws \Exception + */ + abstract public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []); +} From d209dff999d5bad526f8f0ce4a0dfddf7a0c2a72 Mon Sep 17 00:00:00 2001 From: shalvah Date: Mon, 2 Sep 2019 17:03:42 +0100 Subject: [PATCH 084/312] Move Responses to strategies --- config/apidoc.php | 4 + .../Responses/ResponseCalls.php} | 25 ++- .../Responses/UseResponseFileTag.php} | 21 +- .../Responses/UseResponseTag.php} | 21 +- .../Responses/UseTransformerTags.php} | 21 +- src/Tools/Generator.php | 102 ++++----- src/Tools/ResponseResolver.php | 80 ------- tests/Fixtures/TestController.php | 11 +- tests/Fixtures/TestResourceController.php | 4 +- tests/Fixtures/collection.json | 209 +++++++++++++++++- .../collection_with_body_parameters.json | 4 +- tests/Fixtures/index.md | 61 ++++- tests/GenerateDocumentationTest.php | 20 +- tests/Unit/GeneratorTestCase.php | 5 +- 14 files changed, 405 insertions(+), 183 deletions(-) rename src/{Tools/ResponseStrategies/ResponseCallStrategy.php => Strategies/Responses/ResponseCalls.php} (92%) rename src/{Tools/ResponseStrategies/ResponseFileStrategy.php => Strategies/Responses/UseResponseFileTag.php} (66%) rename src/{Tools/ResponseStrategies/ResponseTagStrategy.php => Strategies/Responses/UseResponseTag.php} (59%) rename src/{Tools/ResponseStrategies/TransformerTagsStrategy.php => Strategies/Responses/UseTransformerTags.php} (85%) delete mode 100644 src/Tools/ResponseResolver.php diff --git a/config/apidoc.php b/config/apidoc.php index bb2dbd17..ac8595da 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -190,6 +190,10 @@ \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromDocBlocks::class, ], 'responses' => [ + \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, + \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class, + \Mpociot\ApiDoc\Strategies\Responses\UseTransformerTags::class, + \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class, ], ], diff --git a/src/Tools/ResponseStrategies/ResponseCallStrategy.php b/src/Strategies/Responses/ResponseCalls.php similarity index 92% rename from src/Tools/ResponseStrategies/ResponseCallStrategy.php rename to src/Strategies/Responses/ResponseCalls.php index bf232615..8fc0d106 100644 --- a/src/Tools/ResponseStrategies/ResponseCallStrategy.php +++ b/src/Strategies/Responses/ResponseCalls.php @@ -1,6 +1,6 @@ shouldMakeApiCall($route, $rulesToApply)) { return null; } $this->configureEnvironment($rulesToApply); - $request = $this->prepareRequest($route, $rulesToApply, $routeProps['body'], $routeProps['query']); + + // Mix in parsed parameters with manually specified parameters. + $bodyParameters = array_merge($context['cleanBodyParameters'], $rulesToApply['body'] ?? []); + $queryParameters = array_merge($context['cleanQueryParameters'], $rulesToApply['query'] ?? []); + $request = $this->prepareRequest($route, $rulesToApply, $bodyParameters, $queryParameters); try { $response = [$this->makeApiCall($request)]; @@ -80,10 +87,6 @@ protected function prepareRequest(Route $route, array $rulesToApply, array $body $request = Request::create($uri, $method, [], $cookies, [], $this->transformHeadersToServerVars($rulesToApply['headers'] ?? [])); $request = $this->addHeaders($request, $route, $rulesToApply['headers'] ?? []); - // Mix in parsed parameters with manually specified parameters. - $queryParams = collect($this->cleanParams($queryParams))->merge($rulesToApply['query'] ?? [])->toArray(); - $bodyParams = collect($this->cleanParams($bodyParams))->merge($rulesToApply['body'] ?? [])->toArray(); - $request = $this->addQueryParameters($request, $queryParams); $request = $this->addBodyParameters($request, $bodyParams); diff --git a/src/Tools/ResponseStrategies/ResponseFileStrategy.php b/src/Strategies/Responses/UseResponseFileTag.php similarity index 66% rename from src/Tools/ResponseStrategies/ResponseFileStrategy.php rename to src/Strategies/Responses/UseResponseFileTag.php index 1b780559..0db2a9d4 100644 --- a/src/Tools/ResponseStrategies/ResponseFileStrategy.php +++ b/src/Strategies/Responses/UseResponseFileTag.php @@ -1,26 +1,35 @@ getFileResponses($tags); + $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); + /** @var DocBlock $methodDocBlock */ + $methodDocBlock = $docBlocks['method']; + return $this->getFileResponses($methodDocBlock->getTags()); } /** diff --git a/src/Tools/ResponseStrategies/ResponseTagStrategy.php b/src/Strategies/Responses/UseResponseTag.php similarity index 59% rename from src/Tools/ResponseStrategies/ResponseTagStrategy.php rename to src/Strategies/Responses/UseResponseTag.php index 9cd2ff09..73355ce6 100644 --- a/src/Tools/ResponseStrategies/ResponseTagStrategy.php +++ b/src/Strategies/Responses/UseResponseTag.php @@ -1,26 +1,35 @@ getDocBlockResponses($tags); + $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); + /** @var DocBlock $methodDocBlock */ + $methodDocBlock = $docBlocks['method']; + return $this->getDocBlockResponses($methodDocBlock->getTags()); } /** diff --git a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php b/src/Strategies/Responses/UseTransformerTags.php similarity index 85% rename from src/Tools/ResponseStrategies/TransformerTagsStrategy.php rename to src/Strategies/Responses/UseTransformerTags.php index 62ab3e9a..f9a48a3e 100644 --- a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php +++ b/src/Strategies/Responses/UseTransformerTags.php @@ -1,31 +1,40 @@ getTransformerResponse($tags); + $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); + /** @var DocBlock $methodDocBlock */ + $methodDocBlock = $docBlocks['method']; + return $this->getTransformerResponse($methodDocBlock->getTags()); } /** diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 4850bfbf..8b8738f3 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -7,8 +7,8 @@ use ReflectionMethod; use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; -use Mpociot\Reflection\DocBlock\Tag; use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; +use Symfony\Component\HttpFoundation\Response; class Generator { @@ -57,80 +57,72 @@ public function processRoute(Route $route, array $rulesToApply = []) $controller = new ReflectionClass($controllerName); $method = $controller->getMethod($methodName); - $metadata = $this->fetchMetadata($controller, $method, $route, $rulesToApply); - $bodyParameters = $this->fetchBodyParameters($controller, $method, $route, $rulesToApply); - $queryParameters = $this->fetchQueryParameters($controller, $method, $route, $rulesToApply); - // $this->fetchResponse(); - - $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); - /** @var DocBlock $methodDocBlock */ - $methodDocBlock = $docBlocks['method']; - $content = ResponseResolver::getResponse($route, $methodDocBlock->getTags(), [ - 'rules' => $rulesToApply, - 'body' => $bodyParameters, - 'query' => $queryParameters, - ]); - $parsedRoute = [ 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), 'methods' => $this->getMethods($route), 'uri' => $this->getUri($route), 'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])), - 'queryParameters' => $queryParameters, - 'bodyParameters' => $bodyParameters, - 'cleanBodyParameters' => $this->cleanParams($bodyParameters), - 'cleanQueryParameters' => $this->cleanParams($queryParameters), - 'response' => $content, - 'showresponse' => ! empty($content), ]; - $parsedRoute['headers'] = $rulesToApply['headers'] ?? []; + $metadata = $this->fetchMetadata($controller, $method, $route, $rulesToApply); $parsedRoute += $metadata; + $bodyParameters = $this->fetchBodyParameters($controller, $method, $route, $rulesToApply, $parsedRoute); + $parsedRoute['bodyParameters'] = $bodyParameters; + $parsedRoute['cleanBodyParameters'] = $this->cleanParams($bodyParameters); + + $queryParameters = $this->fetchQueryParameters($controller, $method, $route, $rulesToApply, $parsedRoute); + $parsedRoute['queryParameters'] = $queryParameters; + $parsedRoute['cleanQueryParameters'] = $this->cleanParams($queryParameters); + + $responses = $this->fetchResponses($controller, $method, $route, $rulesToApply, $parsedRoute); + $parsedRoute['response'] = $responses; + $parsedRoute['showresponse'] = ! empty($responses); + + $parsedRoute['headers'] = $rulesToApply['headers'] ?? []; return $parsedRoute; } protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply) { - $metadataStrategies = $this->config->get('strategies.metadata', []); - $results = []; - - foreach ($metadataStrategies as $strategyClass) { - $strategy = new $strategyClass($this->config); - $results = $strategy($route, $controller, $method, $rulesToApply); - if (count($results)) { - break; - } - } - return count($results) ? $results : []; + return $this->iterateThroughStrategies('metadata', [$route, $controller, $method, $rulesToApply]); + } + + protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) + { + return $this->iterateThroughStrategies('bodyParameters', [$route, $controller, $method, $rulesToApply]); } - protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply) + protected function fetchQueryParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) { - $bodyParametersStrategies = $this->config->get('strategies.bodyParameters', []); - $results = []; - - foreach ($bodyParametersStrategies as $strategyClass) { - $strategy = new $strategyClass($this->config); - $results = $strategy($route, $controller, $method, $rulesToApply); - if (count($results)) { - break; - } + return $this->iterateThroughStrategies('queryParameters', [$route, $controller, $method, $rulesToApply]); + } + + protected function fetchResponses(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) + { + $responses = $this->iterateThroughStrategies('responses', [$route, $controller, $method, $rulesToApply, $context]); + if (count($responses)) { + return array_map(function (Response $response) { + return [ + 'status' => $response->getStatusCode(), + 'content' => $response->getContent() + ]; + }, $responses); } - return count($results) ? $results : []; + return null; } - protected function fetchQueryParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply) + protected function iterateThroughStrategies(string $key, array $arguments) { - $queryParametersStrategies = $this->config->get('strategies.queryParameters', []); - $results = []; - - foreach ($queryParametersStrategies as $strategyClass) { - $strategy = new $strategyClass($this->config); - $results = $strategy($route, $controller, $method, $rulesToApply); - if (count($results)) { - break; - } + $strategies = $this->config->get("strategies.$key", []); + $results = []; + + foreach ($strategies as $strategyClass) { + $strategy = new $strategyClass($this->config); + $results = $strategy(...$arguments); + if (! is_null($results)) { + break; } - return count($results) ? $results : []; } + return is_null($results) ? [] : $results; +} } diff --git a/src/Tools/ResponseResolver.php b/src/Tools/ResponseResolver.php deleted file mode 100644 index 3d37140b..00000000 --- a/src/Tools/ResponseResolver.php +++ /dev/null @@ -1,80 +0,0 @@ -route = $route; - } - - /** - * @param array $tags - * @param array $routeProps - * - * @return array|null - */ - private function resolve(array $tags, array $routeProps) - { - foreach (static::$strategies as $strategy) { - $strategy = new $strategy(); - - /** @var Response[]|null $response */ - $responses = $strategy($this->route, $tags, $routeProps); - - if (! is_null($responses)) { - return array_map(function (Response $response) { - return ['status' => $response->getStatusCode(), 'content' => $this->getResponseContent($response)]; - }, $responses); - } - } - } - - /** - * @param Route $route - * @param array $tags - * @param array $routeProps - * - * @return array - */ - public static function getResponse(Route $route, array $tags, array $routeProps) - { - return (new static($route))->resolve($tags, $routeProps); - } - - /** - * @param Response $response - * - * @return string - */ - private function getResponseContent(Response $response) - { - return $response->getContent() ?: ''; - } -} diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index 1bf0c10f..c763e2f4 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -30,7 +30,7 @@ public function withEndpointDescription() */ public function withGroupOverride() { - return ''; + return "Group B, baby!"; } /** @@ -66,6 +66,8 @@ public function withGroupOverride4() } /** + * Endpoint with body parameters + * * @bodyParam user_id int required The id of the user. Example: 9 * @bodyParam room_id string The id of the room. * @bodyParam forever boolean Whether to ban the user forever. Example: false @@ -152,6 +154,7 @@ public function shouldFetchRouteResponse() 'color' => strtolower($fruit->color), 'weight' => $fruit->weight.' kg', 'delicious' => $fruit->delicious, + 'responseCall' => true, ]; } @@ -203,7 +206,8 @@ public function skip() * "name": "banana", * "color": "red", * "weight": "1 kg", - * "delicious": true + * "delicious": true, + * "responseTag": true * } */ public function withResponseTag() @@ -227,7 +231,8 @@ public function withResponseTagAndStatusCode() * "name": "banana", * "color": "red", * "weight": "1 kg", - * "delicious": true + * "delicious": true, + * "multipleResponseTagsAndStatusCodes": true * } * @response 401 { * "message": "Unauthorized" diff --git a/tests/Fixtures/TestResourceController.php b/tests/Fixtures/TestResourceController.php index 175fccdf..3c5a7caf 100644 --- a/tests/Fixtures/TestResourceController.php +++ b/tests/Fixtures/TestResourceController.php @@ -64,7 +64,7 @@ public function store(Request $request) public function show($id) { return [ - 'show_resource' => $id, + 'show_resource' => true, ]; } @@ -82,7 +82,7 @@ public function show($id) public function edit($id) { return [ - 'edit_resource' => $id, + 'edit_resource' => true, ]; } diff --git a/tests/Fixtures/collection.json b/tests/Fixtures/collection.json index 03168bcc..9fc17ec7 100644 --- a/tests/Fixtures/collection.json +++ b/tests/Fixtures/collection.json @@ -14,12 +14,20 @@ { "name": "Example title.", "request": { - "url": "http:\/\/localhost\/api\/test", + "url": "http:\/\/localhost\/api\/withDescription", "method": "GET", "header": [ + { + "key": "Authorization", + "value": "customAuthToken" + }, + { + "key": "Custom-Header", + "value": "NotSoCustom" + }, { "key": "Accept", - "value": "application/json" + "value": "application\/json" } ], "body": { @@ -31,14 +39,203 @@ } }, { - "name": "http:\/\/localhost\/api\/responseTag", + "name": "http:\/\/localhost\/api\/withResponseTag", + "request": { + "url": "http:\/\/localhost\/api\/withResponseTag", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "customAuthToken" + }, + { + "key": "Custom-Header", + "value": "NotSoCustom" + }, + { + "key": "Accept", + "value": "application\/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "", + "response": [] + } + }, + { + "name": "Endpoint with body parameters", "request": { - "url": "http:\/\/localhost\/api\/responseTag", + "url": "http:\/\/localhost\/api\/withBodyParameters", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "customAuthToken" + }, + { + "key": "Custom-Header", + "value": "NotSoCustom" + }, + { + "key": "Accept", + "value": "application\/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "user_id", + "value": 9, + "type": "text", + "enabled": true + }, + { + "key": "room_id", + "value": "consequatur", + "type": "text", + "enabled": true + }, + { + "key": "forever", + "value": false, + "type": "text", + "enabled": true + }, + { + "key": "another_one", + "value": 11613.31890586, + "type": "text", + "enabled": true + }, + { + "key": "yet_another_param", + "value": {}, + "type": "text", + "enabled": true + }, + { + "key": "even_more_param", + "value": [], + "type": "text", + "enabled": true + }, + { + "key": "book.name", + "value": "consequatur", + "type": "text", + "enabled": true + }, + { + "key": "book.author_id", + "value": 17, + "type": "text", + "enabled": true + }, + { + "key": "book[pages_count]", + "value": 17, + "type": "text", + "enabled": true + }, + { + "key": "ids.*", + "value": 17, + "type": "text", + "enabled": true + }, + { + "key": "users.*.first_name", + "value": "John", + "type": "text", + "enabled": true + }, + { + "key": "users.*.last_name", + "value": "Doe", + "type": "text", + "enabled": true + } + ] + }, + "description": "", + "response": [] + } + }, + { + "name": "http:\/\/localhost\/api\/withQueryParameters", + "request": { + "url": "http:\/\/localhost\/api\/withQueryParameters?location_id=consequatur&user_id=me&page=4&filters=consequatur&url_encoded=%2B+%5B%5D%26%3D", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "customAuthToken" + }, + { + "key": "Custom-Header", + "value": "NotSoCustom" + }, + { + "key": "Accept", + "value": "application\/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "", + "response": [] + } + }, + { + "name": "http:\/\/localhost\/api\/withAuthTag", + "request": { + "url": "http:\/\/localhost\/api\/withAuthTag", + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "customAuthToken" + }, + { + "key": "Custom-Header", + "value": "NotSoCustom" + }, + { + "key": "Accept", + "value": "application\/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "", + "response": [] + } + }, + { + "name": "http:\/\/localhost\/api\/withMultipleResponseTagsAndStatusCode", + "request": { + "url": "http:\/\/localhost\/api\/withMultipleResponseTagsAndStatusCode", "method": "POST", "header": [ + { + "key": "Authorization", + "value": "customAuthToken" + }, + { + "key": "Custom-Header", + "value": "NotSoCustom" + }, { "key": "Accept", - "value": "application/json" + "value": "application\/json" } ], "body": { @@ -52,4 +249,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/tests/Fixtures/collection_with_body_parameters.json b/tests/Fixtures/collection_with_body_parameters.json index 438f2f88..13e8f4d1 100644 --- a/tests/Fixtures/collection_with_body_parameters.json +++ b/tests/Fixtures/collection_with_body_parameters.json @@ -12,7 +12,7 @@ "description": "", "item": [ { - "name": "http://localhost/api/withBodyParameters", + "name": "Endpoint with body parameters", "request": { "url": "http:\/\/localhost\/api\/withBodyParameters", "method": "GET", @@ -106,4 +106,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/tests/Fixtures/index.md b/tests/Fixtures/index.md index f9bca545..f933cccf 100644 --- a/tests/Fixtures/index.md +++ b/tests/Fixtures/index.md @@ -105,7 +105,8 @@ fetch(url, { "name": "banana", "color": "red", "weight": "1 kg", - "delicious": true + "delicious": true, + "responseTag": true } ``` @@ -116,7 +117,8 @@ fetch(url, { -## api/withBodyParameters +## Endpoint with body parameters + > Example request: ```bash @@ -300,4 +302,59 @@ null + +## api/withMultipleResponseTagsAndStatusCode +> Example request: + +```bash +curl -X POST "http://localhost/api/withMultipleResponseTagsAndStatusCode" \ + -H "Authorization: customAuthToken" \ + -H "Custom-Header: NotSoCustom" +``` + +```javascript +const url = new URL("http://localhost/api/withMultipleResponseTagsAndStatusCode"); + +let headers = { + "Authorization": "customAuthToken", + "Custom-Header": "NotSoCustom", + "Accept": "application/json", + "Content-Type": "application/json", +} + +fetch(url, { + method: "POST", + headers: headers, +}) + .then(response => response.json()) + .then(json => console.log(json)); +``` + + +> Example response (200): + +```json +{ + "id": 4, + "name": "banana", + "color": "red", + "weight": "1 kg", + "delicious": true, + "multipleResponseTagsAndStatusCodes": true +} +``` +> Example response (401): + +```json +{ + "message": "Unauthorized" +} +``` + +### HTTP Request +`POST api/withMultipleResponseTagsAndStatusCode` + + + + diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 04793d6c..123a3c48 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -29,7 +29,7 @@ public function setUp() public function tearDown() { - Utils::deleteDirectoryAndContents('/public/docs'); + Utils::deleteDirectoryAndContents('/public/docs'); } /** @@ -176,6 +176,7 @@ public function generated_markdown_file_is_correct() RouteFacade::get('/api/withBodyParameters', TestController::class.'@withBodyParameters'); RouteFacade::get('/api/withQueryParameters', TestController::class.'@withQueryParameters'); RouteFacade::get('/api/withAuthTag', TestController::class.'@withAuthenticatedTag'); + RouteFacade::post('/api/withMultipleResponseTagsAndStatusCode', [TestController::class, 'withMultipleResponseTagsAndStatusCode']); // We want to have the same values for params each time config(['apidoc.faker_seed' => 1234]); @@ -220,10 +221,23 @@ public function can_prepend_and_append_data_to_generated_markdown() /** @test */ public function generated_postman_collection_file_is_correct() { - RouteFacade::get('/api/test', TestController::class.'@withEndpointDescription'); - RouteFacade::post('/api/responseTag', TestController::class.'@withResponseTag'); + RouteFacade::get('/api/withDescription', TestController::class.'@withEndpointDescription'); + RouteFacade::get('/api/withResponseTag', TestController::class.'@withResponseTag'); + RouteFacade::get('/api/withBodyParameters', TestController::class.'@withBodyParameters'); + RouteFacade::get('/api/withQueryParameters', TestController::class.'@withQueryParameters'); + RouteFacade::get('/api/withAuthTag', TestController::class.'@withAuthenticatedTag'); + RouteFacade::post('/api/withMultipleResponseTagsAndStatusCode', [TestController::class, 'withMultipleResponseTagsAndStatusCode']); + // We want to have the same values for params each time + config(['apidoc.faker_seed' => 1234]); config(['apidoc.routes.0.match.prefixes' => ['api/*']]); + config([ + 'apidoc.routes.0.apply.headers' => [ + 'Authorization' => 'customAuthToken', + 'Custom-Header' => 'NotSoCustom', + ], + ]); + $this->artisan('apidoc:generate'); $generatedCollection = json_decode(file_get_contents(__DIR__.'/../public/docs/collection.json'), true); diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index e19f6d99..d17cc6de 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -25,7 +25,10 @@ abstract class GeneratorTestCase extends TestCase \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromDocBlocks::class, ], 'responses' => [ - + \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, + \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class, + \Mpociot\ApiDoc\Strategies\Responses\UseTransformerTags::class, + \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class, ], ], 'default_group' => 'general', From 1da32f9cbac0810cd37e28bb0032de6af4502fa7 Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Mon, 2 Sep 2019 16:20:32 +0000 Subject: [PATCH 085/312] Apply fixes from StyleCI --- .../BodyParameters/GetFromDocBlocks.php | 2 +- src/Strategies/Metadata/GetFromDocBlocks.php | 15 +++++------ .../QueryParameters/GetFromDocBlocks.php | 2 +- .../Responses/UseResponseFileTag.php | 4 ++- src/Strategies/Responses/UseResponseTag.php | 4 ++- .../Responses/UseTransformerTags.php | 6 +++-- src/Strategies/Strategy.php | 3 ++- src/Tools/Generator.php | 26 +++++++++---------- src/Tools/RouteDocBlocker.php | 5 ++-- tests/Fixtures/TestController.php | 4 +-- tests/GenerateDocumentationTest.php | 2 +- tests/Unit/GeneratorTestCase.php | 12 ++++----- 12 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/Strategies/BodyParameters/GetFromDocBlocks.php b/src/Strategies/BodyParameters/GetFromDocBlocks.php index a6c37795..720ffb54 100644 --- a/src/Strategies/BodyParameters/GetFromDocBlocks.php +++ b/src/Strategies/BodyParameters/GetFromDocBlocks.php @@ -15,7 +15,6 @@ class GetFromDocBlocks extends Strategy { - use ParamHelpers; public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) @@ -49,6 +48,7 @@ public function __invoke(Route $route, ReflectionClass $controller, ReflectionMe } $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method']; + return $this->getBodyParametersFromDocBlock($methodDocBlock->getTags()); } diff --git a/src/Strategies/Metadata/GetFromDocBlocks.php b/src/Strategies/Metadata/GetFromDocBlocks.php index da92676a..dfa83bcd 100644 --- a/src/Strategies/Metadata/GetFromDocBlocks.php +++ b/src/Strategies/Metadata/GetFromDocBlocks.php @@ -46,7 +46,6 @@ protected function getAuthStatusFromDocBlock(array $tags) /** * @param DocBlock $methodDocBlock - * * @param DocBlock $controllerDocBlock * * @return array The route group name, the group description, ad the route title @@ -86,15 +85,15 @@ protected function getRouteGroupDescriptionAndTitle(DocBlock $methodDocBlock, Do } } - foreach ($controllerDocBlock->getTags() as $tag) { - if ($tag->getName() === 'group') { - $routeGroupParts = explode("\n", trim($tag->getContent())); - $routeGroupName = array_shift($routeGroupParts); - $routeGroupDescription = implode("\n", $routeGroupParts); + foreach ($controllerDocBlock->getTags() as $tag) { + if ($tag->getName() === 'group') { + $routeGroupParts = explode("\n", trim($tag->getContent())); + $routeGroupName = array_shift($routeGroupParts); + $routeGroupDescription = implode("\n", $routeGroupParts); - return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()]; - } + return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()]; } + } return [$this->config->get('default_group'), '', $methodDocBlock->getShortDescription()]; } diff --git a/src/Strategies/QueryParameters/GetFromDocBlocks.php b/src/Strategies/QueryParameters/GetFromDocBlocks.php index d770dbd7..9b54596e 100644 --- a/src/Strategies/QueryParameters/GetFromDocBlocks.php +++ b/src/Strategies/QueryParameters/GetFromDocBlocks.php @@ -15,7 +15,6 @@ class GetFromDocBlocks extends Strategy { - use ParamHelpers; public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) @@ -49,6 +48,7 @@ public function __invoke(Route $route, ReflectionClass $controller, ReflectionMe } $methodDocBlock = RouteDocBlocker::getDocBlocksFromRoute($route)['method']; + return $this->getqueryParametersFromDocBlock($methodDocBlock->getTags()); } diff --git a/src/Strategies/Responses/UseResponseFileTag.php b/src/Strategies/Responses/UseResponseFileTag.php index 0db2a9d4..19265fc7 100644 --- a/src/Strategies/Responses/UseResponseFileTag.php +++ b/src/Strategies/Responses/UseResponseFileTag.php @@ -21,14 +21,16 @@ class UseResponseFileTag extends Strategy * @param array $routeRules * @param array $context * - * @return array|null * @throws \Exception + * + * @return array|null */ public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $routeRules, array $context = []) { $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); /** @var DocBlock $methodDocBlock */ $methodDocBlock = $docBlocks['method']; + return $this->getFileResponses($methodDocBlock->getTags()); } diff --git a/src/Strategies/Responses/UseResponseTag.php b/src/Strategies/Responses/UseResponseTag.php index 73355ce6..dd4cb13e 100644 --- a/src/Strategies/Responses/UseResponseTag.php +++ b/src/Strategies/Responses/UseResponseTag.php @@ -21,14 +21,16 @@ class UseResponseTag extends Strategy * @param array $routeRules * @param array $context * - * @return array|null * @throws \Exception + * + * @return array|null */ public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $routeRules, array $context = []) { $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); /** @var DocBlock $methodDocBlock */ $methodDocBlock = $docBlocks['method']; + return $this->getDocBlockResponses($methodDocBlock->getTags()); } diff --git a/src/Strategies/Responses/UseTransformerTags.php b/src/Strategies/Responses/UseTransformerTags.php index f9a48a3e..c3d4bdd8 100644 --- a/src/Strategies/Responses/UseTransformerTags.php +++ b/src/Strategies/Responses/UseTransformerTags.php @@ -10,8 +10,8 @@ use Mpociot\Reflection\DocBlock; use League\Fractal\Resource\Item; use Mpociot\Reflection\DocBlock\Tag; -use Mpociot\ApiDoc\Strategies\Strategy; use League\Fractal\Resource\Collection; +use Mpociot\ApiDoc\Strategies\Strategy; use Mpociot\ApiDoc\Tools\RouteDocBlocker; /** @@ -26,14 +26,16 @@ class UseTransformerTags extends Strategy * @param array $rulesToApply * @param array $context * - * @return array|null * @throws \Exception + * + * @return array|null */ public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $rulesToApply, array $context = []) { $docBlocks = RouteDocBlocker::getDocBlocksFromRoute($route); /** @var DocBlock $methodDocBlock */ $methodDocBlock = $docBlocks['method']; + return $this->getTransformerResponse($methodDocBlock->getTags()); } diff --git a/src/Strategies/Strategy.php b/src/Strategies/Strategy.php index 3e59fd40..0d904688 100644 --- a/src/Strategies/Strategy.php +++ b/src/Strategies/Strategy.php @@ -23,8 +23,9 @@ public function __construct(DocumentationConfig $config) * @param array $routeRules Array of rules for the ruleset which this route belongs to. * @param array $context Results from the previous stages * - * @return array * @throws \Exception + * + * @return array */ abstract public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []); } diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 8b8738f3..566c1229 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -2,11 +2,9 @@ namespace Mpociot\ApiDoc\Tools; -use Faker\Factory; use ReflectionClass; use ReflectionMethod; use Illuminate\Routing\Route; -use Mpociot\Reflection\DocBlock; use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; use Symfony\Component\HttpFoundation\Response; @@ -104,25 +102,27 @@ protected function fetchResponses(ReflectionClass $controller, ReflectionMethod return array_map(function (Response $response) { return [ 'status' => $response->getStatusCode(), - 'content' => $response->getContent() + 'content' => $response->getContent(), ]; }, $responses); } + return null; } protected function iterateThroughStrategies(string $key, array $arguments) { - $strategies = $this->config->get("strategies.$key", []); - $results = []; - - foreach ($strategies as $strategyClass) { - $strategy = new $strategyClass($this->config); - $results = $strategy(...$arguments); - if (! is_null($results)) { - break; + $strategies = $this->config->get("strategies.$key", []); + $results = []; + + foreach ($strategies as $strategyClass) { + $strategy = new $strategyClass($this->config); + $results = $strategy(...$arguments); + if (! is_null($results)) { + break; + } } + + return is_null($results) ? [] : $results; } - return is_null($results) ? [] : $results; -} } diff --git a/src/Tools/RouteDocBlocker.php b/src/Tools/RouteDocBlocker.php index e96ba033..65604f24 100644 --- a/src/Tools/RouteDocBlocker.php +++ b/src/Tools/RouteDocBlocker.php @@ -8,7 +8,6 @@ class RouteDocBlocker { - public static $docBlocks = []; public static function getDocBlocksFromRoute(Route $route) @@ -27,15 +26,17 @@ public static function getDocBlocksFromRoute(Route $route) $docBlocks = [ 'method' => new DocBlock($class->getMethod($methodName)->getDocComment() ?: ''), - 'class' => new DocBlock($class->getDocComment() ?: '') + 'class' => new DocBlock($class->getDocComment() ?: ''), ]; self::cacheDocBlocks($route, $className, $methodName, $docBlocks); + return $docBlocks; } protected static function getCachedDocBlock(Route $route, string $className, string $methodName) { $routeId = self::getRouteId($route, $className, $methodName); + return self::$docBlocks[$routeId] ?? null; } diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index c763e2f4..282ae8c6 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -30,7 +30,7 @@ public function withEndpointDescription() */ public function withGroupOverride() { - return "Group B, baby!"; + return 'Group B, baby!'; } /** @@ -66,7 +66,7 @@ public function withGroupOverride4() } /** - * Endpoint with body parameters + * Endpoint with body parameters. * * @bodyParam user_id int required The id of the user. Example: 9 * @bodyParam room_id string The id of the room. diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 123a3c48..3c4063ac 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -29,7 +29,7 @@ public function setUp() public function tearDown() { - Utils::deleteDirectoryAndContents('/public/docs'); + Utils::deleteDirectoryAndContents('/public/docs'); } /** diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index d17cc6de..b15da543 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -451,7 +451,7 @@ public function can_parse_transformer_collection_tag() $this->assertEquals(200, $response['status']); $this->assertSame( $response['content'], - '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},' . + '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},'. '{"id":1,"description":"Welcome on this test versions","name":"TestName"}]}' ); } @@ -470,7 +470,7 @@ public function can_parse_transformer_collection_tag_with_model() $this->assertEquals(200, $response['status']); $this->assertSame( $response['content'], - '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},' . + '{"data":[{"id":1,"description":"Welcome on this test versions","name":"TestName"},'. '{"id":1,"description":"Welcome on this test versions","name":"TestName"}]}' ); } @@ -608,7 +608,7 @@ public function uses_correct_bindings_by_prefix() public function can_parse_response_file_tag() { // copy file to storage - $filePath = __DIR__ . '/../Fixtures/response_test.json'; + $filePath = __DIR__.'/../Fixtures/response_test.json'; $fixtureFileJson = file_get_contents($filePath); copy($filePath, storage_path('response_test.json')); @@ -633,7 +633,7 @@ public function can_parse_response_file_tag() public function can_add_or_replace_key_value_pair_in_response_file() { // copy file to storage - $filePath = __DIR__ . '/../Fixtures/response_test.json'; + $filePath = __DIR__.'/../Fixtures/response_test.json'; $fixtureFileJson = file_get_contents($filePath); copy($filePath, storage_path('response_test.json')); @@ -658,10 +658,10 @@ public function can_add_or_replace_key_value_pair_in_response_file() public function can_parse_multiple_response_file_tags_with_status_codes() { // copy file to storage - $successFilePath = __DIR__ . '/../Fixtures/response_test.json'; + $successFilePath = __DIR__.'/../Fixtures/response_test.json'; $successFixtureFileJson = file_get_contents($successFilePath); copy($successFilePath, storage_path('response_test.json')); - $errorFilePath = __DIR__ . '/../Fixtures/response_error_test.json'; + $errorFilePath = __DIR__.'/../Fixtures/response_error_test.json'; $errorFixtureFileJson = file_get_contents($errorFilePath); copy($errorFilePath, storage_path('response_error_test.json')); From c3cc56177fa0ad2f948c0de4d03acf1735e1c529 Mon Sep 17 00:00:00 2001 From: shalvah Date: Mon, 2 Sep 2019 21:46:56 +0100 Subject: [PATCH 086/312] Improve plugin API --- docs/index.md | 1 + docs/plugins.md | 108 ++++++++++++++++++ src/Strategies/Responses/ResponseCalls.php | 11 +- .../Responses/UseResponseFileTag.php | 7 +- src/Strategies/Responses/UseResponseTag.php | 7 +- .../Responses/UseTransformerTags.php | 2 +- src/Tools/Generator.php | 72 +++++++----- tests/Fixtures/index.md | 8 +- tests/Unit/DingoGeneratorTest.php | 12 +- tests/Unit/GeneratorTestCase.php | 36 +++++- tests/Unit/LaravelGeneratorTest.php | 12 +- 11 files changed, 222 insertions(+), 54 deletions(-) create mode 100644 docs/plugins.md diff --git a/docs/index.md b/docs/index.md index decb22c5..56587fbb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,6 +9,7 @@ Automatically generate your API documentation from your existing Laravel/Lumen/[ * [Configuration](config.md) * [Generating Documentation](generating-documentation.md) * [Documenting Your API](documenting.md) +* [Extending functionality with plugins](plugins.md) * [Internal Architecture](architecture.md) ## Installation diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 00000000..e8510a66 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,108 @@ +# Extending functionality with plugins +You can use plugins to alter how the Generator fetches data about your routes. For instance, suppose all your routes have a body parameter `organizationId`, and you don't want to annotate this with `@queryParam` on each method. You can create a plugin that adds this to all your body parameters. Let's see how to do this. + +## The stages of route processing +Route processing is performed in four stages: +- metadata (this covers route title, route description, route group name, route group description, and authentication status) +- bodyParameters +- queryParameters +- responses + +For each stage, the Generator attempts one or more configured strategies to fetch data. The Generator will call of the strategies configured, progressively combining their results together before to produce the final output of that stage. + +## Strategies +To create a strategy, create a class that extends `\Mpociot\ApiDoc\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`) +- the controller class handling the route (`\ReflectionClass`) +- the controller method (`\ReflectionMethod $method`) + - the rules specified in the apidoc.php config file for the group this route belongs to, under the `apply` section (array) + - the context. This contains all data for the route that has been parsed thus far in the previous stages. + + Here's what your strategy in our example would look like: + + ```php + [ + 'type' => 'integer', + 'description' => 'The ID of the organization', + 'required' => true, + 'value' => 2, + ] + ]; + } +} +``` + +The last thing to do is to register the strategy. Strategies are registered in a `strategies` key in the `apidoc.php` file. Here's what the file looks like by default: + +```php +... + 'strategies' => [ + 'metadata' => [ + \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, + ], + 'bodyParameters' => [ + \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromDocBlocks::class, + ], + 'queryParameters' => [ + \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromDocBlocks::class, + ], + 'responses' => [ + \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, + \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class, + \Mpociot\ApiDoc\Strategies\Responses\UseTransformerTags::class, + \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class, + ], + ], +... +``` + +You can add, replace or remove strategies from here. In our case, we're adding our bodyParameter strategy: + +```php + + 'bodyParameters' => [ + \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromDocBlocks::class, + AddOrganizationIdBodyParameter::class, + ], +``` + +And we're done. Now, when we run `php artisan docs:generate`, all our routes will have this bodyParameter added. + + +We could go further and modify our strategy so it doesn't add this parameter if the route is a GET route or is authenticated: + +```php +public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $routeRules, array $context = []) +{ + if (in_array('GET', $route->methods()) { + return null; + } + + if ($context['metadata']['authenticated']) { + return null; + } + + return [ + 'organizationId' => [ + 'type' => 'integer', + 'description' => 'The ID of the organization', + 'required' => true, + 'value' => 2, + ] + ]; +} +``` + +The strategy class also has access to the current apidoc configuration via its config property. For instance, you can retrieve the deafult group with `$this->config->get('default_group')`. diff --git a/src/Strategies/Responses/ResponseCalls.php b/src/Strategies/Responses/ResponseCalls.php index 8fc0d106..0ff6a831 100644 --- a/src/Strategies/Responses/ResponseCalls.php +++ b/src/Strategies/Responses/ResponseCalls.php @@ -30,7 +30,7 @@ class ResponseCalls extends Strategy public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $routeRules, array $context = []) { $rulesToApply = $routeRules['response_calls'] ?? []; - if (! $this->shouldMakeApiCall($route, $rulesToApply)) { + if (! $this->shouldMakeApiCall($route, $rulesToApply, $context)) { return null; } @@ -42,7 +42,7 @@ public function __invoke(Route $route, \ReflectionClass $controller, \Reflection $request = $this->prepareRequest($route, $rulesToApply, $bodyParameters, $queryParameters); try { - $response = [$this->makeApiCall($request)]; + $response = [200 => $this->makeApiCall($request)->getContent()]; } catch (\Exception $e) { echo 'Exception thrown during response call for ['.implode(',', $route->methods)."] {$route->uri}.\n"; if (Flags::$shouldBeVerbose) { @@ -297,13 +297,18 @@ protected function callLaravelRoute(Request $request): \Symfony\Component\HttpFo * * @return bool */ - private function shouldMakeApiCall(Route $route, array $rulesToApply): bool + private function shouldMakeApiCall(Route $route, array $rulesToApply, array $context): bool { $allowedMethods = $rulesToApply['methods'] ?? []; if (empty($allowedMethods)) { return false; } + if (!empty($context['responses'])) { + // Don't attempt a response call if there are already responses + return false; + } + if (is_string($allowedMethods) && $allowedMethods == '*') { return true; } diff --git a/src/Strategies/Responses/UseResponseFileTag.php b/src/Strategies/Responses/UseResponseFileTag.php index 0db2a9d4..a66cc09d 100644 --- a/src/Strategies/Responses/UseResponseFileTag.php +++ b/src/Strategies/Responses/UseResponseFileTag.php @@ -52,14 +52,17 @@ protected function getFileResponses(array $tags) return null; } - return array_map(function (Tag $responseFileTag) { + $responses = array_map(function (Tag $responseFileTag) { preg_match('/^(\d{3})?\s?([\S]*[\s]*?)(\{.*\})?$/', $responseFileTag->getContent(), $result); $status = $result[1] ?: 200; $content = $result[2] ? file_get_contents(storage_path(trim($result[2])), true) : '{}'; $json = ! empty($result[3]) ? str_replace("'", '"', $result[3]) : '{}'; $merged = array_merge(json_decode($content, true), json_decode($json, true)); - return new JsonResponse($merged, (int) $status); + return [json_encode($merged), (int) $status]; }, $responseFileTags); + + // Convert responses to [200 => 'response', 401 => 'response'] + return collect($responses)->pluck(0, 1)->toArray(); } } diff --git a/src/Strategies/Responses/UseResponseTag.php b/src/Strategies/Responses/UseResponseTag.php index 73355ce6..3842bd10 100644 --- a/src/Strategies/Responses/UseResponseTag.php +++ b/src/Strategies/Responses/UseResponseTag.php @@ -51,13 +51,16 @@ protected function getDocBlockResponses(array $tags) return null; } - return array_map(function (Tag $responseTag) { + $responses = array_map(function (Tag $responseTag) { preg_match('/^(\d{3})?\s?([\s\S]*)$/', $responseTag->getContent(), $result); $status = $result[1] ?: 200; $content = $result[2] ?: '{}'; - return new JsonResponse(json_decode($content, true), (int) $status); + return [$content, (int) $status]; }, $responseTags); + + // Convert responses to [200 => 'response', 401 => 'response'] + return collect($responses)->pluck(0, 1)->toArray(); } } diff --git a/src/Strategies/Responses/UseTransformerTags.php b/src/Strategies/Responses/UseTransformerTags.php index f9a48a3e..da674774 100644 --- a/src/Strategies/Responses/UseTransformerTags.php +++ b/src/Strategies/Responses/UseTransformerTags.php @@ -65,7 +65,7 @@ protected function getTransformerResponse(array $tags) ? new Collection([$modelInstance, $modelInstance], new $transformer) : new Item($modelInstance, new $transformer); - return [response($fractal->createData($resource)->toJson())]; + return [200 => response($fractal->createData($resource)->toJson())->getContent()]; } catch (\Exception $e) { return null; } diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 8b8738f3..bf984623 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -6,9 +6,7 @@ use ReflectionClass; use ReflectionMethod; use Illuminate\Routing\Route; -use Mpociot\Reflection\DocBlock; use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; -use Symfony\Component\HttpFoundation\Response; class Generator { @@ -58,13 +56,13 @@ public function processRoute(Route $route, array $rulesToApply = []) $method = $controller->getMethod($methodName); $parsedRoute = [ - 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), + 'id' => md5($this->getUri($route) . ':' . implode($this->getMethods($route))), 'methods' => $this->getMethods($route), 'uri' => $this->getUri($route), 'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])), ]; - $metadata = $this->fetchMetadata($controller, $method, $route, $rulesToApply); - $parsedRoute += $metadata; + $metadata = $this->fetchMetadata($controller, $method, $route, $rulesToApply, $parsedRoute); + $parsedRoute['metadata'] = $metadata; $bodyParameters = $this->fetchBodyParameters($controller, $method, $route, $rulesToApply, $parsedRoute); $parsedRoute['bodyParameters'] = $bodyParameters; $parsedRoute['cleanBodyParameters'] = $this->cleanParams($bodyParameters); @@ -75,54 +73,76 @@ public function processRoute(Route $route, array $rulesToApply = []) $responses = $this->fetchResponses($controller, $method, $route, $rulesToApply, $parsedRoute); $parsedRoute['response'] = $responses; - $parsedRoute['showresponse'] = ! empty($responses); + $parsedRoute['showresponse'] = !empty($responses); $parsedRoute['headers'] = $rulesToApply['headers'] ?? []; + // Currently too lazy to tinker with Blade files; change this later + unset($parsedRoute['metadata']); + $parsedRoute += $metadata; + return $parsedRoute; } - protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply) + protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) { - return $this->iterateThroughStrategies('metadata', [$route, $controller, $method, $rulesToApply]); + $context['metadata'] = [ + 'groupName' => $this->config->get('default_group'), + 'groupDescription' => '', + 'title' => '', + 'description' => '', + 'authenticated' => false, + ]; + return $this->iterateThroughStrategies('metadata', $context, [$route, $controller, $method, $rulesToApply]); } protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) { - return $this->iterateThroughStrategies('bodyParameters', [$route, $controller, $method, $rulesToApply]); + return $this->iterateThroughStrategies('bodyParameters', $context, [$route, $controller, $method, $rulesToApply]); } protected function fetchQueryParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) { - return $this->iterateThroughStrategies('queryParameters', [$route, $controller, $method, $rulesToApply]); + return $this->iterateThroughStrategies('queryParameters', $context, [$route, $controller, $method, $rulesToApply]); } protected function fetchResponses(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) { - $responses = $this->iterateThroughStrategies('responses', [$route, $controller, $method, $rulesToApply, $context]); + $responses = $this->iterateThroughStrategies('responses', $context, [$route, $controller, $method, $rulesToApply]); if (count($responses)) { - return array_map(function (Response $response) { + return collect($responses)->map(function (string $response, int $status) { return [ - 'status' => $response->getStatusCode(), - 'content' => $response->getContent() + 'status' => $status ?: 200, + 'content' => $response, ]; - }, $responses); + })->values()->toArray(); } return null; } - protected function iterateThroughStrategies(string $key, array $arguments) + protected function iterateThroughStrategies(string $key, array $context, array $arguments) { - $strategies = $this->config->get("strategies.$key", []); - $results = []; - - foreach ($strategies as $strategyClass) { - $strategy = new $strategyClass($this->config); - $results = $strategy(...$arguments); - if (! is_null($results)) { - break; + $strategies = $this->config->get("strategies.$key", []); + $context[$key] = $context[$key] ?? []; + foreach ($strategies as $strategyClass) { + $strategy = new $strategyClass($this->config); + $arguments[] = $context; + $results = $strategy(...$arguments); + if (!is_null($results)) { + foreach ($results as $index => $item) { + // Using a for loop rather than array_merge or += + // so it does not renumber numeric keys + // and also allows values to be overwritten + + // Don't allow overwriting if an empty value is trying to replace a set one + if (! in_array($context[$key], [null, ''], true) && in_array($item, [null, ''], true)) { + continue; + } else { + $context[$key][$index] = $item; + } + } + } } + return $context[$key]; } - return is_null($results) ? [] : $results; -} } diff --git a/tests/Fixtures/index.md b/tests/Fixtures/index.md index f933cccf..91203ee2 100644 --- a/tests/Fixtures/index.md +++ b/tests/Fixtures/index.md @@ -56,7 +56,7 @@ fetch(url, { ``` -> Example response (200): +> Example response: ```json null @@ -173,7 +173,7 @@ fetch(url, { ``` -> Example response (200): +> Example response: ```json null @@ -239,7 +239,7 @@ fetch(url, { ``` -> Example response (200): +> Example response: ```json null @@ -290,7 +290,7 @@ fetch(url, { ``` -> Example response (200): +> Example response: ```json null diff --git a/tests/Unit/DingoGeneratorTest.php b/tests/Unit/DingoGeneratorTest.php index de56f333..1d105541 100644 --- a/tests/Unit/DingoGeneratorTest.php +++ b/tests/Unit/DingoGeneratorTest.php @@ -23,25 +23,25 @@ public function setUp() config(['apidoc.router' => 'dingo']); } - public function createRoute(string $httpMethod, string $path, string $controllerMethod, $register = false) + public function createRoute(string $httpMethod, string $path, string $controllerMethod, $register = false, $class = TestController::class) { $route = null; /** @var Router $api */ $api = app(Router::class); - $api->version('v1', function (Router $api) use ($controllerMethod, $path, $httpMethod, &$route) { - $route = $api->$httpMethod($path, TestController::class."@$controllerMethod"); + $api->version('v1', function (Router $api) use ($class, $controllerMethod, $path, $httpMethod, &$route) { + $route = $api->$httpMethod($path, $class."@$controllerMethod"); }); return $route; } - public function createRouteUsesArray(string $httpMethod, string $path, string $controllerMethod, $register = false) + public function createRouteUsesArray(string $httpMethod, string $path, string $controllerMethod, $register = false, $class = TestController::class) { $route = null; /** @var Router $api */ $api = app(Router::class); - $api->version('v1', function (Router $api) use ($controllerMethod, $path, $httpMethod, &$route) { - $route = $api->$httpMethod($path, [TestController::class, $controllerMethod]); + $api->version('v1', function (Router $api) use ($class, $controllerMethod, $path, $httpMethod, &$route) { + $route = $api->$httpMethod($path, [$class, $controllerMethod]); }); return $route; diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index d17cc6de..3326e7cd 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -5,7 +5,9 @@ use Orchestra\Testbench\TestCase; use Mpociot\ApiDoc\Tools\Generator; use Mpociot\ApiDoc\Tools\DocumentationConfig; +use Mpociot\ApiDoc\Tests\Fixtures\TestController; use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider; +use Mpociot\ApiDoc\Tests\Fixtures\TestResourceController; abstract class GeneratorTestCase extends TestCase { @@ -414,8 +416,8 @@ public function can_parse_transformer_tag($serializer, $expected) $this->assertTrue(is_array($response)); $this->assertEquals(200, $response['status']); $this->assertSame( - $response['content'], - $expected + $expected, + $response['content'] ); } @@ -763,9 +765,35 @@ public function can_use_arrays_in_routes_uses() $this->assertSame("This will be the long description.\nIt can also be multiple lines long.", $parsed['description']); } - abstract public function createRoute(string $httpMethod, string $path, string $controllerMethod, $register = false); + /** @test */ + public function combines_responses_from_different_strategies() + { + $route = $this->createRoute('GET', '/api/indexResource', 'index', true, TestResourceController::class); + $rules = [ + 'response_calls' => [ + 'methods' => ['*'], + 'headers' => [ + 'Accept' => 'application/json', + ], + ], + ]; + + $parsed = $this->generator->processRoute($route, $rules); + + $this->assertTrue(is_array($parsed)); + $this->assertArrayHasKey('showresponse', $parsed); + $this->assertTrue($parsed['showresponse']); + $this->assertSame(1, count($parsed['response'])); + $this->assertTrue(is_array($parsed['response'][0])); + $this->assertEquals(200, $parsed['response'][0]['status']); + $this->assertArraySubset([ + 'index_resource' => true, + ], json_decode($parsed['response'][0]['content'], true)); + } + + abstract public function createRoute(string $httpMethod, string $path, string $controllerMethod, $register = false, $class = TestController::class); - abstract public function createRouteUsesArray(string $httpMethod, string $path, string $controllerMethod, $register = false); + abstract public function createRouteUsesArray(string $httpMethod, string $path, string $controllerMethod, $register = false, $class = TestController::class); public function dataResources() { diff --git a/tests/Unit/LaravelGeneratorTest.php b/tests/Unit/LaravelGeneratorTest.php index d17d3027..41d4b3ee 100644 --- a/tests/Unit/LaravelGeneratorTest.php +++ b/tests/Unit/LaravelGeneratorTest.php @@ -16,21 +16,21 @@ protected function getPackageProviders($app) ]; } - public function createRoute(string $httpMethod, string $path, string $controllerMethod, $register = false) + public function createRoute(string $httpMethod, string $path, string $controllerMethod, $register = false, $class = TestController::class) { if ($register) { - return RouteFacade::{$httpMethod}($path, TestController::class."@$controllerMethod"); + return RouteFacade::{$httpMethod}($path, $class."@$controllerMethod"); } else { - return new Route([$httpMethod], $path, ['uses' => TestController::class."@$controllerMethod"]); + return new Route([$httpMethod], $path, ['uses' => $class."@$controllerMethod"]); } } - public function createRouteUsesArray(string $httpMethod, string $path, string $controllerMethod, $register = false) + public function createRouteUsesArray(string $httpMethod, string $path, string $controllerMethod, $register = false, $class = TestController::class) { if ($register) { - return RouteFacade::{$httpMethod}($path, TestController::class."@$controllerMethod"); + return RouteFacade::{$httpMethod}($path, [$class. "$controllerMethod"]); } else { - return new Route([$httpMethod], $path, ['uses' => [TestController::class, $controllerMethod]]); + return new Route([$httpMethod], $path, ['uses' => [$class, $controllerMethod]]); } } } From 7ae97a008129dcf4a737f28dd32b0268a47016a8 Mon Sep 17 00:00:00 2001 From: shalvah Date: Mon, 2 Sep 2019 21:56:00 +0100 Subject: [PATCH 087/312] Clean up --- src/Strategies/Responses/UseResponseFileTag.php | 2 +- src/Strategies/Responses/UseResponseTag.php | 2 +- tests/Fixtures/collection.json | 2 +- tests/Fixtures/collection_with_body_parameters.json | 2 +- tests/Fixtures/index.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Strategies/Responses/UseResponseFileTag.php b/src/Strategies/Responses/UseResponseFileTag.php index db53cbbe..239450bf 100644 --- a/src/Strategies/Responses/UseResponseFileTag.php +++ b/src/Strategies/Responses/UseResponseFileTag.php @@ -65,6 +65,6 @@ protected function getFileResponses(array $tags) }, $responseFileTags); // Convert responses to [200 => 'response', 401 => 'response'] - return collect($responses)->pluck(0, 1)->toArray(); + return collect($responses)->pluck("0", "1")->toArray(); } } diff --git a/src/Strategies/Responses/UseResponseTag.php b/src/Strategies/Responses/UseResponseTag.php index 453cc324..0349bf98 100644 --- a/src/Strategies/Responses/UseResponseTag.php +++ b/src/Strategies/Responses/UseResponseTag.php @@ -63,6 +63,6 @@ protected function getDocBlockResponses(array $tags) }, $responseTags); // Convert responses to [200 => 'response', 401 => 'response'] - return collect($responses)->pluck(0, 1)->toArray(); + return collect($responses)->pluck("0", "1")->toArray(); } } diff --git a/tests/Fixtures/collection.json b/tests/Fixtures/collection.json index 9fc17ec7..e541fb42 100644 --- a/tests/Fixtures/collection.json +++ b/tests/Fixtures/collection.json @@ -66,7 +66,7 @@ } }, { - "name": "Endpoint with body parameters", + "name": "Endpoint with body parameters.", "request": { "url": "http:\/\/localhost\/api\/withBodyParameters", "method": "GET", diff --git a/tests/Fixtures/collection_with_body_parameters.json b/tests/Fixtures/collection_with_body_parameters.json index 13e8f4d1..9380b37f 100644 --- a/tests/Fixtures/collection_with_body_parameters.json +++ b/tests/Fixtures/collection_with_body_parameters.json @@ -12,7 +12,7 @@ "description": "", "item": [ { - "name": "Endpoint with body parameters", + "name": "Endpoint with body parameters.", "request": { "url": "http:\/\/localhost\/api\/withBodyParameters", "method": "GET", diff --git a/tests/Fixtures/index.md b/tests/Fixtures/index.md index 91203ee2..407b381e 100644 --- a/tests/Fixtures/index.md +++ b/tests/Fixtures/index.md @@ -117,7 +117,7 @@ fetch(url, { -## Endpoint with body parameters +## Endpoint with body parameters. > Example request: From ffee319db448a4c8a4f1043c20e9bf50f0a5d2ca Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Mon, 2 Sep 2019 20:56:16 +0000 Subject: [PATCH 088/312] Apply fixes from StyleCI --- src/Strategies/Responses/ResponseCalls.php | 2 +- src/Strategies/Responses/UseResponseFileTag.php | 3 +-- src/Strategies/Responses/UseResponseTag.php | 3 +-- src/Tools/Generator.php | 9 +++++---- tests/Unit/LaravelGeneratorTest.php | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Strategies/Responses/ResponseCalls.php b/src/Strategies/Responses/ResponseCalls.php index 0ff6a831..9fc89662 100644 --- a/src/Strategies/Responses/ResponseCalls.php +++ b/src/Strategies/Responses/ResponseCalls.php @@ -304,7 +304,7 @@ private function shouldMakeApiCall(Route $route, array $rulesToApply, array $con return false; } - if (!empty($context['responses'])) { + if (! empty($context['responses'])) { // Don't attempt a response call if there are already responses return false; } diff --git a/src/Strategies/Responses/UseResponseFileTag.php b/src/Strategies/Responses/UseResponseFileTag.php index 239450bf..8772ad6d 100644 --- a/src/Strategies/Responses/UseResponseFileTag.php +++ b/src/Strategies/Responses/UseResponseFileTag.php @@ -4,7 +4,6 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; -use Illuminate\Http\JsonResponse; use Mpociot\Reflection\DocBlock\Tag; use Mpociot\ApiDoc\Strategies\Strategy; use Mpociot\ApiDoc\Tools\RouteDocBlocker; @@ -65,6 +64,6 @@ protected function getFileResponses(array $tags) }, $responseFileTags); // Convert responses to [200 => 'response', 401 => 'response'] - return collect($responses)->pluck("0", "1")->toArray(); + return collect($responses)->pluck('0', '1')->toArray(); } } diff --git a/src/Strategies/Responses/UseResponseTag.php b/src/Strategies/Responses/UseResponseTag.php index 0349bf98..67bcb11c 100644 --- a/src/Strategies/Responses/UseResponseTag.php +++ b/src/Strategies/Responses/UseResponseTag.php @@ -4,7 +4,6 @@ use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; -use Illuminate\Http\JsonResponse; use Mpociot\Reflection\DocBlock\Tag; use Mpociot\ApiDoc\Strategies\Strategy; use Mpociot\ApiDoc\Tools\RouteDocBlocker; @@ -63,6 +62,6 @@ protected function getDocBlockResponses(array $tags) }, $responseTags); // Convert responses to [200 => 'response', 401 => 'response'] - return collect($responses)->pluck("0", "1")->toArray(); + return collect($responses)->pluck('0', '1')->toArray(); } } diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 8b1a0b1a..d77e8995 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -6,7 +6,6 @@ use ReflectionMethod; use Illuminate\Routing\Route; use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; -use Symfony\Component\HttpFoundation\Response; class Generator { @@ -56,7 +55,7 @@ public function processRoute(Route $route, array $rulesToApply = []) $method = $controller->getMethod($methodName); $parsedRoute = [ - 'id' => md5($this->getUri($route) . ':' . implode($this->getMethods($route))), + 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), 'methods' => $this->getMethods($route), 'uri' => $this->getUri($route), 'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])), @@ -73,7 +72,7 @@ public function processRoute(Route $route, array $rulesToApply = []) $responses = $this->fetchResponses($controller, $method, $route, $rulesToApply, $parsedRoute); $parsedRoute['response'] = $responses; - $parsedRoute['showresponse'] = !empty($responses); + $parsedRoute['showresponse'] = ! empty($responses); $parsedRoute['headers'] = $rulesToApply['headers'] ?? []; @@ -93,6 +92,7 @@ protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $ 'description' => '', 'authenticated' => false, ]; + return $this->iterateThroughStrategies('metadata', $context, [$route, $controller, $method, $rulesToApply]); } @@ -129,7 +129,7 @@ protected function iterateThroughStrategies(string $key, array $context, array $ $strategy = new $strategyClass($this->config); $arguments[] = $context; $results = $strategy(...$arguments); - if (!is_null($results)) { + if (! is_null($results)) { foreach ($results as $index => $item) { // Using a for loop rather than array_merge or += // so it does not renumber numeric keys @@ -144,6 +144,7 @@ protected function iterateThroughStrategies(string $key, array $context, array $ } } } + return $context[$key]; } } diff --git a/tests/Unit/LaravelGeneratorTest.php b/tests/Unit/LaravelGeneratorTest.php index 41d4b3ee..6bbfcd1e 100644 --- a/tests/Unit/LaravelGeneratorTest.php +++ b/tests/Unit/LaravelGeneratorTest.php @@ -28,7 +28,7 @@ public function createRoute(string $httpMethod, string $path, string $controller public function createRouteUsesArray(string $httpMethod, string $path, string $controllerMethod, $register = false, $class = TestController::class) { if ($register) { - return RouteFacade::{$httpMethod}($path, [$class. "$controllerMethod"]); + return RouteFacade::{$httpMethod}($path, [$class."$controllerMethod"]); } else { return new Route([$httpMethod], $path, ['uses' => [$class, $controllerMethod]]); } From 7ab3127fb54ede55241a526b1d6262242d029fa8 Mon Sep 17 00:00:00 2001 From: Sang Nguyen Date: Wed, 4 Sep 2019 15:04:17 +0700 Subject: [PATCH 089/312] Add support Laravel 6.0 & remove deprecated functions --- composer.json | 6 ++-- resources/views/documentarian.blade.php | 2 +- src/Tools/Generator.php | 3 +- .../TransformerTagsStrategy.php | 7 ++-- src/Tools/RouteMatcher.php | 13 ++++---- src/Tools/Traits/ParamHelpers.php | 7 ++-- tests/Unit/GeneratorTestCase.php | 33 ++++++++++--------- tests/Unit/RouteMatcherTest.php | 15 +++++---- 8 files changed, 47 insertions(+), 39 deletions(-) diff --git a/composer.json b/composer.json index 90512c6a..6400ca38 100644 --- a/composer.json +++ b/composer.json @@ -17,9 +17,9 @@ "require": { "php": ">=7.0.0", "fzaninotto/faker": "~1.8", - "illuminate/routing": "5.5.* || 5.6.* || 5.7.* || 5.8.*", - "illuminate/support": "5.5.* || 5.6.* || 5.7.* || 5.8.*", - "illuminate/console": "5.5.* || 5.6.* || 5.7.* || 5.8.*", + "illuminate/routing": "5.5.*|^6.0", + "illuminate/support": "5.5.*|^6.0", + "illuminate/console": "5.5.*|^6.0", "mpociot/documentarian": "^0.2.0", "mpociot/reflection-docblock": "^1.0.1", "ramsey/uuid": "^3.8", diff --git a/resources/views/documentarian.blade.php b/resources/views/documentarian.blade.php index 90151a73..3c2dfa68 100644 --- a/resources/views/documentarian.blade.php +++ b/resources/views/documentarian.blade.php @@ -8,7 +8,7 @@ @foreach($parsedRoutes as $groupName => $routes) #{!! $groupName !!} {{-- We pick the first non-empty description we see. --}} -{!! array_first($routes, function ($route) { return $route['groupDescription'] !== ''; })['groupDescription'] ?? '' !!} +{!! Arr::first($routes, function ($route) { return $route['groupDescription'] !== ''; })['groupDescription'] ?? '' !!} @foreach($routes as $parsedRoute) @if($writeCompareFile === true) {!! $parsedRoute['output'] !!} diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 9153e14d..0a9593be 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -3,6 +3,7 @@ namespace Mpociot\ApiDoc\Tools; use Faker\Factory; +use Illuminate\Support\Str; use ReflectionClass; use ReflectionMethod; use Illuminate\Routing\Route; @@ -227,7 +228,7 @@ protected function getQueryParametersFromDocBlock(array $tags) list($description, $value) = $this->parseDescription($description, 'string'); if (is_null($value) && ! $this->shouldExcludeExample($tag)) { - $value = str_contains($description, ['number', 'count', 'page']) + $value = Str::contains($description, ['number', 'count', 'page']) ? $this->generateDummyValue('integer') : $this->generateDummyValue('string'); } diff --git a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php index 62ab3e9a..2c023d1d 100644 --- a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php +++ b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php @@ -2,6 +2,7 @@ namespace Mpociot\ApiDoc\Tools\ResponseStrategies; +use Illuminate\Support\Arr; use ReflectionClass; use ReflectionMethod; use League\Fractal\Manager; @@ -80,7 +81,7 @@ private function getTransformerClass($tag) */ private function getClassToBeTransformed(array $tags, ReflectionMethod $transformerMethod) { - $modelTag = array_first(array_filter($tags, function ($tag) { + $modelTag = Arr::first(array_filter($tags, function ($tag) { return ($tag instanceof Tag) && strtolower($tag->getName()) == 'transformermodel'; })); @@ -88,7 +89,7 @@ private function getClassToBeTransformed(array $tags, ReflectionMethod $transfor if ($modelTag) { $type = $modelTag->getContent(); } else { - $parameter = array_first($transformerMethod->getParameters()); + $parameter = Arr::first($transformerMethod->getParameters()); if ($parameter->hasType() && ! $parameter->getType()->isBuiltin() && class_exists((string) $parameter->getType())) { // ladies and gentlemen, we have a type! $type = (string) $parameter->getType(); @@ -146,6 +147,6 @@ private function getTransformerTag(array $tags) }) ); - return array_first($transFormerTags); + return Arr::first($transFormerTags); } } diff --git a/src/Tools/RouteMatcher.php b/src/Tools/RouteMatcher.php index c98885f0..987328e5 100644 --- a/src/Tools/RouteMatcher.php +++ b/src/Tools/RouteMatcher.php @@ -5,6 +5,7 @@ use Illuminate\Routing\Route; use Dingo\Api\Routing\RouteCollection; use Illuminate\Support\Facades\Route as RouteFacade; +use Illuminate\Support\Str; class RouteMatcher { @@ -71,10 +72,10 @@ private function shouldIncludeRoute(Route $route, array $routeRule, array $mustI ? ! empty(array_intersect($route->versions(), $routeRule['match']['versions'] ?? [])) : true; - return str_is($mustIncludes, $route->getName()) - || str_is($mustIncludes, $route->uri()) - || (str_is($routeRule['match']['domains'] ?? [], $route->getDomain()) - && str_is($routeRule['match']['prefixes'] ?? [], $route->uri()) + return Str::is($mustIncludes, $route->getName()) + || Str::is($mustIncludes, $route->uri()) + || (Str::is($routeRule['match']['domains'] ?? [], $route->getDomain()) + && Str::is($routeRule['match']['prefixes'] ?? [], $route->uri()) && $matchesVersion); } @@ -82,7 +83,7 @@ private function shouldExcludeRoute(Route $route, array $routeRule) { $excludes = $routeRule['exclude'] ?? []; - return str_is($excludes, $route->getName()) - || str_is($excludes, $route->uri()); + return Str::is($excludes, $route->getName()) + || Str::is($excludes, $route->uri()); } } diff --git a/src/Tools/Traits/ParamHelpers.php b/src/Tools/Traits/ParamHelpers.php index 6f72be10..352e6489 100644 --- a/src/Tools/Traits/ParamHelpers.php +++ b/src/Tools/Traits/ParamHelpers.php @@ -2,6 +2,9 @@ namespace Mpociot\ApiDoc\Tools\Traits; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; + trait ParamHelpers { /** @@ -36,9 +39,9 @@ protected function cleanParams(array $params) */ protected function cleanValueFrom($name, $value, array &$values = []) { - if (str_contains($name, '[')) { + if (Str::contains($name, '[')) { $name = str_replace(['][', '[', ']', '..'], ['.', '.', '', '.*.'], $name); } - array_set($values, str_replace('.*', '.0', $name), $value); + Arr::set($values, str_replace('.*', '.0', $name), $value); } } diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 4a44090b..093b9081 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -2,6 +2,7 @@ namespace Mpociot\ApiDoc\Tests\Unit; +use Illuminate\Support\Arr; use Orchestra\Testbench\TestCase; use Mpociot\ApiDoc\Tools\Generator; use Mpociot\ApiDoc\Tools\DocumentationConfig; @@ -315,7 +316,7 @@ public function can_parse_response_tag() { $route = $this->createRoute('POST', '/responseTag', 'withResponseTag'); $parsed = $this->generator->processRoute($route); - $response = array_first($parsed['response']); + $response = Arr::first($parsed['response']); $this->assertTrue(is_array($parsed)); $this->assertArrayHasKey('showresponse', $parsed); @@ -336,7 +337,7 @@ public function can_parse_response_tag_with_status_code() { $route = $this->createRoute('POST', '/responseTag', 'withResponseTagAndStatusCode'); $parsed = $this->generator->processRoute($route); - $response = array_first($parsed['response']); + $response = Arr::first($parsed['response']); $this->assertTrue(is_array($parsed)); $this->assertArrayHasKey('showresponse', $parsed); @@ -385,7 +386,7 @@ public function can_parse_transformer_tag($serializer, $expected) config(['apidoc.fractal.serializer' => $serializer]); $route = $this->createRoute('GET', '/transformerTag', 'transformerTag'); $parsed = $this->generator->processRoute($route); - $response = array_first($parsed['response']); + $response = Arr::first($parsed['response']); $this->assertTrue(is_array($parsed)); $this->assertArrayHasKey('showresponse', $parsed); @@ -403,7 +404,7 @@ public function can_parse_transformer_tag_with_model() { $route = $this->createRoute('GET', '/transformerTagWithModel', 'transformerTagWithModel'); $parsed = $this->generator->processRoute($route); - $response = array_first($parsed['response']); + $response = Arr::first($parsed['response']); $this->assertTrue(is_array($parsed)); $this->assertArrayHasKey('showresponse', $parsed); @@ -421,7 +422,7 @@ public function can_parse_transformer_collection_tag() { $route = $this->createRoute('GET', '/transformerCollectionTag', 'transformerCollectionTag'); $parsed = $this->generator->processRoute($route); - $response = array_first($parsed['response']); + $response = Arr::first($parsed['response']); $this->assertTrue(is_array($parsed)); $this->assertArrayHasKey('showresponse', $parsed); @@ -440,7 +441,7 @@ public function can_parse_transformer_collection_tag_with_model() { $route = $this->createRoute('GET', '/transformerCollectionTagWithModel', 'transformerCollectionTagWithModel'); $parsed = $this->generator->processRoute($route); - $response = array_first($parsed['response']); + $response = Arr::first($parsed['response']); $this->assertTrue(is_array($parsed)); $this->assertArrayHasKey('showresponse', $parsed); @@ -469,7 +470,7 @@ public function can_call_route_and_generate_response() ], ]; $parsed = $this->generator->processRoute($route, $rules); - $response = array_first($parsed['response']); + $response = Arr::first($parsed['response']); $this->assertTrue(is_array($parsed)); $this->assertArrayHasKey('showresponse', $parsed); @@ -496,7 +497,7 @@ public function can_override_config_during_response_call() ], ]; $parsed = $this->generator->processRoute($route, $rules); - $response = json_decode(array_first($parsed['response'])['content'], true); + $response = json_decode(Arr::first($parsed['response'])['content'], true); $originalValue = $response['app.env']; $now = time(); @@ -509,7 +510,7 @@ public function can_override_config_during_response_call() ], ]; $parsed = $this->generator->processRoute($route, $rules); - $response = json_decode(array_first($parsed['response'])['content'], true); + $response = json_decode(Arr::first($parsed['response'])['content'], true); $newValue = $response['app.env']; $this->assertEquals($now, $newValue); $this->assertNotEquals($originalValue, $newValue); @@ -530,7 +531,7 @@ public function can_override_url_path_parameters_with_bindings() ], ]; $parsed = $this->generator->processRoute($route, $rules); - $response = json_decode(array_first($parsed['response'])['content'], true); + $response = json_decode(Arr::first($parsed['response'])['content'], true); $param = $response['param']; $this->assertEquals($rand, $param); } @@ -550,7 +551,7 @@ public function replaces_optional_url_path_parameters_with_bindings() ], ]; $parsed = $this->generator->processRoute($route, $rules); - $response = json_decode(array_first($parsed['response'])['content'], true); + $response = json_decode(Arr::first($parsed['response'])['content'], true); $param = $response['param']; $this->assertEquals($rand, $param); } @@ -573,12 +574,12 @@ public function uses_correct_bindings_by_prefix() ], ]; $parsed = $this->generator->processRoute($route1, $rules); - $response = json_decode(array_first($parsed['response'])['content'], true); + $response = json_decode(Arr::first($parsed['response'])['content'], true); $param = $response['param']; $this->assertEquals($rand1, $param); $parsed = $this->generator->processRoute($route2, $rules); - $response = json_decode(array_first($parsed['response'])['content'], true); + $response = json_decode(Arr::first($parsed['response'])['content'], true); $param = $response['param']; $this->assertEquals($rand2, $param); } @@ -593,7 +594,7 @@ public function can_parse_response_file_tag() $route = $this->createRoute('GET', '/responseFileTag', 'responseFileTag'); $parsed = $this->generator->processRoute($route); - $response = array_first($parsed['response']); + $response = Arr::first($parsed['response']); $this->assertTrue(is_array($parsed)); $this->assertArrayHasKey('showresponse', $parsed); @@ -618,7 +619,7 @@ public function can_add_or_replace_key_value_pair_in_response_file() $route = $this->createRoute('GET', '/responseFileTagAndCustomJson', 'responseFileTagAndCustomJson'); $parsed = $this->generator->processRoute($route); - $response = array_first($parsed['response']); + $response = Arr::first($parsed['response']); $this->assertTrue(is_array($parsed)); $this->assertArrayHasKey('showresponse', $parsed); @@ -717,7 +718,7 @@ public function uses_configured_settings_when_calling_route() ], ]; $parsed = $this->generator->processRoute($route, $rules); - $response = array_first($parsed['response']); + $response = Arr::first($parsed['response']); $this->assertTrue(is_array($parsed)); $this->assertArrayHasKey('showresponse', $parsed); diff --git a/tests/Unit/RouteMatcherTest.php b/tests/Unit/RouteMatcherTest.php index 07ea60db..ccce939a 100644 --- a/tests/Unit/RouteMatcherTest.php +++ b/tests/Unit/RouteMatcherTest.php @@ -3,6 +3,7 @@ namespace Mpociot\ApiDoc\Tests\Unit; use Dingo\Api\Routing\Router; +use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; use Mpociot\ApiDoc\Tools\RouteMatcher; use Illuminate\Support\Facades\Route as RouteFacade; @@ -108,7 +109,7 @@ public function testRespectsPrefixesRuleForLaravelRouter() $routes = $this->matcher->getRoutesToBeDocumented($routeRules); $this->assertCount(4, $routes); foreach ($routes as $route) { - $this->assertTrue(str_is('prefix2/*', $route['route']->uri())); + $this->assertTrue(Str::is('prefix2/*', $route['route']->uri())); } } @@ -130,14 +131,14 @@ public function testRespectsPrefixesRuleForDingoRouter() $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); $this->assertCount(4, $routes); foreach ($routes as $route) { - $this->assertTrue(str_is('prefix1/*', $route['route']->uri())); + $this->assertTrue(Str::is('prefix1/*', $route['route']->uri())); } $routeRules[0]['match']['prefixes'] = ['prefix2/*']; $routes = $this->matcher->getDingoRoutesToBeDocumented($routeRules); $this->assertCount(4, $routes); foreach ($routes as $route) { - $this->assertTrue(str_is('prefix2/*', $route['route']->uri())); + $this->assertTrue(Str::is('prefix2/*', $route['route']->uri())); } } @@ -337,14 +338,14 @@ public function testMergesRoutesFromDifferentRuleGroupsForLaravelRouter() $routes = collect($routes); $firstRuleGroup = $routes->filter(function ($route) { - return str_is('prefix1/*', $route['route']->uri()) - && str_is('domain1.*', $route['route']->getDomain()); + return Str::is('prefix1/*', $route['route']->uri()) + && Str::is('domain1.*', $route['route']->getDomain()); }); $this->assertCount(2, $firstRuleGroup); $secondRuleGroup = $routes->filter(function ($route) { - return str_is('prefix2/*', $route['route']->uri()) - && str_is('domain2.*', $route['route']->getDomain()); + return Str::is('prefix2/*', $route['route']->uri()) + && Str::is('domain2.*', $route['route']->getDomain()); }); $this->assertCount(2, $secondRuleGroup); } From 201c8243702118fa802104193be6a621f97e56cc Mon Sep 17 00:00:00 2001 From: Sang Nguyen Date: Wed, 4 Sep 2019 15:08:57 +0700 Subject: [PATCH 090/312] Fix StyleCI --- src/Tools/Generator.php | 2 +- src/Tools/ResponseStrategies/TransformerTagsStrategy.php | 2 +- src/Tools/RouteMatcher.php | 2 +- tests/Unit/RouteMatcherTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 0a9593be..9777b185 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -3,9 +3,9 @@ namespace Mpociot\ApiDoc\Tools; use Faker\Factory; -use Illuminate\Support\Str; use ReflectionClass; use ReflectionMethod; +use Illuminate\Support\Str; use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; diff --git a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php index 2c023d1d..65e222a0 100644 --- a/src/Tools/ResponseStrategies/TransformerTagsStrategy.php +++ b/src/Tools/ResponseStrategies/TransformerTagsStrategy.php @@ -2,9 +2,9 @@ namespace Mpociot\ApiDoc\Tools\ResponseStrategies; -use Illuminate\Support\Arr; use ReflectionClass; use ReflectionMethod; +use Illuminate\Support\Arr; use League\Fractal\Manager; use Illuminate\Routing\Route; use Mpociot\ApiDoc\Tools\Flags; diff --git a/src/Tools/RouteMatcher.php b/src/Tools/RouteMatcher.php index 987328e5..3f9ab607 100644 --- a/src/Tools/RouteMatcher.php +++ b/src/Tools/RouteMatcher.php @@ -2,10 +2,10 @@ namespace Mpociot\ApiDoc\Tools; +use Illuminate\Support\Str; use Illuminate\Routing\Route; use Dingo\Api\Routing\RouteCollection; use Illuminate\Support\Facades\Route as RouteFacade; -use Illuminate\Support\Str; class RouteMatcher { diff --git a/tests/Unit/RouteMatcherTest.php b/tests/Unit/RouteMatcherTest.php index ccce939a..8fe9a9a3 100644 --- a/tests/Unit/RouteMatcherTest.php +++ b/tests/Unit/RouteMatcherTest.php @@ -2,8 +2,8 @@ namespace Mpociot\ApiDoc\Tests\Unit; -use Dingo\Api\Routing\Router; use Illuminate\Support\Str; +use Dingo\Api\Routing\Router; use Orchestra\Testbench\TestCase; use Mpociot\ApiDoc\Tools\RouteMatcher; use Illuminate\Support\Facades\Route as RouteFacade; From ec1c4e90ac4b82e2b939f796d0aba1aa0fe4fff8 Mon Sep 17 00:00:00 2001 From: Sang Nguyen Date: Wed, 4 Sep 2019 15:48:25 +0700 Subject: [PATCH 091/312] Fix Class 'Arr' not found --- resources/views/documentarian.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/documentarian.blade.php b/resources/views/documentarian.blade.php index 3c2dfa68..ae973f83 100644 --- a/resources/views/documentarian.blade.php +++ b/resources/views/documentarian.blade.php @@ -8,7 +8,7 @@ @foreach($parsedRoutes as $groupName => $routes) #{!! $groupName !!} {{-- We pick the first non-empty description we see. --}} -{!! Arr::first($routes, function ($route) { return $route['groupDescription'] !== ''; })['groupDescription'] ?? '' !!} +{!! \Illuminate\Support\Arr::first($routes, function ($route) { return $route['groupDescription'] !== ''; })['groupDescription'] ?? '' !!} @foreach($routes as $parsedRoute) @if($writeCompareFile === true) {!! $parsedRoute['output'] !!} From cac35a884bb236fdaa0264f679dd7f33df0f481b Mon Sep 17 00:00:00 2001 From: Sang Nguyen Date: Wed, 4 Sep 2019 20:44:43 +0700 Subject: [PATCH 092/312] Update composer.json --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 6400ca38..bd6c470d 100644 --- a/composer.json +++ b/composer.json @@ -17,9 +17,9 @@ "require": { "php": ">=7.0.0", "fzaninotto/faker": "~1.8", - "illuminate/routing": "5.5.*|^6.0", - "illuminate/support": "5.5.*|^6.0", - "illuminate/console": "5.5.*|^6.0", + "illuminate/routing": "^5.5|^6.0", + "illuminate/support": "^5.5|^6.0", + "illuminate/console": "^5.5|^6.0", "mpociot/documentarian": "^0.2.0", "mpociot/reflection-docblock": "^1.0.1", "ramsey/uuid": "^3.8", From 7b1fca7d6f87a577b60dc289a1896e8dadb3fb58 Mon Sep 17 00:00:00 2001 From: Shalvah Date: Wed, 4 Sep 2019 16:10:46 +0100 Subject: [PATCH 093/312] Update Testbench version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bd6c470d..62604e24 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "league/flysystem": "^1.0" }, "require-dev": { - "orchestra/testbench": "3.5.* || 3.6.* || 3.7.*", + "orchestra/testbench": "^3.5|^4.0", "phpunit/phpunit": "^6.0.0 || ^7.4.0", "dingo/api": "2.0.0-alpha1", "mockery/mockery": "^1.2.0", From 39187dbdeeb0c447c3a360eef7a0684436e3f1aa Mon Sep 17 00:00:00 2001 From: Shalvah Date: Wed, 4 Sep 2019 16:18:28 +0100 Subject: [PATCH 094/312] Revert Testbench upgrade - upgrade requires upgrading PHPUnit, which has breaking changes --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 62604e24..bd6c470d 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "league/flysystem": "^1.0" }, "require-dev": { - "orchestra/testbench": "^3.5|^4.0", + "orchestra/testbench": "3.5.* || 3.6.* || 3.7.*", "phpunit/phpunit": "^6.0.0 || ^7.4.0", "dingo/api": "2.0.0-alpha1", "mockery/mockery": "^1.2.0", From b81cd1c01bf5dfd9f66cbd3c78a523042f0733db Mon Sep 17 00:00:00 2001 From: Shalvah Date: Wed, 4 Sep 2019 16:27:39 +0100 Subject: [PATCH 095/312] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1f1f51..2e0961c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [3.16.0] - Wednesday, 4 September 2019 +### Added +- Support for Laravel 6 (https://github.com/mpociot/laravel-apidoc-generator/pull/572) + ## [3.15.0] - Saturday, 31 August 2019 ### Added - Ability to exclude a query or body parameter from being included in the example requests (https://github.com/mpociot/laravel-apidoc-generator/pull/552) From fa20e3611127e1c1a972c6678aa31da5363cd52e Mon Sep 17 00:00:00 2001 From: Nicholas Catanchin Date: Thu, 5 Sep 2019 03:49:47 +1000 Subject: [PATCH 096/312] Resolves #543 For the HTTP scheme if baseUrl starts with https --- src/Postman/CollectionWriter.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Postman/CollectionWriter.php b/src/Postman/CollectionWriter.php index 64019789..8ff2c41f 100644 --- a/src/Postman/CollectionWriter.php +++ b/src/Postman/CollectionWriter.php @@ -3,6 +3,7 @@ namespace Mpociot\ApiDoc\Postman; use Ramsey\Uuid\Uuid; +use Illuminate\Support\Str; use Illuminate\Support\Collection; use Illuminate\Support\Facades\URL; @@ -33,6 +34,9 @@ public function getCollection() { try { URL::forceRootUrl($this->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"; From 35a7ee8f6af6df8e3c3561f78010a4fdc50e3ee8 Mon Sep 17 00:00:00 2001 From: Nicholas Catanchin Date: Thu, 5 Sep 2019 04:25:06 +1000 Subject: [PATCH 097/312] #543 Add test for https url, fixture --- .../Fixtures/collection_with_secure_url.json | 55 +++++++++++++++++++ tests/GenerateDocumentationTest.php | 17 ++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/Fixtures/collection_with_secure_url.json diff --git a/tests/Fixtures/collection_with_secure_url.json b/tests/Fixtures/collection_with_secure_url.json new file mode 100644 index 00000000..0e030e2c --- /dev/null +++ b/tests/Fixtures/collection_with_secure_url.json @@ -0,0 +1,55 @@ +{ + "variables": [], + "info": { + "name": "Laravel API", + "_postman_id": "", + "description": "", + "schema": "https:\/\/schema.getpostman.com\/json\/collection\/v2.0.0\/collection.json" + }, + "item": [ + { + "name": "Group A", + "description": "", + "item": [ + { + "name": "Example title.", + "request": { + "url": "https:\/\/yourapp.app\/api\/test", + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "This will be the long description.\nIt can also be multiple lines long.", + "response": [] + } + }, + { + "name": "https:\/\/yourapp.app\/api\/responseTag", + "request": { + "url": "https:\/\/yourapp.app\/api\/responseTag", + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "description": "", + "response": [] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 04793d6c..eb704dd3 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -265,6 +265,23 @@ public function generated_postman_collection_can_have_custom_url() $this->assertEquals($generatedCollection, $fixtureCollection); } + /** @test */ + public function generated_postman_collection_can_have_secure_url() + { + Config::set('apidoc.base_url', 'https://yourapp.app/'); + RouteFacade::get('/api/test', TestController::class.'@withEndpointDescription'); + RouteFacade::post('/api/responseTag', TestController::class.'@withResponseTag'); + + config(['apidoc.routes.0.match.prefixes' => ['api/*']]); + $this->artisan('apidoc:generate'); + + $generatedCollection = json_decode(file_get_contents(__DIR__.'/../public/docs/collection.json'), true); + // The Postman ID varies from call to call; erase it to make the test data reproducible. + $generatedCollection['info']['_postman_id'] = ''; + $fixtureCollection = json_decode(file_get_contents(__DIR__.'/Fixtures/collection_with_secure_url.json'), true); + $this->assertEquals($generatedCollection, $fixtureCollection); + } + /** @test */ public function generated_postman_collection_can_append_custom_http_headers() { From bca5c062d13bdcba3d224826e748cdf0531b068e Mon Sep 17 00:00:00 2001 From: Nicholas Catanchin Date: Thu, 5 Sep 2019 07:24:40 +1000 Subject: [PATCH 098/312] #543 Add exception for forceScheme to appease phpstan --- phpstan.neon | 1 + 1 file changed, 1 insertion(+) diff --git a/phpstan.neon b/phpstan.neon index f07d6f9a..b2aee221 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,6 +5,7 @@ parameters: ignoreErrors: - '#Call to an undefined static method Illuminate\\Support\\Facades\\Route::getRoutes().#' - '#Call to an undefined static method Illuminate\\Support\\Facades\\URL::forceRootUrl()#' + - '#Call to an undefined static method Illuminate\\Support\\Facades\\URL::forceScheme()#' - '#Call to an undefined method Illuminate\\Routing\\Route::versions().#' - '#(.*)NunoMaduro\\Collision(.*)#' - '#Instantiated class Whoops\\Exception\\Inspector not found\.#' From 188225aff9d446a939618364c456cae6cb074d47 Mon Sep 17 00:00:00 2001 From: Shalvah Date: Wed, 4 Sep 2019 23:12:36 +0100 Subject: [PATCH 099/312] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0961c6..6361f69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [3.16.1] - Wednesday, 4 September 2019 +### Added +- Use HTTPS in Postman collection if base_url is HTTPS (https://github.com/mpociot/laravel-apidoc-generator/pull/575) + ## [3.16.0] - Wednesday, 4 September 2019 ### Added - Support for Laravel 6 (https://github.com/mpociot/laravel-apidoc-generator/pull/572) From f7dd8d19b75755763e8e20ab4025075eba5cd51a Mon Sep 17 00:00:00 2001 From: Shalvah Date: Wed, 4 Sep 2019 23:24:08 +0100 Subject: [PATCH 100/312] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bd6c470d..00bb46e6 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "illuminate/routing": "^5.5|^6.0", "illuminate/support": "^5.5|^6.0", "illuminate/console": "^5.5|^6.0", - "mpociot/documentarian": "^0.2.0", + "mpociot/documentarian": "^0.3.0", "mpociot/reflection-docblock": "^1.0.1", "ramsey/uuid": "^3.8", "league/flysystem": "^1.0" From 741f6d16975c1e264821c33e59c45e25c02de429 Mon Sep 17 00:00:00 2001 From: Shalvah Date: Wed, 4 Sep 2019 23:28:32 +0100 Subject: [PATCH 101/312] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6361f69f..d56e99c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Removed +c-generator/pull/575) + +## [3.16.2] - Wednesday, 4 September 2019 +### Fixed +- Support for Laravel 6 (https://github.com/mpociot/laravel-apidoc-generator/commit/f7dd8d19b75755763e8e20ab4025075eba5cd51a) ## [3.16.1] - Wednesday, 4 September 2019 ### Added From 2d1be5b87d0d60a47b3216b80dfe89896616c4f3 Mon Sep 17 00:00:00 2001 From: shalvah Date: Wed, 4 Sep 2019 23:42:15 +0100 Subject: [PATCH 102/312] Remove deprecated helper functions --- src/Strategies/QueryParameters/GetFromDocBlocks.php | 3 ++- tests/Unit/RouteMatcherTest.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Strategies/QueryParameters/GetFromDocBlocks.php b/src/Strategies/QueryParameters/GetFromDocBlocks.php index 9b54596e..5156095c 100644 --- a/src/Strategies/QueryParameters/GetFromDocBlocks.php +++ b/src/Strategies/QueryParameters/GetFromDocBlocks.php @@ -4,6 +4,7 @@ use ReflectionClass; use ReflectionMethod; +use Illuminate\Support\Str; use Illuminate\Routing\Route; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; @@ -78,7 +79,7 @@ private function getQueryParametersFromDocBlock($tags) list($description, $value) = $this->parseParamDescription($description, 'string'); if (is_null($value) && ! $this->shouldExcludeExample($tag)) { - $value = str_contains($description, ['number', 'count', 'page']) + $value = Str::contains($description, ['number', 'count', 'page']) ? $this->generateDummyValue('integer') : $this->generateDummyValue('string'); } diff --git a/tests/Unit/RouteMatcherTest.php b/tests/Unit/RouteMatcherTest.php index 8fe9a9a3..9e0a3440 100644 --- a/tests/Unit/RouteMatcherTest.php +++ b/tests/Unit/RouteMatcherTest.php @@ -102,7 +102,7 @@ public function testRespectsPrefixesRuleForLaravelRouter() $routes = $this->matcher->getRoutesToBeDocumented($routeRules); $this->assertCount(4, $routes); foreach ($routes as $route) { - $this->assertTrue(str_is('prefix1/*', $route['route']->uri())); + $this->assertTrue(Str::is('prefix1/*', $route['route']->uri())); } $routeRules[0]['match']['prefixes'] = ['prefix2/*']; From ffb3563bfdfaa18c7cdf218b535616b168043b50 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 5 Sep 2019 00:36:48 +0100 Subject: [PATCH 103/312] Update docs, refactor API --- config/apidoc.php | 4 +- docs/plugins.md | 45 ++++++++-- ...mDocBlocks.php => GetFromBodyParamTag.php} | 10 ++- ...DocBlocks.php => GetFromQueryParamTag.php} | 6 +- src/Tools/Generator.php | 51 ++++++++++- src/Tools/RouteDocBlocker.php | 18 ++-- src/Tools/Traits/DocBlockParamHelpers.php | 48 ++++++++++ src/Tools/Traits/ParamHelpers.php | 90 +++---------------- tests/Unit/GeneratorTestCase.php | 4 +- 9 files changed, 171 insertions(+), 105 deletions(-) rename src/Strategies/BodyParameters/{GetFromDocBlocks.php => GetFromBodyParamTag.php} (93%) rename src/Strategies/QueryParameters/{GetFromDocBlocks.php => GetFromQueryParamTag.php} (96%) create mode 100644 src/Tools/Traits/DocBlockParamHelpers.php diff --git a/config/apidoc.php b/config/apidoc.php index ac8595da..76bad9bf 100644 --- a/config/apidoc.php +++ b/config/apidoc.php @@ -184,10 +184,10 @@ \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, ], 'bodyParameters' => [ - \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class, ], 'queryParameters' => [ - \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class, ], 'responses' => [ \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, diff --git a/docs/plugins.md b/docs/plugins.md index e8510a66..7859b922 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -3,7 +3,7 @@ You can use plugins to alter how the Generator fetches data about your routes. F ## The stages of route processing Route processing is performed in four stages: -- metadata (this covers route title, route description, route group name, route group description, and authentication status) +- metadata (this covers route `title`, route `description`, route `groupName`, route `groupDescription`, and authentication status (`authenticated`)) - bodyParameters - queryParameters - responses @@ -18,7 +18,7 @@ The `__invoke` method of the strategy is where you perform your actions and retu - the controller class handling the route (`\ReflectionClass`) - the controller method (`\ReflectionMethod $method`) - the rules specified in the apidoc.php config file for the group this route belongs to, under the `apply` section (array) - - the context. This contains all data for the route that has been parsed thus far in the previous stages. + - the context. This contains all data for the route that has been parsed thus far in the previous stages. This means, by the `responses` stage, the context will contain the following keys: `metadata`, `bodyParameters` and `queryParameters`. Here's what your strategy in our example would look like: @@ -53,10 +53,10 @@ The last thing to do is to register the strategy. Strategies are registered in a \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, ], 'bodyParameters' => [ - \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class, ], 'queryParameters' => [ - \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class, ], 'responses' => [ \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, @@ -73,7 +73,7 @@ You can add, replace or remove strategies from here. In our case, we're adding o ```php 'bodyParameters' => [ - \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class, AddOrganizationIdBodyParameter::class, ], ``` @@ -106,3 +106,38 @@ public function __invoke(Route $route, \ReflectionClass $controller, \Reflection ``` The strategy class also has access to the current apidoc configuration via its config property. For instance, you can retrieve the deafult group with `$this->config->get('default_group')`. + + +## 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 `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. + +## 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. +- In the `metadata` stage, strategies should return an array. These are the expected keys (you may omit some, or all): + +``` +'groupName' +'groupDescription' +'title' +'description' +'authenticated' // boolean +``` + +- In the `bodyParameters` and `queryParameters` stages, you can return an array with arbitrary keys. These keys will serve as the names of your parameters. Array keys can be indicated with Laravel's dot notation. The value of each key should be an array with the following keys: + +``` +'type', // Only used in bodyParameters +'description', +'required', // boolean +'value', // An example value for the parameter +``` +- In the `responses` stage, your strategy should return an array containing the responses for different status codes. Each key in the array should be a HTTP status code, and each value should be an array with two keys: + +``` +status // The same status code +content // The response content as a string +``` diff --git a/src/Strategies/BodyParameters/GetFromDocBlocks.php b/src/Strategies/BodyParameters/GetFromBodyParamTag.php similarity index 93% rename from src/Strategies/BodyParameters/GetFromDocBlocks.php rename to src/Strategies/BodyParameters/GetFromBodyParamTag.php index 720ffb54..908e87b5 100644 --- a/src/Strategies/BodyParameters/GetFromDocBlocks.php +++ b/src/Strategies/BodyParameters/GetFromBodyParamTag.php @@ -9,13 +9,13 @@ use Mpociot\Reflection\DocBlock\Tag; use Mpociot\ApiDoc\Strategies\Strategy; use Mpociot\ApiDoc\Tools\RouteDocBlocker; -use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; use Dingo\Api\Http\FormRequest as DingoFormRequest; +use Mpociot\ApiDoc\Tools\Traits\DocBlockParamHelpers; use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; -class GetFromDocBlocks extends Strategy +class GetFromBodyParamTag extends Strategy { - use ParamHelpers; + use DocBlockParamHelpers; public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) { @@ -78,7 +78,9 @@ private function getBodyParametersFromDocBlock($tags) $type = $this->normalizeParameterType($type); list($description, $example) = $this->parseParamDescription($description, $type); - $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example; + $value = is_null($example) && ! $this->shouldExcludeExample($tag) + ? $this->generateDummyValue($type) + : $example; return [$name => compact('type', 'description', 'required', 'value')]; })->toArray(); diff --git a/src/Strategies/QueryParameters/GetFromDocBlocks.php b/src/Strategies/QueryParameters/GetFromQueryParamTag.php similarity index 96% rename from src/Strategies/QueryParameters/GetFromDocBlocks.php rename to src/Strategies/QueryParameters/GetFromQueryParamTag.php index 5156095c..c64cb7a1 100644 --- a/src/Strategies/QueryParameters/GetFromDocBlocks.php +++ b/src/Strategies/QueryParameters/GetFromQueryParamTag.php @@ -10,13 +10,13 @@ use Mpociot\Reflection\DocBlock\Tag; use Mpociot\ApiDoc\Strategies\Strategy; use Mpociot\ApiDoc\Tools\RouteDocBlocker; -use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; use Dingo\Api\Http\FormRequest as DingoFormRequest; +use Mpociot\ApiDoc\Tools\Traits\DocBlockParamHelpers; use Illuminate\Foundation\Http\FormRequest as LaravelFormRequest; -class GetFromDocBlocks extends Strategy +class GetFromQueryParamTag extends Strategy { - use ParamHelpers; + use DocBlockParamHelpers; public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) { diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 8a8da1d9..31b6ccf3 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -4,15 +4,12 @@ use ReflectionClass; use ReflectionMethod; +use Illuminate\Support\Arr; use Illuminate\Support\Str; use Illuminate\Routing\Route; -use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; -use Symfony\Component\HttpFoundation\Response; class Generator { - use ParamHelpers; - /** * @var DocumentationConfig */ @@ -147,4 +144,50 @@ protected function iterateThroughStrategies(string $key, array $context, array $ } return $context[$key]; } + + /** + * Create samples at index 0 for array parameters. + * Also filter out parameters which were excluded from having examples. + * + * @param array $params + * + * @return array + */ + protected function cleanParams(array $params) + { + $values = []; + + // Remove params which have no examples. + $params = array_filter($params, function ($details) { + return ! is_null($details['value']); + }); + + foreach ($params as $paramName => $details) { + $this->generateConcreteSampleForArrayKeys( + $paramName, $details['value'], $values + ); + } + + return $values; + } + + /** + * For each array notation parameter (eg user.*, item.*.name, object.*.*, user[]) + * generate concrete sample (user.0, item.0.name, object.0.0, user.0) with example as value + * + * @param string $paramName + * @param mixed $paramExample + * @param array $values The array that holds the result + * + * @return void + */ + protected function generateConcreteSampleForArrayKeys($paramName, $paramExample, array &$values = []) + { + if (Str::contains($paramName, '[')) { + // Replace usages of [] with dot notation + $paramName = str_replace(['][', '[', ']', '..'], ['.', '.', '', '.*.'], $paramName); + } + // Then generate a sample item for the dot notation + Arr::set($values, str_replace('.*', '.0', $paramName), $paramExample); + } } diff --git a/src/Tools/RouteDocBlocker.php b/src/Tools/RouteDocBlocker.php index 65604f24..0036c448 100644 --- a/src/Tools/RouteDocBlocker.php +++ b/src/Tools/RouteDocBlocker.php @@ -8,9 +8,15 @@ class RouteDocBlocker { - public static $docBlocks = []; - - public static function getDocBlocksFromRoute(Route $route) + protected static $docBlocks = []; + + /** + * @param Route $route + * + * @return array Method and class docblocks + * @throws \ReflectionException + */ + public static function getDocBlocksFromRoute(Route $route): array { list($className, $methodName) = Utils::getRouteClassAndMethodNames($route); $docBlocks = self::getCachedDocBlock($route, $className, $methodName); @@ -35,18 +41,18 @@ public static function getDocBlocksFromRoute(Route $route) protected static function getCachedDocBlock(Route $route, string $className, string $methodName) { - $routeId = self::getRouteId($route, $className, $methodName); + $routeId = self::getRouteCacheId($route, $className, $methodName); return self::$docBlocks[$routeId] ?? null; } protected static function cacheDocBlocks(Route $route, string $className, string $methodName, array $docBlocks) { - $routeId = self::getRouteId($route, $className, $methodName); + $routeId = self::getRouteCacheId($route, $className, $methodName); self::$docBlocks[$routeId] = $docBlocks; } - private static function getRouteId(Route $route, string $className, string $methodName) + private static function getRouteCacheId(Route $route, string $className, string $methodName): string { return $route->uri() .':' diff --git a/src/Tools/Traits/DocBlockParamHelpers.php b/src/Tools/Traits/DocBlockParamHelpers.php new file mode 100644 index 00000000..499de610 --- /dev/null +++ b/src/Tools/Traits/DocBlockParamHelpers.php @@ -0,0 +1,48 @@ +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('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) { + $description = $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/Traits/ParamHelpers.php b/src/Tools/Traits/ParamHelpers.php index 3a8cc8f7..be652706 100644 --- a/src/Tools/Traits/ParamHelpers.php +++ b/src/Tools/Traits/ParamHelpers.php @@ -3,64 +3,10 @@ namespace Mpociot\ApiDoc\Tools\Traits; use Faker\Factory; -use Illuminate\Support\Arr; -use Illuminate\Support\Str; -use Mpociot\Reflection\DocBlock\Tag; trait ParamHelpers { - /** - * Create proper arrays from dot-noted parameter names. Also filter out parameters which were excluded from having examples. - * - * @param array $params - * - * @return array - */ - protected function cleanParams(array $params) - { - $values = []; - $params = array_filter($params, function ($details) { - return ! is_null($details['value']); - }); - - foreach ($params as $name => $details) { - $this->cleanValueFrom($name, $details['value'], $values); - } - - return $values; - } - - /** - * Converts dot notation names to arrays and sets the value at the right depth. - * - * @param string $name - * @param mixed $value - * @param array $values The array that holds the result - * - * @return void - */ - protected function cleanValueFrom($name, $value, array &$values = []) - { - if (Str::contains($name, '[')) { - $name = str_replace(['][', '[', ']', '..'], ['.', '.', '', '.*.'], $name); - } - Arr::set($values, str_replace('.*', '.0', $name), $value); - } - - /** - * Allows users to specify that we shouldn't generate an example for the parameter - * by writing 'No-example'. - * - * @param Tag $tag - * - * @return bool Whether no example should be generated - */ - private function shouldExcludeExample(Tag $tag) - { - return strpos($tag->getContent(), ' No-example') !== false; - } - - private function generateDummyValue(string $type) + protected function generateDummyValue(string $type) { $faker = Factory::create(); if ($this->config->get('faker_seed')) { @@ -95,28 +41,6 @@ private function generateDummyValue(string $type) return $fakeFactory(); } - /** - * 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. - */ - private function parseParamDescription(string $description, string $type) - { - $example = null; - if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) { - $description = $content[1]; - - // examples are parsed as strings by default, we need to cast them properly - $example = $this->castToType($content[2], $type); - } - - return [$description, $example]; - } - /** * Cast a value from a string to a specified type. * @@ -125,7 +49,7 @@ private function parseParamDescription(string $description, string $type) * * @return mixed */ - private function castToType(string $value, string $type) + protected function castToType(string $value, string $type) { $casts = [ 'integer' => 'intval', @@ -147,7 +71,15 @@ private function castToType(string $value, string $type) return $value; } - private function normalizeParameterType(string $type) + /** + * Normalizes the stated "type" of a parameter (eg "int", "integer", "double") + * to a number of standard types (integer, boolean, float). + * + * @param string $type + * + * @return mixed|string + */ + protected function normalizeParameterType(string $type) { $typeMap = [ 'int' => 'integer', diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index abdc828d..c8d4dd29 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -22,10 +22,10 @@ abstract class GeneratorTestCase extends TestCase \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, ], 'bodyParameters' => [ - \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class, ], 'queryParameters' => [ - \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromDocBlocks::class, + \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class, ], 'responses' => [ \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, From c53c1c9c1bb70a7da24b1ff4d1707c2f36ebd182 Mon Sep 17 00:00:00 2001 From: Nicolaj Egelund Date: Thu, 5 Sep 2019 10:38:11 +0200 Subject: [PATCH 104/312] Fix references to removed helper functions --- src/Tools/ResponseStrategies/ResponseCallStrategy.php | 3 ++- tests/Unit/RouteMatcherTest.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Tools/ResponseStrategies/ResponseCallStrategy.php b/src/Tools/ResponseStrategies/ResponseCallStrategy.php index bf232615..c7f10a57 100644 --- a/src/Tools/ResponseStrategies/ResponseCallStrategy.php +++ b/src/Tools/ResponseStrategies/ResponseCallStrategy.php @@ -3,6 +3,7 @@ namespace Mpociot\ApiDoc\Tools\ResponseStrategies; use Dingo\Api\Dispatcher; +use Illuminate\Support\Str; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Routing\Route; @@ -330,7 +331,7 @@ protected function transformHeadersToServerVars(array $headers) $prefix = 'HTTP_'; foreach ($headers as $name => $value) { $name = strtr(strtoupper($name), '-', '_'); - if (! starts_with($name, $prefix) && $name !== 'CONTENT_TYPE') { + if (! Str::startsWith($name, $prefix) && $name !== 'CONTENT_TYPE') { $name = $prefix.$name; } $server[$name] = $value; diff --git a/tests/Unit/RouteMatcherTest.php b/tests/Unit/RouteMatcherTest.php index 8fe9a9a3..9e0a3440 100644 --- a/tests/Unit/RouteMatcherTest.php +++ b/tests/Unit/RouteMatcherTest.php @@ -102,7 +102,7 @@ public function testRespectsPrefixesRuleForLaravelRouter() $routes = $this->matcher->getRoutesToBeDocumented($routeRules); $this->assertCount(4, $routes); foreach ($routes as $route) { - $this->assertTrue(str_is('prefix1/*', $route['route']->uri())); + $this->assertTrue(Str::is('prefix1/*', $route['route']->uri())); } $routeRules[0]['match']['prefixes'] = ['prefix2/*']; From 248a05245a4279c5c44bb7cbf5ef9c2b6f40b763 Mon Sep 17 00:00:00 2001 From: Shalvah Date: Thu, 5 Sep 2019 10:56:37 +0100 Subject: [PATCH 105/312] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d56e99c5..6b50c7d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed c-generator/pull/575) +## [3.16.3] - Thursday, 5 September 2019 +### Fixed +- Removed references to removed helper functions in 6.0 (https://github.com/mpociot/laravel-apidoc-generator/pull/576) + ## [3.16.2] - Wednesday, 4 September 2019 ### Fixed - Support for Laravel 6 (https://github.com/mpociot/laravel-apidoc-generator/commit/f7dd8d19b75755763e8e20ab4025075eba5cd51a) From f86286e695c80f4f41c980934e3360f8d536467f Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Sat, 7 Sep 2019 12:38:37 +0000 Subject: [PATCH 106/312] Apply fixes from StyleCI --- src/Tools/Generator.php | 2 +- src/Tools/RouteDocBlocker.php | 3 ++- src/Tools/Traits/DocBlockParamHelpers.php | 7 ++----- src/Tools/Traits/ParamHelpers.php | 4 ++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 13eeb9b0..b6c3d930 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -175,7 +175,7 @@ protected function cleanParams(array $params) /** * For each array notation parameter (eg user.*, item.*.name, object.*.*, user[]) - * generate concrete sample (user.0, item.0.name, object.0.0, user.0) with example as value + * generate concrete sample (user.0, item.0.name, object.0.0, user.0) with example as value. * * @param string $paramName * @param mixed $paramExample diff --git a/src/Tools/RouteDocBlocker.php b/src/Tools/RouteDocBlocker.php index 0036c448..83f48169 100644 --- a/src/Tools/RouteDocBlocker.php +++ b/src/Tools/RouteDocBlocker.php @@ -13,8 +13,9 @@ class RouteDocBlocker /** * @param Route $route * - * @return array Method and class docblocks * @throws \ReflectionException + * + * @return array Method and class docblocks */ public static function getDocBlocksFromRoute(Route $route): array { diff --git a/src/Tools/Traits/DocBlockParamHelpers.php b/src/Tools/Traits/DocBlockParamHelpers.php index 499de610..dbb0345a 100644 --- a/src/Tools/Traits/DocBlockParamHelpers.php +++ b/src/Tools/Traits/DocBlockParamHelpers.php @@ -2,9 +2,6 @@ namespace Mpociot\ApiDoc\Tools\Traits; -use Faker\Factory; -use Illuminate\Support\Arr; -use Illuminate\Support\Str; use Mpociot\Reflection\DocBlock\Tag; trait DocBlockParamHelpers @@ -19,7 +16,7 @@ trait DocBlockParamHelpers * * @return bool Whether no example should be generated */ - protected function shouldExcludeExample(Tag $tag) + protected function shouldExcludeExample(Tag $tag) { return strpos($tag->getContent(), ' No-example') !== false; } @@ -33,7 +30,7 @@ protected function shouldExcludeExample(Tag $tag) * * @return array The description and included example. */ - protected function parseParamDescription(string $description, string $type) + protected function parseParamDescription(string $description, string $type) { $example = null; if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) { diff --git a/src/Tools/Traits/ParamHelpers.php b/src/Tools/Traits/ParamHelpers.php index be652706..2afefb19 100644 --- a/src/Tools/Traits/ParamHelpers.php +++ b/src/Tools/Traits/ParamHelpers.php @@ -6,7 +6,7 @@ trait ParamHelpers { - protected function generateDummyValue(string $type) + protected function generateDummyValue(string $type) { $faker = Factory::create(); if ($this->config->get('faker_seed')) { @@ -49,7 +49,7 @@ protected function generateDummyValue(string $type) * * @return mixed */ - protected function castToType(string $value, string $type) + protected function castToType(string $value, string $type) { $casts = [ 'integer' => 'intval', From 90560e9729e5778649e7259a91dc23196a1d1441 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 7 Sep 2019 15:44:02 +0100 Subject: [PATCH 107/312] Add tests for plugin system --- docs/plugins.md | 11 +- src/Strategies/Strategy.php | 11 +- src/Tools/Generator.php | 18 +- tests/Unit/GeneratorPluginSystemTestCase.php | 266 +++++++++++++++++++ 4 files changed, 288 insertions(+), 18 deletions(-) create mode 100644 tests/Unit/GeneratorPluginSystemTestCase.php diff --git a/docs/plugins.md b/docs/plugins.md index 7859b922..a49fe09f 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -105,7 +105,9 @@ public function __invoke(Route $route, \ReflectionClass $controller, \Reflection } ``` -The strategy class also has access to the current apidoc configuration via its config property. For instance, you can retrieve the deafult group with `$this->config->get('default_group')`. +The strategy class also has access to the current apidoc configuration via its `config` property. For instance, you can retrieve the default group with `$this->config->get('default_group')`. + +Yopu are also provided with the instance pproperty `stage`, which is set to the name of the currently executing stage. ## Utilities @@ -135,9 +137,4 @@ Each strategy class must implement the __invoke method with the parameters as de 'required', // boolean 'value', // An example value for the parameter ``` -- In the `responses` stage, your strategy should return an array containing the responses for different status codes. Each key in the array should be a HTTP status code, and each value should be an array with two keys: - -``` -status // The same status code -content // The response content as a string -``` +- In the `responses` stage, your strategy should return an array containing the responses for different status codes. Each key in the array should be a HTTP status code, and each value should be a string containing the response. diff --git a/src/Strategies/Strategy.php b/src/Strategies/Strategy.php index 0d904688..81c797c6 100644 --- a/src/Strategies/Strategy.php +++ b/src/Strategies/Strategy.php @@ -9,10 +9,19 @@ abstract class Strategy { + /** + * @var DocumentationConfig The apidoc config + */ protected $config; - public function __construct(DocumentationConfig $config) + /** + * @var string The current stage of route processing + */ + protected $stage; + + public function __construct(string $stage, DocumentationConfig $config) { + $this->stage = $stage; $this->config = $config; } diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index b6c3d930..38d9238c 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -75,8 +75,6 @@ public function processRoute(Route $route, array $rulesToApply = []) $parsedRoute['headers'] = $rulesToApply['headers'] ?? []; - // Currently too lazy to tinker with Blade files; change this later - unset($parsedRoute['metadata']); $parsedRoute += $metadata; return $parsedRoute; @@ -85,7 +83,7 @@ public function processRoute(Route $route, array $rulesToApply = []) protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = []) { $context['metadata'] = [ - 'groupName' => $this->config->get('default_group'), + 'groupName' => $this->config->get('default_group', ''), 'groupDescription' => '', 'title' => '', 'description' => '', @@ -120,12 +118,12 @@ protected function fetchResponses(ReflectionClass $controller, ReflectionMethod return null; } - protected function iterateThroughStrategies(string $key, array $context, array $arguments) + protected function iterateThroughStrategies(string $stage, array $context, array $arguments) { - $strategies = $this->config->get("strategies.$key", []); - $context[$key] = $context[$key] ?? []; + $strategies = $this->config->get("strategies.$stage", []); + $context[$stage] = $context[$stage] ?? []; foreach ($strategies as $strategyClass) { - $strategy = new $strategyClass($this->config); + $strategy = new $strategyClass($stage, $this->config); $arguments[] = $context; $results = $strategy(...$arguments); if (! is_null($results)) { @@ -135,16 +133,16 @@ protected function iterateThroughStrategies(string $key, array $context, array $ // and also allows values to be overwritten // Don't allow overwriting if an empty value is trying to replace a set one - if (! in_array($context[$key], [null, ''], true) && in_array($item, [null, ''], true)) { + if (! in_array($context[$stage], [null, ''], true) && in_array($item, [null, ''], true)) { continue; } else { - $context[$key][$index] = $item; + $context[$stage][$index] = $item; } } } } - return $context[$key]; + return $context[$stage]; } /** diff --git a/tests/Unit/GeneratorPluginSystemTestCase.php b/tests/Unit/GeneratorPluginSystemTestCase.php new file mode 100644 index 00000000..b56b077e --- /dev/null +++ b/tests/Unit/GeneratorPluginSystemTestCase.php @@ -0,0 +1,266 @@ + [ + 'metadata' => [ EmptyStrategy1::class, ], + 'bodyParameters' => [ + EmptyStrategy1::class, + EmptyStrategy2::class, + ], + 'responses' => [ EmptyStrategy1::class ], + ], + ]; + $route = $this->createRoute('GET', '/api/test', 'dummy', true, TestController::class); + $generator = new Generator(new DocumentationConfig($config)); + $generator->processRoute($route); + + // Probably not the best way to do this, but 🤷‍♂️ + $this->assertTrue(EmptyStrategy1::$called['metadata']); + + $this->assertTrue(EmptyStrategy1::$called['bodyParameters']); + $this->assertTrue(EmptyStrategy2::$called['bodyParameters']); + + $this->assertArrayNotHasKey('queryParameters', EmptyStrategy1::$called); + + $this->assertTrue(EmptyStrategy1::$called['responses']); + + } + + /** @test */ + public function combines_responses_from_different_strategies() + { + $config = [ + 'strategies' => [ + 'responses' => [ DummyResponseStrategy200::class, DummyResponseStrategy400::class ], + ], + ]; + $route = $this->createRoute('GET', '/api/test', 'dummy', true, TestController::class); + $generator = new Generator(new DocumentationConfig($config)); + $parsed = $generator->processRoute($route); + + $this->assertTrue($parsed['showresponse']); + $this->assertCount(2, $parsed['response']); + $first = array_shift($parsed['response']); + $this->assertTrue(is_array($first)); + $this->assertEquals(200, $first['status']); + $this->assertEquals('dummy', $first['content']); + $second = array_shift($parsed['response']); + $this->assertTrue(is_array($second)); + $this->assertEquals(400, $second['status']); + $this->assertEquals('dummy2', $second['content']); + } + + // This is a generalized test, as opposed to the one above for responses only + /** @test */ + public function combines_results_from_different_strategies_in_same_stage() + { + $config = [ + 'strategies' => [ + 'metadata' => [PartialDummyMetadataStrategy1::class, PartialDummyMetadataStrategy2::class], + ], + ]; + $route = $this->createRoute('GET', '/api/test', 'dummy', true, TestController::class); + $generator = new Generator(new DocumentationConfig($config)); + $parsed = $generator->processRoute($route); + + $expectedMetadata = [ + 'groupName' => 'dummy', + 'groupDescription' => 'dummy', + 'title' => 'dummy', + 'description' => 'dummy', + 'authenticated' => false, + ]; + $this->assertArraySubset($expectedMetadata, $parsed['metadata']); // Forwards-compatibility + $this->assertArraySubset($expectedMetadata, $parsed); // Backwards-compatibility + } + + /** @test */ + public function missing_metadata_is_filled_in() + { + $config = [ + 'strategies' => [ + 'metadata' => [ PartialDummyMetadataStrategy2::class, ], + ], + ]; + $route = $this->createRoute('GET', '/api/test', 'dummy', true, TestController::class); + $generator = new Generator(new DocumentationConfig($config)); + $parsed = $generator->processRoute($route); + + $expectedMetadata = [ + 'groupName' => '', + 'groupDescription' => 'dummy', + 'title' => '', + 'description' => 'dummy', + 'authenticated' => false, + ]; + $this->assertArraySubset($expectedMetadata, $parsed['metadata']); // Forwards-compatibility + $this->assertArraySubset($expectedMetadata, $parsed); // Backwards-compatibility + } + + /** @test */ + public function overwrites_results_from_previous_strategies_in_same_stage() + { + $config = [ + 'strategies' => [ + 'responses' => [ DummyResponseStrategy200::class, StillDummyResponseStrategyAlso200::class ], + ], + ]; + $route = $this->createRoute('GET', '/api/test', 'dummy', true, TestController::class); + $generator = new Generator(new DocumentationConfig($config)); + $parsed = $generator->processRoute($route); + + $this->assertTrue($parsed['showresponse']); + $this->assertCount(1, $parsed['response']); + $first = array_shift($parsed['response']); + $this->assertTrue(is_array($first)); + $this->assertEquals(200, $first['status']); + $this->assertEquals('stilldummy', $first['content']); + + $config = [ + 'strategies' => [ + 'metadata' => [NotDummyMetadataStrategy::class, PartialDummyMetadataStrategy1::class], + ], + ]; + $route = $this->createRoute('GET', '/api/test', 'dummy', true, TestController::class); + $generator = new Generator(new DocumentationConfig($config)); + $parsed = $generator->processRoute($route); + + $expectedMetadata = [ + 'groupName' => 'dummy', + 'groupDescription' => 'notdummy', + 'title' => 'dummy', + 'description' => 'dummy', + 'authenticated' => false, + ]; + $this->assertArraySubset($expectedMetadata, $parsed['metadata']); // Forwards-compatibility + $this->assertArraySubset($expectedMetadata, $parsed); // Backwards-compatibility + } + + public function dataResources() + { + return [ + [ + null, + '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}', + ], + [ + 'League\Fractal\Serializer\JsonApiSerializer', + '{"data":{"type":null,"id":"1","attributes":{"description":"Welcome on this test versions","name":"TestName"}}}', + ], + ]; + } +} + + +class EmptyStrategy1 extends Strategy +{ + public static $called = []; + + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) + { + static::$called[$this->stage] = true; + } +} + +class EmptyStrategy2 extends Strategy +{ + public static $called = []; + + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) + { + static::$called[$this->stage] = true; + } +} + +class NotDummyMetadataStrategy extends Strategy +{ + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) + { + return [ + 'groupName' => 'notdummy', + 'groupDescription' => 'notdummy', + 'title' => 'notdummy', + 'description' => 'notdummy', + 'authenticated' => true, + ]; + } +} + +class PartialDummyMetadataStrategy1 extends Strategy +{ + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) + { + return [ + 'groupName' => 'dummy', + 'title' => 'dummy', + 'description' => 'dummy', + 'authenticated' => false, + ]; + } +} + +class PartialDummyMetadataStrategy2 extends Strategy +{ + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) + { + return [ + 'description' => 'dummy', + 'groupDescription' => 'dummy', + ]; + } +} + +class DummyResponseStrategy200 extends Strategy +{ + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) + { + return [ 200 => 'dummy', ]; + } +} + +class StillDummyResponseStrategyAlso200 extends Strategy +{ + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) + { + return [ 200 => 'stilldummy', ]; + } +} + +class DummyResponseStrategy400 extends Strategy +{ + public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) + { + return [ 400 => 'dummy2', ]; + } +} From 555fb8665d07ed418347a3b93863085d02a6b873 Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Sat, 7 Sep 2019 14:44:22 +0000 Subject: [PATCH 108/312] Apply fixes from StyleCI --- tests/Unit/GeneratorPluginSystemTestCase.php | 28 +++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/Unit/GeneratorPluginSystemTestCase.php b/tests/Unit/GeneratorPluginSystemTestCase.php index b56b077e..a14e17a8 100644 --- a/tests/Unit/GeneratorPluginSystemTestCase.php +++ b/tests/Unit/GeneratorPluginSystemTestCase.php @@ -2,17 +2,14 @@ namespace Mpociot\ApiDoc\Tests\Unit; +use ReflectionClass; +use ReflectionMethod; use Illuminate\Routing\Route; -use Illuminate\Support\Arr; -use Mpociot\ApiDoc\Strategies\Strategy; -use Orchestra\Testbench\TestCase; use Mpociot\ApiDoc\Tools\Generator; +use Mpociot\ApiDoc\Strategies\Strategy; use Mpociot\ApiDoc\Tools\DocumentationConfig; use Mpociot\ApiDoc\Tests\Fixtures\TestController; use Mpociot\ApiDoc\ApiDocGeneratorServiceProvider; -use Mpociot\ApiDoc\Tests\Fixtures\TestResourceController; -use ReflectionClass; -use ReflectionMethod; class GeneratorPluginSystemTestCase extends LaravelGeneratorTest { @@ -33,12 +30,12 @@ public function only_specified_strategies_are_loaded() { $config = [ 'strategies' => [ - 'metadata' => [ EmptyStrategy1::class, ], + 'metadata' => [EmptyStrategy1::class], 'bodyParameters' => [ EmptyStrategy1::class, EmptyStrategy2::class, ], - 'responses' => [ EmptyStrategy1::class ], + 'responses' => [EmptyStrategy1::class], ], ]; $route = $this->createRoute('GET', '/api/test', 'dummy', true, TestController::class); @@ -54,7 +51,6 @@ public function only_specified_strategies_are_loaded() $this->assertArrayNotHasKey('queryParameters', EmptyStrategy1::$called); $this->assertTrue(EmptyStrategy1::$called['responses']); - } /** @test */ @@ -62,7 +58,7 @@ public function combines_responses_from_different_strategies() { $config = [ 'strategies' => [ - 'responses' => [ DummyResponseStrategy200::class, DummyResponseStrategy400::class ], + 'responses' => [DummyResponseStrategy200::class, DummyResponseStrategy400::class], ], ]; $route = $this->createRoute('GET', '/api/test', 'dummy', true, TestController::class); @@ -82,6 +78,7 @@ public function combines_responses_from_different_strategies() } // This is a generalized test, as opposed to the one above for responses only + /** @test */ public function combines_results_from_different_strategies_in_same_stage() { @@ -110,7 +107,7 @@ public function missing_metadata_is_filled_in() { $config = [ 'strategies' => [ - 'metadata' => [ PartialDummyMetadataStrategy2::class, ], + 'metadata' => [PartialDummyMetadataStrategy2::class], ], ]; $route = $this->createRoute('GET', '/api/test', 'dummy', true, TestController::class); @@ -133,7 +130,7 @@ public function overwrites_results_from_previous_strategies_in_same_stage() { $config = [ 'strategies' => [ - 'responses' => [ DummyResponseStrategy200::class, StillDummyResponseStrategyAlso200::class ], + 'responses' => [DummyResponseStrategy200::class, StillDummyResponseStrategyAlso200::class], ], ]; $route = $this->createRoute('GET', '/api/test', 'dummy', true, TestController::class); @@ -182,7 +179,6 @@ public function dataResources() } } - class EmptyStrategy1 extends Strategy { public static $called = []; @@ -245,7 +241,7 @@ class DummyResponseStrategy200 extends Strategy { public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) { - return [ 200 => 'dummy', ]; + return [200 => 'dummy']; } } @@ -253,7 +249,7 @@ class StillDummyResponseStrategyAlso200 extends Strategy { public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) { - return [ 200 => 'stilldummy', ]; + return [200 => 'stilldummy']; } } @@ -261,6 +257,6 @@ class DummyResponseStrategy400 extends Strategy { public function __invoke(Route $route, ReflectionClass $controller, ReflectionMethod $method, array $routeRules, array $context = []) { - return [ 400 => 'dummy2', ]; + return [400 => 'dummy2']; } } From 270264268f9b613f0df143da887d53308585a7fc Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 7 Sep 2019 15:57:05 +0100 Subject: [PATCH 109/312] Exclude Laravel Telescope routes by default --- .gitignore | 3 ++- composer.json | 2 +- src/Strategies/Responses/ResponseCalls.php | 2 +- src/Tools/RouteMatcher.php | 5 +++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 335b8b06..115d9205 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ composer.lock .php_cs.cache /vendor/ -/public +public/ +tests/public/ .idea/ coverage.xml results.xml diff --git a/composer.json b/composer.json index 00bb46e6..3554cef8 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "scripts": { "lint": "phpstan analyse -c ./phpstan.neon src", "test": "phpunit --stop-on-failure", - "test-ci": "phpunit --coverage-clover=coverage.xml" + "test-ci": "phpunit" }, "extra": { "laravel": { diff --git a/src/Strategies/Responses/ResponseCalls.php b/src/Strategies/Responses/ResponseCalls.php index c3b502ec..5c58ae1b 100644 --- a/src/Strategies/Responses/ResponseCalls.php +++ b/src/Strategies/Responses/ResponseCalls.php @@ -99,7 +99,7 @@ protected function prepareRequest(Route $route, array $rulesToApply, array $body * * @return void * - * @deprecated in favour of Laravel config variables + * @deprecated Not guaranteed to overwrite application's env. Use Laravel config variables instead. */ private function setEnvironmentVariables(array $env) { diff --git a/src/Tools/RouteMatcher.php b/src/Tools/RouteMatcher.php index 3f9ab607..f842cf46 100644 --- a/src/Tools/RouteMatcher.php +++ b/src/Tools/RouteMatcher.php @@ -83,6 +83,11 @@ private function shouldExcludeRoute(Route $route, array $routeRule) { $excludes = $routeRule['exclude'] ?? []; + // Exclude Laravel Telescope routes + if (class_exists("Laravel\Telescope\Telescope")) { + $excludes[] = 'telescope/*'; + } + return Str::is($excludes, $route->getName()) || Str::is($excludes, $route->uri()); } From 18938eb4074af264a1d00bdd0bcf53dccabf1dc3 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 7 Sep 2019 16:14:31 +0100 Subject: [PATCH 110/312] Set status code for transformer response from tag if present --- src/Strategies/Responses/ResponseCalls.php | 3 ++- .../Responses/UseTransformerTags.php | 15 ++++++++----- tests/Fixtures/TestController.php | 8 +++++++ tests/Unit/GeneratorTestCase.php | 22 +++++++++++++++++-- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/Strategies/Responses/ResponseCalls.php b/src/Strategies/Responses/ResponseCalls.php index 5c58ae1b..99350c8d 100644 --- a/src/Strategies/Responses/ResponseCalls.php +++ b/src/Strategies/Responses/ResponseCalls.php @@ -43,7 +43,8 @@ public function __invoke(Route $route, \ReflectionClass $controller, \Reflection $request = $this->prepareRequest($route, $rulesToApply, $bodyParameters, $queryParameters); try { - $response = [200 => $this->makeApiCall($request)->getContent()]; + $response = $this->makeApiCall($request); + $response = [$response->getStatusCode() => $response->getContent()]; } catch (\Exception $e) { echo 'Exception thrown during response call for ['.implode(',', $route->methods)."] {$route->uri}.\n"; if (Flags::$shouldBeVerbose) { diff --git a/src/Strategies/Responses/UseTransformerTags.php b/src/Strategies/Responses/UseTransformerTags.php index fbe01d4e..fd6e9c2f 100644 --- a/src/Strategies/Responses/UseTransformerTags.php +++ b/src/Strategies/Responses/UseTransformerTags.php @@ -54,7 +54,7 @@ protected function getTransformerResponse(array $tags) return null; } - $transformer = $this->getTransformerClass($transformerTag); + list($statusCode, $transformer) = $this->getStatusCodeAmdTransformerClass($transformerTag); $model = $this->getClassToBeTransformed($tags, (new ReflectionClass($transformer))->getMethod('transform')); $modelInstance = $this->instantiateTransformerModel($model); @@ -68,7 +68,7 @@ protected function getTransformerResponse(array $tags) ? new Collection([$modelInstance, $modelInstance], new $transformer) : new Item($modelInstance, new $transformer); - return [200 => response($fractal->createData($resource)->toJson())->getContent()]; + return [$statusCode => response($fractal->createData($resource)->toJson())->getContent()]; } catch (\Exception $e) { return null; } @@ -77,11 +77,16 @@ protected function getTransformerResponse(array $tags) /** * @param Tag $tag * - * @return string|null + * @return array */ - private function getTransformerClass($tag) + private function getStatusCodeAmdTransformerClass($tag): array { - return $tag->getContent(); + $content = $tag->getContent(); + preg_match('/^(\d{3})?\s?([\s\S]*)$/', $content, $result); + $status = $result[1] ?: 200; + $transformerClass = $result[2]; + + return [$status, $transformerClass]; } /** diff --git a/tests/Fixtures/TestController.php b/tests/Fixtures/TestController.php index 282ae8c6..c0dd8390 100644 --- a/tests/Fixtures/TestController.php +++ b/tests/Fixtures/TestController.php @@ -251,6 +251,14 @@ public function transformerTag() return ''; } + /** + * @transformer 201 \Mpociot\ApiDoc\Tests\Fixtures\TestTransformer + */ + public function transformerTagWithStatusCode() + { + return ''; + } + /** * @transformer \Mpociot\ApiDoc\Tests\Fixtures\TestTransformer * @transformermodel \Mpociot\ApiDoc\Tests\Fixtures\TestModel diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index c8d4dd29..23de4836 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -435,8 +435,26 @@ public function can_parse_transformer_tag_with_model() $this->assertTrue(is_array($response)); $this->assertEquals(200, $response['status']); $this->assertSame( - $response['content'], - '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}' + '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}', + $response['content'] + ); + } + + /** @test */ + public function can_parse_transformer_tag_with_status_code() + { + $route = $this->createRoute('GET', '/transformerTagWithStatusCode', 'transformerTagWithStatusCode'); + $parsed = $this->generator->processRoute($route); + $response = Arr::first($parsed['response']); + + $this->assertTrue(is_array($parsed)); + $this->assertArrayHasKey('showresponse', $parsed); + $this->assertTrue($parsed['showresponse']); + $this->assertTrue(is_array($response)); + $this->assertEquals(201, $response['status']); + $this->assertSame( + '{"data":{"id":1,"description":"Welcome on this test versions","name":"TestName"}}', + $response['content'] ); } From e94dc4ed92780693e9b4c9970e693d128750c8d9 Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 7 Sep 2019 16:33:19 +0100 Subject: [PATCH 111/312] Use default strategies for each stage if not specified --- docs/plugins.md | 6 +++++- src/Tools/Generator.php | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index a49fe09f..4ca0c9b2 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -8,7 +8,11 @@ Route processing is performed in four stages: - queryParameters - responses -For each stage, the Generator attempts one or more configured strategies to fetch data. The Generator will call of the strategies configured, progressively combining their results together before to produce the final output of that stage. +For each stage, the Generator attempts the specified strategies to fetch data. The Generator will call of the strategies configured, progressively combining their results together before to produce the final output of that stage. + +There are a number of strategies inccluded with the package, so you don't have to set up anything to get it working. + +> Note: The included ResponseCalls strategy is designed to stop if a response has already been gotten from any other strategy. ## Strategies To create a strategy, create a class that extends `\Mpociot\ApiDoc\Strategies\Strategy`. diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index 38d9238c..afc120f6 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -120,7 +120,26 @@ protected function fetchResponses(ReflectionClass $controller, ReflectionMethod protected function iterateThroughStrategies(string $stage, array $context, array $arguments) { - $strategies = $this->config->get("strategies.$stage", []); + $defaultStrategies = [ + 'metadata' => [ + \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class, + ], + 'bodyParameters' => [ + \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class, + ], + 'queryParameters' => [ + \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class, + ], + 'responses' => [ + \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class, + \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class, + \Mpociot\ApiDoc\Strategies\Responses\UseTransformerTags::class, + \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class, + ] + ]; + + // Use the default strategies for the stage, unless they were explicitly set + $strategies = $this->config->get("strategies.$stage", $defaultStrategies[$stage]); $context[$stage] = $context[$stage] ?? []; foreach ($strategies as $strategyClass) { $strategy = new $strategyClass($stage, $this->config); From 0901dc01d8a2d770d7588ad73cd213305d855492 Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Sat, 7 Sep 2019 15:34:10 +0000 Subject: [PATCH 112/312] Apply fixes from StyleCI --- src/Tools/Generator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/Generator.php b/src/Tools/Generator.php index afc120f6..26914a69 100644 --- a/src/Tools/Generator.php +++ b/src/Tools/Generator.php @@ -135,7 +135,7 @@ protected function iterateThroughStrategies(string $stage, array $context, array \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class, \Mpociot\ApiDoc\Strategies\Responses\UseTransformerTags::class, \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class, - ] + ], ]; // Use the default strategies for the stage, unless they were explicitly set From 9b79b977a8c0f8bc91c1463f60b0c46e9386c7ab Mon Sep 17 00:00:00 2001 From: Shalvah Date: Sat, 7 Sep 2019 16:44:36 +0100 Subject: [PATCH 113/312] Update CHANGELOG.md --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b50c7d2..4d14fac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Removed -c-generator/pull/575) + +## [3.17.0] - Saturday, 5 September 2019 +### Added +- Switched to a plugin architecture that allows support for external strategies (https://github.com/mpociot/laravel-apidoc-generator/pull/570) + +### Changed +- Exclude Laravel Telescope routes when present (https://github.com/mpociot/laravel-apidoc-generator/pull/579) +- Set status code for transformer response from tag if present (https://github.com/mpociot/laravel-apidoc-generator/pull/581) +- Set status code for response call from actual response (https://github.com/mpociot/laravel-apidoc-generator/pull/581) ## [3.16.3] - Thursday, 5 September 2019 ### Fixed From 4893170252adc9f37401c78a98543bbc6d01d69d Mon Sep 17 00:00:00 2001 From: Shalvah Date: Sat, 7 Sep 2019 16:44:55 +0100 Subject: [PATCH 114/312] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d14fac3..47ac30da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed -## [3.17.0] - Saturday, 5 September 2019 +## [3.17.0] - Saturday, 7 September 2019 ### Added - Switched to a plugin architecture that allows support for external strategies (https://github.com/mpociot/laravel-apidoc-generator/pull/570) From c7740eae2b33ef8c87dc17bb4e379535c62a3f2d Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 7 Sep 2019 17:23:22 +0100 Subject: [PATCH 115/312] Upgrade dependencies --- composer.json | 24 ++++++++++++------------ tests/GenerateDocumentationTest.php | 10 +--------- tests/Unit/DingoGeneratorTest.php | 2 +- tests/Unit/GeneratorTestCase.php | 2 +- tests/Unit/RouteMatcherTest.php | 2 +- 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/composer.json b/composer.json index 3554cef8..77930155 100644 --- a/composer.json +++ b/composer.json @@ -15,27 +15,27 @@ } ], "require": { - "php": ">=7.0.0", - "fzaninotto/faker": "~1.8", - "illuminate/routing": "^5.5|^6.0", - "illuminate/support": "^5.5|^6.0", - "illuminate/console": "^5.5|^6.0", + "php": ">=7.2.0", + "fzaninotto/faker": "^1.8", + "illuminate/routing": "^5.7|^6.0", + "illuminate/support": "^5.7|^6.0", + "illuminate/console": "^5.7|^6.0", "mpociot/documentarian": "^0.3.0", "mpociot/reflection-docblock": "^1.0.1", "ramsey/uuid": "^3.8", - "league/flysystem": "^1.0" + "league/flysystem": "^1.0", + "nunomaduro/collision": "^3.0" }, "require-dev": { - "orchestra/testbench": "3.5.* || 3.6.* || 3.7.*", - "phpunit/phpunit": "^6.0.0 || ^7.4.0", - "dingo/api": "2.0.0-alpha1", + "orchestra/testbench": "^3.7.0 || ^4.0", + "phpunit/phpunit": "^7.5.0", + "dingo/api": "^2.3.0", "mockery/mockery": "^1.2.0", "league/fractal": "^0.17.0", - "phpstan/phpstan": "^0.9.0 || ^0.10.0 || ^0.11.15" + "phpstan/phpstan": "^0.11.15" }, "suggest": { - "league/fractal": "Required for transformers support", - "nunomaduro/collision": "For better reporting of errors that are thrown when generating docs" + "league/fractal": "Required for transformers support" }, "autoload": { "psr-4": { diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index 65850280..80dedd0b 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -19,15 +19,7 @@ class GenerateDocumentationTest extends TestCase { use TestHelpers; - /** - * Setup the test environment. - */ - public function setUp() - { - parent::setUp(); - } - - public function tearDown() + public function tearDown(): void { Utils::deleteDirectoryAndContents('/public/docs'); } diff --git a/tests/Unit/DingoGeneratorTest.php b/tests/Unit/DingoGeneratorTest.php index 1d105541..636ade81 100644 --- a/tests/Unit/DingoGeneratorTest.php +++ b/tests/Unit/DingoGeneratorTest.php @@ -16,7 +16,7 @@ protected function getPackageProviders($app) ]; } - public function setUp() + public function setUp(): void { parent::setUp(); diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php index 23de4836..a91c238d 100644 --- a/tests/Unit/GeneratorTestCase.php +++ b/tests/Unit/GeneratorTestCase.php @@ -48,7 +48,7 @@ protected function getPackageProviders($app) /** * Setup the test environment. */ - public function setUp() + public function setUp(): void { parent::setUp(); diff --git a/tests/Unit/RouteMatcherTest.php b/tests/Unit/RouteMatcherTest.php index 9e0a3440..2857add3 100644 --- a/tests/Unit/RouteMatcherTest.php +++ b/tests/Unit/RouteMatcherTest.php @@ -15,7 +15,7 @@ class RouteMatcherTest extends TestCase */ private $matcher; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->matcher = new RouteMatcher(); From d318033920b30b68ed248270b1a8935991d389ef Mon Sep 17 00:00:00 2001 From: shalvah Date: Sat, 7 Sep 2019 17:30:21 +0100 Subject: [PATCH 116/312] Update Travis config --- .travis.yml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 74dbffc5..ea16c2ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: php php: - - 7.2.0 - - 7.1.3 - - 7.0.0 + - 7.3 + - 7.2 env: - PREFER_LOWEST="" @@ -13,16 +12,10 @@ jobs: include: - script: composer lint name: "Lint code" - php: 7.2.0 + php: 7.3 env: PREFER_LOWEST="" script: composer test-ci before_script: - travis_retry composer self-update - travis_retry composer update --no-interaction --prefer-dist $PREFER_LOWEST - -before_install: - - pip install --user codecov - -after_success: - - codecov From 893f9b214fc83bffd496d6ccb63e50432f8b5515 Mon Sep 17 00:00:00 2001 From: Jeff Siebach Date: Tue, 10 Sep 2019 18:33:15 -0500 Subject: [PATCH 117/312] Support Lumen kernel --- src/Strategies/Responses/ResponseCalls.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Strategies/Responses/ResponseCalls.php b/src/Strategies/Responses/ResponseCalls.php index 99350c8d..0a9f681d 100644 --- a/src/Strategies/Responses/ResponseCalls.php +++ b/src/Strategies/Responses/ResponseCalls.php @@ -286,9 +286,16 @@ protected function makeApiCall(Request $request) */ protected function callLaravelRoute(Request $request): \Symfony\Component\HttpFoundation\Response { - $kernel = app(\Illuminate\Contracts\Http\Kernel::class); - $response = $kernel->handle($request); - $kernel->terminate($request, $response); + // Confirm we're running in Laravel, not Lumen + if(app()->bound(\Illuminate\Contracts\Http\Kernel::class)){ + $kernel = app(\Illuminate\Contracts\Http\Kernel::class); + $response = $kernel->handle($request); + $kernel->terminate($request, $response); + } else { + // Handle the request using the Lumen application. + $kernel = app(); + $response = $kernel->handle($request); + } return $response; } From cdbc1db05d0be7f861de4fa24627ad5e00441839 Mon Sep 17 00:00:00 2001 From: Jeff Siebach Date: Tue, 10 Sep 2019 18:38:21 -0500 Subject: [PATCH 118/312] Update style rules --- src/Strategies/Responses/ResponseCalls.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Strategies/Responses/ResponseCalls.php b/src/Strategies/Responses/ResponseCalls.php index 0a9f681d..06b3f4fd 100644 --- a/src/Strategies/Responses/ResponseCalls.php +++ b/src/Strategies/Responses/ResponseCalls.php @@ -287,7 +287,7 @@ protected function makeApiCall(Request $request) protected function callLaravelRoute(Request $request): \Symfony\Component\HttpFoundation\Response { // Confirm we're running in Laravel, not Lumen - if(app()->bound(\Illuminate\Contracts\Http\Kernel::class)){ + if (app()->bound(\Illuminate\Contracts\Http\Kernel::class)) { $kernel = app(\Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle($request); $kernel->terminate($request, $response); From 02fb719d0d6c25e6ce72f30dc8b9604449061156 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 12 Sep 2019 23:21:50 +0100 Subject: [PATCH 119/312] Update python template to work with new clean*Params format (fixes #587) --- .../example-requests/python.blade.php | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/resources/views/partials/example-requests/python.blade.php b/resources/views/partials/example-requests/python.blade.php index 4633aa2b..fdf26362 100644 --- a/resources/views/partials/example-requests/python.blade.php +++ b/resources/views/partials/example-requests/python.blade.php @@ -5,22 +5,28 @@ url = '{{ rtrim($baseUrl, '/') }}/{{ ltrim($route['boundUri'], '/') }}' @if(count($route['cleanBodyParameters'])) payload = { - @foreach($route['cleanBodyParameters'] as $attribute => $parameter) - '{{ $attribute }}': '{{ $parameter['value'] }}'@if(!($loop->last)),@endif {{ !$parameter['required'] ? '# optional' : '' }} - @endforeach +@foreach($route['cleanBodyParameters'] as $parameter => $value) + '{{ $parameter }}': '{{ $value }}'@if(!($loop->last)), +@endif +@endforeach + } @endif @if(count($route['cleanQueryParameters'])) params = { - @foreach($route['cleanQueryParameters'] as $attribute => $parameter) - '{{ $attribute }}': '{{ $parameter['value'] }}'@if(!($loop->last)),@endif {{ !$parameter['required'] ? '# optional' : '' }} - @endforeach +@foreach($route['cleanQueryParameters'] as $parameter => $value) + '{{ $parameter }}': '{{ $value }}'@if(!($loop->last)), +@endif +@endforeach + } @endif headers = { - @foreach($route['headers'] as $header => $value) - '{{$header}}': '{{$value}}'@if(!($loop->last)),@endif - @endforeach +@foreach($route['headers'] as $header => $value) + '{{$header}}': '{{$value}}'@if(!($loop->last)), +@endif +@endforeach + } response = requests.request('{{$route['methods'][0]}}', url, headers=headers{{ count($route['cleanBodyParameters']) ? ', json=payload' : '' }}{{ count($route['cleanQueryParameters']) ? ', params=params' : ''}}) response.json() From 762e2e1003d389d6e785d31144eca89c40515926 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 12 Sep 2019 23:23:14 +0100 Subject: [PATCH 120/312] Bugfix: *really* exclude parameters from examples. --- src/Strategies/BodyParameters/GetFromBodyParamTag.php | 2 +- src/Strategies/QueryParameters/GetFromQueryParamTag.php | 2 +- src/Tools/Traits/DocBlockParamHelpers.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Strategies/BodyParameters/GetFromBodyParamTag.php b/src/Strategies/BodyParameters/GetFromBodyParamTag.php index 908e87b5..a5c091e6 100644 --- a/src/Strategies/BodyParameters/GetFromBodyParamTag.php +++ b/src/Strategies/BodyParameters/GetFromBodyParamTag.php @@ -60,7 +60,7 @@ private function getBodyParametersFromDocBlock($tags) }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); - $content = preg_replace('/\s?No-example.?/', '', $content); + $content = preg_replace('/Example:\s*No-example.?/', '', $content); if (empty($content)) { // this means only name and type were supplied list($name, $type) = preg_split('/\s+/', $tag->getContent()); diff --git a/src/Strategies/QueryParameters/GetFromQueryParamTag.php b/src/Strategies/QueryParameters/GetFromQueryParamTag.php index c64cb7a1..3b40735e 100644 --- a/src/Strategies/QueryParameters/GetFromQueryParamTag.php +++ b/src/Strategies/QueryParameters/GetFromQueryParamTag.php @@ -61,7 +61,7 @@ private function getQueryParametersFromDocBlock($tags) }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); - $content = preg_replace('/\s?No-example.?/', '', $content); + $content = preg_replace('/Example:\s*No-example.?/', '', $content); if (empty($content)) { // this means only name was supplied list($name) = preg_split('/\s+/', $tag->getContent()); diff --git a/src/Tools/Traits/DocBlockParamHelpers.php b/src/Tools/Traits/DocBlockParamHelpers.php index dbb0345a..9bda3cf0 100644 --- a/src/Tools/Traits/DocBlockParamHelpers.php +++ b/src/Tools/Traits/DocBlockParamHelpers.php @@ -33,7 +33,7 @@ protected function shouldExcludeExample(Tag $tag) protected function parseParamDescription(string $description, string $type) { $example = null; - if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) { + if (preg_match('/(.*)\s+Example:\s*(.+)\s*/', $description, $content)) { $description = $content[1]; // examples are parsed as strings by default, we need to cast them properly From e54b474578b53f97f4737664a63131b315aaf82d Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 12 Sep 2019 23:31:19 +0100 Subject: [PATCH 121/312] Revert mistaken change --- src/Strategies/BodyParameters/GetFromBodyParamTag.php | 2 +- src/Strategies/QueryParameters/GetFromQueryParamTag.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Strategies/BodyParameters/GetFromBodyParamTag.php b/src/Strategies/BodyParameters/GetFromBodyParamTag.php index a5c091e6..908e87b5 100644 --- a/src/Strategies/BodyParameters/GetFromBodyParamTag.php +++ b/src/Strategies/BodyParameters/GetFromBodyParamTag.php @@ -60,7 +60,7 @@ private function getBodyParametersFromDocBlock($tags) }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); - $content = preg_replace('/Example:\s*No-example.?/', '', $content); + $content = preg_replace('/\s?No-example.?/', '', $content); if (empty($content)) { // this means only name and type were supplied list($name, $type) = preg_split('/\s+/', $tag->getContent()); diff --git a/src/Strategies/QueryParameters/GetFromQueryParamTag.php b/src/Strategies/QueryParameters/GetFromQueryParamTag.php index 3b40735e..c64cb7a1 100644 --- a/src/Strategies/QueryParameters/GetFromQueryParamTag.php +++ b/src/Strategies/QueryParameters/GetFromQueryParamTag.php @@ -61,7 +61,7 @@ private function getQueryParametersFromDocBlock($tags) }) ->mapWithKeys(function ($tag) { preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); - $content = preg_replace('/Example:\s*No-example.?/', '', $content); + $content = preg_replace('/\s?No-example.?/', '', $content); if (empty($content)) { // this means only name was supplied list($name) = preg_split('/\s+/', $tag->getContent()); From 009d7603bf40262e37be329872c99a9ba97471c8 Mon Sep 17 00:00:00 2001 From: shalvah Date: Thu, 12 Sep 2019 23:34:17 +0100 Subject: [PATCH 122/312] Update Changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ac30da..81a2f615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [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) +- Update usage of `clean*Parameters` in python template (https://github.com/mpociot/laravel-apidoc-generator/commit/02fb719d0d6c25e6ce72f30dc8b9604449061156) +- Bugfix: *really* exclude parameters from examples, not just send empty strings (https://github.com/mpociot/laravel-apidoc-generator/commit/762e2e1003d389d6e785d31144eca89c40515926, https://github.com/mpociot/laravel-apidoc-generator/commit/e54b474578b53f97f4737664a63131b315aaf82d) + ## [3.17.0] - Saturday, 7 September 2019 ### Added - Switched to a plugin architecture that allows support for external strategies (https://github.com/mpociot/laravel-apidoc-generator/pull/570) From 4fadb91a667b475ffd92c745589c8a4e925add60 Mon Sep 17 00:00:00 2001 From: soenderby <33911621+soenderby@users.noreply.github.com> Date: Thu, 26 Sep 2019 11:25:23 +0200 Subject: [PATCH 123/312] Update documenting.md Fixed a small spelling error In line 5. Changed "conetns" to "contents" --- docs/documenting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documenting.md b/docs/documenting.md index 51f8a5f3..ad169917 100644 --- a/docs/documenting.md +++ b/docs/documenting.md @@ -2,7 +2,7 @@ This package generates documentation from your code using mainly annotations (in doc block comments). ## Grouping endpoints -All endpoints are grouped for easy organization. Using `@group` in a controller doc block creates a Group within the API documentation. All routes handled by that controller will be grouped under this group in the table of conetns shown in the sidebar. +All endpoints are grouped for easy organization. Using `@group` in a controller doc block creates a Group within the API documentation. All routes handled by that controller will be grouped under this group in the table of contents shown in the sidebar. The short description after the `@group` should be unique to allow anchor tags to navigate to this section. A longer description can be included below. Custom formatting and `